Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/point-of-sale/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from './merchant/devices/routes'
import { PosDeviceService } from './merchant/devices/service'
import { MerchantService } from './merchant/service'
import { PaymentContext, PaymentRoutes } from './payments/routes'

export interface AppServices {
logger: Promise<Logger>
Expand All @@ -27,6 +28,7 @@ export interface AppServices {
posDeviceRoutes: Promise<PosDeviceRoutes>
posDeviceService: Promise<PosDeviceService>
merchantService: Promise<MerchantService>
paymentRoutes: Promise<PaymentRoutes>
}

export type AppContainer = IocContract<AppServices>
Expand Down Expand Up @@ -71,6 +73,7 @@ export class App {

const merchantRoutes = await this.container.use('merchantRoutes')
const posDeviceRoutes = await this.container.use('posDeviceRoutes')
const paymentRoutes = await this.container.use('paymentRoutes')

// POST /merchants
// Create merchant
Expand All @@ -93,6 +96,10 @@ export class App {
posDeviceRoutes.register
)

// POST /payment
// Initiate a payment
router.post<DefaultState, PaymentContext>('/payment', paymentRoutes.payment)

koa.use(cors())
koa.use(router.routes())

Expand Down
22 changes: 18 additions & 4 deletions packages/point-of-sale/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import { createPosDeviceService } from './merchant/devices/service'
import { createMerchantRoutes } from './merchant/routes'
import { createPaymentService } from './payments/service'
import { createPosDeviceRoutes } from './merchant/devices/routes'
import { createPaymentRoutes } from './payments/routes'
import axios from 'axios'
import { createCardServiceClient } from './card-service-client/client'

export function initIocContainer(
config: typeof Config
Expand Down Expand Up @@ -183,20 +185,32 @@ export function initIocContainer(
async (deps: IocContract<AppServices>) => {
const logger = await deps.use('logger')
const knex = await deps.use('knex')
return await createPosDeviceService({
logger,
knex
})
return await createPosDeviceService({ logger, knex })
}
)

container.singleton('cardServiceClient', async (deps) => {
return createCardServiceClient({
logger: await deps.use('logger'),
axios: await deps.use('axios')
})
})

container.singleton('posDeviceRoutes', async (deps) =>
createPosDeviceRoutes({
logger: await deps.use('logger'),
posDeviceService: await deps.use('posDeviceService')
})
)

container.singleton('paymentRoutes', async (deps) => {
return createPaymentRoutes({
logger: await deps.use('logger'),
paymentService: await deps.use('paymentClient'),
cardServiceClient: await deps.use('cardServiceClient')
})
})

return container
}

Expand Down
128 changes: 128 additions & 0 deletions packages/point-of-sale/src/payments/routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { IocContract } from '@adonisjs/fold'
import { initIocContainer } from '..'
import { AppServices } from '../app'
import { Config } from '../config/app'
import { TestContainer, createTestApp } from '../tests/app'
import { PaymentContext, PaymentRoutes } from './routes'
import { truncateTables } from '../tests/tableManager'
import { PaymentService } from './service'
import { CardServiceClient, Result } from '../card-service-client/client'
import { createContext } from '../tests/context'
import { CardServiceClientError } from '../card-service-client/errors'

describe('Payment Routes', () => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let paymentRoutes: PaymentRoutes
let paymentService: PaymentService
let cardServiceClient: CardServiceClient

beforeAll(async () => {
deps = initIocContainer(Config)
appContainer = await createTestApp(deps)
paymentService = await deps.use('paymentClient')
cardServiceClient = await deps.use('cardServiceClient')
paymentRoutes = await deps.use('paymentRoutes')
})

beforeEach(() => {
jest.clearAllMocks()
})

afterEach(async (): Promise<void> => {
await truncateTables(deps)
})

afterAll(async (): Promise<void> => {
await appContainer.shutdown()
})

describe('payment', () => {
test('returns 200 with result approved', async () => {
const ctx = createPaymentContext()
mockPaymentService()
jest
.spyOn(cardServiceClient, 'sendPayment')
.mockResolvedValueOnce(Result.APPROVED)

await paymentRoutes.payment(ctx)
expect(ctx.response.body).toBe(Result.APPROVED)
expect(ctx.status).toBe(200)
})

test('returns 401 with result card_expired or invalid_signature', async () => {
const ctx = createPaymentContext()
mockPaymentService()
jest
.spyOn(cardServiceClient, 'sendPayment')
.mockResolvedValueOnce(Result.CARD_EXPIRED)

await paymentRoutes.payment(ctx)
expect(ctx.response.body).toBe(Result.CARD_EXPIRED)
expect(ctx.status).toBe(401)
})

test('returns cardService error code when thrown', async () => {
const ctx = createPaymentContext()
mockPaymentService()
jest
.spyOn(cardServiceClient, 'sendPayment')
.mockRejectedValue(new CardServiceClientError('Some error', 404))
await paymentRoutes.payment(ctx)
expect(ctx.response.body).toBe('Some error')
expect(ctx.status).toBe(404)
})

test('returns 400 when there is a paymentService error', async () => {
const ctx = createPaymentContext()
jest
.spyOn(paymentService, 'getWalletAddress')
.mockRejectedValueOnce(new Error('Wallet address error'))
await paymentRoutes.payment(ctx)
expect(ctx.response.body).toBe('Wallet address error')
expect(ctx.status).toBe(400)
})

test('returns 500 when an unknown error is thrown', async () => {
const ctx = createPaymentContext()
jest
.spyOn(paymentService, 'getWalletAddress')
.mockRejectedValueOnce('Unknown error')
await paymentRoutes.payment(ctx)
expect(ctx.response.body).toBe('Unknown error')
expect(ctx.status).toBe(500)
})

function mockPaymentService() {
jest.spyOn(paymentService, 'getWalletAddress').mockResolvedValueOnce({
id: 'id',
assetCode: 'USD',
assetScale: 1,
authServer: 'authServer',
resourceServer: 'resourceServer',
cardService: 'cardService'
})
jest
.spyOn(paymentService, 'createIncomingPayment')
.mockResolvedValueOnce('incoming-payment-url')
}
})
})

function createPaymentContext() {
return createContext<PaymentContext>({
headers: { Accept: 'application/json' },
method: 'POST',
url: `/payment`,
body: {
card: {
walletAddress: 'wallet-address',
trasactionCounter: 0,
expiry: new Date(new Date().getDate() + 1)
},
signature: 'signature',
value: 100,
merchantWalletAddress: 'merchant-wallet-address'
}
})
}
93 changes: 93 additions & 0 deletions packages/point-of-sale/src/payments/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { AppContext } from '../app'
import { CardServiceClient, Result } from '../card-service-client/client'
import { AmountInput } from '../graphql/generated/graphql'
import { BaseService } from '../shared/baseService'
import { PaymentService } from './service'
import { CardServiceClientError } from '../card-service-client/errors'

interface ServiceDependencies extends BaseService {
paymentService: PaymentService
cardServiceClient: CardServiceClient
}

export type PaymentBody = {
card: {
walletAddress: string
trasactionCounter: number
expiry: Date
}
signature: string
value: bigint
merchantWalletAddress: string
}
type PaymentRequest = Exclude<AppContext['request'], 'body'> & {
body: PaymentBody
}

export type PaymentContext = Exclude<AppContext, 'request'> & {
request: PaymentRequest
}

export interface PaymentRoutes {
payment(ctx: PaymentContext): Promise<void>
}

export function createPaymentRoutes(deps_: ServiceDependencies): PaymentRoutes {
const log = deps_.logger.child({
service: 'PaymentRoutes'
})

const deps = {
...deps_,
logger: log
}

return {
payment: (ctx: PaymentContext) => payment(deps, ctx)
}
}

async function payment(
deps: ServiceDependencies,
ctx: PaymentContext
): Promise<void> {
const body = ctx.request.body
try {
const walletAddress = await deps.paymentService.getWalletAddress(
body.card.walletAddress
)
const incomingAmount: AmountInput = {
assetCode: walletAddress.assetCode,
assetScale: walletAddress.assetScale,
value: body.value
}
const incomingPaymentUrl = await deps.paymentService.createIncomingPayment(
walletAddress.id,
incomingAmount
)
const result = await deps.cardServiceClient.sendPayment({
merchantWalletAddress: body.merchantWalletAddress,
incomingPaymentUrl,
date: new Date(),
signature: body.signature,
card: body.card
})

ctx.body = result
ctx.status = result === Result.APPROVED ? 200 : 401
} catch (err) {
const { body, status } = handlePaymentError(err)
ctx.body = body
ctx.status = status
}
}

function handlePaymentError(err: unknown) {
if (err instanceof CardServiceClientError) {
return { body: err.message, status: err.status }
}
if (err instanceof Error) {
return { body: err.message, status: 400 }
}
return { body: err, status: 500 }
}