From 6cf9bf229d9824761cf751e92bc362d0bc1b5997 Mon Sep 17 00:00:00 2001 From: oana-lolea Date: Wed, 22 Oct 2025 10:00:36 +0300 Subject: [PATCH 1/4] add filtering incoming payments by initiatedBy --- .../Get Incoming Payments.bru | 46 +++ .../generated/graphql.ts | 21 ++ .../src/graphql/generated/graphql.schema.json | 125 ++++++++ .../backend/src/graphql/generated/graphql.ts | 21 ++ .../resolvers/incoming_payment.test.ts | 282 +++++++++++++++++- .../src/graphql/resolvers/incoming_payment.ts | 37 +++ .../backend/src/graphql/resolvers/index.ts | 4 +- packages/backend/src/graphql/schema.graphql | 23 ++ .../payment/incoming/service.test.ts | 97 +++++- .../open_payments/payment/incoming/service.ts | 78 ++++- .../src/graphql/generated/graphql.ts | 21 ++ packages/frontend/app/generated/graphql.ts | 21 ++ .../src/generated/graphql.ts | 21 ++ .../src/graphql/generated/graphql.ts | 21 ++ test/test-lib/src/generated/graphql.ts | 21 ++ 15 files changed, 835 insertions(+), 4 deletions(-) create mode 100644 bruno/collections/Rafiki/Rafiki Admin APIs/Get Incoming Payments.bru diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Get Incoming Payments.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Incoming Payments.bru new file mode 100644 index 0000000000..7a9a12b5ee --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Incoming Payments.bru @@ -0,0 +1,46 @@ +meta { + name: Get Incoming Payments + type: graphql + seq: 60 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query IncomingPayments($filter: IncomingPaymentFilter) { + incomingPayments(filter: $filter) { + edges { + node { + id + walletAddressId + client + state + expiresAt + incomingAmount { + value + assetCode + assetScale + } + receivedAmount { + value + assetCode + assetScale + } + metadata + createdAt + } + cursor + } + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index a9b32f062b..fec5ce58f7 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -661,6 +661,11 @@ export type IncomingPaymentEdge = { node: IncomingPayment; }; +export type IncomingPaymentFilter = { + /** Filter for incoming payments based on the initiation reason. */ + initiatedBy?: InputMaybe; +}; + export type IncomingPaymentResponse = { __typename?: 'IncomingPaymentResponse'; /** The incoming payment object returned in the response. */ @@ -1228,6 +1233,8 @@ export type Query = { assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ incomingPayment?: Maybe; + /** Fetch a paginated list of incoming payments. */ + incomingPayments: IncomingPaymentConnection; /** Fetch an Open Payments outgoing payment by its ID. */ outgoingPayment?: Maybe; /** Fetch a paginated list of outgoing payments by receiver. */ @@ -1293,6 +1300,17 @@ export type QueryIncomingPaymentArgs = { }; +export type QueryIncomingPaymentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; + tenantId?: InputMaybe; +}; + + export type QueryOutgoingPaymentArgs = { id: Scalars['String']['input']; }; @@ -2004,6 +2022,7 @@ export type ResolversTypes = { IncomingPayment: ResolverTypeWrapper>; IncomingPaymentConnection: ResolverTypeWrapper>; IncomingPaymentEdge: ResolverTypeWrapper>; + IncomingPaymentFilter: ResolverTypeWrapper>; IncomingPaymentResponse: ResolverTypeWrapper>; IncomingPaymentState: ResolverTypeWrapper>; Int: ResolverTypeWrapper>; @@ -2148,6 +2167,7 @@ export type ResolversParentTypes = { IncomingPayment: Partial; IncomingPaymentConnection: Partial; IncomingPaymentEdge: Partial; + IncomingPaymentFilter: Partial; IncomingPaymentResponse: Partial; Int: Partial; JSONObject: Partial; @@ -2576,6 +2596,7 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; + incomingPayments?: Resolver>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 72493c9f07..7a2049b170 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -3997,6 +3997,30 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "IncomingPaymentFilter", + "description": null, + "isOneOf": false, + "fields": null, + "inputFields": [ + { + "name": "initiatedBy", + "description": "Filter for incoming payments based on the initiation reason.", + "type": { + "kind": "INPUT_OBJECT", + "name": "FilterString", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "IncomingPaymentResponse", @@ -7074,6 +7098,107 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "incomingPayments", + "description": "Fetch a paginated list of incoming payments.", + "args": [ + { + "name": "after", + "description": "Forward pagination: Cursor (incoming payment ID) to start retrieving incoming payments after this point.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Backward pagination: Cursor (outgoing payment ID) to start retrieving incoming payments before this point.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": "Filter incoming payments based on specific criteria such as initiation reason.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IncomingPaymentFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Forward pagination: Limit the result to the first **n** incoming payments after the `after` cursor.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Backward pagination: Limit the result to the last **n** incoming payments before the `before` cursor.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Specify the sort order of incoming payments based on their creation date, either ascending or descending.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant associated with the incoming payment. Optional, if not provided, the tenantId will be obtained from the signature.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "IncomingPaymentConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "outgoingPayment", "description": "Fetch an Open Payments outgoing payment by its ID.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index a9b32f062b..fec5ce58f7 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -661,6 +661,11 @@ export type IncomingPaymentEdge = { node: IncomingPayment; }; +export type IncomingPaymentFilter = { + /** Filter for incoming payments based on the initiation reason. */ + initiatedBy?: InputMaybe; +}; + export type IncomingPaymentResponse = { __typename?: 'IncomingPaymentResponse'; /** The incoming payment object returned in the response. */ @@ -1228,6 +1233,8 @@ export type Query = { assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ incomingPayment?: Maybe; + /** Fetch a paginated list of incoming payments. */ + incomingPayments: IncomingPaymentConnection; /** Fetch an Open Payments outgoing payment by its ID. */ outgoingPayment?: Maybe; /** Fetch a paginated list of outgoing payments by receiver. */ @@ -1293,6 +1300,17 @@ export type QueryIncomingPaymentArgs = { }; +export type QueryIncomingPaymentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; + tenantId?: InputMaybe; +}; + + export type QueryOutgoingPaymentArgs = { id: Scalars['String']['input']; }; @@ -2004,6 +2022,7 @@ export type ResolversTypes = { IncomingPayment: ResolverTypeWrapper>; IncomingPaymentConnection: ResolverTypeWrapper>; IncomingPaymentEdge: ResolverTypeWrapper>; + IncomingPaymentFilter: ResolverTypeWrapper>; IncomingPaymentResponse: ResolverTypeWrapper>; IncomingPaymentState: ResolverTypeWrapper>; Int: ResolverTypeWrapper>; @@ -2148,6 +2167,7 @@ export type ResolversParentTypes = { IncomingPayment: Partial; IncomingPaymentConnection: Partial; IncomingPaymentEdge: Partial; + IncomingPaymentFilter: Partial; IncomingPaymentResponse: Partial; Int: Partial; JSONObject: Partial; @@ -2576,6 +2596,7 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; + incomingPayments?: Resolver>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; diff --git a/packages/backend/src/graphql/resolvers/incoming_payment.test.ts b/packages/backend/src/graphql/resolvers/incoming_payment.test.ts index 4cd4b63381..754c02da3c 100644 --- a/packages/backend/src/graphql/resolvers/incoming_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/incoming_payment.test.ts @@ -1,4 +1,9 @@ -import { ApolloError, gql } from '@apollo/client' +import { + ApolloClient, + ApolloError, + NormalizedCacheObject, + gql +} from '@apollo/client' import { getPageTests } from './page.test' import { createApolloClient, @@ -24,6 +29,7 @@ import { import { IncomingPaymentInitiationReason } from '../../open_payments/payment/incoming/types' import { IncomingPayment, + IncomingPaymentConnection, IncomingPaymentResponse, IncomingPaymentState as SchemaPaymentState } from '../generated/graphql' @@ -742,4 +748,278 @@ describe('Incoming Payment Resolver', (): void => { expect(createSpy).toHaveBeenCalledWith({ ...input, tenantId }) }) }) + + describe('Query.incomingPayments', () => { + let randomAsset: Asset + const pageQuery = gql` + query IncomingPayments( + $filter: IncomingPaymentFilter + $tenantId: String + ) { + incomingPayments(filter: $filter, tenantId: $tenantId) { + edges { + node { + id + } + } + } + } + ` + describe('page tests', () => { + beforeEach(async (): Promise => { + randomAsset = await createAsset(deps) + walletAddressId = ( + await createWalletAddress(deps, { + tenantId, + assetId: randomAsset.id + }) + ).id + }) + + getPageTests({ + getClient: () => appContainer.apolloClient, + createModel: () => + createIncomingPayment(deps, { + walletAddressId, + tenantId, + initiationReason: IncomingPaymentInitiationReason.Card + }), + pagedQuery: 'incomingPayments' + }) + + afterEach(async (): Promise => { + await truncateTables(deps) + }) + }) + describe('page filter tests', () => { + let firstIncomingPayment: IncomingPaymentModel + let secondIncomingPayment: IncomingPaymentModel + + beforeEach(async (): Promise => { + randomAsset = await createAsset(deps) + const firstWalletAddress = await createWalletAddress(deps, { + tenantId, + assetId: randomAsset.id + }) + const secondWalletAddress = await createWalletAddress(deps, { + tenantId, + assetId: randomAsset.id + }) + + firstIncomingPayment = await createIncomingPayment(deps, { + walletAddressId: firstWalletAddress.id, + tenantId: Config.operatorTenantId, + initiationReason: IncomingPaymentInitiationReason.Admin + }) + + secondIncomingPayment = await createIncomingPayment(deps, { + walletAddressId: secondWalletAddress.id, + tenantId: Config.operatorTenantId, + initiationReason: IncomingPaymentInitiationReason.Card + }) + }) + + test('can filter payments by initiatedBy', async (): Promise => { + const query = await appContainer.apolloClient + .query({ + query: pageQuery, + variables: { + filter: { + initiatedBy: { + in: [IncomingPaymentInitiationReason.Card] + } + }, + tenantId + } + }) + .then((query): IncomingPaymentConnection => { + if (query.data) { + return query.data.incomingPayments + } else { + throw new Error('Data was empty') + } + }) + + expect(query.edges).toHaveLength(1) + expect(query.edges[0].node).toMatchObject( + expect.objectContaining({ + id: secondIncomingPayment.id + }) + ) + expect(query.edges).not.toContainEqual( + expect.objectContaining({ + id: firstIncomingPayment.id + }) + ) + }) + + describe('tenant boundaries', (): void => { + let tenantPayment: IncomingPaymentModel + let secondTenantPayment: IncomingPaymentModel + let tenantedApolloClient: ApolloClient + + beforeEach(async (): Promise => { + const tenant = await createTenant(deps) + tenantedApolloClient = await createApolloClient( + appContainer.container, + appContainer.app, + tenant.id + ) + const tenantWalletAddress = await createWalletAddress(deps, { + tenantId: tenant.id + }) + + tenantPayment = await createIncomingPayment(deps, { + walletAddressId: tenantWalletAddress.id, + tenantId: tenant.id, + initiationReason: IncomingPaymentInitiationReason.Card + }) + + secondTenantPayment = await createIncomingPayment(deps, { + walletAddressId: tenantWalletAddress.id, + tenantId: tenant.id, + initiationReason: IncomingPaymentInitiationReason.Card + }) + }) + + test('operator can get incoming payments across all tenants', async (): Promise => { + const query = await appContainer.apolloClient + .query({ + query: pageQuery + }) + .then((query): IncomingPaymentConnection => { + if (query.data) { + return query.data.incomingPayments + } else { + throw new Error() + } + }) + + expect(query.edges).toHaveLength(4) + expect(query.edges).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + node: expect.objectContaining({ + id: tenantPayment.id + }) + }), + expect.objectContaining({ + node: expect.objectContaining({ + id: secondTenantPayment.id + }) + }), + expect.objectContaining({ + node: expect.objectContaining({ + id: tenantPayment.id + }) + }), + expect.objectContaining({ + node: expect.objectContaining({ + id: secondTenantPayment.id + }) + }) + ]) + ) + }) + + test('tenant cannot get incoming payments across all tenants', async (): Promise => { + const query = await tenantedApolloClient + .query({ + query: pageQuery + }) + .then((query): IncomingPaymentConnection => { + if (query.data) { + return query.data.incomingPayments + } else { + throw new Error() + } + }) + + expect(query.edges).toHaveLength(2) + expect(query.edges).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + node: expect.objectContaining({ + id: tenantPayment.id + }) + }), + expect.objectContaining({ + node: expect.objectContaining({ + id: secondTenantPayment.id + }) + }) + ]) + ) + }) + + test('operator can filter incoming payments across all tenants', async (): Promise => { + const query = await appContainer.apolloClient + .query({ + query: pageQuery, + variables: { + tenantId: tenantPayment.tenantId + } + }) + .then((query): IncomingPaymentConnection => { + if (query.data) { + return query.data.incomingPayments + } else { + throw new Error() + } + }) + + expect(query.edges).toHaveLength(2) + expect(query.edges).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + node: expect.objectContaining({ + id: tenantPayment.id + }) + }), + expect.objectContaining({ + node: expect.objectContaining({ + id: secondTenantPayment.id + }) + }) + ]) + ) + }) + + test('tenant cannot filter incoming payments across all tenants', async (): Promise => { + const query = await tenantedApolloClient + .query({ + query: pageQuery, + variables: { tenantId: tenantPayment.tenantId } + }) + .then((query): IncomingPaymentConnection => { + if (query.data) { + return query.data.incomingPayments + } else { + throw new Error() + } + }) + + expect(query.edges).toHaveLength(2) + expect(query.edges).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + node: expect.objectContaining({ + id: tenantPayment.id + }) + }), + expect.objectContaining({ + node: expect.objectContaining({ + id: secondTenantPayment.id + }) + }) + ]) + ) + }) + }) + + afterEach(async () => { + await truncateTables(deps) + }) + }) + }) }) diff --git a/packages/backend/src/graphql/resolvers/incoming_payment.ts b/packages/backend/src/graphql/resolvers/incoming_payment.ts index 7c63d1aa03..8490f178ba 100644 --- a/packages/backend/src/graphql/resolvers/incoming_payment.ts +++ b/packages/backend/src/graphql/resolvers/incoming_payment.ts @@ -39,6 +39,43 @@ export const getIncomingPayment: QueryResolvers['incoming return paymentToGraphql(payment, config) } +export const getIncomingPayments: QueryResolvers['incomingPayments'] = + async ( + parent, + args, + ctx + ): Promise => { + const incomingPaymentService = await ctx.container.use( + 'incomingPaymentService' + ) + const { tenantId, filter, sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + const getPageFn = (pagination_: Pagination, sortOrder_?: SortOrder) => + incomingPaymentService.getPage({ + tenantId: ctx.isOperator ? tenantId : ctx.tenant.id, + pagination: pagination_, + filter, + sortOrder: sortOrder_ + }) + const incomingPayments = await getPageFn(pagination, order) + const pageInfo = await getPageInfo({ + getPage: (pagination_: Pagination, sortOrder_?: SortOrder) => + getPageFn(pagination_, sortOrder_), + page: incomingPayments, + sortOrder: order + }) + + const config = await ctx.container.use('config') + + return { + pageInfo, + edges: incomingPayments.map((incomingPayment: IncomingPayment) => ({ + cursor: incomingPayment.id, + node: paymentToGraphql(incomingPayment, config) + })) + } + } + export const getWalletAddressIncomingPayments: WalletAddressResolvers['incomingPayments'] = async ( parent, diff --git a/packages/backend/src/graphql/resolvers/index.ts b/packages/backend/src/graphql/resolvers/index.ts index 303b10dbee..af4bf2a03e 100644 --- a/packages/backend/src/graphql/resolvers/index.ts +++ b/packages/backend/src/graphql/resolvers/index.ts @@ -24,7 +24,8 @@ import { getIncomingPayment, updateIncomingPayment, approveIncomingPayment, - cancelIncomingPayment + cancelIncomingPayment, + getIncomingPayments } from './incoming_payment' import { getQuote, createQuote, getWalletAddressQuotes } from './quote' import { @@ -111,6 +112,7 @@ export const resolvers: Resolvers = { outgoingPayment: getOutgoingPayment, outgoingPayments: getOutgoingPayments, incomingPayment: getIncomingPayment, + incomingPayments: getIncomingPayments, peer: getPeer, peerByAddressAndAsset: getPeerByAddressAndAsset, peers: getPeers, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 499c85fa2e..312b1a9a54 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -111,6 +111,24 @@ type Query { id: String! ): IncomingPayment + "Fetch a paginated list of incoming payments." + incomingPayments( + "Forward pagination: Cursor (incoming payment ID) to start retrieving incoming payments after this point." + after: String + "Backward pagination: Cursor (outgoing payment ID) to start retrieving incoming payments before this point." + before: String + "Forward pagination: Limit the result to the first **n** incoming payments after the `after` cursor." + first: Int + "Backward pagination: Limit the result to the last **n** incoming payments before the `before` cursor." + last: Int + "Specify the sort order of incoming payments based on their creation date, either ascending or descending." + sortOrder: SortOrder + "Filter incoming payments based on specific criteria such as initiation reason." + filter: IncomingPaymentFilter + "Unique identifier of the tenant associated with the incoming payment. Optional, if not provided, the tenantId will be obtained from the signature." + tenantId: String + ): IncomingPaymentConnection! + "Fetch a paginated list of webhook events." webhookEvents( "Forward pagination: Cursor (webhook event ID) to start retrieving webhook events after this point." @@ -921,6 +939,11 @@ type IncomingPaymentEdge { cursor: String! } +input IncomingPaymentFilter { + "Filter for incoming payments based on the initiation reason." + initiatedBy: FilterString +} + interface BasePayment { "Unique identifier for the payment." id: ID! diff --git a/packages/backend/src/open_payments/payment/incoming/service.test.ts b/packages/backend/src/open_payments/payment/incoming/service.test.ts index c17021b9d4..d44577461b 100644 --- a/packages/backend/src/open_payments/payment/incoming/service.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/service.test.ts @@ -20,7 +20,10 @@ import { AppServices } from '../../../app' import { Asset } from '../../../asset/model' import { createAsset } from '../../../tests/asset' import { createIncomingPayment } from '../../../tests/incomingPayment' -import { createWalletAddress } from '../../../tests/walletAddress' +import { + createWalletAddress, + MockWalletAddress +} from '../../../tests/walletAddress' import { truncateTables } from '../../../tests/tableManager' import { IncomingPaymentError, isIncomingPaymentError } from './errors' import { Amount } from '../../amount' @@ -29,6 +32,8 @@ import { WalletAddress } from '../../wallet_address/model' import { withConfigOverride } from '../../../tests/helpers' import { poll } from '../../../shared/utils' import { createTenant } from '../../../tests/tenant' +import { Pagination, SortOrder } from '../../../shared/baseModel' +import { getPageTests } from '../../../shared/baseModel.test' describe('Incoming Payment Service', (): void => { let deps: IocContract @@ -1115,4 +1120,94 @@ describe('Incoming Payment Service', (): void => { }) }) }) + + describe('getPage', (): void => { + let receiverWalletAddress: MockWalletAddress + let assetId: string + const receiverAsset = { + scale: 9, + code: 'XRP' + } + beforeEach(async () => { + asset = await createAsset(deps) + assetId = asset.id + const { id: receiverAssetId } = await createAsset(deps, { + assetOptions: receiverAsset + }) + receiverWalletAddress = await createWalletAddress(deps, { + tenantId, + assetId: receiverAssetId, + mockServerPort: appContainer.openPaymentsPort + }) + }) + getPageTests({ + createModel: () => + createIncomingPayment(deps, { + tenantId, + walletAddressId, + client, + initiationReason: IncomingPaymentInitiationReason.Card + }), + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => + incomingPaymentService.getPage({ pagination, sortOrder }) + }) + + describe('filters', () => { + let otherSenderWalletAddress: WalletAddress + let otherReceiver: string + let incomingPayment: IncomingPayment + let otherIncomingPayment: IncomingPayment + beforeEach(async (): Promise => { + otherSenderWalletAddress = await createWalletAddress(deps, { + tenantId, + assetId + }) + incomingPayment = await createIncomingPayment(deps, { + walletAddressId: receiverWalletAddress.id, + tenantId: Config.operatorTenantId, + initiationReason: IncomingPaymentInitiationReason.Card + }) + otherReceiver = incomingPayment.getUrl(config.openPaymentsUrl) + + otherIncomingPayment = await createIncomingPayment(deps, { + tenantId, + walletAddressId: otherSenderWalletAddress.id, + client, + initiationReason: IncomingPaymentInitiationReason.Card + }) + }) + + test('can filter by initiatedBy', async (): Promise => { + const page = await incomingPaymentService.getPage({ + filter: { + initiatedBy: { in: [IncomingPaymentInitiationReason.Card] } + } + }) + + expect(page).toContainEqual( + expect.objectContaining({ id: incomingPayment.id }) + ) + expect(page).not.toContainEqual( + expect.objectContaining({ + id: otherIncomingPayment.id, + receiver: otherReceiver + }) + ) + }) + + test('can filter by tenantId', async (): Promise => { + await expect( + incomingPaymentService.getPage({ + tenantId: crypto.randomUUID() + }) + ).resolves.toHaveLength(0) + + await expect( + incomingPaymentService.getPage({ + tenantId: Config.operatorTenantId + }) + ).resolves.toHaveLength(2) + }) + }) + }) }) diff --git a/packages/backend/src/open_payments/payment/incoming/service.ts b/packages/backend/src/open_payments/payment/incoming/service.ts index d9b8354d06..b1ca6cff5d 100644 --- a/packages/backend/src/open_payments/payment/incoming/service.ts +++ b/packages/backend/src/open_payments/payment/incoming/service.ts @@ -20,6 +20,8 @@ import { IAppConfig } from '../../../config/app' import { poll } from '../../../shared/utils' import { AssetService } from '../../../asset/service' import { finalizeWebhookRecipients } from '../../../webhook/service' +import { Pagination, SortOrder } from '../../../shared/baseModel' +import { IncomingPaymentFilter } from '../../../graphql/generated/graphql' export const POSITIVE_SLIPPAGE = BigInt(1) // First retry waits 10 seconds @@ -43,8 +45,16 @@ export interface UpdateOptions { tenantId: string } +interface GetPageOptions { + pagination?: Pagination + filter?: IncomingPaymentFilter + sortOrder?: SortOrder + tenantId?: string +} + export interface IncomingPaymentService extends WalletAddressSubresourceService { + getPage(options?: GetPageOptions): Promise create( options: CreateIncomingPaymentOptions, trx?: Knex.Transaction @@ -93,7 +103,8 @@ export async function createIncomingPaymentService( complete: (id, tenantId) => completeIncomingPayment(deps, id, tenantId), getWalletAddressPage: (options) => getWalletAddressPage(deps, options), processNext: () => processNextIncomingPayment(deps), - update: (options) => updateIncomingPayment(deps, options) + update: (options) => updateIncomingPayment(deps, options), + getPage: (options) => getPage(deps, options) } } @@ -568,3 +579,68 @@ async function addReceivedAmount( return payment } + +async function getPage( + deps: ServiceDependencies, + options?: GetPageOptions +): Promise { + const { filter, pagination, sortOrder, tenantId } = options ?? {} + const query = IncomingPayment.query(deps.knex) + + if (tenantId) { + query.where('tenantId', tenantId) + } + + if (filter?.initiatedBy?.in && filter.initiatedBy.in.length) { + query.whereIn('initiatedBy', filter.initiatedBy.in) + } + + const page = await query.getPage(pagination, sortOrder) + for (const payment of page) { + payment.walletAddress = await deps.walletAddressService.get( + payment.walletAddressId + ) + const asset = await deps.assetService.get(payment.assetId) + if (asset) payment.asset = asset + } + + const amounts = await deps.accountingService.getAccountsTotalReceived( + page.map((payment: IncomingPayment) => payment.id) + ) + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + return page.map((payment: IncomingPayment, i: number) => { + payment.receivedAmount = { + value: validateReceiveAmount(deps, payment, amounts[i]), + assetCode: payment.asset.code, + assetScale: payment.asset.scale + } + return payment + }) +} + +function validateReceiveAmount( + deps: ServiceDependencies, + payment: IncomingPayment, + sentAmount: bigint | undefined +): bigint { + if (sentAmount !== undefined) { + return sentAmount + } + + if ( + [IncomingPaymentState.Pending, IncomingPaymentState.Expired].includes( + payment.state + ) + ) { + return BigInt(0) + } + + const errorMessage = + 'Could not get amount received for payment. There was a problem getting the associated liquidity account.' + + deps.logger.error( + { outgoingPayment: payment.id, state: payment.state }, + errorMessage + ) + throw new Error(errorMessage) +} diff --git a/packages/card-service/src/graphql/generated/graphql.ts b/packages/card-service/src/graphql/generated/graphql.ts index f0649e3ee5..7f099f113c 100644 --- a/packages/card-service/src/graphql/generated/graphql.ts +++ b/packages/card-service/src/graphql/generated/graphql.ts @@ -661,6 +661,11 @@ export type IncomingPaymentEdge = { node: IncomingPayment; }; +export type IncomingPaymentFilter = { + /** Filter for incoming payments based on the initiation reason. */ + initiatedBy?: InputMaybe; +}; + export type IncomingPaymentResponse = { __typename?: 'IncomingPaymentResponse'; /** The incoming payment object returned in the response. */ @@ -1228,6 +1233,8 @@ export type Query = { assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ incomingPayment?: Maybe; + /** Fetch a paginated list of incoming payments. */ + incomingPayments: IncomingPaymentConnection; /** Fetch an Open Payments outgoing payment by its ID. */ outgoingPayment?: Maybe; /** Fetch a paginated list of outgoing payments by receiver. */ @@ -1293,6 +1300,17 @@ export type QueryIncomingPaymentArgs = { }; +export type QueryIncomingPaymentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; + tenantId?: InputMaybe; +}; + + export type QueryOutgoingPaymentArgs = { id: Scalars['String']['input']; }; @@ -2004,6 +2022,7 @@ export type ResolversTypes = { IncomingPayment: ResolverTypeWrapper>; IncomingPaymentConnection: ResolverTypeWrapper>; IncomingPaymentEdge: ResolverTypeWrapper>; + IncomingPaymentFilter: ResolverTypeWrapper>; IncomingPaymentResponse: ResolverTypeWrapper>; IncomingPaymentState: ResolverTypeWrapper>; Int: ResolverTypeWrapper>; @@ -2148,6 +2167,7 @@ export type ResolversParentTypes = { IncomingPayment: Partial; IncomingPaymentConnection: Partial; IncomingPaymentEdge: Partial; + IncomingPaymentFilter: Partial; IncomingPaymentResponse: Partial; Int: Partial; JSONObject: Partial; @@ -2576,6 +2596,7 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; + incomingPayments?: Resolver>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 4abd2d48ee..665830c412 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -661,6 +661,11 @@ export type IncomingPaymentEdge = { node: IncomingPayment; }; +export type IncomingPaymentFilter = { + /** Filter for incoming payments based on the initiation reason. */ + initiatedBy?: InputMaybe; +}; + export type IncomingPaymentResponse = { __typename?: 'IncomingPaymentResponse'; /** The incoming payment object returned in the response. */ @@ -1228,6 +1233,8 @@ export type Query = { assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ incomingPayment?: Maybe; + /** Fetch a paginated list of incoming payments. */ + incomingPayments: IncomingPaymentConnection; /** Fetch an Open Payments outgoing payment by its ID. */ outgoingPayment?: Maybe; /** Fetch a paginated list of outgoing payments by receiver. */ @@ -1293,6 +1300,17 @@ export type QueryIncomingPaymentArgs = { }; +export type QueryIncomingPaymentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; + tenantId?: InputMaybe; +}; + + export type QueryOutgoingPaymentArgs = { id: Scalars['String']['input']; }; @@ -2004,6 +2022,7 @@ export type ResolversTypes = { IncomingPayment: ResolverTypeWrapper>; IncomingPaymentConnection: ResolverTypeWrapper>; IncomingPaymentEdge: ResolverTypeWrapper>; + IncomingPaymentFilter: ResolverTypeWrapper>; IncomingPaymentResponse: ResolverTypeWrapper>; IncomingPaymentState: ResolverTypeWrapper>; Int: ResolverTypeWrapper>; @@ -2148,6 +2167,7 @@ export type ResolversParentTypes = { IncomingPayment: Partial; IncomingPaymentConnection: Partial; IncomingPaymentEdge: Partial; + IncomingPaymentFilter: Partial; IncomingPaymentResponse: Partial; Int: Partial; JSONObject: Partial; @@ -2576,6 +2596,7 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; + incomingPayments?: Resolver>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index a9b32f062b..fec5ce58f7 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -661,6 +661,11 @@ export type IncomingPaymentEdge = { node: IncomingPayment; }; +export type IncomingPaymentFilter = { + /** Filter for incoming payments based on the initiation reason. */ + initiatedBy?: InputMaybe; +}; + export type IncomingPaymentResponse = { __typename?: 'IncomingPaymentResponse'; /** The incoming payment object returned in the response. */ @@ -1228,6 +1233,8 @@ export type Query = { assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ incomingPayment?: Maybe; + /** Fetch a paginated list of incoming payments. */ + incomingPayments: IncomingPaymentConnection; /** Fetch an Open Payments outgoing payment by its ID. */ outgoingPayment?: Maybe; /** Fetch a paginated list of outgoing payments by receiver. */ @@ -1293,6 +1300,17 @@ export type QueryIncomingPaymentArgs = { }; +export type QueryIncomingPaymentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; + tenantId?: InputMaybe; +}; + + export type QueryOutgoingPaymentArgs = { id: Scalars['String']['input']; }; @@ -2004,6 +2022,7 @@ export type ResolversTypes = { IncomingPayment: ResolverTypeWrapper>; IncomingPaymentConnection: ResolverTypeWrapper>; IncomingPaymentEdge: ResolverTypeWrapper>; + IncomingPaymentFilter: ResolverTypeWrapper>; IncomingPaymentResponse: ResolverTypeWrapper>; IncomingPaymentState: ResolverTypeWrapper>; Int: ResolverTypeWrapper>; @@ -2148,6 +2167,7 @@ export type ResolversParentTypes = { IncomingPayment: Partial; IncomingPaymentConnection: Partial; IncomingPaymentEdge: Partial; + IncomingPaymentFilter: Partial; IncomingPaymentResponse: Partial; Int: Partial; JSONObject: Partial; @@ -2576,6 +2596,7 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; + incomingPayments?: Resolver>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; diff --git a/packages/point-of-sale/src/graphql/generated/graphql.ts b/packages/point-of-sale/src/graphql/generated/graphql.ts index dd0665c1d2..d2a87ae9ca 100644 --- a/packages/point-of-sale/src/graphql/generated/graphql.ts +++ b/packages/point-of-sale/src/graphql/generated/graphql.ts @@ -661,6 +661,11 @@ export type IncomingPaymentEdge = { node: IncomingPayment; }; +export type IncomingPaymentFilter = { + /** Filter for incoming payments based on the initiation reason. */ + initiatedBy?: InputMaybe; +}; + export type IncomingPaymentResponse = { __typename?: 'IncomingPaymentResponse'; /** The incoming payment object returned in the response. */ @@ -1228,6 +1233,8 @@ export type Query = { assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ incomingPayment?: Maybe; + /** Fetch a paginated list of incoming payments. */ + incomingPayments: IncomingPaymentConnection; /** Fetch an Open Payments outgoing payment by its ID. */ outgoingPayment?: Maybe; /** Fetch a paginated list of outgoing payments by receiver. */ @@ -1293,6 +1300,17 @@ export type QueryIncomingPaymentArgs = { }; +export type QueryIncomingPaymentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; + tenantId?: InputMaybe; +}; + + export type QueryOutgoingPaymentArgs = { id: Scalars['String']['input']; }; @@ -2004,6 +2022,7 @@ export type ResolversTypes = { IncomingPayment: ResolverTypeWrapper>; IncomingPaymentConnection: ResolverTypeWrapper>; IncomingPaymentEdge: ResolverTypeWrapper>; + IncomingPaymentFilter: ResolverTypeWrapper>; IncomingPaymentResponse: ResolverTypeWrapper>; IncomingPaymentState: ResolverTypeWrapper>; Int: ResolverTypeWrapper>; @@ -2148,6 +2167,7 @@ export type ResolversParentTypes = { IncomingPayment: Partial; IncomingPaymentConnection: Partial; IncomingPaymentEdge: Partial; + IncomingPaymentFilter: Partial; IncomingPaymentResponse: Partial; Int: Partial; JSONObject: Partial; @@ -2576,6 +2596,7 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; + incomingPayments?: Resolver>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; diff --git a/test/test-lib/src/generated/graphql.ts b/test/test-lib/src/generated/graphql.ts index a9b32f062b..fec5ce58f7 100644 --- a/test/test-lib/src/generated/graphql.ts +++ b/test/test-lib/src/generated/graphql.ts @@ -661,6 +661,11 @@ export type IncomingPaymentEdge = { node: IncomingPayment; }; +export type IncomingPaymentFilter = { + /** Filter for incoming payments based on the initiation reason. */ + initiatedBy?: InputMaybe; +}; + export type IncomingPaymentResponse = { __typename?: 'IncomingPaymentResponse'; /** The incoming payment object returned in the response. */ @@ -1228,6 +1233,8 @@ export type Query = { assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ incomingPayment?: Maybe; + /** Fetch a paginated list of incoming payments. */ + incomingPayments: IncomingPaymentConnection; /** Fetch an Open Payments outgoing payment by its ID. */ outgoingPayment?: Maybe; /** Fetch a paginated list of outgoing payments by receiver. */ @@ -1293,6 +1300,17 @@ export type QueryIncomingPaymentArgs = { }; +export type QueryIncomingPaymentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; + tenantId?: InputMaybe; +}; + + export type QueryOutgoingPaymentArgs = { id: Scalars['String']['input']; }; @@ -2004,6 +2022,7 @@ export type ResolversTypes = { IncomingPayment: ResolverTypeWrapper>; IncomingPaymentConnection: ResolverTypeWrapper>; IncomingPaymentEdge: ResolverTypeWrapper>; + IncomingPaymentFilter: ResolverTypeWrapper>; IncomingPaymentResponse: ResolverTypeWrapper>; IncomingPaymentState: ResolverTypeWrapper>; Int: ResolverTypeWrapper>; @@ -2148,6 +2167,7 @@ export type ResolversParentTypes = { IncomingPayment: Partial; IncomingPaymentConnection: Partial; IncomingPaymentEdge: Partial; + IncomingPaymentFilter: Partial; IncomingPaymentResponse: Partial; Int: Partial; JSONObject: Partial; @@ -2576,6 +2596,7 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; + incomingPayments?: Resolver>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; From c61770d52315cab38837a1b84af427147722b4d3 Mon Sep 17 00:00:00 2001 From: oana-lolea Date: Fri, 24 Oct 2025 10:04:56 +0300 Subject: [PATCH 2/4] Added filter to walletAddress.incomingPayments resolver --- .../generated/graphql.ts | 1 + .../src/graphql/generated/graphql.schema.json | 12 +++ .../backend/src/graphql/generated/graphql.ts | 1 + .../src/graphql/resolvers/incoming_payment.ts | 12 +-- packages/backend/src/graphql/schema.graphql | 2 + .../open_payments/payment/incoming/service.ts | 80 ++++--------------- .../src/graphql/generated/graphql.ts | 1 + packages/frontend/app/generated/graphql.ts | 1 + .../src/generated/graphql.ts | 1 + .../src/graphql/generated/graphql.ts | 1 + test/test-lib/src/generated/graphql.ts | 1 + 11 files changed, 42 insertions(+), 71 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index fec5ce58f7..8128e22925 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1728,6 +1728,7 @@ export type WalletAddress = Model & { export type WalletAddressIncomingPaymentsArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 7a2049b170..1c3fa279e9 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -9768,6 +9768,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "Filter incoming payments based on specific criteria such as initiation reason.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IncomingPaymentFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Forward pagination: Limit the result to the first **n** incoming payments after the `after` cursor.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index fec5ce58f7..8128e22925 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1728,6 +1728,7 @@ export type WalletAddress = Model & { export type WalletAddressIncomingPaymentsArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; diff --git a/packages/backend/src/graphql/resolvers/incoming_payment.ts b/packages/backend/src/graphql/resolvers/incoming_payment.ts index 8490f178ba..a7ad643009 100644 --- a/packages/backend/src/graphql/resolvers/incoming_payment.ts +++ b/packages/backend/src/graphql/resolvers/incoming_payment.ts @@ -92,21 +92,23 @@ export const getWalletAddressIncomingPayments: WalletAddressResolvers - incomingPaymentService.getWalletAddressPage({ + incomingPaymentService.getPage({ walletAddressId: parent.id as string, pagination, sortOrder, - tenantId: ctx.tenant.id + tenantId: ctx.tenant.id, + filter }), page: incomingPayments, sortOrder: order diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 312b1a9a54..3c38683651 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -838,6 +838,8 @@ type WalletAddress implements Model { last: Int "Specify the sort order of incoming payments based on their creation date, either ascending or descending." sortOrder: SortOrder + "Filter incoming payments based on specific criteria such as initiation reason." + filter: IncomingPaymentFilter ): IncomingPaymentConnection "List of quotes created at this wallet address" diff --git a/packages/backend/src/open_payments/payment/incoming/service.ts b/packages/backend/src/open_payments/payment/incoming/service.ts index b1ca6cff5d..b9b8ec786f 100644 --- a/packages/backend/src/open_payments/payment/incoming/service.ts +++ b/packages/backend/src/open_payments/payment/incoming/service.ts @@ -50,6 +50,8 @@ interface GetPageOptions { filter?: IncomingPaymentFilter sortOrder?: SortOrder tenantId?: string + walletAddressId?: string + client?: string } export interface IncomingPaymentService @@ -411,43 +413,7 @@ async function getWalletAddressPage( deps: ServiceDependencies, options: ListOptions ): Promise { - const pageQuery = IncomingPayment.query(deps.knex) - - if (options.tenantId) pageQuery.where('tenantId', options.tenantId) - - const page = await pageQuery.list(options) - - for (const payment of page) { - const asset = await deps.assetService.get(payment.assetId) - if (asset) payment.asset = asset - - payment.walletAddress = await deps.walletAddressService.get( - payment.walletAddressId - ) - } - - const amounts = await deps.accountingService.getAccountsTotalReceived( - page.map((payment: IncomingPayment) => payment.id) - ) - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - return page.map((payment: IncomingPayment, i: number) => { - try { - payment.receivedAmount = { - value: amounts[i] || BigInt(0), - assetCode: payment.asset.code, - assetScale: payment.asset.scale - } - } catch (_) { - deps.logger.error( - { payment: payment.id }, - 'incoming payment account not found' - ) - throw new Error( - `Underlying TB account not found, incoming payment id: ${payment.id}` - ) - } - return payment - }) + return await getPage(deps, options) } async function approveIncomingPayment( @@ -584,13 +550,22 @@ async function getPage( deps: ServiceDependencies, options?: GetPageOptions ): Promise { - const { filter, pagination, sortOrder, tenantId } = options ?? {} + const { filter, pagination, sortOrder, tenantId, walletAddressId, client } = + options ?? {} const query = IncomingPayment.query(deps.knex) if (tenantId) { query.where('tenantId', tenantId) } + if (client) { + query.where('client', client) + } + + if (walletAddressId) { + query.where('walletAddressId', walletAddressId) + } + if (filter?.initiatedBy?.in && filter.initiatedBy.in.length) { query.whereIn('initiatedBy', filter.initiatedBy.in) } @@ -610,37 +585,10 @@ async function getPage( // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types return page.map((payment: IncomingPayment, i: number) => { payment.receivedAmount = { - value: validateReceiveAmount(deps, payment, amounts[i]), + value: amounts[i] || BigInt(0), assetCode: payment.asset.code, assetScale: payment.asset.scale } return payment }) } - -function validateReceiveAmount( - deps: ServiceDependencies, - payment: IncomingPayment, - sentAmount: bigint | undefined -): bigint { - if (sentAmount !== undefined) { - return sentAmount - } - - if ( - [IncomingPaymentState.Pending, IncomingPaymentState.Expired].includes( - payment.state - ) - ) { - return BigInt(0) - } - - const errorMessage = - 'Could not get amount received for payment. There was a problem getting the associated liquidity account.' - - deps.logger.error( - { outgoingPayment: payment.id, state: payment.state }, - errorMessage - ) - throw new Error(errorMessage) -} diff --git a/packages/card-service/src/graphql/generated/graphql.ts b/packages/card-service/src/graphql/generated/graphql.ts index 7f099f113c..19987c953a 100644 --- a/packages/card-service/src/graphql/generated/graphql.ts +++ b/packages/card-service/src/graphql/generated/graphql.ts @@ -1728,6 +1728,7 @@ export type WalletAddress = Model & { export type WalletAddressIncomingPaymentsArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 665830c412..c3247ea4c6 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1728,6 +1728,7 @@ export type WalletAddress = Model & { export type WalletAddressIncomingPaymentsArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index fec5ce58f7..8128e22925 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1728,6 +1728,7 @@ export type WalletAddress = Model & { export type WalletAddressIncomingPaymentsArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; diff --git a/packages/point-of-sale/src/graphql/generated/graphql.ts b/packages/point-of-sale/src/graphql/generated/graphql.ts index d2a87ae9ca..750aa9a0e8 100644 --- a/packages/point-of-sale/src/graphql/generated/graphql.ts +++ b/packages/point-of-sale/src/graphql/generated/graphql.ts @@ -1728,6 +1728,7 @@ export type WalletAddress = Model & { export type WalletAddressIncomingPaymentsArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; diff --git a/test/test-lib/src/generated/graphql.ts b/test/test-lib/src/generated/graphql.ts index fec5ce58f7..8128e22925 100644 --- a/test/test-lib/src/generated/graphql.ts +++ b/test/test-lib/src/generated/graphql.ts @@ -1728,6 +1728,7 @@ export type WalletAddress = Model & { export type WalletAddressIncomingPaymentsArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; From 2a6304f4902f1f40a279b6b2af62db2b831f808a Mon Sep 17 00:00:00 2001 From: oana-lolea Date: Tue, 28 Oct 2025 17:20:38 +0200 Subject: [PATCH 3/4] added filtering by initiatedBy to also work with notIn --- .../payment/incoming/service.test.ts | 48 +++++++++++++------ .../open_payments/payment/incoming/service.ts | 4 ++ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/open_payments/payment/incoming/service.test.ts b/packages/backend/src/open_payments/payment/incoming/service.test.ts index d44577461b..f4c1f832fc 100644 --- a/packages/backend/src/open_payments/payment/incoming/service.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/service.test.ts @@ -1173,26 +1173,46 @@ describe('Incoming Payment Service', (): void => { tenantId, walletAddressId: otherSenderWalletAddress.id, client, - initiationReason: IncomingPaymentInitiationReason.Card + initiationReason: IncomingPaymentInitiationReason.Admin }) }) - test('can filter by initiatedBy', async (): Promise => { - const page = await incomingPaymentService.getPage({ - filter: { - initiatedBy: { in: [IncomingPaymentInitiationReason.Card] } - } + describe('can filter by initiatedBy', () => { + test('state: in', async (): Promise => { + const page = await incomingPaymentService.getPage({ + filter: { + initiatedBy: { in: [IncomingPaymentInitiationReason.Card] } + } + }) + + expect(page).toContainEqual( + expect.objectContaining({ id: incomingPayment.id }) + ) + expect(page).not.toContainEqual( + expect.objectContaining({ + id: otherIncomingPayment.id, + receiver: otherReceiver + }) + ) }) - expect(page).toContainEqual( - expect.objectContaining({ id: incomingPayment.id }) - ) - expect(page).not.toContainEqual( - expect.objectContaining({ - id: otherIncomingPayment.id, - receiver: otherReceiver + test('state: notIn', async (): Promise => { + const page = await incomingPaymentService.getPage({ + filter: { + initiatedBy: { notIn: [IncomingPaymentInitiationReason.Card] } + } }) - ) + + expect(page).toContainEqual( + expect.objectContaining({ id: otherIncomingPayment.id }) + ) + expect(page).not.toContainEqual( + expect.objectContaining({ + id: incomingPayment.id, + receiver: otherReceiver + }) + ) + }) }) test('can filter by tenantId', async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/incoming/service.ts b/packages/backend/src/open_payments/payment/incoming/service.ts index b9b8ec786f..0a37138312 100644 --- a/packages/backend/src/open_payments/payment/incoming/service.ts +++ b/packages/backend/src/open_payments/payment/incoming/service.ts @@ -570,6 +570,10 @@ async function getPage( query.whereIn('initiatedBy', filter.initiatedBy.in) } + if (filter?.initiatedBy?.notIn && filter.initiatedBy.notIn.length) { + query.whereNotIn('initiatedBy', filter.initiatedBy.notIn) + } + const page = await query.getPage(pagination, sortOrder) for (const payment of page) { payment.walletAddress = await deps.walletAddressService.get( From 82de37fda3e64d161bd27846133a4e1e035d6ee4 Mon Sep 17 00:00:00 2001 From: oana-lolea Date: Wed, 29 Oct 2025 10:29:51 +0200 Subject: [PATCH 4/4] Added filter to getPage functions --- .../src/graphql/resolvers/incoming_payment.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/incoming_payment.ts b/packages/backend/src/graphql/resolvers/incoming_payment.ts index a7ad643009..0f7afe6a3c 100644 --- a/packages/backend/src/graphql/resolvers/incoming_payment.ts +++ b/packages/backend/src/graphql/resolvers/incoming_payment.ts @@ -3,7 +3,8 @@ import { WalletAddressResolvers, MutationResolvers, IncomingPayment as SchemaIncomingPayment, - QueryResolvers + QueryResolvers, + IncomingPaymentFilter } from '../generated/graphql' import { IncomingPayment } from '../../open_payments/payment/incoming/model' import { IncomingPaymentInitiationReason } from '../../open_payments/payment/incoming/types' @@ -50,17 +51,24 @@ export const getIncomingPayments: QueryResolvers['incomin ) const { tenantId, filter, sortOrder, ...pagination } = args const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc - const getPageFn = (pagination_: Pagination, sortOrder_?: SortOrder) => + const getPageFn = ( + pagination_: Pagination, + sortOrder_?: SortOrder, + filter_?: IncomingPaymentFilter + ) => incomingPaymentService.getPage({ tenantId: ctx.isOperator ? tenantId : ctx.tenant.id, pagination: pagination_, - filter, + filter: filter_, sortOrder: sortOrder_ }) - const incomingPayments = await getPageFn(pagination, order) + const incomingPayments = await getPageFn(pagination, order, filter) const pageInfo = await getPageInfo({ - getPage: (pagination_: Pagination, sortOrder_?: SortOrder) => - getPageFn(pagination_, sortOrder_), + getPage: ( + pagination_: Pagination, + sortOrder_?: SortOrder, + filter_?: IncomingPaymentFilter + ) => getPageFn(pagination_, sortOrder_, filter_), page: incomingPayments, sortOrder: order })