Skip to content

Commit 61c4920

Browse files
committed
feat(point-of-sale): allow refunding incoming payments
1 parent f6024e3 commit 61c4920

File tree

9 files changed

+476
-3
lines changed

9 files changed

+476
-3
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import cors from '@koa/cors'
99
import {
1010
GetPaymentsContext,
1111
PaymentContext,
12-
PaymentRoutes
12+
PaymentRoutes,
13+
RefundContext
1314
} from './payments/routes'
1415
import {
1516
HandleWebhookContext,
@@ -93,6 +94,13 @@ export class App {
9394
// Initiate a payment
9495
router.post<DefaultState, PaymentContext>('/payment', paymentRoutes.payment)
9596

97+
// POST /refund
98+
// Refund a payment
99+
router.post<DefaultState, RefundContext>(
100+
'/refund',
101+
paymentRoutes.refundPayment
102+
)
103+
96104
// POST /webhook-events
97105
// Handle webhook
98106
// Currently only handles incoming_payment.completed webhooks

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

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { gql } from '@apollo/client'
2+
3+
export const CREATE_OUTGOING_PAYMENT_FROM_INCOMING_PAYMENT = gql`
4+
mutation CreateOutgoingPaymentFromIncomingPayment(
5+
$input: CreateOutgoingPaymentFromIncomingPaymentInput!
6+
) {
7+
createOutgoingPaymentFromIncomingPayment(input: $input) {
8+
payment {
9+
id
10+
walletAddressId
11+
createdAt
12+
}
13+
}
14+
}
15+
`
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { gql } from '@apollo/client'
2+
3+
export const CREATE_RECEIVER = gql`
4+
mutation CreateReceiver($input: CreateReceiverInput!) {
5+
createReceiver(input: $input) {
6+
receiver {
7+
id
8+
metadata
9+
incomingAmount {
10+
value
11+
assetCode
12+
assetScale
13+
}
14+
}
15+
}
16+
}
17+
`
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { gql } from '@apollo/client'
2+
3+
export const GET_INCOMING_PAYMENT = gql`
4+
query GetIncomingPaymentSenderAndAmount($id: String!) {
5+
incomingPayment(id: $id) {
6+
id
7+
senderWalletAddress
8+
incomingAmount {
9+
value
10+
assetCode
11+
assetScale
12+
}
13+
}
14+
}
15+
`

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,32 @@ describe('Payment Routes', () => {
376376
})
377377
})
378378
})
379+
380+
describe('refund payment', () => {
381+
test('returns 200 when refunding incoming payment', async (): Promise<void> => {
382+
const ctx = createRefundContext()
383+
jest
384+
.spyOn(paymentService, 'refundIncomingPayment')
385+
.mockResolvedValueOnce({
386+
id: v4()
387+
})
388+
389+
await paymentRoutes.refundPayment(ctx)
390+
expect(ctx.status).toEqual(200)
391+
})
392+
393+
test('returns 400 if incoming payment refund fails', async (): Promise<void> => {
394+
const ctx = createRefundContext()
395+
const refundError = new Error('Failed to refund incoming payment')
396+
jest
397+
.spyOn(paymentService, 'refundIncomingPayment')
398+
.mockRejectedValueOnce(refundError)
399+
400+
await paymentRoutes.refundPayment(ctx)
401+
expect(ctx.status).toEqual(400)
402+
expect(ctx.body).toEqual(refundError.message)
403+
})
404+
})
379405
})
380406

381407
function createPaymentContext(bodyOverrides?: Record<string, unknown>) {
@@ -416,3 +442,15 @@ function createGetPaymentsContext(
416442
query
417443
})
418444
}
445+
446+
function createRefundContext() {
447+
return createContext<RefundContext>({
448+
headers: { Accept: 'application/json' },
449+
method: 'POST',
450+
url: '/refund',
451+
body: {
452+
incomingPaymentId: v4(),
453+
posWalletAddress: faker.internet.url()
454+
}
455+
})
456+
}

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,23 @@ export type GetPaymentsContext = Exclude<AppContext, 'request'> & {
6868
request: GetPaymentsRequest
6969
}
7070

71+
export interface RefundRequestBody {
72+
incomingPaymentId: string
73+
posWalletAddress: string
74+
}
75+
76+
export type RefundRequest = Exclude<AppContext['request'], 'body'> & {
77+
body: RefundRequestBody
78+
}
79+
80+
export type RefundContext = Exclude<AppContext, ['request']> & {
81+
request: RefundRequest
82+
}
83+
7184
export interface PaymentRoutes {
7285
getPayments(ctx: GetPaymentsContext): Promise<void>
7386
payment(ctx: PaymentContext): Promise<void>
87+
refundPayment(ctx: RefundContext): Promise<void>
7488
}
7589

7690
export function createPaymentRoutes(deps_: ServiceDependencies): PaymentRoutes {
@@ -85,7 +99,8 @@ export function createPaymentRoutes(deps_: ServiceDependencies): PaymentRoutes {
8599

86100
return {
87101
payment: (ctx: PaymentContext) => payment(deps, ctx),
88-
getPayments: (ctx: GetPaymentsContext) => getPayments(deps, ctx)
102+
getPayments: (ctx: GetPaymentsContext) => getPayments(deps, ctx),
103+
refundPayment: (ctx: RefundContext) => refundPayment(deps, ctx)
89104
}
90105
}
91106

@@ -184,6 +199,24 @@ async function payment(
184199
}
185200
}
186201

202+
async function refundPayment(
203+
deps: ServiceDependencies,
204+
ctx: RefundContext
205+
): Promise<void> {
206+
const { incomingPaymentId, posWalletAddress } = ctx.request.body
207+
try {
208+
await deps.paymentService.refundIncomingPayment(
209+
incomingPaymentId,
210+
posWalletAddress
211+
)
212+
return
213+
} catch (err) {
214+
ctx.status = 400
215+
ctx.body = (err as Error).message
216+
return
217+
}
218+
}
219+
187220
async function waitForIncomingPaymentEvent(
188221
config: IAppConfig,
189222
deferred: Deferred<WebhookBody>

0 commit comments

Comments
 (0)