Skip to content

Commit 673623a

Browse files
authored
feat(point-of-sale): pos service API for getting incoming payments (#3708)
* feat(point-of-sale): API for getting pos service's incoming payments * fix: tests * feat: filter out unnecessary fields in response * chore: bruno collection urls * feat: include senderWalletAddress in query * fix: bruno urls * fix: serialize query params properly for gql requests * fix: expose tenant id in payment gql requests * feat: use open payments-esque response format * feat: include metadata * feat: remove unnecessary fields * fix: regenerate gql * fix: imports * fix: proper object assign use and pr comments
1 parent a3c9587 commit 673623a

File tree

10 files changed

+446
-34
lines changed

10 files changed

+446
-34
lines changed

.eslintrc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ parserOptions:
1818
plugins:
1919
- '@typescript-eslint'
2020
rules:
21-
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }]
21+
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]
2222
overrides:
2323
- files: ["*.js"]
2424
rules:
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
meta {
2+
name: Get Incoming Payments
3+
type: http
4+
seq: 4
5+
}
6+
7+
get {
8+
url: http://localhost:4008/payments?receiverWalletAddress=https://happy-life-bank-backend/accounts/pfry
9+
body: none
10+
auth: inherit
11+
}
12+
13+
params:query {
14+
receiverWalletAddress: https://happy-life-bank-backend/accounts/pfry
15+
}

packages/backend/src/graphql/resolvers/incoming_payment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ export function paymentToGraphql(
234234
receivedAmount: payment.receivedAmount,
235235
metadata: payment.metadata,
236236
createdAt: new Date(+payment.createdAt).toISOString(),
237-
senderWalletAddress: payment.senderWalletAddress
237+
senderWalletAddress: payment.senderWalletAddress,
238+
tenantId: payment.tenantId
238239
}
239240
}

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import Koa, { DefaultState } from 'koa'
66
import Router from '@koa/router'
77
import bodyParser from 'koa-bodyparser'
88
import cors from '@koa/cors'
9-
10-
import { PaymentContext, PaymentRoutes } from './payments/routes'
9+
import {
10+
GetPaymentsContext,
11+
PaymentContext,
12+
PaymentRoutes
13+
} from './payments/routes'
1114
import {
1215
HandleWebhookContext,
1316
WebhookHandlerRoutes
@@ -65,6 +68,14 @@ export class App {
6568
const webhookHandlerRoutes = await this.container.use(
6669
'webhookHandlerRoutes'
6770
)
71+
72+
// GET /payments
73+
// Get payments made to a given device
74+
router.get<DefaultState, GetPaymentsContext>(
75+
'/payments',
76+
paymentRoutes.getPayments
77+
)
78+
6879
// POST /payment
6980
// Initiate a payment
7081
router.post<DefaultState, PaymentContext>('/payment', paymentRoutes.payment)

packages/point-of-sale/src/graphql/generated/graphql.ts

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { gql } from '@apollo/client/core'
2+
3+
export const GET_WALLET_ADDRESS_BY_URL = gql`
4+
query GetWalletAddress(
5+
$url: String!
6+
$first: Int
7+
$last: Int
8+
$before: String
9+
$after: String
10+
$sortOrder: SortOrder
11+
) {
12+
walletAddressByUrl(url: $url) {
13+
id
14+
incomingPayments(
15+
first: $first
16+
last: $last
17+
before: $before
18+
after: $after
19+
sortOrder: $sortOrder
20+
) {
21+
edges {
22+
node {
23+
id
24+
url
25+
walletAddressId
26+
senderWalletAddress
27+
state
28+
metadata
29+
incomingAmount {
30+
assetCode
31+
assetScale
32+
value
33+
}
34+
receivedAmount {
35+
assetCode
36+
assetScale
37+
value
38+
}
39+
expiresAt
40+
createdAt
41+
}
42+
cursor
43+
}
44+
pageInfo {
45+
endCursor
46+
hasNextPage
47+
hasPreviousPage
48+
startCursor
49+
}
50+
}
51+
}
52+
}
53+
`

packages/point-of-sale/src/payments/routes.test.ts

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { initIocContainer } from '..'
44
import { AppServices } from '../app'
55
import { Config, IAppConfig } from '../config/app'
66
import { TestContainer, createTestApp } from '../tests/app'
7-
import { PaymentContext, PaymentRoutes } from './routes'
7+
import { GetPaymentsContext, PaymentContext, PaymentRoutes } from './routes'
88
import { PaymentService } from './service'
99
import { CardServiceClient, Result } from '../card-service-client/client'
1010
import { createContext } from '../tests/context'
1111
import { CardServiceClientError } from '../card-service-client/errors'
1212
import { webhookWaitMap } from '../webhook-handlers/request-map'
1313
import { faker } from '@faker-js/faker'
1414
import { withConfigOverride } from '../tests/helpers'
15+
import { IncomingPaymentState } from '../graphql/generated/graphql'
1516

1617
describe('Payment Routes', () => {
1718
let deps: IocContract<AppServices>
@@ -21,6 +22,24 @@ describe('Payment Routes', () => {
2122
let cardServiceClient: CardServiceClient
2223
let config: IAppConfig
2324

25+
function mockPaymentService() {
26+
jest.spyOn(paymentService, 'getWalletAddress').mockResolvedValueOnce({
27+
id: 'id',
28+
assetCode: 'USD',
29+
assetScale: 1,
30+
authServer: 'authServer',
31+
resourceServer: 'resourceServer',
32+
cardService: 'cardService'
33+
})
34+
jest.spyOn(paymentService, 'createIncomingPayment').mockResolvedValueOnce({
35+
id: 'incoming-payment-url',
36+
url: faker.internet.url()
37+
})
38+
jest
39+
.spyOn(paymentService, 'getWalletAddressIdByUrl')
40+
.mockResolvedValueOnce(faker.internet.url())
41+
}
42+
2443
beforeAll(async () => {
2544
deps = initIocContainer(Config)
2645
appContainer = await createTestApp(deps)
@@ -135,26 +154,94 @@ describe('Payment Routes', () => {
135154
}
136155
)
137156
)
157+
})
138158

139-
function mockPaymentService() {
140-
jest.spyOn(paymentService, 'getWalletAddress').mockResolvedValueOnce({
141-
id: 'id',
142-
assetCode: 'USD',
143-
assetScale: 1,
144-
authServer: 'authServer',
145-
resourceServer: 'resourceServer',
146-
cardService: 'cardService'
147-
})
159+
describe('get incoming payments', (): void => {
160+
test('can get incoming payments for pos device', async (): Promise<void> => {
161+
const walletAddressId = v4()
162+
const mockServiceResponse = {
163+
edges: [
164+
{
165+
node: {
166+
__typename: 'IncomingPayment' as const,
167+
id: v4(),
168+
url: faker.internet.url(),
169+
walletAddressId,
170+
client: faker.internet.url(),
171+
state: IncomingPaymentState.Pending,
172+
incomingAmount: {
173+
__typename: 'Amount' as const,
174+
value: BigInt(500),
175+
assetCode: 'USD',
176+
assetScale: 2
177+
},
178+
receivedAmount: {
179+
__typename: 'Amount' as const,
180+
value: BigInt(500),
181+
assetCode: 'USD',
182+
assetScale: 2
183+
},
184+
expiresAt: new Date().toString(),
185+
createdAt: new Date().toString(),
186+
tenantId: v4()
187+
},
188+
cursor: walletAddressId
189+
}
190+
],
191+
pageInfo: {
192+
endCursor: walletAddressId,
193+
hasNextPage: false,
194+
hasPreviousPage: false,
195+
startCursor: walletAddressId
196+
}
197+
}
148198
jest
149-
.spyOn(paymentService, 'createIncomingPayment')
150-
.mockResolvedValueOnce({
151-
id: 'incoming-payment-url',
152-
url: faker.internet.url()
153-
})
199+
.spyOn(paymentService, 'getIncomingPayments')
200+
.mockResolvedValue(mockServiceResponse)
201+
const ctx = createGetPaymentsContext()
202+
203+
await paymentRoutes.getPayments(ctx)
204+
expect(ctx.status).toEqual(200)
205+
expect(ctx.body).toEqual({
206+
// Ensure that typename is sanitized
207+
result: mockServiceResponse.edges.map((edge) => {
208+
const {
209+
__typename: _nodeTypename,
210+
receivedAmount,
211+
incomingAmount,
212+
...restOfNode
213+
} = edge.node
214+
const { __typename: _receivedTypename, ...restOfReceived } =
215+
receivedAmount
216+
const { __typename: _incomingTypename, ...restOfIncoming } =
217+
incomingAmount
218+
return {
219+
...restOfNode,
220+
incomingAmount: restOfIncoming,
221+
receivedAmount: restOfReceived
222+
}
223+
}),
224+
pagination: mockServiceResponse.pageInfo
225+
})
226+
})
227+
228+
test('returns empty page if no incoming payments', async (): Promise<void> => {
154229
jest
155-
.spyOn(paymentService, 'getWalletAddressIdByUrl')
156-
.mockResolvedValueOnce(faker.internet.url())
157-
}
230+
.spyOn(paymentService, 'getIncomingPayments')
231+
.mockResolvedValue(undefined)
232+
233+
const ctx = createGetPaymentsContext()
234+
235+
await paymentRoutes.getPayments(ctx)
236+
expect(ctx.status).toEqual(200)
237+
expect(ctx.body).toMatchObject({
238+
result: [],
239+
pagination: {
240+
hasNextPage: false,
241+
hasPreviousPage: false
242+
}
243+
})
244+
})
158245
})
159246
})
160247

@@ -173,3 +260,14 @@ function createPaymentContext() {
173260
}
174261
})
175262
}
263+
264+
function createGetPaymentsContext() {
265+
return createContext<GetPaymentsContext>({
266+
headers: { Accept: 'application/json' },
267+
method: 'GET',
268+
url: `/payments`,
269+
query: {
270+
receiverWalletAddress: faker.internet.url()
271+
}
272+
})
273+
}

0 commit comments

Comments
 (0)