Skip to content

Commit 4bd9565

Browse files
dragosp1011njlie
authored andcommitted
feat(card-service): add endpoints to initiate payment and receive payment result (#3547)
* feat(card-service): add endpoints to initiate payment and recieve payment result * fix: improve open api spec validation * fix: rename paymetn timeout var and comment terminal cert prop * fix: add PaymentRouteError * fix: rename env var and remove deleted prop
1 parent 764ca8b commit 4bd9565

File tree

18 files changed

+733
-7
lines changed

18 files changed

+733
-7
lines changed

packages/card-service/jest.env.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
// Jest environment configuration for card-service
22
process.env.NODE_ENV = 'test'
33
process.env.LOG_LEVEL = process.env.LOG_LEVEL || 'silent'
4+
process.env.CARD_PAYMENT_TIMEOUT_MS = '50'

packages/card-service/package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@
1616
},
1717
"dependencies": {
1818
"@adonisjs/fold": "^8.2.0",
19+
"@interledger/openapi": "2.0.2",
1920
"@koa/cors": "^5.0.0",
2021
"ioredis": "^5.3.2",
2122
"@koa/router": "^12.0.2",
22-
"koa-bodyparser": "^4.4.1",
23-
"koa": "^2.15.4",
2423
"knex": "^3.1.0",
24+
"koa": "^2.15.4",
25+
"koa-bodyparser": "^4.4.1",
2526
"objection": "^3.1.5",
26-
"pg": "^8.11.3",
2727
"objection-db-errors": "^1.1.2",
28+
"pg": "^8.11.3",
2829
"pino": "^8.19.0",
2930
"uuid": "^9.0.1"
3031
},
@@ -35,8 +36,9 @@
3536
"@types/koa__router": "^12.0.4",
3637
"@types/uuid": "^9.0.8",
3738
"jest-environment-node": "^29.7.0",
39+
"nock": "14.0.0-beta.19",
40+
"node-mocks-http": "^1.16.2",
3841
"testcontainers": "^10.16.0",
39-
"ts-node-dev": "^2.0.0",
40-
"nock": "14.0.0-beta.19"
42+
"ts-node-dev": "^2.0.0"
4143
}
4244
}

packages/card-service/src/app.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import Router from '@koa/router'
66
import bodyParser from 'koa-bodyparser'
77
import { Server } from 'http'
88
import cors from '@koa/cors'
9+
import { createValidatorMiddleware, HttpMethod } from '@interledger/openapi'
10+
import { PaymentContext } from './payment/types'
11+
import { PaymentEventContext } from './payment/types'
912

1013
export interface AppServices {
1114
logger: Promise<Logger>
@@ -45,7 +48,26 @@ export class App {
4548
ctx.status = 200
4649
})
4750

48-
// Add routes here...
51+
const openApi = await this.container.use('openApi')
52+
53+
const paymentRoutes = await this.container.use('paymentRoutes')
54+
router.post<DefaultState, PaymentContext>(
55+
'/payment',
56+
createValidatorMiddleware<PaymentContext>(openApi.cardServerSpec, {
57+
path: '/payment',
58+
method: HttpMethod.POST
59+
}),
60+
paymentRoutes.create
61+
)
62+
63+
router.post<DefaultState, PaymentEventContext>(
64+
'/payment-event',
65+
createValidatorMiddleware<PaymentEventContext>(openApi.cardServerSpec, {
66+
path: '/payment-event',
67+
method: HttpMethod.POST
68+
}),
69+
paymentRoutes.handlePaymentEvent
70+
)
4971

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

packages/card-service/src/config/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const Config = {
3232
env: envString('NODE_ENV', 'development'),
3333
cardServicePort: envInt('CARD_SERVICE_PORT', 3007),
3434
cardServiceUrl: envString('CARD_SERVICE_URL', 'http://localhost:3007'),
35+
cardPaymentTimeoutMS: envInt('CARD_PAYMENT_TIMEOUT_MS', 30000),
3536
redisUrl: envString('REDIS_URL', 'redis://127.0.0.1:6379'),
3637
redisTls: parseRedisTlsConfig(
3738
process.env.REDIS_TLS_CA_FILE_PATH,

packages/card-service/src/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { knex } from 'knex'
66
import { Model } from 'objection'
77
import Redis from 'ioredis'
88
import { createPOSStore, PosStoreService } from './pos-store/service'
9+
import { createPaymentService } from './payment/service'
10+
import { createPaymentRoutes } from './payment/routes'
11+
import { createOpenAPI } from '@interledger/openapi'
12+
import path from 'path'
913

1014
export function initIocContainer(
1115
config: typeof Config
@@ -76,6 +80,36 @@ export function initIocContainer(
7680
return createPOSStore({ redis, logger })
7781
})
7882

83+
container.singleton('openApi', async () => {
84+
const cardServerSpec = await createOpenAPI(
85+
path.resolve(__dirname, './openapi/specs/card-server.yaml')
86+
)
87+
88+
return {
89+
cardServerSpec
90+
}
91+
})
92+
93+
container.singleton(
94+
'paymentService',
95+
async (deps: IocContract<AppServices>) => {
96+
return createPaymentService({
97+
logger: await deps.use('logger'),
98+
config: await deps.use('config')
99+
})
100+
}
101+
)
102+
103+
container.singleton(
104+
'paymentRoutes',
105+
async (deps: IocContract<AppServices>) => {
106+
return createPaymentRoutes({
107+
logger: await deps.use('logger'),
108+
paymentService: await deps.use('paymentService')
109+
})
110+
}
111+
)
112+
79113
return container
80114
}
81115

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Card Service API
4+
version: 1.0.0
5+
paths:
6+
/payment:
7+
post:
8+
summary: Accept a payment request from POS
9+
operationId: createPayment
10+
requestBody:
11+
required: true
12+
content:
13+
application/json:
14+
schema:
15+
type: object
16+
required:
17+
- requestId
18+
- card
19+
- merchantWalletAddress
20+
- incomingPaymentUrl
21+
- date
22+
- signature
23+
# - terminalCert
24+
- terminalId
25+
properties:
26+
requestId:
27+
type: string
28+
format: uuid
29+
card:
30+
type: object
31+
required:
32+
- walletAddress
33+
- transactionCounter
34+
- expiry
35+
properties:
36+
walletAddress:
37+
type: string
38+
format: uri
39+
transactionCounter:
40+
type: integer
41+
expiry:
42+
type: string
43+
pattern: '^(0[1-9]|1[0-2])\/\d{2,4}$'
44+
example: '12/25'
45+
description: 'Card expiry in MM/YY or MM/YYYY format'
46+
merchantWalletAddress:
47+
type: string
48+
format: uri
49+
incomingPaymentUrl:
50+
type: string
51+
format: uri
52+
date:
53+
type: string
54+
format: date-time
55+
signature:
56+
type: string
57+
# terminalCert:
58+
# type: string
59+
terminalId:
60+
type: string
61+
format: uuid
62+
responses:
63+
'201':
64+
description: Payment request accepted
65+
content:
66+
application/json:
67+
schema:
68+
type: object
69+
properties:
70+
requestId:
71+
type: string
72+
result:
73+
type: string
74+
enum:
75+
- approved
76+
'400':
77+
description: Invalid request
78+
'401':
79+
description: Card expired or invalid signature
80+
'500':
81+
description: Internal server error
82+
/payment-event:
83+
post:
84+
summary: Handle payment event result from backend
85+
operationId: handlePaymentEvent
86+
requestBody:
87+
required: true
88+
content:
89+
application/json:
90+
schema:
91+
type: object
92+
required:
93+
- requestId
94+
- outgoingPaymentId
95+
- result
96+
properties:
97+
requestId:
98+
type: string
99+
format: uuid
100+
outgoingPaymentId:
101+
type: string
102+
format: uuid
103+
result:
104+
type: object
105+
required:
106+
- code
107+
properties:
108+
code:
109+
type: string
110+
enum:
111+
- completed
112+
- card_expired
113+
- invalid_signature
114+
responses:
115+
'202':
116+
description: Payment event accepted and forwarded
117+
'400':
118+
description: Malformed request body
119+
'404':
120+
description: Request not found
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export class PaymentRouteError extends Error {
2+
public status: number
3+
public details?: Record<string, unknown>
4+
5+
constructor(
6+
status: number,
7+
message: string,
8+
details?: Record<string, unknown>
9+
) {
10+
super(message)
11+
this.name = 'PaymentRouteError'
12+
this.status = status
13+
this.details = details
14+
}
15+
}
16+
17+
export class PaymentTimeoutError extends PaymentRouteError {
18+
constructor(message = 'Timeout waiting for payment-event') {
19+
super(504, message)
20+
}
21+
}

0 commit comments

Comments
 (0)