Skip to content

Commit da483ea

Browse files
authored
feat: reason input when canceling incoming payments (#3931)
* feat: add reason input to cancelIncomingPayment * add test for input reason on canceling payment * update bruno cancelIncomingPayment to receive reason * add migration and update naming to cancellationReason
1 parent fb2e7d9 commit da483ea

15 files changed

Lines changed: 157 additions & 7 deletions

File tree

bruno/collections/Rafiki/Rafiki Admin APIs/Cancel Incoming Payment.bru

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ body:graphql {
1515
cancelIncomingPayment(input: $input) {
1616
payment {
1717
id
18+
cancellationReason
1819
}
1920
}
2021
}
@@ -24,7 +25,8 @@ body:graphql {
2425
body:graphql:vars {
2526
{
2627
"input": {
27-
"id": "{{incomingPaymentId}}"
28+
"id": "{{incomingPaymentId}}",
29+
"cancellationReason": "{{cancellationReason}}"
2830
}
2931
}
3032
}

localenv/mock-account-servicing-entity/generated/graphql.ts

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @param { import("knex").Knex } knex
3+
* @returns { Promise<void> }
4+
*/
5+
exports.up = function (knex) {
6+
return knex.schema.alterTable('incomingPayments', (table) => {
7+
table.string('cancellationReason').nullable()
8+
})
9+
}
10+
11+
/**
12+
* @param { import("knex").Knex } knex
13+
* @returns { Promise<void> }
14+
*/
15+
exports.down = function (knex) {
16+
return knex.schema.alterTable('incomingPayments', (table) => {
17+
table.dropColumn('cancellationReason')
18+
})
19+
}

packages/backend/src/graphql/generated/graphql.schema.json

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/graphql/generated/graphql.ts

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ export const cancelIncomingPayment: MutationResolvers<TenantedApolloContext>['ca
255255

256256
const incomingPaymentOrError = await incomingPaymentService.cancel(
257257
args.input.id,
258-
ctx.tenant.id
258+
ctx.tenant.id,
259+
args.input.cancellationReason
259260
)
260261

261262
if (isIncomingPaymentError(incomingPaymentOrError)) {
@@ -395,6 +396,10 @@ export function paymentToGraphql(
395396
receivedAmount: payment.receivedAmount,
396397
metadata: payment.metadata,
397398
createdAt: new Date(+payment.createdAt).toISOString(),
398-
senderWalletAddress: payment.senderWalletAddress
399+
senderWalletAddress: payment.senderWalletAddress,
400+
cancellationReason: payment.cancellationReason,
401+
cancelledAt: payment.cancelledAt
402+
? new Date(payment.cancelledAt).toISOString()
403+
: undefined
399404
}
400405
}

packages/backend/src/graphql/schema.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,8 @@ input ApproveIncomingPaymentInput {
422422
input CancelIncomingPaymentInput {
423423
"Unique identifier of the incoming payment to be canceled. Note: incoming payment must be PENDING."
424424
id: ID!
425+
"Reason for canceling the incoming payment."
426+
cancellationReason: String
425427
}
426428

427429
input CreateAssetInput {
@@ -1008,6 +1010,10 @@ type IncomingPayment implements BasePayment & Model {
10081010
senderWalletAddress: String
10091011
"The tenant associated with the incoming payment. If not provided, it will be obtained from the signature."
10101012
tenant: Tenant
1013+
"Reason why the incoming payment was canceled."
1014+
cancellationReason: String
1015+
"The date and time that the incoming payment was canceled."
1016+
cancelledAt: String
10111017
}
10121018

10131019
type Receiver {

packages/backend/src/open_payments/payment/incoming/model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export class IncomingPayment
113113
private receivedAmountValue?: bigint
114114
public readonly tenantId!: string
115115
public readonly senderWalletAddress?: string | null
116+
public cancellationReason?: string | null
116117

117118
public get completed(): boolean {
118119
return this.state === IncomingPaymentState.Completed

packages/backend/src/open_payments/payment/incoming/service.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,27 @@ describe('Incoming Payment Service', (): void => {
312312
expect(canceledIncomingPayment.cancelledAt).toBeDefined()
313313
expect(!canceledIncomingPayment.approvedAt).toBeTruthy()
314314
})
315+
316+
it('should cancel incoming payment with reason', async (): Promise<void> => {
317+
const reason = 'cancelled by operator'
318+
const incomingPayment = await createIncomingPaymentHelper()
319+
assert.ok(!isIncomingPaymentError(incomingPayment))
320+
321+
await IncomingPayment.query(knex)
322+
.findOne({ id: incomingPayment.id })
323+
.patch({ state: IncomingPaymentState.Pending })
324+
325+
const canceledIncomingPayment = await incomingPaymentService.cancel(
326+
incomingPayment.id,
327+
Config.operatorTenantId,
328+
reason
329+
)
330+
assert.ok(!isIncomingPaymentError(canceledIncomingPayment))
331+
expect(canceledIncomingPayment.id).toBe(incomingPayment.id)
332+
expect(canceledIncomingPayment.cancelledAt).toBeDefined()
333+
expect(!canceledIncomingPayment.approvedAt).toBeTruthy()
334+
expect(canceledIncomingPayment.cancellationReason).toBe(reason)
335+
})
315336
})
316337
})
317338

packages/backend/src/open_payments/payment/incoming/service.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ export interface IncomingPaymentService
6969
): Promise<IncomingPayment | IncomingPaymentError>
7070
cancel(
7171
id: string,
72-
tenantId: string
72+
tenantId: string,
73+
cancellationReason?: string
7374
): Promise<IncomingPayment | IncomingPaymentError>
7475
complete(
7576
id: string,
@@ -115,7 +116,8 @@ export async function createIncomingPaymentService(
115116
get: (options) => getIncomingPayment(deps, options),
116117
create: (options, trx) => createIncomingPayment(deps, options, trx),
117118
approve: (id, tenantId) => approveIncomingPayment(deps, id, tenantId),
118-
cancel: (id, tenantId) => cancelIncomingPayment(deps, id, tenantId),
119+
cancel: (id, tenantId, cancellationReason) =>
120+
cancelIncomingPayment(deps, id, tenantId, cancellationReason),
119121
complete: (id, tenantId) => completeIncomingPayment(deps, id, tenantId),
120122
getWalletAddressPage: (options) => getWalletAddressPage(deps, options),
121123
processNext: () => processNextIncomingPayment(deps),
@@ -479,7 +481,8 @@ async function approveIncomingPayment(
479481
async function cancelIncomingPayment(
480482
deps: ServiceDependencies,
481483
id: string,
482-
tenantId: string
484+
tenantId: string,
485+
cancellationReason?: string
483486
): Promise<IncomingPayment | IncomingPaymentError> {
484487
return deps.knex.transaction(async (trx) => {
485488
const payment = await IncomingPayment.query(trx)
@@ -508,7 +511,8 @@ async function cancelIncomingPayment(
508511

509512
if (!payment.cancelledAt) {
510513
await payment.$query(trx).patch({
511-
cancelledAt: new Date(Date.now())
514+
cancelledAt: new Date(Date.now()),
515+
cancellationReason: cancellationReason
512516
})
513517
}
514518

0 commit comments

Comments
 (0)