Skip to content

Commit 058a0ed

Browse files
oana-loleanjlie
authored andcommitted
feat(point-of-sale): POST payment route (#3597)
* Payment route structure * Added routes test file * Removed some comments * Added test for unknown error * Updated graphql generated files
1 parent c09345b commit 058a0ed

File tree

4 files changed

+246
-4
lines changed

4 files changed

+246
-4
lines changed

packages/point-of-sale/src/app.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from './merchant/devices/routes'
1919
import { PosDeviceService } from './merchant/devices/service'
2020
import { MerchantService } from './merchant/service'
21+
import { PaymentContext, PaymentRoutes } from './payments/routes'
2122

2223
export interface AppServices {
2324
logger: Promise<Logger>
@@ -27,6 +28,7 @@ export interface AppServices {
2728
posDeviceRoutes: Promise<PosDeviceRoutes>
2829
posDeviceService: Promise<PosDeviceService>
2930
merchantService: Promise<MerchantService>
31+
paymentRoutes: Promise<PaymentRoutes>
3032
}
3133

3234
export type AppContainer = IocContract<AppServices>
@@ -71,6 +73,7 @@ export class App {
7173

7274
const merchantRoutes = await this.container.use('merchantRoutes')
7375
const posDeviceRoutes = await this.container.use('posDeviceRoutes')
76+
const paymentRoutes = await this.container.use('paymentRoutes')
7477

7578
// POST /merchants
7679
// Create merchant
@@ -93,6 +96,10 @@ export class App {
9396
posDeviceRoutes.register
9497
)
9598

99+
// POST /payment
100+
// Initiate a payment
101+
router.post<DefaultState, PaymentContext>('/payment', paymentRoutes.payment)
102+
96103
koa.use(cors())
97104
koa.use(router.routes())
98105

packages/point-of-sale/src/index.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import { createPosDeviceService } from './merchant/devices/service'
2020
import { createMerchantRoutes } from './merchant/routes'
2121
import { createPaymentService } from './payments/service'
2222
import { createPosDeviceRoutes } from './merchant/devices/routes'
23+
import { createPaymentRoutes } from './payments/routes'
2324
import axios from 'axios'
25+
import { createCardServiceClient } from './card-service-client/client'
2426

2527
export function initIocContainer(
2628
config: typeof Config
@@ -183,20 +185,32 @@ export function initIocContainer(
183185
async (deps: IocContract<AppServices>) => {
184186
const logger = await deps.use('logger')
185187
const knex = await deps.use('knex')
186-
return await createPosDeviceService({
187-
logger,
188-
knex
189-
})
188+
return await createPosDeviceService({ logger, knex })
190189
}
191190
)
192191

192+
container.singleton('cardServiceClient', async (deps) => {
193+
return createCardServiceClient({
194+
logger: await deps.use('logger'),
195+
axios: await deps.use('axios')
196+
})
197+
})
198+
193199
container.singleton('posDeviceRoutes', async (deps) =>
194200
createPosDeviceRoutes({
195201
logger: await deps.use('logger'),
196202
posDeviceService: await deps.use('posDeviceService')
197203
})
198204
)
199205

206+
container.singleton('paymentRoutes', async (deps) => {
207+
return createPaymentRoutes({
208+
logger: await deps.use('logger'),
209+
paymentService: await deps.use('paymentClient'),
210+
cardServiceClient: await deps.use('cardServiceClient')
211+
})
212+
})
213+
200214
return container
201215
}
202216

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { IocContract } from '@adonisjs/fold'
2+
import { initIocContainer } from '..'
3+
import { AppServices } from '../app'
4+
import { Config } from '../config/app'
5+
import { TestContainer, createTestApp } from '../tests/app'
6+
import { PaymentContext, PaymentRoutes } from './routes'
7+
import { truncateTables } from '../tests/tableManager'
8+
import { PaymentService } from './service'
9+
import { CardServiceClient, Result } from '../card-service-client/client'
10+
import { createContext } from '../tests/context'
11+
import { CardServiceClientError } from '../card-service-client/errors'
12+
13+
describe('Payment Routes', () => {
14+
let deps: IocContract<AppServices>
15+
let appContainer: TestContainer
16+
let paymentRoutes: PaymentRoutes
17+
let paymentService: PaymentService
18+
let cardServiceClient: CardServiceClient
19+
20+
beforeAll(async () => {
21+
deps = initIocContainer(Config)
22+
appContainer = await createTestApp(deps)
23+
paymentService = await deps.use('paymentClient')
24+
cardServiceClient = await deps.use('cardServiceClient')
25+
paymentRoutes = await deps.use('paymentRoutes')
26+
})
27+
28+
beforeEach(() => {
29+
jest.clearAllMocks()
30+
})
31+
32+
afterEach(async (): Promise<void> => {
33+
await truncateTables(deps)
34+
})
35+
36+
afterAll(async (): Promise<void> => {
37+
await appContainer.shutdown()
38+
})
39+
40+
describe('payment', () => {
41+
test('returns 200 with result approved', async () => {
42+
const ctx = createPaymentContext()
43+
mockPaymentService()
44+
jest
45+
.spyOn(cardServiceClient, 'sendPayment')
46+
.mockResolvedValueOnce(Result.APPROVED)
47+
48+
await paymentRoutes.payment(ctx)
49+
expect(ctx.response.body).toBe(Result.APPROVED)
50+
expect(ctx.status).toBe(200)
51+
})
52+
53+
test('returns 401 with result card_expired or invalid_signature', async () => {
54+
const ctx = createPaymentContext()
55+
mockPaymentService()
56+
jest
57+
.spyOn(cardServiceClient, 'sendPayment')
58+
.mockResolvedValueOnce(Result.CARD_EXPIRED)
59+
60+
await paymentRoutes.payment(ctx)
61+
expect(ctx.response.body).toBe(Result.CARD_EXPIRED)
62+
expect(ctx.status).toBe(401)
63+
})
64+
65+
test('returns cardService error code when thrown', async () => {
66+
const ctx = createPaymentContext()
67+
mockPaymentService()
68+
jest
69+
.spyOn(cardServiceClient, 'sendPayment')
70+
.mockRejectedValue(new CardServiceClientError('Some error', 404))
71+
await paymentRoutes.payment(ctx)
72+
expect(ctx.response.body).toBe('Some error')
73+
expect(ctx.status).toBe(404)
74+
})
75+
76+
test('returns 400 when there is a paymentService error', async () => {
77+
const ctx = createPaymentContext()
78+
jest
79+
.spyOn(paymentService, 'getWalletAddress')
80+
.mockRejectedValueOnce(new Error('Wallet address error'))
81+
await paymentRoutes.payment(ctx)
82+
expect(ctx.response.body).toBe('Wallet address error')
83+
expect(ctx.status).toBe(400)
84+
})
85+
86+
test('returns 500 when an unknown error is thrown', async () => {
87+
const ctx = createPaymentContext()
88+
jest
89+
.spyOn(paymentService, 'getWalletAddress')
90+
.mockRejectedValueOnce('Unknown error')
91+
await paymentRoutes.payment(ctx)
92+
expect(ctx.response.body).toBe('Unknown error')
93+
expect(ctx.status).toBe(500)
94+
})
95+
96+
function mockPaymentService() {
97+
jest.spyOn(paymentService, 'getWalletAddress').mockResolvedValueOnce({
98+
id: 'id',
99+
assetCode: 'USD',
100+
assetScale: 1,
101+
authServer: 'authServer',
102+
resourceServer: 'resourceServer',
103+
cardService: 'cardService'
104+
})
105+
jest
106+
.spyOn(paymentService, 'createIncomingPayment')
107+
.mockResolvedValueOnce('incoming-payment-url')
108+
}
109+
})
110+
})
111+
112+
function createPaymentContext() {
113+
return createContext<PaymentContext>({
114+
headers: { Accept: 'application/json' },
115+
method: 'POST',
116+
url: `/payment`,
117+
body: {
118+
card: {
119+
walletAddress: 'wallet-address',
120+
trasactionCounter: 0,
121+
expiry: new Date(new Date().getDate() + 1)
122+
},
123+
signature: 'signature',
124+
value: 100,
125+
merchantWalletAddress: 'merchant-wallet-address'
126+
}
127+
})
128+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { AppContext } from '../app'
2+
import { CardServiceClient, Result } from '../card-service-client/client'
3+
import { AmountInput } from '../graphql/generated/graphql'
4+
import { BaseService } from '../shared/baseService'
5+
import { PaymentService } from './service'
6+
import { CardServiceClientError } from '../card-service-client/errors'
7+
8+
interface ServiceDependencies extends BaseService {
9+
paymentService: PaymentService
10+
cardServiceClient: CardServiceClient
11+
}
12+
13+
export type PaymentBody = {
14+
card: {
15+
walletAddress: string
16+
trasactionCounter: number
17+
expiry: Date
18+
}
19+
signature: string
20+
value: bigint
21+
merchantWalletAddress: string
22+
}
23+
type PaymentRequest = Exclude<AppContext['request'], 'body'> & {
24+
body: PaymentBody
25+
}
26+
27+
export type PaymentContext = Exclude<AppContext, 'request'> & {
28+
request: PaymentRequest
29+
}
30+
31+
export interface PaymentRoutes {
32+
payment(ctx: PaymentContext): Promise<void>
33+
}
34+
35+
export function createPaymentRoutes(deps_: ServiceDependencies): PaymentRoutes {
36+
const log = deps_.logger.child({
37+
service: 'PaymentRoutes'
38+
})
39+
40+
const deps = {
41+
...deps_,
42+
logger: log
43+
}
44+
45+
return {
46+
payment: (ctx: PaymentContext) => payment(deps, ctx)
47+
}
48+
}
49+
50+
async function payment(
51+
deps: ServiceDependencies,
52+
ctx: PaymentContext
53+
): Promise<void> {
54+
const body = ctx.request.body
55+
try {
56+
const walletAddress = await deps.paymentService.getWalletAddress(
57+
body.card.walletAddress
58+
)
59+
const incomingAmount: AmountInput = {
60+
assetCode: walletAddress.assetCode,
61+
assetScale: walletAddress.assetScale,
62+
value: body.value
63+
}
64+
const incomingPaymentUrl = await deps.paymentService.createIncomingPayment(
65+
walletAddress.id,
66+
incomingAmount
67+
)
68+
const result = await deps.cardServiceClient.sendPayment({
69+
merchantWalletAddress: body.merchantWalletAddress,
70+
incomingPaymentUrl,
71+
date: new Date(),
72+
signature: body.signature,
73+
card: body.card
74+
})
75+
76+
ctx.body = result
77+
ctx.status = result === Result.APPROVED ? 200 : 401
78+
} catch (err) {
79+
const { body, status } = handlePaymentError(err)
80+
ctx.body = body
81+
ctx.status = status
82+
}
83+
}
84+
85+
function handlePaymentError(err: unknown) {
86+
if (err instanceof CardServiceClientError) {
87+
return { body: err.message, status: err.status }
88+
}
89+
if (err instanceof Error) {
90+
return { body: err.message, status: 400 }
91+
}
92+
return { body: err, status: 500 }
93+
}

0 commit comments

Comments
 (0)