From cff6bf3974b50f63ad0b99dfd3458da1cd1ec9d7 Mon Sep 17 00:00:00 2001 From: golobitch Date: Wed, 1 May 2024 18:18:34 +0200 Subject: [PATCH 01/22] feat(localenv): cancel outgoing payment on created webhook --- localenv/cloud-nine-wallet/seed.yml | 6 +++++ .../app/lib/webhooks.server.ts | 22 +++++++++++++++++++ .../generated/graphql.ts | 19 ++++++++++++++++ .../seed.example.yml | 6 +++++ 4 files changed, 53 insertions(+) diff --git a/localenv/cloud-nine-wallet/seed.yml b/localenv/cloud-nine-wallet/seed.yml index 35cc9c2cef..eff7d687df 100644 --- a/localenv/cloud-nine-wallet/seed.yml +++ b/localenv/cloud-nine-wallet/seed.yml @@ -40,6 +40,12 @@ accounts: path: accounts/wbdc brunoEnvVar: wbdcWalletAddress assetCode: USD + - name: "Broke Account" + id: 5a95366f-8cb4-4925-88d9-ae57dcb444bb + initialBalance: 50 + path: accounts/broke + brunoEnvVar: brokeWalletAddress + assetCode: USD rates: EUR: MXN: 18.78 diff --git a/localenv/mock-account-servicing-entity/app/lib/webhooks.server.ts b/localenv/mock-account-servicing-entity/app/lib/webhooks.server.ts index b8cb327a65..b170300f9c 100644 --- a/localenv/mock-account-servicing-entity/app/lib/webhooks.server.ts +++ b/localenv/mock-account-servicing-entity/app/lib/webhooks.server.ts @@ -69,6 +69,28 @@ export async function handleOutgoingPaymentCreated(wh: Webhook) { } const amt = parseAmount(payment['debitAmount'] as AmountJSON) + const accBalance = BigInt(acc.creditsPosted) - BigInt(acc.debitsPosted) + + if (accBalance < amt.value) { + await apolloClient.mutate({ + mutation: gql` + mutation CancelOutgoingPayment($input: CancelOutgoingPaymentInput!) { + cancelOutgoingPayment(input: $input) { + code + success + message + } + } + `, + variables: { + input: { + id: payment.id, + reason: 'Account does not have enough balance.' + } + } + }) + return + } await mockAccounts.pendingDebit(acc.id, amt.value) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index f97c7554bd..30dac61e55 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -101,6 +101,13 @@ export type BasePayment = { walletAddressId: Scalars['ID']['output']; }; +export type CancelOutgoingPaymentInput = { + /** Outgoing payment id */ + id: Scalars['ID']['input']; + /** Reason why this Outgoing Payment has been cancelled */ + reason: Scalars['String']['input']; +}; + export type CreateAssetInput = { /** [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217), e.g. `USD` */ code: Scalars['String']['input']; @@ -551,6 +558,8 @@ export type Model = { export type Mutation = { __typename?: 'Mutation'; + /** Cancel Outgoing Payment */ + cancelOutgoingPayment: OutgoingPaymentResponse; /** Create an asset */ createAsset: AssetMutationResponse; /** Withdraw asset liquidity */ @@ -618,6 +627,11 @@ export type Mutation = { }; +export type MutationCancelOutgoingPaymentArgs = { + input: CancelOutgoingPaymentInput; +}; + + export type MutationCreateAssetArgs = { input: CreateAssetInput; }; @@ -819,6 +833,8 @@ export type OutgoingPaymentResponse = { }; export enum OutgoingPaymentState { + /** Payment cancelled */ + Cancelled = 'CANCELLED', /** Successful completion */ Completed = 'COMPLETED', /** Payment failed */ @@ -1488,6 +1504,7 @@ export type ResolversTypes = { AssetsConnection: ResolverTypeWrapper>; BasePayment: ResolverTypeWrapper['BasePayment']>; Boolean: ResolverTypeWrapper>; + CancelOutgoingPaymentInput: ResolverTypeWrapper>; CreateAssetInput: ResolverTypeWrapper>; CreateAssetLiquidityWithdrawalInput: ResolverTypeWrapper>; CreateIncomingPaymentInput: ResolverTypeWrapper>; @@ -1607,6 +1624,7 @@ export type ResolversParentTypes = { AssetsConnection: Partial; BasePayment: ResolversInterfaceTypes['BasePayment']; Boolean: Partial; + CancelOutgoingPaymentInput: Partial; CreateAssetInput: Partial; CreateAssetLiquidityWithdrawalInput: Partial; CreateIncomingPaymentInput: Partial; @@ -1899,6 +1917,7 @@ export type ModelResolvers = { + cancelOutgoingPayment?: Resolver>; createAsset?: Resolver>; createAssetLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; createIncomingPayment?: Resolver>; diff --git a/localenv/mock-account-servicing-entity/seed.example.yml b/localenv/mock-account-servicing-entity/seed.example.yml index 04a18cbbe4..f2c56fc943 100644 --- a/localenv/mock-account-servicing-entity/seed.example.yml +++ b/localenv/mock-account-servicing-entity/seed.example.yml @@ -32,6 +32,12 @@ accounts: assetCode: USD scale: 2 path: accounts/wbdc + - name: "Broke account" + id: 5a95366f-8cb4-4925-88d9-ae57dcb444bb + initialBalance: 0 + assetCode: USD + scale: 2 + path: accounts/broke fees: - fixed: 100 basisPoints: 200 From 94e585b8fd671eb466456c53eaa26fae0af40bc3 Mon Sep 17 00:00:00 2001 From: golobitch Date: Wed, 1 May 2024 19:10:48 +0200 Subject: [PATCH 02/22] feat(graphql): cancel outgoing payment api --- .../src/graphql/generated/graphql.schema.json | 82 +++++++++++++++++++ .../backend/src/graphql/generated/graphql.ts | 19 +++++ .../src/graphql/resolvers/outgoing_payment.ts | 25 ++++++ packages/backend/src/graphql/schema.graphql | 14 ++++ packages/frontend/app/generated/graphql.ts | 19 +++++ .../src/generated/graphql.ts | 19 +++++ test/integration/lib/generated/graphql.ts | 19 +++++ 7 files changed, 197 insertions(+) diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 8f8d1ad33a..e74bab2b6d 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -637,6 +637,49 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CancelOutgoingPaymentInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Outgoing payment id", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reason", + "description": "Reason why this Outgoing Payment has been cancelled", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CreateAssetInput", @@ -3730,6 +3773,39 @@ "name": "Mutation", "description": null, "fields": [ + { + "name": "cancelOutgoingPayment", + "description": "Cancel Outgoing Payment", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CancelOutgoingPaymentInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "OutgoingPaymentResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "createAsset", "description": "Create an asset", @@ -5162,6 +5238,12 @@ "inputFields": null, "interfaces": null, "enumValues": [ + { + "name": "CANCELLED", + "description": "Payment cancelled", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "COMPLETED", "description": "Successful completion", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index f97c7554bd..30dac61e55 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -101,6 +101,13 @@ export type BasePayment = { walletAddressId: Scalars['ID']['output']; }; +export type CancelOutgoingPaymentInput = { + /** Outgoing payment id */ + id: Scalars['ID']['input']; + /** Reason why this Outgoing Payment has been cancelled */ + reason: Scalars['String']['input']; +}; + export type CreateAssetInput = { /** [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217), e.g. `USD` */ code: Scalars['String']['input']; @@ -551,6 +558,8 @@ export type Model = { export type Mutation = { __typename?: 'Mutation'; + /** Cancel Outgoing Payment */ + cancelOutgoingPayment: OutgoingPaymentResponse; /** Create an asset */ createAsset: AssetMutationResponse; /** Withdraw asset liquidity */ @@ -618,6 +627,11 @@ export type Mutation = { }; +export type MutationCancelOutgoingPaymentArgs = { + input: CancelOutgoingPaymentInput; +}; + + export type MutationCreateAssetArgs = { input: CreateAssetInput; }; @@ -819,6 +833,8 @@ export type OutgoingPaymentResponse = { }; export enum OutgoingPaymentState { + /** Payment cancelled */ + Cancelled = 'CANCELLED', /** Successful completion */ Completed = 'COMPLETED', /** Payment failed */ @@ -1488,6 +1504,7 @@ export type ResolversTypes = { AssetsConnection: ResolverTypeWrapper>; BasePayment: ResolverTypeWrapper['BasePayment']>; Boolean: ResolverTypeWrapper>; + CancelOutgoingPaymentInput: ResolverTypeWrapper>; CreateAssetInput: ResolverTypeWrapper>; CreateAssetLiquidityWithdrawalInput: ResolverTypeWrapper>; CreateIncomingPaymentInput: ResolverTypeWrapper>; @@ -1607,6 +1624,7 @@ export type ResolversParentTypes = { AssetsConnection: Partial; BasePayment: ResolversInterfaceTypes['BasePayment']; Boolean: Partial; + CancelOutgoingPaymentInput: Partial; CreateAssetInput: Partial; CreateAssetLiquidityWithdrawalInput: Partial; CreateIncomingPaymentInput: Partial; @@ -1899,6 +1917,7 @@ export type ModelResolvers = { + cancelOutgoingPayment?: Resolver>; createAsset?: Resolver>; createAssetLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; createIncomingPayment?: Resolver>; diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.ts index 0b4d8b8dca..967e9037eb 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.ts @@ -29,6 +29,31 @@ export const getOutgoingPayment: QueryResolvers['outgoingPayment' return paymentToGraphql(payment) } +export const cancelOutgoingPayment: MutationResolvers['cancelOutgoingPayment'] = + async (parent, args, ctx): Promise => { + const outgoingPaymentService = await ctx.container.use('outgoingPaymentService') + + return outgoingPaymentService + .cancel(args.input) + .then((paymentOrError: OutgoingPayment | OutgoingPaymentError) => + isOutgoingPaymentError(paymentOrError) + ? { + code: errorToCode[paymentOrError].toString(), + success: false, + message: errorToMessage[paymentOrError] + } + : { + code: '200', + success: true, + payment: paymentToGraphql(paymentOrError) + } + ).catch(() => ({ + code: '500', + success: false, + message: 'Error trying to cancel outgoing payment' + })) + } + export const createOutgoingPayment: MutationResolvers['createOutgoingPayment'] = async ( parent, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 207847e72c..da0a02f60f 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -182,6 +182,11 @@ type Mutation { input: CreateOutgoingPaymentInput! ): OutgoingPaymentResponse! + "Cancel Outgoing Payment" + cancelOutgoingPayment( + input: CancelOutgoingPaymentInput! + ): OutgoingPaymentResponse! + "Create an Open Payments Outgoing Payment from an incoming payment" createOutgoingPaymentFromIncomingPayment( input: CreateOutgoingPaymentFromIncomingPaymentInput! @@ -807,6 +812,8 @@ enum OutgoingPaymentState { COMPLETED "Payment failed" FAILED + "Payment cancelled" + CANCELLED } type PaymentConnection { @@ -922,6 +929,13 @@ input CreateOutgoingPaymentInput { idempotencyKey: String } +input CancelOutgoingPaymentInput { + "Outgoing payment id" + id: ID! + "Reason why this Outgoing Payment has been cancelled" + reason: String! +} + input CreateOutgoingPaymentFromIncomingPaymentInput { "Id of the wallet address under which the outgoing payment will be created" walletAddressId: String! diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index cdc9e8fa2a..0aa88873e6 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -101,6 +101,13 @@ export type BasePayment = { walletAddressId: Scalars['ID']['output']; }; +export type CancelOutgoingPaymentInput = { + /** Outgoing payment id */ + id: Scalars['ID']['input']; + /** Reason why this Outgoing Payment has been cancelled */ + reason: Scalars['String']['input']; +}; + export type CreateAssetInput = { /** [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217), e.g. `USD` */ code: Scalars['String']['input']; @@ -551,6 +558,8 @@ export type Model = { export type Mutation = { __typename?: 'Mutation'; + /** Cancel Outgoing Payment */ + cancelOutgoingPayment: OutgoingPaymentResponse; /** Create an asset */ createAsset: AssetMutationResponse; /** Withdraw asset liquidity */ @@ -618,6 +627,11 @@ export type Mutation = { }; +export type MutationCancelOutgoingPaymentArgs = { + input: CancelOutgoingPaymentInput; +}; + + export type MutationCreateAssetArgs = { input: CreateAssetInput; }; @@ -819,6 +833,8 @@ export type OutgoingPaymentResponse = { }; export enum OutgoingPaymentState { + /** Payment cancelled */ + Cancelled = 'CANCELLED', /** Successful completion */ Completed = 'COMPLETED', /** Payment failed */ @@ -1488,6 +1504,7 @@ export type ResolversTypes = { AssetsConnection: ResolverTypeWrapper>; BasePayment: ResolverTypeWrapper['BasePayment']>; Boolean: ResolverTypeWrapper>; + CancelOutgoingPaymentInput: ResolverTypeWrapper>; CreateAssetInput: ResolverTypeWrapper>; CreateAssetLiquidityWithdrawalInput: ResolverTypeWrapper>; CreateIncomingPaymentInput: ResolverTypeWrapper>; @@ -1607,6 +1624,7 @@ export type ResolversParentTypes = { AssetsConnection: Partial; BasePayment: ResolversInterfaceTypes['BasePayment']; Boolean: Partial; + CancelOutgoingPaymentInput: Partial; CreateAssetInput: Partial; CreateAssetLiquidityWithdrawalInput: Partial; CreateIncomingPaymentInput: Partial; @@ -1899,6 +1917,7 @@ export type ModelResolvers = { + cancelOutgoingPayment?: Resolver>; createAsset?: Resolver>; createAssetLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; createIncomingPayment?: Resolver>; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index f97c7554bd..30dac61e55 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -101,6 +101,13 @@ export type BasePayment = { walletAddressId: Scalars['ID']['output']; }; +export type CancelOutgoingPaymentInput = { + /** Outgoing payment id */ + id: Scalars['ID']['input']; + /** Reason why this Outgoing Payment has been cancelled */ + reason: Scalars['String']['input']; +}; + export type CreateAssetInput = { /** [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217), e.g. `USD` */ code: Scalars['String']['input']; @@ -551,6 +558,8 @@ export type Model = { export type Mutation = { __typename?: 'Mutation'; + /** Cancel Outgoing Payment */ + cancelOutgoingPayment: OutgoingPaymentResponse; /** Create an asset */ createAsset: AssetMutationResponse; /** Withdraw asset liquidity */ @@ -618,6 +627,11 @@ export type Mutation = { }; +export type MutationCancelOutgoingPaymentArgs = { + input: CancelOutgoingPaymentInput; +}; + + export type MutationCreateAssetArgs = { input: CreateAssetInput; }; @@ -819,6 +833,8 @@ export type OutgoingPaymentResponse = { }; export enum OutgoingPaymentState { + /** Payment cancelled */ + Cancelled = 'CANCELLED', /** Successful completion */ Completed = 'COMPLETED', /** Payment failed */ @@ -1488,6 +1504,7 @@ export type ResolversTypes = { AssetsConnection: ResolverTypeWrapper>; BasePayment: ResolverTypeWrapper['BasePayment']>; Boolean: ResolverTypeWrapper>; + CancelOutgoingPaymentInput: ResolverTypeWrapper>; CreateAssetInput: ResolverTypeWrapper>; CreateAssetLiquidityWithdrawalInput: ResolverTypeWrapper>; CreateIncomingPaymentInput: ResolverTypeWrapper>; @@ -1607,6 +1624,7 @@ export type ResolversParentTypes = { AssetsConnection: Partial; BasePayment: ResolversInterfaceTypes['BasePayment']; Boolean: Partial; + CancelOutgoingPaymentInput: Partial; CreateAssetInput: Partial; CreateAssetLiquidityWithdrawalInput: Partial; CreateIncomingPaymentInput: Partial; @@ -1899,6 +1917,7 @@ export type ModelResolvers = { + cancelOutgoingPayment?: Resolver>; createAsset?: Resolver>; createAssetLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; createIncomingPayment?: Resolver>; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index f97c7554bd..30dac61e55 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -101,6 +101,13 @@ export type BasePayment = { walletAddressId: Scalars['ID']['output']; }; +export type CancelOutgoingPaymentInput = { + /** Outgoing payment id */ + id: Scalars['ID']['input']; + /** Reason why this Outgoing Payment has been cancelled */ + reason: Scalars['String']['input']; +}; + export type CreateAssetInput = { /** [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217), e.g. `USD` */ code: Scalars['String']['input']; @@ -551,6 +558,8 @@ export type Model = { export type Mutation = { __typename?: 'Mutation'; + /** Cancel Outgoing Payment */ + cancelOutgoingPayment: OutgoingPaymentResponse; /** Create an asset */ createAsset: AssetMutationResponse; /** Withdraw asset liquidity */ @@ -618,6 +627,11 @@ export type Mutation = { }; +export type MutationCancelOutgoingPaymentArgs = { + input: CancelOutgoingPaymentInput; +}; + + export type MutationCreateAssetArgs = { input: CreateAssetInput; }; @@ -819,6 +833,8 @@ export type OutgoingPaymentResponse = { }; export enum OutgoingPaymentState { + /** Payment cancelled */ + Cancelled = 'CANCELLED', /** Successful completion */ Completed = 'COMPLETED', /** Payment failed */ @@ -1488,6 +1504,7 @@ export type ResolversTypes = { AssetsConnection: ResolverTypeWrapper>; BasePayment: ResolverTypeWrapper['BasePayment']>; Boolean: ResolverTypeWrapper>; + CancelOutgoingPaymentInput: ResolverTypeWrapper>; CreateAssetInput: ResolverTypeWrapper>; CreateAssetLiquidityWithdrawalInput: ResolverTypeWrapper>; CreateIncomingPaymentInput: ResolverTypeWrapper>; @@ -1607,6 +1624,7 @@ export type ResolversParentTypes = { AssetsConnection: Partial; BasePayment: ResolversInterfaceTypes['BasePayment']; Boolean: Partial; + CancelOutgoingPaymentInput: Partial; CreateAssetInput: Partial; CreateAssetLiquidityWithdrawalInput: Partial; CreateIncomingPaymentInput: Partial; @@ -1899,6 +1917,7 @@ export type ModelResolvers = { + cancelOutgoingPayment?: Resolver>; createAsset?: Resolver>; createAssetLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; createIncomingPayment?: Resolver>; From 9831007767284d4b089a020274eadce77e1d0666 Mon Sep 17 00:00:00 2001 From: golobitch Date: Wed, 1 May 2024 19:11:55 +0200 Subject: [PATCH 03/22] feat(outgoing-payment): cancel outgoing payment service implementation --- .../open_payments/payment/outgoing/model.ts | 7 +++- .../open_payments/payment/outgoing/service.ts | 39 ++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/model.ts b/packages/backend/src/open_payments/payment/outgoing/model.ts index 6f8b5e0222..30f65450cf 100644 --- a/packages/backend/src/open_payments/payment/outgoing/model.ts +++ b/packages/backend/src/open_payments/payment/outgoing/model.ts @@ -81,7 +81,7 @@ export class OutgoingPayment } public get failed(): boolean { - return this.state === OutgoingPaymentState.Failed + return [OutgoingPaymentState.Cancelled, OutgoingPaymentState.Failed].includes(this.state) } // Outgoing peer @@ -176,6 +176,7 @@ export enum OutgoingPaymentState { // Awaiting money from the user's wallet account to be deposited to the payment account to reserve it for the payment. // On success, transition to `SENDING`. // On failure, transition to `FAILED`. + // Can also go to CANCELLED state if cancelOutgoingPayment mutation is called Funding = 'FUNDING', // Pay from the account to the destination. // On success, transition to `COMPLETED`. @@ -183,7 +184,9 @@ export enum OutgoingPaymentState { // The payment failed. (Though some money may have been delivered). Failed = 'FAILED', // Successful completion. - Completed = 'COMPLETED' + Completed = 'COMPLETED', + // Transaction has been cancelled by ASE + Cancelled = "CANCELLED" } export enum OutgoingPaymentDepositType { diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index be4205c207..b86ee9d426 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -46,6 +46,9 @@ export interface OutgoingPaymentService create( options: CreateOutgoingPaymentOptions ): Promise + cancel( + options: CancelOutgoingPaymentOptions + ): Promise fund( options: FundOutgoingPaymentOptions ): Promise @@ -73,6 +76,7 @@ export async function createOutgoingPaymentService( return { get: (options) => getOutgoingPayment(deps, options), create: (options) => createOutgoingPayment(deps, options), + cancel: (options) => cancelOutgoingPayment(deps, options), fund: (options) => fundPayment(deps, options), processNext: () => worker.processPendingPayment(deps), getWalletAddressPage: (options) => getWalletAddressPage(deps, options) @@ -108,6 +112,11 @@ export interface CreateFromIncomingPayment extends BaseOptions { debitAmount: Amount } +export type CancelOutgoingPaymentOptions = { + id: string; + reason: string; +} + export type CreateOutgoingPaymentOptions = | CreateFromQuote | CreateFromIncomingPayment @@ -118,6 +127,34 @@ export function isCreateFromIncomingPayment( return 'incomingPayment' in options && 'debitAmount' in options } +async function cancelOutgoingPayment( + deps: ServiceDependencies, + options: CancelOutgoingPaymentOptions +): Promise { + const { id } = options + + return deps.knex.transaction(async (trx) => { + let payment = await OutgoingPayment.query(trx) + .findById(id) + .forUpdate() + + if (!payment) return OutgoingPaymentError.UnknownPayment + if (payment.state !== OutgoingPaymentState.Funding) { + return OutgoingPaymentError.WrongState + } + + payment = await payment.$query(trx).patchAndFetch({ + state: OutgoingPaymentState.Cancelled, + metadata: { + ...payment.metadata, + cancellationReason: options.reason + } + }) + + return addSentAmount(deps, payment) + }) +} + async function createOutgoingPayment( deps: ServiceDependencies, options: CreateOutgoingPaymentOptions @@ -481,7 +518,7 @@ function validateSentAmount( return sentAmount } - if (payment.state === OutgoingPaymentState.Funding) { + if ([OutgoingPaymentState.Funding, OutgoingPaymentState.Cancelled].includes(payment.state)) { return BigInt(0) } From 93983a6f53975194ecc06ae1edd50cf8fb98ca6c Mon Sep 17 00:00:00 2001 From: golobitch Date: Thu, 2 May 2024 17:33:37 +0200 Subject: [PATCH 04/22] feat(backend): add cancel outgoing payment resolver --- packages/backend/src/graphql/resolvers/index.ts | 4 +++- .../backend/src/open_payments/payment/outgoing/service.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/index.ts b/packages/backend/src/graphql/resolvers/index.ts index 5769239354..9696a2a8ad 100644 --- a/packages/backend/src/graphql/resolvers/index.ts +++ b/packages/backend/src/graphql/resolvers/index.ts @@ -25,7 +25,8 @@ import { getOutgoingPayment, createOutgoingPayment, getWalletAddressOutgoingPayments, - createOutgoingPaymentFromIncomingPayment + createOutgoingPaymentFromIncomingPayment, + cancelOutgoingPayment } from './outgoing_payment' import { getPeer, getPeers, createPeer, updatePeer, deletePeer } from './peer' import { @@ -115,6 +116,7 @@ export const resolvers: Resolvers = { createQuote, createOutgoingPayment, createOutgoingPaymentFromIncomingPayment, + cancelOutgoingPayment, createIncomingPayment, createReceiver, createPeer: createPeer, diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index b86ee9d426..79937c106e 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -149,7 +149,7 @@ async function cancelOutgoingPayment( ...payment.metadata, cancellationReason: options.reason } - }) + }).withGraphFetched('[quote.asset, walletAddress]') return addSentAmount(deps, payment) }) From f177e970dca43ba06a3412f9565580ef7cfad8be Mon Sep 17 00:00:00 2001 From: golobitch Date: Thu, 2 May 2024 17:35:37 +0200 Subject: [PATCH 05/22] test(outgoing-payment): cancel outgoing payment tests --- .../resolvers/outgoing_payment.test.ts | 85 +++++++++++++++++++ .../payment/outgoing/service.test.ts | 3 +- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts index 0d63b4886a..979c636ca2 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts @@ -539,6 +539,91 @@ describe('OutgoingPayment Resolvers', (): void => { }) }) + describe('Mutation.cancelOutgoingPayment', (): void => { + let payment: OutgoingPaymentModel + + beforeEach(async (): Promise => { + const { id: walletAddressId } = await createWalletAddress(deps, { + assetId: asset.id + }) + payment = await createPayment({ walletAddressId }) + }) + + const states: [string, OutgoingPaymentError | null][] = + Object.values(OutgoingPaymentState).flatMap((state) => [ + ["Not enough balance", state == OutgoingPaymentState.Funding ? null : OutgoingPaymentError.WrongState], + ["Missing KYC", state == OutgoingPaymentState.Funding ? null : OutgoingPaymentError.WrongState], + ]) + test.each(states)( + '200 - %s, error: %s', + async (reason, error): Promise => { + jest + .spyOn(outgoingPaymentService, 'cancel') + .mockImplementation(async () => { + if (error) { + return error + } + + const updatedPayment = payment + updatedPayment.state = OutgoingPaymentState.Cancelled + updatedPayment.metadata = { + ...updatedPayment.metadata, + cancellationReason: reason + } + return updatedPayment + }) + + const response = await appContainer.apolloClient + .mutate({ + mutation: gql` + mutation cancelOutgoingPayment($input: CancelOutgoingPaymentInput!) { + cancelOutgoingPayment(input: $input) { + code + success + message + payment { + id + walletAddressId + state + metadata + } + } + } + `, + variables: { + input: { + id: payment.id, + reason + } + } + }) + .then((query): OutgoingPaymentResponse => { + if (query.data) return query.data.cancelOutgoingPayment + throw new Error('Data was empty') + }) + + + expect(response.success).toBe(!error) + expect(response.code).toEqual(error ? '409' : '200') + + if (!error) { + expect(response.payment).toEqual({ + __typename: "OutgoingPayment", + id: payment.id, + walletAddressId: payment.walletAddressId, + state: OutgoingPaymentState.Cancelled, + metadata: { + cancellationReason: reason + } + }) + } else { + expect(response.message).toEqual('wrong state') + expect(response.payment).toBeNull(); + } + } + ) + }) + describe('Wallet address outgoingPayments', (): void => { let walletAddressId: string diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index a3543cc600..447583328b 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -90,7 +90,8 @@ describe('OutgoingPaymentService', (): void => { [OutgoingPaymentState.Funding]: OutgoingPaymentEventType.PaymentCreated, [OutgoingPaymentState.Sending]: undefined, [OutgoingPaymentState.Failed]: OutgoingPaymentEventType.PaymentFailed, - [OutgoingPaymentState.Completed]: OutgoingPaymentEventType.PaymentCompleted + [OutgoingPaymentState.Completed]: OutgoingPaymentEventType.PaymentCompleted, + [OutgoingPaymentState.Cancelled]: OutgoingPaymentEventType.PaymentFailed } async function processNext( From e7d0da708a69333388a2905660af517125636090 Mon Sep 17 00:00:00 2001 From: golobitch Date: Thu, 2 May 2024 17:36:57 +0200 Subject: [PATCH 06/22] chore(lint): cancel outgoing payment code --- .../resolvers/outgoing_payment.test.ts | 30 +++++++++++++------ .../src/graphql/resolvers/outgoing_payment.ts | 29 +++++++++++------- .../open_payments/payment/outgoing/model.ts | 7 +++-- .../open_payments/payment/outgoing/service.ts | 25 ++++++++-------- 4 files changed, 57 insertions(+), 34 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts index 979c636ca2..660f2cdd0f 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts @@ -549,11 +549,22 @@ describe('OutgoingPayment Resolvers', (): void => { payment = await createPayment({ walletAddressId }) }) - const states: [string, OutgoingPaymentError | null][] = - Object.values(OutgoingPaymentState).flatMap((state) => [ - ["Not enough balance", state == OutgoingPaymentState.Funding ? null : OutgoingPaymentError.WrongState], - ["Missing KYC", state == OutgoingPaymentState.Funding ? null : OutgoingPaymentError.WrongState], - ]) + const states: [string, OutgoingPaymentError | null][] = Object.values( + OutgoingPaymentState + ).flatMap((state) => [ + [ + 'Not enough balance', + state == OutgoingPaymentState.Funding + ? null + : OutgoingPaymentError.WrongState + ], + [ + 'Missing KYC', + state == OutgoingPaymentState.Funding + ? null + : OutgoingPaymentError.WrongState + ] + ]) test.each(states)( '200 - %s, error: %s', async (reason, error): Promise => { @@ -576,7 +587,9 @@ describe('OutgoingPayment Resolvers', (): void => { const response = await appContainer.apolloClient .mutate({ mutation: gql` - mutation cancelOutgoingPayment($input: CancelOutgoingPaymentInput!) { + mutation cancelOutgoingPayment( + $input: CancelOutgoingPaymentInput! + ) { cancelOutgoingPayment(input: $input) { code success @@ -602,13 +615,12 @@ describe('OutgoingPayment Resolvers', (): void => { throw new Error('Data was empty') }) - expect(response.success).toBe(!error) expect(response.code).toEqual(error ? '409' : '200') if (!error) { expect(response.payment).toEqual({ - __typename: "OutgoingPayment", + __typename: 'OutgoingPayment', id: payment.id, walletAddressId: payment.walletAddressId, state: OutgoingPaymentState.Cancelled, @@ -618,7 +630,7 @@ describe('OutgoingPayment Resolvers', (): void => { }) } else { expect(response.message).toEqual('wrong state') - expect(response.payment).toBeNull(); + expect(response.payment).toBeNull() } } ) diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.ts index 967e9037eb..20894fdeca 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.ts @@ -30,24 +30,31 @@ export const getOutgoingPayment: QueryResolvers['outgoingPayment' } export const cancelOutgoingPayment: MutationResolvers['cancelOutgoingPayment'] = - async (parent, args, ctx): Promise => { - const outgoingPaymentService = await ctx.container.use('outgoingPaymentService') + async ( + parent, + args, + ctx + ): Promise => { + const outgoingPaymentService = await ctx.container.use( + 'outgoingPaymentService' + ) return outgoingPaymentService .cancel(args.input) .then((paymentOrError: OutgoingPayment | OutgoingPaymentError) => isOutgoingPaymentError(paymentOrError) ? { - code: errorToCode[paymentOrError].toString(), - success: false, - message: errorToMessage[paymentOrError] - } + code: errorToCode[paymentOrError].toString(), + success: false, + message: errorToMessage[paymentOrError] + } : { - code: '200', - success: true, - payment: paymentToGraphql(paymentOrError) - } - ).catch(() => ({ + code: '200', + success: true, + payment: paymentToGraphql(paymentOrError) + } + ) + .catch(() => ({ code: '500', success: false, message: 'Error trying to cancel outgoing payment' diff --git a/packages/backend/src/open_payments/payment/outgoing/model.ts b/packages/backend/src/open_payments/payment/outgoing/model.ts index 30f65450cf..5497044917 100644 --- a/packages/backend/src/open_payments/payment/outgoing/model.ts +++ b/packages/backend/src/open_payments/payment/outgoing/model.ts @@ -81,7 +81,10 @@ export class OutgoingPayment } public get failed(): boolean { - return [OutgoingPaymentState.Cancelled, OutgoingPaymentState.Failed].includes(this.state) + return [ + OutgoingPaymentState.Cancelled, + OutgoingPaymentState.Failed + ].includes(this.state) } // Outgoing peer @@ -186,7 +189,7 @@ export enum OutgoingPaymentState { // Successful completion. Completed = 'COMPLETED', // Transaction has been cancelled by ASE - Cancelled = "CANCELLED" + Cancelled = 'CANCELLED' } export enum OutgoingPaymentDepositType { diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 79937c106e..f35e880cf3 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -113,8 +113,8 @@ export interface CreateFromIncomingPayment extends BaseOptions { } export type CancelOutgoingPaymentOptions = { - id: string; - reason: string; + id: string + reason: string } export type CreateOutgoingPaymentOptions = @@ -134,22 +134,23 @@ async function cancelOutgoingPayment( const { id } = options return deps.knex.transaction(async (trx) => { - let payment = await OutgoingPayment.query(trx) - .findById(id) - .forUpdate() + let payment = await OutgoingPayment.query(trx).findById(id).forUpdate() if (!payment) return OutgoingPaymentError.UnknownPayment if (payment.state !== OutgoingPaymentState.Funding) { return OutgoingPaymentError.WrongState } - payment = await payment.$query(trx).patchAndFetch({ - state: OutgoingPaymentState.Cancelled, - metadata: { - ...payment.metadata, - cancellationReason: options.reason - } - }).withGraphFetched('[quote.asset, walletAddress]') + payment = await payment + .$query(trx) + .patchAndFetch({ + state: OutgoingPaymentState.Cancelled, + metadata: { + ...payment.metadata, + cancellationReason: options.reason + } + }) + .withGraphFetched('[quote.asset, walletAddress]') return addSentAmount(deps, payment) }) From 283a8d427986f42847ac9328e1b2856aa3d876cd Mon Sep 17 00:00:00 2001 From: golobitch Date: Thu, 2 May 2024 18:43:38 +0200 Subject: [PATCH 07/22] feat(frontend): add canceled badge color --- packages/frontend/app/shared/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontend/app/shared/utils.ts b/packages/frontend/app/shared/utils.ts index db5dd36f13..bbfaaa66fb 100644 --- a/packages/frontend/app/shared/utils.ts +++ b/packages/frontend/app/shared/utils.ts @@ -78,7 +78,8 @@ export const badgeColorByPaymentState: { PROCESSING: BadgeColor.Yellow, FAILED: BadgeColor.Red, FUNDING: BadgeColor.Yellow, - SENDING: BadgeColor.Yellow + SENDING: BadgeColor.Yellow, + CANCELLED: BadgeColor.Red } export const badgeColorByWalletAddressStatus: Record< From a52097e9825eb863976fe43843034581066b3b61 Mon Sep 17 00:00:00 2001 From: golobitch Date: Thu, 2 May 2024 19:53:56 +0200 Subject: [PATCH 08/22] docs(event-handlers): cancel outgoing payment --- .../content/docs/integration/event-handlers.mdx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/documentation/src/content/docs/integration/event-handlers.mdx b/packages/documentation/src/content/docs/integration/event-handlers.mdx index d1ae35a822..087658f2fe 100644 --- a/packages/documentation/src/content/docs/integration/event-handlers.mdx +++ b/packages/documentation/src/content/docs/integration/event-handlers.mdx @@ -59,7 +59,9 @@ Example: An incoming payment has expired and received $2.55. ## `outgoing_payment.created` -The `outgoing_payment.created` event indicates that an outgoing payment has been created and is awaiting liquidity. The Account Servicing Entity should put a hold on the sender's account and deposit the funds into Rafiki. +The `outgoing_payment.created` event indicates that an outgoing payment has been created and is awaiting liquidity. The Account Servicing Entity should perform a check here to see if there is enough balance, or some other kind of checks that must pass in order to fullfill this outgoing payment. +In case that outgoing payment should not be fullfilled, the Account Servicing Entity can cancel outgoing payment. +Otherwise, the Account Servicing Entity should put a hold on the sender’s account and deposit the funds into Rafiki. Example: An outgoing payment for $12 has been created. @@ -75,6 +77,18 @@ Example: An outgoing payment for $12 has been created. `} /> +Example: Cancel outgoing payment due to low funds + +>ASE: webhook event: outgoing payment created,
debit amount: $12 + ASE->>ASE: check if account has enough balance + ASE->>R: admin API call: CancelOutgoingPayment
reason: Not enough balance +`} +/> + ## `outgoing_payment.completed` The `outgoing_payment.completed` event indicates that an outgoing payment has successfully sent as many funds as possible to the receiver. The Account Servicing Entity should withdraw any excess liquidity from that outgoing payment in Rafiki and use it as they see fit. One option would be to return it to the sender. Another option is that the excess liquidity is considered a fee and retained by the Account Servicing Entity. Furthermore, the Account Servicing Entity should remove the hold on the sender's account and debit it. From 1093442349ccfcdfc11d020f21fd8234d6fd1803 Mon Sep 17 00:00:00 2001 From: golobitch Date: Thu, 2 May 2024 19:56:57 +0200 Subject: [PATCH 09/22] feat(bruno): cancel outgoing payment --- .../Cancel Outgoing Payment.bru | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 bruno/collections/Rafiki/Rafiki Admin APIs/Cancel Outgoing Payment.bru diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Cancel Outgoing Payment.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Cancel Outgoing Payment.bru new file mode 100644 index 0000000000..c1ae36e746 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Cancel Outgoing Payment.bru @@ -0,0 +1,30 @@ +meta { + name: Cancel Outgoing Payment + type: graphql + seq: 43 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CancelOutgoingPayment($input: CancelOutgoingPaymentInput!) { + cancelOutgoingPayment(input: $input) { + code + message + success + } + } +} + +body:graphql:vars { + { + "input": { + "id": "{{outgoingPaymentId}}", + "reason": "Not enough balance" + } + } +} From 9ba6a7d0c00a2fe0e23ecb325f2c5e2a763c05dc Mon Sep 17 00:00:00 2001 From: golobitch Date: Thu, 2 May 2024 22:14:30 +0200 Subject: [PATCH 10/22] chore(lint): outgoing payment service --- .../backend/src/open_payments/payment/outgoing/service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index f35e880cf3..3083ebc26a 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -519,7 +519,11 @@ function validateSentAmount( return sentAmount } - if ([OutgoingPaymentState.Funding, OutgoingPaymentState.Cancelled].includes(payment.state)) { + if ( + [OutgoingPaymentState.Funding, OutgoingPaymentState.Cancelled].includes( + payment.state + ) + ) { return BigInt(0) } From 3a0c14bec7f3d3dc26e0cec06567d41bc6c53285 Mon Sep 17 00:00:00 2001 From: golobitch Date: Fri, 3 May 2024 20:51:04 +0200 Subject: [PATCH 11/22] feat(mase): cancel outgoing payment if pending debit throws error --- .../app/lib/webhooks.server.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/localenv/mock-account-servicing-entity/app/lib/webhooks.server.ts b/localenv/mock-account-servicing-entity/app/lib/webhooks.server.ts index b170300f9c..26ee7f2d37 100644 --- a/localenv/mock-account-servicing-entity/app/lib/webhooks.server.ts +++ b/localenv/mock-account-servicing-entity/app/lib/webhooks.server.ts @@ -69,9 +69,10 @@ export async function handleOutgoingPaymentCreated(wh: Webhook) { } const amt = parseAmount(payment['debitAmount'] as AmountJSON) - const accBalance = BigInt(acc.creditsPosted) - BigInt(acc.debitsPosted) - if (accBalance < amt.value) { + try { + await mockAccounts.pendingDebit(acc.id, amt.value) + } catch (err) { await apolloClient.mutate({ mutation: gql` mutation CancelOutgoingPayment($input: CancelOutgoingPaymentInput!) { @@ -92,8 +93,6 @@ export async function handleOutgoingPaymentCreated(wh: Webhook) { return } - await mockAccounts.pendingDebit(acc.id, amt.value) - // notify rafiki await apolloClient .mutate({ From 329555d8ed167a34e195f4c72279ce4901fbe517 Mon Sep 17 00:00:00 2001 From: Tadej Golobic Date: Fri, 3 May 2024 20:52:41 +0200 Subject: [PATCH 12/22] test(outgoing-payment): mark cancel state as undefined webhook type Co-authored-by: Max Kurapov --- .../backend/src/open_payments/payment/outgoing/service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index 447583328b..a949a50a86 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -91,7 +91,7 @@ describe('OutgoingPaymentService', (): void => { [OutgoingPaymentState.Sending]: undefined, [OutgoingPaymentState.Failed]: OutgoingPaymentEventType.PaymentFailed, [OutgoingPaymentState.Completed]: OutgoingPaymentEventType.PaymentCompleted, - [OutgoingPaymentState.Cancelled]: OutgoingPaymentEventType.PaymentFailed + [OutgoingPaymentState.Cancelled]: undefined } async function processNext( From 3dec80db501a676d2afdd6362a4151b07e2d8d3b Mon Sep 17 00:00:00 2001 From: Tadej Golobic Date: Fri, 3 May 2024 20:54:05 +0200 Subject: [PATCH 13/22] docs(event-handlers): wording Co-authored-by: Max Kurapov --- .../src/content/docs/integration/event-handlers.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/content/docs/integration/event-handlers.mdx b/packages/documentation/src/content/docs/integration/event-handlers.mdx index 087658f2fe..21b8dbd86d 100644 --- a/packages/documentation/src/content/docs/integration/event-handlers.mdx +++ b/packages/documentation/src/content/docs/integration/event-handlers.mdx @@ -59,7 +59,7 @@ Example: An incoming payment has expired and received $2.55. ## `outgoing_payment.created` -The `outgoing_payment.created` event indicates that an outgoing payment has been created and is awaiting liquidity. The Account Servicing Entity should perform a check here to see if there is enough balance, or some other kind of checks that must pass in order to fullfill this outgoing payment. +The `outgoing_payment.created` event indicates that an outgoing payment has been created and is awaiting liquidity. The Account Servicing Entity should verify the user account balance (and perform any other kinds of checks necessary) before funding or cancelling the outgoing payment. In case that outgoing payment should not be fullfilled, the Account Servicing Entity can cancel outgoing payment. Otherwise, the Account Servicing Entity should put a hold on the sender’s account and deposit the funds into Rafiki. From 7c9f5c0e6fcfc736f83dc358e3dd0b6b571675f0 Mon Sep 17 00:00:00 2001 From: golobitch Date: Fri, 3 May 2024 21:00:24 +0200 Subject: [PATCH 14/22] docs(event-handlers): merge two diagrams with alt --- .../docs/integration/event-handlers.mdx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/documentation/src/content/docs/integration/event-handlers.mdx b/packages/documentation/src/content/docs/integration/event-handlers.mdx index 21b8dbd86d..7b72d5af9c 100644 --- a/packages/documentation/src/content/docs/integration/event-handlers.mdx +++ b/packages/documentation/src/content/docs/integration/event-handlers.mdx @@ -69,23 +69,16 @@ Example: An outgoing payment for $12 has been created. graph={`sequenceDiagram participant ASE as Account Servicing Entity participant R as Rafiki - R->>ASE: webhook event: outgoing payment created,
debitAmount: $12 + ASE->>ASE: check if account has enough balance + alt Account has enough balance ASE->>ASE: put hold of $12 on sender's account ASE->>R: admin API call: DepositOutgoingPaymentLiquidity - -`} -/> - -Example: Cancel outgoing payment due to low funds - ->ASE: webhook event: outgoing payment created,
debit amount: $12 - ASE->>ASE: check if account has enough balance + end + alt Account does not have enough balance ASE->>R: admin API call: CancelOutgoingPayment
reason: Not enough balance + end + `} /> From 7a5ec07105353cecd3ba3af2af4f9845ba98e48e Mon Sep 17 00:00:00 2001 From: golobitch Date: Fri, 3 May 2024 21:05:30 +0200 Subject: [PATCH 15/22] feat(graphql): make reason in cancel outgoing payment optional --- .../mock-account-servicing-entity/generated/graphql.ts | 2 +- .../backend/src/graphql/generated/graphql.schema.json | 10 +++------- packages/backend/src/graphql/generated/graphql.ts | 2 +- packages/backend/src/graphql/schema.graphql | 2 +- .../src/open_payments/payment/outgoing/service.ts | 2 +- packages/frontend/app/generated/graphql.ts | 2 +- .../mock-account-service-lib/src/generated/graphql.ts | 2 +- test/integration/lib/generated/graphql.ts | 2 +- 8 files changed, 10 insertions(+), 14 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 30dac61e55..1f914cf528 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -105,7 +105,7 @@ export type CancelOutgoingPaymentInput = { /** Outgoing payment id */ id: Scalars['ID']['input']; /** Reason why this Outgoing Payment has been cancelled */ - reason: Scalars['String']['input']; + reason?: InputMaybe; }; export type CreateAssetInput = { diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index e74bab2b6d..b4ec66bcc6 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -663,13 +663,9 @@ "name": "reason", "description": "Reason why this Outgoing Payment has been cancelled", "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 30dac61e55..1f914cf528 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -105,7 +105,7 @@ export type CancelOutgoingPaymentInput = { /** Outgoing payment id */ id: Scalars['ID']['input']; /** Reason why this Outgoing Payment has been cancelled */ - reason: Scalars['String']['input']; + reason?: InputMaybe; }; export type CreateAssetInput = { diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index da0a02f60f..60bee16cb9 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -933,7 +933,7 @@ input CancelOutgoingPaymentInput { "Outgoing payment id" id: ID! "Reason why this Outgoing Payment has been cancelled" - reason: String! + reason: String } input CreateOutgoingPaymentFromIncomingPaymentInput { diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 3083ebc26a..37d3ecb589 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -114,7 +114,7 @@ export interface CreateFromIncomingPayment extends BaseOptions { export type CancelOutgoingPaymentOptions = { id: string - reason: string + reason?: string } export type CreateOutgoingPaymentOptions = diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 0aa88873e6..a4361ec3c3 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -105,7 +105,7 @@ export type CancelOutgoingPaymentInput = { /** Outgoing payment id */ id: Scalars['ID']['input']; /** Reason why this Outgoing Payment has been cancelled */ - reason: Scalars['String']['input']; + reason?: InputMaybe; }; export type CreateAssetInput = { diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 30dac61e55..1f914cf528 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -105,7 +105,7 @@ export type CancelOutgoingPaymentInput = { /** Outgoing payment id */ id: Scalars['ID']['input']; /** Reason why this Outgoing Payment has been cancelled */ - reason: Scalars['String']['input']; + reason?: InputMaybe; }; export type CreateAssetInput = { diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 30dac61e55..1f914cf528 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -105,7 +105,7 @@ export type CancelOutgoingPaymentInput = { /** Outgoing payment id */ id: Scalars['ID']['input']; /** Reason why this Outgoing Payment has been cancelled */ - reason: Scalars['String']['input']; + reason?: InputMaybe; }; export type CreateAssetInput = { From c6a51b555de0654b8f8137627649eb3d07e8c90f Mon Sep 17 00:00:00 2001 From: golobitch Date: Fri, 3 May 2024 23:59:11 +0200 Subject: [PATCH 16/22] test(outgoing-payment): resolver --- .../resolvers/outgoing_payment.test.ts | 152 ++++++++++-------- 1 file changed, 83 insertions(+), 69 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts index 660f2cdd0f..165c1fe628 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts @@ -539,101 +539,115 @@ describe('OutgoingPayment Resolvers', (): void => { }) }) - describe('Mutation.cancelOutgoingPayment', (): void => { + describe.only('Mutation.cancelOutgoingPayment', (): void => { let payment: OutgoingPaymentModel - - beforeEach(async (): Promise => { - const { id: walletAddressId } = await createWalletAddress(deps, { + beforeEach(async () => { + const walletAddress = await createWalletAddress(deps, { assetId: asset.id }) - payment = await createPayment({ walletAddressId }) + + payment = await createPayment({ walletAddressId: walletAddress.id }) }) - const states: [string, OutgoingPaymentError | null][] = Object.values( - OutgoingPaymentState - ).flatMap((state) => [ - [ - 'Not enough balance', - state == OutgoingPaymentState.Funding - ? null - : OutgoingPaymentError.WrongState - ], - [ - 'Missing KYC', - state == OutgoingPaymentState.Funding - ? null - : OutgoingPaymentError.WrongState - ] - ]) - test.each(states)( - '200 - %s, error: %s', - async (reason, error): Promise => { - jest + const reasons: (string | undefined)[] = [undefined, "Not enough balance"] + test.each(reasons)( + 'should cancel outgoing payment with reason %s', + async(reason): Promise => { + const input = { + id: payment.id, + reason + } + + payment.state = OutgoingPaymentState.Cancelled + payment.metadata = { + cancellationReason: input.reason + } + + const cancelSpy = jest .spyOn(outgoingPaymentService, 'cancel') - .mockImplementation(async () => { - if (error) { - return error - } - - const updatedPayment = payment - updatedPayment.state = OutgoingPaymentState.Cancelled - updatedPayment.metadata = { - ...updatedPayment.metadata, - cancellationReason: reason - } - return updatedPayment - }) - - const response = await appContainer.apolloClient + .mockResolvedValue(payment) + + const mutationResponse = await appContainer.apolloClient .mutate({ mutation: gql` - mutation cancelOutgoingPayment( - $input: CancelOutgoingPaymentInput! - ) { + mutation CancelOutgoingPayment($input: CancelOutgoingPaymentInput!) { cancelOutgoingPayment(input: $input) { code success - message payment { id - walletAddressId state metadata } } } `, - variables: { - input: { - id: payment.id, - reason - } - } + variables: { input } }) - .then((query): OutgoingPaymentResponse => { - if (query.data) return query.data.cancelOutgoingPayment - throw new Error('Data was empty') - }) - - expect(response.success).toBe(!error) - expect(response.code).toEqual(error ? '409' : '200') - - if (!error) { - expect(response.payment).toEqual({ - __typename: 'OutgoingPayment', - id: payment.id, - walletAddressId: payment.walletAddressId, + .then( + (query): OutgoingPaymentResponse => + query.data?.cancelOutgoingPayment + ) + + expect(cancelSpy).toHaveBeenCalledWith(input) + expect(mutationResponse.code).toBe('200') + expect(mutationResponse.success).toBe(true) + expect(mutationResponse.payment).toEqual({ + __typename: "OutgoingPayment", + id: input.id, state: OutgoingPaymentState.Cancelled, metadata: { - cancellationReason: reason + cancellationReason: input.reason } }) - } else { - expect(response.message).toEqual('wrong state') - expect(response.payment).toBeNull() - } } ) + + const errors: [string, OutgoingPaymentError][] = [ + [ '409', OutgoingPaymentError.WrongState ], + [ '404', OutgoingPaymentError.UnknownPayment ] + ] + test.each(errors)( + '%s - outgoing payment error', + async(statusCode, paymentError): Promise => { + const cancelSpy = jest + .spyOn(outgoingPaymentService, 'cancel') + .mockResolvedValueOnce(paymentError) + + const input = { id: uuid() } + + const mutationResponse = await appContainer.apolloClient + .mutate({ + mutation: gql` + mutation CancelOutgoingPayment($input: CancelOutgoingPaymentInput!) { + cancelOutgoingPayment(input: $input) { + code + message + success + payment { + id + state + metadata + } + } + } + `, + variables: { input } + }) + .then( + (query): OutgoingPaymentResponse => + query.data?.cancelOutgoingPayment + ) + + expect(cancelSpy).toHaveBeenCalledWith(input) + expect(mutationResponse).toEqual({ + __typename: 'OutgoingPaymentResponse', + code: statusCode, + message: errorToMessage[paymentError], + success: false, + payment: null + }) + }) }) describe('Wallet address outgoingPayments', (): void => { From 0cc3727b7e4dd9ae37f22e00172a884b2c9a9e8c Mon Sep 17 00:00:00 2001 From: golobitch Date: Sat, 4 May 2024 20:50:26 +0200 Subject: [PATCH 17/22] test(outgoing-payment): add service tests + lint --- .../resolvers/outgoing_payment.test.ts | 59 ++++++++-------- .../payment/outgoing/service.test.ts | 67 +++++++++++++++++++ 2 files changed, 99 insertions(+), 27 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts index 165c1fe628..994862abf0 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts @@ -545,32 +545,34 @@ describe('OutgoingPayment Resolvers', (): void => { const walletAddress = await createWalletAddress(deps, { assetId: asset.id }) - + payment = await createPayment({ walletAddressId: walletAddress.id }) }) - const reasons: (string | undefined)[] = [undefined, "Not enough balance"] + const reasons: (string | undefined)[] = [undefined, 'Not enough balance'] test.each(reasons)( 'should cancel outgoing payment with reason %s', - async(reason): Promise => { + async (reason): Promise => { const input = { id: payment.id, reason } - + payment.state = OutgoingPaymentState.Cancelled payment.metadata = { cancellationReason: input.reason } - + const cancelSpy = jest .spyOn(outgoingPaymentService, 'cancel') .mockResolvedValue(payment) - + const mutationResponse = await appContainer.apolloClient .mutate({ mutation: gql` - mutation CancelOutgoingPayment($input: CancelOutgoingPaymentInput!) { + mutation CancelOutgoingPayment( + $input: CancelOutgoingPaymentInput! + ) { cancelOutgoingPayment(input: $input) { code success @@ -588,38 +590,40 @@ describe('OutgoingPayment Resolvers', (): void => { (query): OutgoingPaymentResponse => query.data?.cancelOutgoingPayment ) - - expect(cancelSpy).toHaveBeenCalledWith(input) - expect(mutationResponse.code).toBe('200') - expect(mutationResponse.success).toBe(true) - expect(mutationResponse.payment).toEqual({ - __typename: "OutgoingPayment", - id: input.id, - state: OutgoingPaymentState.Cancelled, - metadata: { - cancellationReason: input.reason - } - }) + + expect(cancelSpy).toHaveBeenCalledWith(input) + expect(mutationResponse.code).toBe('200') + expect(mutationResponse.success).toBe(true) + expect(mutationResponse.payment).toEqual({ + __typename: 'OutgoingPayment', + id: input.id, + state: OutgoingPaymentState.Cancelled, + metadata: { + cancellationReason: input.reason + } + }) } ) const errors: [string, OutgoingPaymentError][] = [ - [ '409', OutgoingPaymentError.WrongState ], - [ '404', OutgoingPaymentError.UnknownPayment ] + ['409', OutgoingPaymentError.WrongState], + ['404', OutgoingPaymentError.UnknownPayment] ] test.each(errors)( '%s - outgoing payment error', - async(statusCode, paymentError): Promise => { + async (statusCode, paymentError): Promise => { const cancelSpy = jest - .spyOn(outgoingPaymentService, 'cancel') - .mockResolvedValueOnce(paymentError) + .spyOn(outgoingPaymentService, 'cancel') + .mockResolvedValueOnce(paymentError) const input = { id: uuid() } const mutationResponse = await appContainer.apolloClient .mutate({ mutation: gql` - mutation CancelOutgoingPayment($input: CancelOutgoingPaymentInput!) { + mutation CancelOutgoingPayment( + $input: CancelOutgoingPaymentInput! + ) { cancelOutgoingPayment(input: $input) { code message @@ -638,7 +642,7 @@ describe('OutgoingPayment Resolvers', (): void => { (query): OutgoingPaymentResponse => query.data?.cancelOutgoingPayment ) - + expect(cancelSpy).toHaveBeenCalledWith(input) expect(mutationResponse).toEqual({ __typename: 'OutgoingPaymentResponse', @@ -647,7 +651,8 @@ describe('OutgoingPayment Resolvers', (): void => { success: false, payment: null }) - }) + } + ) }) describe('Wallet address outgoingPayments', (): void => { diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index a949a50a86..49f9ebad09 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -389,6 +389,73 @@ describe('OutgoingPaymentService', (): void => { }) }) + describe('cancel', (): void => { + const states: [ + string, + OutgoingPaymentState, + OutgoingPaymentError | null, + string | undefined + ][] = Object.values(OutgoingPaymentState).flatMap((state) => [ + [ + `should ${state == OutgoingPaymentState.Funding ? 'cancel' : 'not cancel'} outgoing payment in ${state} state with reason`, + state, + state == OutgoingPaymentState.Funding + ? null + : OutgoingPaymentError.WrongState, + 'Not enough balance' + ], + [ + `should ${state == OutgoingPaymentState.Funding ? 'cancel' : 'not cancel'} outgoing payment in ${state} state without reason`, + state, + state == OutgoingPaymentState.Funding + ? null + : OutgoingPaymentError.WrongState, + undefined + ] + ]) + it.each(states)( + '%s', + async (_, state, outgoingPaymentError, reason): Promise => { + /** + * 1. Create outgoing payment + * 2. Update the state of outgoing payment + * 3. Cancel outgoing payment + * 4. Based on state, check the result + */ + const outgoingPayment = await createOutgoingPayment(deps, { + walletAddressId, + client, + receiver, + debitAmount: { + assetCode: asset.code, + assetScale: asset.scale, + value: BigInt(10) + }, + validDestination: true, + method: 'ilp' + }) + + await outgoingPayment.$query(knex).patch({ state }) + + const response = await outgoingPaymentService.cancel({ + id: outgoingPayment.id, + reason + }) + + if (!outgoingPaymentError) { + const dataCheck = response as OutgoingPayment + expect(dataCheck.id).toBe(outgoingPayment.id) + expect(dataCheck.state).toBe(OutgoingPaymentState.Cancelled) + expect(dataCheck.metadata).toEqual({ + cancellationReason: reason + }) + } else { + expect(response as OutgoingPaymentError).toBe(outgoingPaymentError) + } + } + ) + }) + describe('create', (): void => { enum GrantOption { Existing = 'existing', From 4476e06e942cd1f61d76e2061032ce88e3b1ba7d Mon Sep 17 00:00:00 2001 From: Tadej Golobic Date: Tue, 7 May 2024 22:26:50 +0200 Subject: [PATCH 18/22] tests(outgoing-payment): remove only call Co-authored-by: Max Kurapov --- packages/backend/src/graphql/resolvers/outgoing_payment.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts index 994862abf0..4d41cb3f12 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts @@ -539,7 +539,7 @@ describe('OutgoingPayment Resolvers', (): void => { }) }) - describe.only('Mutation.cancelOutgoingPayment', (): void => { + describe('Mutation.cancelOutgoingPayment', (): void => { let payment: OutgoingPaymentModel beforeEach(async () => { const walletAddress = await createWalletAddress(deps, { From 2d09e58c6795595743075992db35682cf0876e9d Mon Sep 17 00:00:00 2001 From: Tadej Golobic Date: Tue, 7 May 2024 22:27:34 +0200 Subject: [PATCH 19/22] chore(outgoing-payment): inplace assertion without new variable Co-authored-by: Max Kurapov --- .../backend/src/open_payments/payment/outgoing/service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index 49f9ebad09..c5e05111ff 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -443,7 +443,7 @@ describe('OutgoingPaymentService', (): void => { }) if (!outgoingPaymentError) { - const dataCheck = response as OutgoingPayment + assert.ok(response instanceof OutgoingPayment) expect(dataCheck.id).toBe(outgoingPayment.id) expect(dataCheck.state).toBe(OutgoingPaymentState.Cancelled) expect(dataCheck.metadata).toEqual({ From 4ac867571b1141a65a71109333788f7ff58328b2 Mon Sep 17 00:00:00 2001 From: Tadej Golobic Date: Tue, 7 May 2024 22:29:31 +0200 Subject: [PATCH 20/22] chore(outgoing-payment): check if reason is defined before adding to metadata Co-authored-by: Max Kurapov --- packages/backend/src/open_payments/payment/outgoing/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 37d3ecb589..1c39f1b51d 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -147,7 +147,7 @@ async function cancelOutgoingPayment( state: OutgoingPaymentState.Cancelled, metadata: { ...payment.metadata, - cancellationReason: options.reason + ...(options.reason ? { cancellationReason: options.reason } : {}) } }) .withGraphFetched('[quote.asset, walletAddress]') From 0d1b4dddc63c1d18d5f17152e88315b21492ec01 Mon Sep 17 00:00:00 2001 From: golobitch Date: Tue, 7 May 2024 22:32:56 +0200 Subject: [PATCH 21/22] chore(schema): add more information regarding cancellation reason --- localenv/mock-account-servicing-entity/generated/graphql.ts | 2 +- packages/backend/src/graphql/generated/graphql.schema.json | 2 +- packages/backend/src/graphql/generated/graphql.ts | 2 +- packages/backend/src/graphql/schema.graphql | 2 +- packages/frontend/app/generated/graphql.ts | 2 +- packages/mock-account-service-lib/src/generated/graphql.ts | 2 +- test/integration/lib/generated/graphql.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 1f914cf528..61ebb6868b 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -104,7 +104,7 @@ export type BasePayment = { export type CancelOutgoingPaymentInput = { /** Outgoing payment id */ id: Scalars['ID']['input']; - /** Reason why this Outgoing Payment has been cancelled */ + /** Reason why this Outgoing Payment has been cancelled. This value will be publicly visible in the metadata field if this outgoing payment is requested through Open Payments. */ reason?: InputMaybe; }; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index b4ec66bcc6..1d3fdf6c38 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -661,7 +661,7 @@ }, { "name": "reason", - "description": "Reason why this Outgoing Payment has been cancelled", + "description": "Reason why this Outgoing Payment has been cancelled. This value will be publicly visible in the metadata field if this outgoing payment is requested through Open Payments.", "type": { "kind": "SCALAR", "name": "String", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 1f914cf528..61ebb6868b 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -104,7 +104,7 @@ export type BasePayment = { export type CancelOutgoingPaymentInput = { /** Outgoing payment id */ id: Scalars['ID']['input']; - /** Reason why this Outgoing Payment has been cancelled */ + /** Reason why this Outgoing Payment has been cancelled. This value will be publicly visible in the metadata field if this outgoing payment is requested through Open Payments. */ reason?: InputMaybe; }; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 60bee16cb9..858ed7e95c 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -932,7 +932,7 @@ input CreateOutgoingPaymentInput { input CancelOutgoingPaymentInput { "Outgoing payment id" id: ID! - "Reason why this Outgoing Payment has been cancelled" + "Reason why this Outgoing Payment has been cancelled. This value will be publicly visible in the metadata field if this outgoing payment is requested through Open Payments." reason: String } diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index a4361ec3c3..cceccad999 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -104,7 +104,7 @@ export type BasePayment = { export type CancelOutgoingPaymentInput = { /** Outgoing payment id */ id: Scalars['ID']['input']; - /** Reason why this Outgoing Payment has been cancelled */ + /** Reason why this Outgoing Payment has been cancelled. This value will be publicly visible in the metadata field if this outgoing payment is requested through Open Payments. */ reason?: InputMaybe; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 1f914cf528..61ebb6868b 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -104,7 +104,7 @@ export type BasePayment = { export type CancelOutgoingPaymentInput = { /** Outgoing payment id */ id: Scalars['ID']['input']; - /** Reason why this Outgoing Payment has been cancelled */ + /** Reason why this Outgoing Payment has been cancelled. This value will be publicly visible in the metadata field if this outgoing payment is requested through Open Payments. */ reason?: InputMaybe; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 1f914cf528..61ebb6868b 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -104,7 +104,7 @@ export type BasePayment = { export type CancelOutgoingPaymentInput = { /** Outgoing payment id */ id: Scalars['ID']['input']; - /** Reason why this Outgoing Payment has been cancelled */ + /** Reason why this Outgoing Payment has been cancelled. This value will be publicly visible in the metadata field if this outgoing payment is requested through Open Payments. */ reason?: InputMaybe; }; From 33ae5548f86cb27bfc3a638d5c34b38ca0cebca2 Mon Sep 17 00:00:00 2001 From: golobitch Date: Tue, 7 May 2024 23:47:30 +0200 Subject: [PATCH 22/22] fix(outgoing-payment): test rename dataCheck variable --- .../src/open_payments/payment/outgoing/service.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index c5e05111ff..7faa54c9eb 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -444,9 +444,9 @@ describe('OutgoingPaymentService', (): void => { if (!outgoingPaymentError) { assert.ok(response instanceof OutgoingPayment) - expect(dataCheck.id).toBe(outgoingPayment.id) - expect(dataCheck.state).toBe(OutgoingPaymentState.Cancelled) - expect(dataCheck.metadata).toEqual({ + expect(response.id).toBe(outgoingPayment.id) + expect(response.state).toBe(OutgoingPaymentState.Cancelled) + expect(response.metadata).toEqual({ cancellationReason: reason }) } else {