From 8ef20279f3a8e37042ad87f184aaadd9a31dcb3b Mon Sep 17 00:00:00 2001 From: bsanduc Date: Tue, 10 Dec 2024 16:39:40 +0200 Subject: [PATCH] feat: tenanted quotes - first iteration with dummy tenant --- .../Examples/Open Payments/Create Quote.bru | 4 +- .../Quotes/Create Quote.bru | 2 +- .../Open Payments APIs/Quotes/Get Quote.bru | 2 +- .../generated/graphql.ts | 13 +++- .../20241203140018_seed_tenant_operator.js | 22 +++++++ .../20241208214023_add_tenant_id_to_quote.js | 22 +++++++ .../src/graphql/generated/graphql.schema.json | 66 ++++++++++++++++++- .../backend/src/graphql/generated/graphql.ts | 13 +++- .../resolvers/combined_payments.test.ts | 5 ++ .../src/graphql/resolvers/liquidity.test.ts | 6 ++ .../resolvers/outgoing_payment.test.ts | 35 ++++++++-- .../src/graphql/resolvers/quote.test.ts | 30 +++++++-- .../backend/src/graphql/resolvers/quote.ts | 5 +- .../src/graphql/resolvers/receiver.test.ts | 13 ++-- .../backend/src/graphql/resolvers/receiver.ts | 2 +- packages/backend/src/graphql/schema.graphql | 10 ++- .../payment/combined/service.test.ts | 3 + .../payment/incoming_remote/service.ts | 3 +- .../payment/outgoing/routes.test.ts | 17 +++-- .../open_payments/payment/outgoing/routes.ts | 1 + .../payment/outgoing/service.test.ts | 54 ++++++++++++++- .../open_payments/payment/outgoing/service.ts | 8 ++- .../backend/src/open_payments/quote/model.ts | 13 +++- .../src/open_payments/quote/routes.test.ts | 14 +++- .../backend/src/open_payments/quote/routes.ts | 5 +- .../src/open_payments/quote/service.test.ts | 38 +++++++++-- .../src/open_payments/quote/service.ts | 16 ++++- .../open_payments/receiver/service.test.ts | 13 ++-- .../src/open_payments/receiver/service.ts | 22 +++++-- .../wallet_address/model.test.ts | 4 +- .../src/open_payments/wallet_address/model.ts | 1 + .../payment-method/handler/service.test.ts | 4 ++ .../src/payment-method/handler/service.ts | 1 + .../src/payment-method/ilp/service.test.ts | 16 +++++ .../src/payment-method/local/service.test.ts | 12 ++++ .../backend/src/shared/pagination.test.ts | 4 ++ packages/backend/src/tests/combinedPayment.ts | 2 + packages/backend/src/tests/outgoingPayment.ts | 6 +- packages/backend/src/tests/quote.ts | 16 ++++- packages/backend/src/tests/tableManager.ts | 2 + packages/backend/src/webhook/service.test.ts | 4 ++ packages/frontend/app/generated/graphql.ts | 13 +++- .../src/generated/graphql.ts | 13 +++- test/integration/integration.test.ts | 5 ++ test/integration/lib/generated/graphql.ts | 13 +++- 45 files changed, 497 insertions(+), 76 deletions(-) create mode 100644 packages/backend/migrations/20241203140018_seed_tenant_operator.js create mode 100644 packages/backend/migrations/20241208214023_add_tenant_id_to_quote.js diff --git a/bruno/collections/Rafiki/Examples/Open Payments/Create Quote.bru b/bruno/collections/Rafiki/Examples/Open Payments/Create Quote.bru index a7708217f4..5f0e702830 100644 --- a/bruno/collections/Rafiki/Examples/Open Payments/Create Quote.bru +++ b/bruno/collections/Rafiki/Examples/Open Payments/Create Quote.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{senderOpenPaymentsHost}}/quotes + url: {{senderOpenPaymentsHost}}/{{senderTenantId}}/quotes body: json auth: none } @@ -17,7 +17,7 @@ headers { body:json { { "walletAddress": "{{senderWalletAddress}}", - "receiver": "{{receiverOpenPaymentsHost}}/incoming-payments/{{incomingPaymentId}}", + "receiver": "{{receiverOpenPaymentsHost}}/{{receiverTenantId}}/incoming-payments/{{incomingPaymentId}}", "method": "ilp" } } diff --git a/bruno/collections/Rafiki/Open Payments APIs/Quotes/Create Quote.bru b/bruno/collections/Rafiki/Open Payments APIs/Quotes/Create Quote.bru index 1e53624a2c..d37c457693 100644 --- a/bruno/collections/Rafiki/Open Payments APIs/Quotes/Create Quote.bru +++ b/bruno/collections/Rafiki/Open Payments APIs/Quotes/Create Quote.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{senderOpenPaymentsHost}}/quotes + url: {{senderOpenPaymentsHost}}/{{senderTenantId}}/quotes body: json auth: none } diff --git a/bruno/collections/Rafiki/Open Payments APIs/Quotes/Get Quote.bru b/bruno/collections/Rafiki/Open Payments APIs/Quotes/Get Quote.bru index 008c63222f..273b72b97d 100644 --- a/bruno/collections/Rafiki/Open Payments APIs/Quotes/Get Quote.bru +++ b/bruno/collections/Rafiki/Open Payments APIs/Quotes/Get Quote.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{senderOpenPaymentsHost}}/quotes/{{quoteId}} + url: {{senderOpenPaymentsHost}}/{{senderTenantId}}/quotes/{{quoteId}} body: none auth: none } diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 9437d6ec24..915af77536 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -337,6 +337,8 @@ export type CreateQuoteInput = { receiveAmount?: InputMaybe; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['input']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['String']['input']; /** Unique identifier of the wallet address under which the quote will be created. */ walletAddressId: Scalars['String']['input']; }; @@ -1142,7 +1144,7 @@ export type Query = { peerByAddressAndAsset?: Maybe; /** Fetch a paginated list of peers. */ peers: PeersConnection; - /** Fetch an Open Payments quote by its ID. */ + /** Fetch an Open Payments quote by its ID and tenant ID. */ quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; @@ -1235,11 +1237,13 @@ export type QueryPeersArgs = { export type QueryQuoteArgs = { id: Scalars['String']['input']; + tenantId: Scalars['String']['input']; }; export type QueryReceiverArgs = { id: Scalars['String']['input']; + tenantId: Scalars['String']['input']; }; @@ -1287,6 +1291,8 @@ export type Quote = { receiveAmount: Amount; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['output']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['ID']['output']; /** Unique identifier of the wallet address under which the quote was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -2307,8 +2313,8 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; peerByAddressAndAsset?: Resolver, ParentType, ContextType, RequireFields>; peers?: Resolver>; - quote?: Resolver, ParentType, ContextType, RequireFields>; - receiver?: Resolver, ParentType, ContextType, RequireFields>; + quote?: Resolver, ParentType, ContextType, RequireFields>; + receiver?: Resolver, ParentType, ContextType, RequireFields>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; @@ -2323,6 +2329,7 @@ export type QuoteResolvers; receiveAmount?: Resolver; receiver?: Resolver; + tenantId?: Resolver; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/migrations/20241203140018_seed_tenant_operator.js b/packages/backend/migrations/20241203140018_seed_tenant_operator.js new file mode 100644 index 0000000000..97bcf55325 --- /dev/null +++ b/packages/backend/migrations/20241203140018_seed_tenant_operator.js @@ -0,0 +1,22 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex('tenants').insert({ + id: '8e1db008-ab2f-4f1d-8c44-593354084100', + email: 'admin@example.com', + publicName: 'Super tenant', + apiSecret: 'secret' + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex('tenants') + .where('id', '8e1db008-ab2f-4f1d-8c44-593354084100') + .del() +} diff --git a/packages/backend/migrations/20241208214023_add_tenant_id_to_quote.js b/packages/backend/migrations/20241208214023_add_tenant_id_to_quote.js new file mode 100644 index 0000000000..638a51f779 --- /dev/null +++ b/packages/backend/migrations/20241208214023_add_tenant_id_to_quote.js @@ -0,0 +1,22 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.alterTable('quotes', function (table) { + // TODO update default value + table.uuid('tenantId').notNullable() + table.foreign('tenantId').references('tenants.id').onDelete('CASCADE') + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.alterTable('quotes', function (table) { + table.dropForeign('tenantId') + table.dropColumn('tenantId') + }) +} diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index a4fa9fb362..a3115e71a3 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -1958,6 +1958,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant under which the quote was created.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "walletAddressId", "description": "Unique identifier of the wallet address under which the quote will be created.", @@ -6726,7 +6742,7 @@ }, { "name": "quote", - "description": "Fetch an Open Payments quote by its ID.", + "description": "Fetch an Open Payments quote by its ID and tenant ID.", "args": [ { "name": "id", @@ -6743,6 +6759,22 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "tenantId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -6772,6 +6804,22 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant of the incoming payment that resolves the receiver", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -7125,6 +7173,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant under which the quote was created.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "walletAddressId", "description": "Unique identifier of the wallet address under which the quote was created.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 9437d6ec24..915af77536 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -337,6 +337,8 @@ export type CreateQuoteInput = { receiveAmount?: InputMaybe; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['input']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['String']['input']; /** Unique identifier of the wallet address under which the quote will be created. */ walletAddressId: Scalars['String']['input']; }; @@ -1142,7 +1144,7 @@ export type Query = { peerByAddressAndAsset?: Maybe; /** Fetch a paginated list of peers. */ peers: PeersConnection; - /** Fetch an Open Payments quote by its ID. */ + /** Fetch an Open Payments quote by its ID and tenant ID. */ quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; @@ -1235,11 +1237,13 @@ export type QueryPeersArgs = { export type QueryQuoteArgs = { id: Scalars['String']['input']; + tenantId: Scalars['String']['input']; }; export type QueryReceiverArgs = { id: Scalars['String']['input']; + tenantId: Scalars['String']['input']; }; @@ -1287,6 +1291,8 @@ export type Quote = { receiveAmount: Amount; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['output']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['ID']['output']; /** Unique identifier of the wallet address under which the quote was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -2307,8 +2313,8 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; peerByAddressAndAsset?: Resolver, ParentType, ContextType, RequireFields>; peers?: Resolver>; - quote?: Resolver, ParentType, ContextType, RequireFields>; - receiver?: Resolver, ParentType, ContextType, RequireFields>; + quote?: Resolver, ParentType, ContextType, RequireFields>; + receiver?: Resolver, ParentType, ContextType, RequireFields>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; @@ -2323,6 +2329,7 @@ export type QuoteResolvers; receiveAmount?: Resolver; receiver?: Resolver; + tenantId?: Resolver; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/resolvers/combined_payments.test.ts b/packages/backend/src/graphql/resolvers/combined_payments.test.ts index 50af7a5d4c..ff660989a6 100644 --- a/packages/backend/src/graphql/resolvers/combined_payments.test.ts +++ b/packages/backend/src/graphql/resolvers/combined_payments.test.ts @@ -23,6 +23,7 @@ describe('Payment', (): void => { let deps: IocContract let appContainer: TestContainer let asset: Asset + let tenantId: string beforeAll(async (): Promise => { deps = initIocContainer(Config) @@ -31,6 +32,7 @@ describe('Payment', (): void => { beforeEach(async (): Promise => { asset = await createAsset(deps) + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' }) afterEach(async (): Promise => { @@ -55,6 +57,7 @@ describe('Payment', (): void => { const client = 'client-test' const outgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId: outWalletAddressId, client: client, method: 'ilp', @@ -161,6 +164,7 @@ describe('Payment', (): void => { const client = 'client-test-type-wallet-address' const outgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId: outWalletAddressId, client: client, method: 'ilp', @@ -171,6 +175,7 @@ describe('Payment', (): void => { assetId: asset.id }) await createOutgoingPayment(deps, { + tenantId, walletAddressId: outWalletAddressId2, client: client, method: 'ilp', diff --git a/packages/backend/src/graphql/resolvers/liquidity.test.ts b/packages/backend/src/graphql/resolvers/liquidity.test.ts index e0968b27e1..c40c231d12 100644 --- a/packages/backend/src/graphql/resolvers/liquidity.test.ts +++ b/packages/backend/src/graphql/resolvers/liquidity.test.ts @@ -1742,11 +1742,13 @@ describe('Liquidity Resolvers', (): void => { ) describe('Event Liquidity', (): void => { + let tenantId: string let walletAddress: WalletAddress let incomingPayment: IncomingPayment let payment: OutgoingPayment beforeEach(async (): Promise => { + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' walletAddress = await createWalletAddress(deps) const walletAddressId = walletAddress.id incomingPayment = await createIncomingPayment(deps, { @@ -1759,6 +1761,7 @@ describe('Liquidity Resolvers', (): void => { expiresAt: new Date(Date.now() + 60 * 1000) }) payment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, method: 'ilp', receiver: `${Config.openPaymentsUrl}/incoming-payments/${uuid()}`, @@ -2152,11 +2155,13 @@ describe('Liquidity Resolvers', (): void => { }) describe('Payment Liquidity', (): void => { + let tenantId: string let walletAddress: WalletAddress let incomingPayment: IncomingPayment let outgoingPayment: OutgoingPayment beforeEach(async (): Promise => { + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' walletAddress = await createWalletAddress(deps) const walletAddressId = walletAddress.id incomingPayment = await createIncomingPayment(deps, { @@ -2169,6 +2174,7 @@ describe('Liquidity Resolvers', (): void => { expiresAt: new Date(Date.now() + 60 * 1000) }) outgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, method: 'ilp', receiver: `${ diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts index dd41c4eb89..394c949bfe 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts @@ -68,6 +68,7 @@ describe('OutgoingPayment Resolvers', (): void => { const createPayment = async ( options: { + tenantId: string walletAddressId: string metadata?: Record }, @@ -94,9 +95,11 @@ describe('OutgoingPayment Resolvers', (): void => { describe('Query.outgoingPayment', (): void => { let payment: OutgoingPaymentModel + let tenantId: string let walletAddressId: string beforeEach(async (): Promise => { + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' walletAddressId = ( await createWalletAddress(deps, { assetId: asset.id @@ -108,6 +111,7 @@ describe('OutgoingPayment Resolvers', (): void => { getClient: () => appContainer.apolloClient, createModel: () => createPayment({ + tenantId, walletAddressId }), pagedQuery: 'outgoingPayments' @@ -160,6 +164,7 @@ describe('OutgoingPayment Resolvers', (): void => { }) receiver = incomingPayment.getUrl(firstReceiverWalletAddress) firstOutgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, receiver, method: 'ilp', @@ -171,6 +176,7 @@ describe('OutgoingPayment Resolvers', (): void => { }) const secondReceiver = secondIncomingPayment.getUrl(secondWalletAddress) secondOutgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId: secondWalletAddress.id, receiver: secondReceiver, method: 'ilp', @@ -329,7 +335,10 @@ describe('OutgoingPayment Resolvers', (): void => { assetId: asset.id }) - const payment = await createPayment({ walletAddressId }, grantId) + const payment = await createPayment( + { tenantId, walletAddressId }, + grantId + ) const query = await appContainer.apolloClient .query({ @@ -367,7 +376,7 @@ describe('OutgoingPayment Resolvers', (): void => { const { id: walletAddressId } = await createWalletAddress(deps, { assetId: asset.id }) - payment = await createPayment({ walletAddressId, metadata }) + payment = await createPayment({ tenantId, walletAddressId, metadata }) }) // Query with each payment state with and without an error @@ -552,10 +561,15 @@ describe('OutgoingPayment Resolvers', (): void => { } test('success (metadata)', async (): Promise => { + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const { id: walletAddressId } = await createWalletAddress(deps, { assetId: asset.id }) - const payment = await createPayment({ walletAddressId, metadata }) + const payment = await createPayment({ + tenantId, + walletAddressId, + metadata + }) const createSpy = jest .spyOn(outgoingPaymentService, 'create') @@ -688,10 +702,14 @@ describe('OutgoingPayment Resolvers', (): void => { const mockIncomingPaymentUrl = `https://${faker.internet.domainName()}/incoming-payments/${uuid()}` test('create', async (): Promise => { + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const walletAddress = await createWalletAddress(deps, { assetId: asset.id }) - const payment = await createPayment({ walletAddressId: walletAddress.id }) + const payment = await createPayment({ + tenantId, + walletAddressId: walletAddress.id + }) const createSpy = jest .spyOn(outgoingPaymentService, 'create') @@ -839,11 +857,15 @@ describe('OutgoingPayment Resolvers', (): void => { describe('Mutation.cancelOutgoingPayment', (): void => { let payment: OutgoingPaymentModel beforeEach(async () => { + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const walletAddress = await createWalletAddress(deps, { assetId: asset.id }) - payment = await createPayment({ walletAddressId: walletAddress.id }) + payment = await createPayment({ + tenantId, + walletAddressId: walletAddress.id + }) }) const reasons: (string | undefined)[] = [undefined, 'Not enough balance'] @@ -952,8 +974,10 @@ describe('OutgoingPayment Resolvers', (): void => { describe('Wallet address outgoingPayments', (): void => { let walletAddressId: string + let tenantId: string beforeEach(async (): Promise => { + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' walletAddressId = ( await createWalletAddress(deps, { assetId: asset.id @@ -965,6 +989,7 @@ describe('OutgoingPayment Resolvers', (): void => { getClient: () => appContainer.apolloClient, createModel: () => createPayment({ + tenantId, walletAddressId }), pagedQuery: 'outgoingPayments', diff --git a/packages/backend/src/graphql/resolvers/quote.test.ts b/packages/backend/src/graphql/resolvers/quote.test.ts index dcb703edc6..2773913685 100644 --- a/packages/backend/src/graphql/resolvers/quote.test.ts +++ b/packages/backend/src/graphql/resolvers/quote.test.ts @@ -28,6 +28,7 @@ describe('Quote Resolvers', (): void => { let appContainer: TestContainer let quoteService: QuoteService let asset: Asset + let tenantId: string const receivingWalletAddress = 'http://wallet2.example/bob' const receiver = `${receivingWalletAddress}/incoming-payments/${uuid()}` @@ -39,6 +40,7 @@ describe('Quote Resolvers', (): void => { }) beforeEach(async (): Promise => { + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' asset = await createAsset(deps) }) @@ -56,6 +58,7 @@ describe('Quote Resolvers', (): void => { walletAddressId: string ): Promise => { return await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount: { @@ -78,9 +81,10 @@ describe('Quote Resolvers', (): void => { const query = await appContainer.apolloClient .query({ query: gql` - query Quote($quoteId: String!) { - quote(id: $quoteId) { + query Quote($quoteId: String!, $tenantId: String!) { + quote(id: $quoteId, tenantId: $tenantId) { id + tenantId walletAddressId receiver debitAmount { @@ -99,13 +103,15 @@ describe('Quote Resolvers', (): void => { } `, variables: { - quoteId: quote.id + quoteId: quote.id, + tenantId: quote.tenantId } }) .then((query): Quote => query.data?.quote) expect(query).toEqual({ id: quote.id, + tenantId: quote.tenantId, walletAddressId, receiver: quote.receiver, debitAmount: { @@ -133,13 +139,17 @@ describe('Quote Resolvers', (): void => { try { await appContainer.apolloClient.query({ query: gql` - query Quote($quoteId: String!) { - quote(id: $quoteId) { + query Quote($quoteId: String!, $tenantId: String!) { + quote(id: $quoteId, tenantId: $tenantId) { id + tenantId } } `, - variables: { quoteId: uuid() } + variables: { + quoteId: uuid(), + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100' + } }) } catch (error) { expect(error).toBeInstanceOf(ApolloError) @@ -175,6 +185,7 @@ describe('Quote Resolvers', (): void => { assetScale: asset.scale } input = { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddressId: uuid(), receiver, debitAmount @@ -191,7 +202,9 @@ describe('Quote Resolvers', (): void => { const { id: walletAddressId } = await createWalletAddress(deps, { assetId: asset.id }) + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const input = { + tenantId, walletAddressId, debitAmount: amount, receiveAmount, @@ -204,6 +217,7 @@ describe('Quote Resolvers', (): void => { .mockImplementationOnce(async (opts) => { quote = await createQuote(deps, { ...opts, + tenantId, validDestination: false }) return quote @@ -216,6 +230,7 @@ describe('Quote Resolvers', (): void => { createQuote(input: $input) { quote { id + tenantId } } } @@ -226,6 +241,7 @@ describe('Quote Resolvers', (): void => { expect(createSpy).toHaveBeenCalledWith({ ...input, method: 'ilp' }) expect(query.quote?.id).toBe(quote?.id) + expect(query.quote?.tenantId).toBe(quote?.tenantId) }) test('unknown walletAddress', async (): Promise => { @@ -238,6 +254,7 @@ describe('Quote Resolvers', (): void => { createQuote(input: $input) { quote { id + tenantId } } } @@ -272,6 +289,7 @@ describe('Quote Resolvers', (): void => { createQuote(input: $input) { quote { id + tenantId } } } diff --git a/packages/backend/src/graphql/resolvers/quote.ts b/packages/backend/src/graphql/resolvers/quote.ts index 16bd2863e1..955efb9736 100644 --- a/packages/backend/src/graphql/resolvers/quote.ts +++ b/packages/backend/src/graphql/resolvers/quote.ts @@ -25,7 +25,8 @@ export const getQuote: QueryResolvers['quote'] = async ( ): Promise => { const quoteService = await ctx.container.use('quoteService') const quote = await quoteService.get({ - id: args.id + id: args.id, + tenantId: args.tenantId }) if (!quote) { throw new GraphQLError('quote does not exist', { @@ -41,6 +42,7 @@ export const createQuote: MutationResolvers['createQuote'] = async (parent, args, ctx): Promise => { const quoteService = await ctx.container.use('quoteService') const options: CreateQuoteOptions = { + tenantId: args.input.tenantId, walletAddressId: args.input.walletAddressId, receiver: args.input.receiver, method: 'ilp' @@ -100,6 +102,7 @@ export const getWalletAddressQuotes: WalletAddressResolvers['quot export function quoteToGraphql(quote: Quote): SchemaQuote { return { id: quote.id, + tenantId: quote.tenantId, walletAddressId: quote.walletAddressId, receiver: quote.receiver, debitAmount: quote.debitAmount, diff --git a/packages/backend/src/graphql/resolvers/receiver.test.ts b/packages/backend/src/graphql/resolvers/receiver.test.ts index caa2cb32ef..399f8ad851 100644 --- a/packages/backend/src/graphql/resolvers/receiver.test.ts +++ b/packages/backend/src/graphql/resolvers/receiver.test.ts @@ -266,8 +266,8 @@ describe('Receiver Resolver', (): void => { const query = await appContainer.apolloClient .query({ query: gql` - query GetReceiver($id: String!) { - receiver(id: $id) { + query GetReceiver($id: String!, $tenantId: String!) { + receiver(id: $id, tenantId: $tenantId) { id walletAddressUrl completed @@ -289,7 +289,8 @@ describe('Receiver Resolver', (): void => { } `, variables: { - id: receiver.incomingPayment.id + id: receiver.incomingPayment.id, + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100' } }) .then((query): Receiver => query.data?.receiver) @@ -326,13 +327,13 @@ describe('Receiver Resolver', (): void => { try { await appContainer.apolloClient.query({ query: gql` - query GetReceiver($id: String!) { - receiver(id: $id) { + query GetReceiver($id: String!, $tenantId: String!) { + receiver(id: $id, tenantId: $tenantId) { id } } `, - variables: { id: uuid() } + variables: { id: uuid(), tenantId: uuid() } }) } catch (error) { expect(error).toBeInstanceOf(ApolloError) diff --git a/packages/backend/src/graphql/resolvers/receiver.ts b/packages/backend/src/graphql/resolvers/receiver.ts index b9002c30f1..6ec7df3c11 100644 --- a/packages/backend/src/graphql/resolvers/receiver.ts +++ b/packages/backend/src/graphql/resolvers/receiver.ts @@ -20,7 +20,7 @@ export const getReceiver: QueryResolvers['receiver'] = async ( ctx ): Promise => { const receiverService = await ctx.container.use('receiverService') - const receiver = await receiverService.get(args.id) + const receiver = await receiverService.get(args.id, args.tenantId) if (!receiver) { ctx.logger.error(`Receiver "${args.id}" was not found.`) throw new GraphQLError('receiver does not exist', { diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index ef622b214d..59f4ffb1b9 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -72,8 +72,8 @@ type Query { sortOrder: SortOrder ): WalletAddressesConnection! - "Fetch an Open Payments quote by its ID." - quote("Unique identifier of the quote." id: String!): Quote + "Fetch an Open Payments quote by its ID and tenant ID." + quote("Unique identifier of the quote." id: String!, tenantId: String!): Quote "Fetch an Open Payments outgoing payment by its ID." outgoingPayment( @@ -147,6 +147,8 @@ type Query { receiver( "Unique identifier of the receiver (incoming payment URL)." id: String! + "Unique identifier of the tenant of the incoming payment that resolves the receiver" + tenantId: String! ): Receiver } @@ -1090,6 +1092,8 @@ type QuoteEdge { type Quote { "Unique identifier of the quote." id: ID! + "Unique identifier of the tenant under which the quote was created." + tenantId: ID! "Unique identifier of the wallet address under which the quote was created." walletAddressId: ID! "Wallet address URL of the receiver." @@ -1116,6 +1120,8 @@ input AmountInput { } input CreateQuoteInput { + "Unique identifier of the tenant under which the quote was created." + tenantId: String! "Unique identifier of the wallet address under which the quote will be created." walletAddressId: String! "Amount to send (fixed send)." diff --git a/packages/backend/src/open_payments/payment/combined/service.test.ts b/packages/backend/src/open_payments/payment/combined/service.test.ts index fddb5efd43..a0347f3d09 100644 --- a/packages/backend/src/open_payments/payment/combined/service.test.ts +++ b/packages/backend/src/open_payments/payment/combined/service.test.ts @@ -27,6 +27,7 @@ describe('Combined Payment Service', (): void => { let appContainer: TestContainer let knex: Knex let combinedPaymentService: CombinedPaymentService + let tenantId: string let sendAsset: Asset let sendWalletAddressId: string let receiveAsset: Asset @@ -40,6 +41,7 @@ describe('Combined Payment Service', (): void => { }) beforeEach(async (): Promise => { + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' sendAsset = await createAsset(deps) receiveAsset = await createAsset(deps) sendWalletAddressId = ( @@ -65,6 +67,7 @@ describe('Combined Payment Service', (): void => { const receiverUrl = incomingPayment.getUrl(receiveWalletAddress) const outgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId: sendWalletAddressId, method: 'ilp', receiver: receiverUrl, diff --git a/packages/backend/src/open_payments/payment/incoming_remote/service.ts b/packages/backend/src/open_payments/payment/incoming_remote/service.ts index 72c830e5c5..5bd12d4e8d 100644 --- a/packages/backend/src/open_payments/payment/incoming_remote/service.ts +++ b/packages/backend/src/open_payments/payment/incoming_remote/service.ts @@ -23,7 +23,8 @@ interface CreateRemoteIncomingPaymentArgs { export interface RemoteIncomingPaymentService { get( - url: string + url: string, + tenantId?: string //TODO this is not optional ): Promise< OpenPaymentsIncomingPaymentWithPaymentMethods | RemoteIncomingPaymentError > diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts index f30dab4641..853e3164b9 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts @@ -39,6 +39,7 @@ describe('Outgoing Payment Routes', (): void => { let config: IAppConfig let outgoingPaymentRoutes: OutgoingPaymentRoutes let outgoingPaymentService: OutgoingPaymentService + let tenantId: string let walletAddress: WalletAddress let baseUrl: string @@ -51,6 +52,7 @@ describe('Outgoing Payment Routes', (): void => { }): Promise => { return await createOutgoingPayment(deps, { ...options, + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddressId: walletAddress.id, method: 'ilp', receiver: `${receivingWalletAddress}/incoming-payments/${uuid()}`, @@ -77,6 +79,7 @@ describe('Outgoing Payment Routes', (): void => { beforeEach(async (): Promise => { const asset = await createAsset(deps) + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' walletAddress = await createWalletAddress(deps, { assetId: asset.id }) baseUrl = new URL(walletAddress.url).origin }) @@ -134,7 +137,7 @@ describe('Outgoing Payment Routes', (): void => { type SetupContextOptions = UnionOmit< CreateOutgoingPaymentOptions, - 'walletAddressId' + 'walletAddressId' | 'tenantId' > describe('create', (): void => { @@ -149,6 +152,9 @@ describe('Outgoing Payment Routes', (): void => { url: `/outgoing-payments`, body: options }, + params: { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100' + }, walletAddress, client: options.client, grant: options.grant @@ -182,6 +188,7 @@ describe('Outgoing Payment Routes', (): void => { CreateOutgoingPaymentBaseOptions, 'walletAddressId' > = { + tenantId, client, grant, metadata @@ -189,7 +196,7 @@ describe('Outgoing Payment Routes', (): void => { if (createFrom === CreateFrom.Quote) { options = { ...options, - quoteId: `${baseUrl}/quotes/${payment.quote.id}` + quoteId: `${baseUrl}/${tenantId}/quotes/${payment.quote.id}` } as CreateFromQuote } else { assert(createFrom === CreateFrom.IncomingPayment) @@ -212,6 +219,7 @@ describe('Outgoing Payment Routes', (): void => { ).resolves.toBeUndefined() let expectedCreateOptions: CreateOutgoingPaymentBaseOptions = { + tenantId, walletAddressId: walletAddress.id, metadata, client, @@ -282,7 +290,7 @@ describe('Outgoing Payment Routes', (): void => { async (error): Promise => { const quoteId = uuid() const ctx = setup({ - quoteId: `${baseUrl}/quotes/${quoteId}` + quoteId: `${baseUrl}/${tenantId}/quotes/${quoteId}` }) const createSpy = jest .spyOn(outgoingPaymentService, 'create') @@ -300,7 +308,8 @@ describe('Outgoing Payment Routes', (): void => { expect(createSpy).toHaveBeenCalledWith({ walletAddressId: walletAddress.id, - quoteId + quoteId, + tenantId }) } ) diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.ts b/packages/backend/src/open_payments/payment/outgoing/routes.ts index e39e0c3baf..dfcecdf9cd 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.ts @@ -98,6 +98,7 @@ async function createOutgoingPayment( ): Promise { const { body } = ctx.request const baseOptions: OutgoingPaymentCreateBaseOptions = { + tenantId: ctx.params.tenantId, walletAddressId: ctx.walletAddress.id, metadata: body.metadata, client: ctx.client, 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 b3e98b502c..fe9b42a4c3 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -65,6 +65,7 @@ describe('OutgoingPaymentService', (): void => { let telemetryService: TelemetryService let knex: Knex let assetId: string + let tenantId: string let walletAddressId: string let incomingPayment: IncomingPayment let receiverWalletAddress: MockWalletAddress @@ -271,6 +272,7 @@ describe('OutgoingPaymentService', (): void => { beforeEach(async (): Promise => { const { id: sendAssetId } = await createAsset(deps, asset) assetId = sendAssetId + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const walletAddress = await createWalletAddress(deps, { assetId: sendAssetId }) @@ -302,7 +304,7 @@ describe('OutgoingPaymentService', (): void => { .spyOn(receiverService, 'get') .mockImplementation(async (url: string) => { // call original instead of receiverService.get to avoid infinite loop - const receiver = await receiverGet.call(receiverService, url) + const receiver = await receiverGet.call(receiverService, url, tenantId) if (receiver) { // "as any" to circumvent "readonly" check (compile time only) to allow overriding "isLocal" here // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -327,6 +329,7 @@ describe('OutgoingPaymentService', (): void => { getTests({ createModel: ({ client }) => createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver, @@ -342,6 +345,7 @@ describe('OutgoingPaymentService', (): void => { describe('get', (): void => { test('throws error if cannot find liquidity account for SENDING payment', async () => { const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -349,6 +353,7 @@ describe('OutgoingPaymentService', (): void => { }) const payment = await outgoingPaymentService.create({ + tenantId, walletAddressId, quoteId: quote.id, client @@ -386,6 +391,7 @@ describe('OutgoingPaymentService', (): void => { getPageTests({ createModel: () => createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver, @@ -418,6 +424,7 @@ describe('OutgoingPaymentService', (): void => { otherReceiver = incomingPayment.getUrl(otherReceiverWalletAddress) outgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver, @@ -431,6 +438,7 @@ describe('OutgoingPaymentService', (): void => { }) otherOutgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId: otherSenderWalletAddress.id, client, receiver: otherReceiver, @@ -504,6 +512,7 @@ describe('OutgoingPaymentService', (): void => { describe('getWalletAddressPage', (): void => { test('throws error if cannot find liquidity account for SENDING payment', async () => { const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -511,6 +520,7 @@ describe('OutgoingPaymentService', (): void => { }) const payment = await outgoingPaymentService.create({ + tenantId, walletAddressId, client, quoteId: quote.id @@ -577,6 +587,7 @@ describe('OutgoingPaymentService', (): void => { * 4. Based on state, check the result */ const outgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver, @@ -631,6 +642,7 @@ describe('OutgoingPaymentService', (): void => { const quoteSpy = jest.spyOn(quoteService, 'create') const payment = await outgoingPaymentService.create({ + tenantId, walletAddressId, debitAmount, incomingPayment: incomingPaymentUrl @@ -638,6 +650,7 @@ describe('OutgoingPaymentService', (): void => { expect(!isOutgoingPaymentError(payment)).toBeTruthy() expect(quoteSpy).toHaveBeenCalledWith({ + tenantId, walletAddressId, receiver: incomingPaymentUrl, debitAmount, @@ -671,6 +684,7 @@ describe('OutgoingPaymentService', (): void => { }) const options: CreateOutgoingPaymentOptions = { + tenantId, walletAddressId: receiverWalletAddress.id, debitAmount, incomingPayment: incomingPayment.toOpenPaymentsTypeWithMethods( @@ -716,6 +730,7 @@ describe('OutgoingPaymentService', (): void => { }) const options: CreateOutgoingPaymentOptions = { + tenantId, walletAddressId: receiverWalletAddress.id, debitAmount, incomingPayment: incomingPayment.toOpenPaymentsTypeWithMethods( @@ -753,6 +768,7 @@ describe('OutgoingPaymentService', (): void => { .mockImplementationOnce(async () => quoteCreateResponse) const payment = await outgoingPaymentService.create({ + tenantId, walletAddressId, debitAmount, incomingPayment: incomingPaymentUrl @@ -761,6 +777,7 @@ describe('OutgoingPaymentService', (): void => { expect(isOutgoingPaymentError(payment)).toBeTruthy() expect(payment).toBe(quoteCreateResponse) expect(quoteSpy).toHaveBeenCalledWith({ + tenantId, walletAddressId, receiver: incomingPaymentUrl, debitAmount, @@ -802,12 +819,14 @@ describe('OutgoingPaymentService', (): void => { const peerService = await deps.use('peerService') const peer = await createPeer(deps) const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, method: 'ilp' }) const options = { + tenantId, walletAddressId, client, quoteId: quote.id, @@ -865,6 +884,7 @@ describe('OutgoingPaymentService', (): void => { it('fails to create on unknown wallet address', async () => { const { id: quoteId } = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -873,6 +893,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId: uuid(), quoteId }) @@ -882,6 +903,7 @@ describe('OutgoingPaymentService', (): void => { it('fails to create on unknown quote', async () => { await expect( outgoingPaymentService.create({ + tenantId, walletAddressId, quoteId: uuid() }) @@ -890,6 +912,7 @@ describe('OutgoingPaymentService', (): void => { it('fails to create on "consumed" quote', async () => { const { quote } = await createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver, @@ -898,6 +921,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId, client, quoteId: quote.id @@ -907,6 +931,7 @@ describe('OutgoingPaymentService', (): void => { it('fails to create on invalid quote wallet address', async () => { const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -915,6 +940,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId: receiverWalletAddress.id, quoteId: quote.id }) @@ -923,6 +949,7 @@ describe('OutgoingPaymentService', (): void => { it('fails to create on expired quote', async () => { const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -934,6 +961,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId, quoteId: quote.id }) @@ -947,6 +975,7 @@ describe('OutgoingPaymentService', (): void => { `fails to create on $state quote receiver`, async ({ state }): Promise => { const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -959,6 +988,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId, quoteId: quote.id }) @@ -968,6 +998,7 @@ describe('OutgoingPaymentService', (): void => { test('fails to create on inactive wallet address', async () => { const { id: quoteId } = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -981,6 +1012,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(!walletAddressUpdated.isActive) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId: walletAddress.id, quoteId }) @@ -997,6 +1029,7 @@ describe('OutgoingPaymentService', (): void => { const quotes = await Promise.all( [0, 1].map(async (_) => { return await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -1006,6 +1039,7 @@ describe('OutgoingPaymentService', (): void => { ) const options = quotes.map((quote) => { return { + tenantId, walletAddressId, client, quoteId: quote.id, @@ -1048,12 +1082,14 @@ describe('OutgoingPaymentService', (): void => { let interval: string beforeEach(async (): Promise => { quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, method: 'ilp' }) options = { + tenantId, walletAddressId, quoteId: quote.id, metadata: { @@ -1152,6 +1188,7 @@ describe('OutgoingPaymentService', (): void => { value: BigInt(190) } const firstPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver: `${ @@ -1239,6 +1276,7 @@ describe('OutgoingPaymentService', (): void => { value: BigInt(7) } const firstPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver: `${ @@ -1321,6 +1359,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(incomingPayment.walletAddress) const createdPayment = await setup({ + tenantId, receiver: incomingPayment.getUrl(incomingPayment.walletAddress), receiveAmount, method: 'ilp' @@ -1353,6 +1392,7 @@ describe('OutgoingPaymentService', (): void => { .spyOn(telemetryService!, 'incrementCounter') .mockImplementation(() => Promise.resolve()) const createdPayment = await setup({ + tenantId, receiver, debitAmount, method: 'ilp' @@ -1402,6 +1442,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(incomingPayment.walletAddress) const createdPayment = await setup({ + tenantId, receiver: incomingPayment.getUrl(incomingPayment.walletAddress), receiveAmount, method: 'ilp' @@ -1437,6 +1478,7 @@ describe('OutgoingPaymentService', (): void => { const spyCounter = jest.spyOn(telemetryService, 'incrementCounter') const createdPayment = await setup({ + tenantId, receiver, debitAmount, receiveAmount, @@ -1490,6 +1532,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(incomingPayment.receivedAmount?.assetScale) const createdPayment = await setup({ + tenantId, receiver: incomingPayment.getUrl(incomingPayment.walletAddress), receiveAmount, method: 'ilp' @@ -1512,6 +1555,7 @@ describe('OutgoingPaymentService', (): void => { test('COMPLETED (with incoming payment initially partially paid)', async (): Promise => { const createdPayment = await setup( { + tenantId, receiver, receiveAmount, method: 'ilp' @@ -1549,6 +1593,7 @@ describe('OutgoingPaymentService', (): void => { test('SENDING -> FAILED (partial payment then retryable Pay error)', async (): Promise => { const createdPayment = await setup({ + tenantId, receiver, debitAmount, method: 'ilp' @@ -1599,6 +1644,7 @@ describe('OutgoingPaymentService', (): void => { test('FAILED (non-retryable error)', async (): Promise => { const createdPayment = await setup({ + tenantId, receiver, debitAmount, method: 'ilp' @@ -1630,6 +1676,7 @@ describe('OutgoingPaymentService', (): void => { test('SENDING→COMPLETED (partial payment, resume, complete)', async (): Promise => { const createdPayment = await setup({ + tenantId, receiver, receiveAmount, method: 'ilp' @@ -1661,6 +1708,7 @@ describe('OutgoingPaymentService', (): void => { test('COMPLETED (already fully paid)', async (): Promise => { const createdPayment = await setup( { + tenantId, receiver, receiveAmount, method: 'ilp' @@ -1692,6 +1740,7 @@ describe('OutgoingPaymentService', (): void => { test('COMPLETED (already fully paid)', async (): Promise => { const { id: paymentId } = await setup( { + tenantId, receiver, receiveAmount, method: 'ilp' @@ -1717,6 +1766,7 @@ describe('OutgoingPaymentService', (): void => { test('FAILED (source asset changed)', async (): Promise => { const { id: paymentId } = await setup( { + tenantId, receiver, receiveAmount, method: 'ilp' @@ -1741,6 +1791,7 @@ describe('OutgoingPaymentService', (): void => { }) test('FAILED (destination asset changed)', async (): Promise => { const createdPayment = await setup({ + tenantId, receiver, debitAmount, method: 'ilp' @@ -1769,6 +1820,7 @@ describe('OutgoingPaymentService', (): void => { beforeEach(async (): Promise => { payment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, receiver, debitAmount, diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 54ede1accb..903165facb 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -167,6 +167,7 @@ async function getOutgoingPayment( } export interface BaseOptions { + tenantId: string walletAddressId: string client?: string grant?: Grant @@ -256,6 +257,7 @@ async function createOutgoingPayment( ) const { debitAmount, incomingPayment } = options const quoteOrError = await deps.quoteService.create({ + tenantId: options.tenantId, receiver: incomingPayment, debitAmount, method: 'ilp', @@ -367,7 +369,11 @@ async function createOutgoingPayment( description: 'Time to retrieve receiver in outgoing payment' } ) - const receiver = await deps.receiverService.get(payment.receiver) + //TODO tenantId should not be taken from `options` but from `payment` + const receiver = await deps.receiverService.get( + payment.receiver, + options.tenantId + ) stopTimerReceiver() if (!receiver) { throw OutgoingPaymentError.InvalidQuote diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index 3c6bd6d135..a05352e228 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -7,6 +7,7 @@ import { import { Asset } from '../../asset/model' import { Quote as OpenPaymentsQuote } from '@interledger/open-payments' import { Fee } from '../../fee/model' +import { Tenant } from '../../tenants/model' export class Quote extends WalletAddressSubresource { public static readonly tableName = 'quotes' @@ -26,6 +27,8 @@ export class Quote extends WalletAddressSubresource { public debitAmountMinusFees?: bigint + public tenantId!: string + static get relationMappings() { return { ...super.relationMappings, @@ -44,6 +47,14 @@ export class Quote extends WalletAddressSubresource { from: 'quotes.feeId', to: 'fees.id' } + }, + tenant: { + relation: Model.BelongsToOneRelation, + modelClass: Tenant, + join: { + from: 'quotes.tenantId', + to: 'tenants.id' + } } } } @@ -56,7 +67,7 @@ export class Quote extends WalletAddressSubresource { public getUrl(walletAddress: WalletAddress): string { const url = new URL(walletAddress.url) - return `${url.origin}${Quote.urlPath}/${this.id}` + return `${url.origin}/${this.tenantId}${Quote.urlPath}/${this.id}` } public get debitAmount(): Amount { diff --git a/packages/backend/src/open_payments/quote/routes.test.ts b/packages/backend/src/open_payments/quote/routes.test.ts index 422759bd73..10588bb4b3 100644 --- a/packages/backend/src/open_payments/quote/routes.test.ts +++ b/packages/backend/src/open_payments/quote/routes.test.ts @@ -31,6 +31,7 @@ describe('Quote Routes', (): void => { let walletAddress: WalletAddress let baseUrl: string + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const receiver = `https://wallet2.example/incoming-payments/${uuid()}` const asset = randomAsset() const debitAmount: Amount = { @@ -47,6 +48,7 @@ describe('Quote Routes', (): void => { client?: string }): Promise => { return await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount: { @@ -101,7 +103,7 @@ describe('Quote Routes', (): void => { get: (ctx) => quoteRoutes.get(ctx), getBody: (quote) => { return { - id: `${baseUrl}/quotes/${quote.id}`, + id: `${baseUrl}/${quote.tenantId}/quotes/${quote.id}`, walletAddress: walletAddress.url, receiver: quote.receiver, debitAmount: serializeAmount(quote.debitAmount), @@ -129,6 +131,9 @@ describe('Quote Routes', (): void => { method: 'POST', url: `/quotes` }, + params: { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100' + }, walletAddress, client }) @@ -194,6 +199,7 @@ describe('Quote Routes', (): void => { }) await expect(quoteRoutes.create(ctx)).resolves.toBeUndefined() expect(quoteSpy).toHaveBeenCalledWith({ + tenantId, walletAddressId: walletAddress.id, receiver, debitAmount: options.debitAmount && { @@ -215,7 +221,7 @@ describe('Quote Routes', (): void => { .pop() assert.ok(quote) expect(ctx.response.body).toEqual({ - id: `${baseUrl}/quotes/${quoteId}`, + id: `${baseUrl}/${tenantId}/quotes/${quoteId}`, walletAddress: walletAddress.url, receiver: quote.receiver, debitAmount: { @@ -246,6 +252,7 @@ describe('Quote Routes', (): void => { .mockImplementationOnce(async (opts) => { quote = await createQuote(deps, { ...opts, + tenantId, validDestination: false, client }) @@ -253,6 +260,7 @@ describe('Quote Routes', (): void => { }) await expect(quoteRoutes.create(ctx)).resolves.toBeUndefined() expect(quoteSpy).toHaveBeenCalledWith({ + tenantId, walletAddressId: walletAddress.id, receiver, client, @@ -266,7 +274,7 @@ describe('Quote Routes', (): void => { .pop() assert.ok(quote) expect(ctx.response.body).toEqual({ - id: `${baseUrl}/quotes/${quoteId}`, + id: `${baseUrl}/${tenantId}/quotes/${quoteId}`, walletAddress: walletAddress.url, receiver: options.receiver, debitAmount: { diff --git a/packages/backend/src/open_payments/quote/routes.ts b/packages/backend/src/open_payments/quote/routes.ts index d0069280fc..86212743c8 100644 --- a/packages/backend/src/open_payments/quote/routes.ts +++ b/packages/backend/src/open_payments/quote/routes.ts @@ -38,7 +38,8 @@ async function getQuote( ): Promise { const quote = await deps.quoteService.get({ id: ctx.params.id, - client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined + client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined, + tenantId: ctx.params.tenantId }) if (!quote) { @@ -73,7 +74,9 @@ async function createQuote( ctx: CreateContext ): Promise { const { body } = ctx.request + const { tenantId } = ctx.params const options: CreateQuoteOptions = { + tenantId, walletAddressId: ctx.walletAddress.id, receiver: body.receiver, client: ctx.client, diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 3840212c2e..7bb3474daa 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -53,6 +53,7 @@ describe('QuoteService', (): void => { // eslint-disable-next-line @typescript-eslint/no-explicit-any any > + let tenantId: string const asset: AssetOptions = { scale: 9, @@ -91,6 +92,8 @@ describe('QuoteService', (): void => { }) beforeEach(async (): Promise => { + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' + const { id: sendAssetId } = await createAsset(deps, { code: debitAmount.assetCode, scale: debitAmount.assetScale @@ -108,9 +111,9 @@ describe('QuoteService', (): void => { receiverGet = receiverService.get receiverGetSpy = jest .spyOn(receiverService, 'get') - .mockImplementation(async (url: string) => { + .mockImplementation(async (url: string, tenantId: string) => { // call original instead of receiverService.get to avoid infinite loop - const receiver = await receiverGet.call(receiverService, url) + const receiver = await receiverGet.call(receiverService, url, tenantId) if (receiver) { // "as any" to circumvent "readonly" check (compile time only) to allow overriding "isLocal" here // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -135,6 +138,7 @@ describe('QuoteService', (): void => { getTests({ createModel: ({ client }) => createQuote(deps, { + tenantId, walletAddressId: sendingWalletAddress.id, receiver: `${receivingWalletAddress.url}/incoming-payments/${uuid()}`, debitAmount: { @@ -179,6 +183,7 @@ describe('QuoteService', (): void => { incomingAmount }) options = { + tenantId, walletAddressId: sendingWalletAddress.id, receiver: incomingPayment.getUrl(receivingWalletAddress), method: 'ilp' @@ -204,7 +209,8 @@ describe('QuoteService', (): void => { async ({ client }): Promise => { const mockedQuote = mockQuote({ receiver: (await receiverService.get( - incomingPayment.getUrl(receivingWalletAddress) + incomingPayment.getUrl(receivingWalletAddress), + tenantId ))!, walletAddress: sendingWalletAddress, exchangeRate: 0.5, @@ -254,6 +260,7 @@ describe('QuoteService', (): void => { await expect( quoteService.get({ + tenantId, id: quote.id }) ).resolves.toEqual(quote) @@ -264,7 +271,8 @@ describe('QuoteService', (): void => { test('fails if receiveAmount exceeds receiver.incomingAmount', async (): Promise => { const mockedQuote = mockQuote({ receiver: (await receiverService.get( - incomingPayment.getUrl(receivingWalletAddress) + incomingPayment.getUrl(receivingWalletAddress), + tenantId ))!, walletAddress: sendingWalletAddress, exchangeRate: 0.5, @@ -303,7 +311,8 @@ describe('QuoteService', (): void => { async ({ client }): Promise => { const mockedQuote = mockQuote({ receiver: (await receiverService.get( - incomingPayment.getUrl(receivingWalletAddress) + incomingPayment.getUrl(receivingWalletAddress), + tenantId ))!, walletAddress: sendingWalletAddress, exchangeRate: 0.5, @@ -340,6 +349,7 @@ describe('QuoteService', (): void => { await expect( quoteService.get({ + tenantId, id: quote.id }) ).resolves.toEqual(quote) @@ -383,6 +393,7 @@ describe('QuoteService', (): void => { expiresAt: expiryDate }) const options: CreateQuoteOptions = { + tenantId, walletAddressId: sendingWalletAddress.id, receiver: incomingPayment.getUrl(receivingWalletAddress), receiveAmount, @@ -391,7 +402,8 @@ describe('QuoteService', (): void => { const mockedQuote = mockQuote({ receiver: (await receiverService.get( - incomingPayment.getUrl(receivingWalletAddress) + incomingPayment.getUrl(receivingWalletAddress), + tenantId ))!, walletAddress: sendingWalletAddress, receiveAmountValue: receiveAmount.value, @@ -425,6 +437,7 @@ describe('QuoteService', (): void => { test('fails on unknown wallet address', async (): Promise => { await expect( quoteService.create({ + tenantId, walletAddressId: uuid(), receiver: `${receivingWalletAddress.url}/incoming-payments/${uuid()}`, debitAmount, @@ -441,6 +454,7 @@ describe('QuoteService', (): void => { assert.ok(!walletAddressUpdated.isActive) await expect( quoteService.create({ + tenantId, walletAddressId: walletAddress.id, receiver: `${receivingWalletAddress.url}/incoming-payments/${uuid()}`, debitAmount, @@ -452,6 +466,7 @@ describe('QuoteService', (): void => { test('fails on invalid receiver', async (): Promise => { await expect( quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: `${receivingWalletAddress.url}/incoming-payments/${uuid()}`, debitAmount, @@ -474,6 +489,7 @@ describe('QuoteService', (): void => { await expect( quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: receiver.incomingPayment!.id, method: 'ilp', @@ -502,6 +518,7 @@ describe('QuoteService', (): void => { walletAddressId: receivingWalletAddress.id }) const options: CreateQuoteOptions = { + tenantId, walletAddressId: sendingWalletAddress.id, receiver: incomingPayment.getUrl(receivingWalletAddress), method: 'ilp' @@ -572,6 +589,7 @@ describe('QuoteService', (): void => { .mockResolvedValueOnce(mockedQuote) const quote = await quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: receiver.incomingPayment!.id, method: 'ilp' @@ -609,6 +627,7 @@ describe('QuoteService', (): void => { await expect( quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: receiver.incomingPayment!.id, method: 'ilp' @@ -679,6 +698,7 @@ describe('QuoteService', (): void => { .mockResolvedValueOnce(mockedQuote) const quote = await quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: receiver.incomingPayment!.id, debitAmount: { @@ -722,6 +742,7 @@ describe('QuoteService', (): void => { await expect( quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: receiver.incomingPayment!.id, debitAmount: { @@ -746,6 +767,7 @@ describe('QuoteService', (): void => { }) const options: CreateQuoteOptions = { + tenantId, walletAddressId: sendingWalletAddress.id, receiver: incomingPayment.getUrl(receivingWalletAddress), method: 'ilp' @@ -753,7 +775,8 @@ describe('QuoteService', (): void => { const mockedQuote = mockQuote({ receiver: (await receiverService.get( - incomingPayment.getUrl(receivingWalletAddress) + incomingPayment.getUrl(receivingWalletAddress), + tenantId ))!, walletAddress: sendingWalletAddress, exchangeRate: 0.5, @@ -791,6 +814,7 @@ describe('QuoteService', (): void => { await expect( quoteService.get({ + tenantId, id: quote.id }) ).resolves.toEqual(quote) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 957c4065f4..6407a733d8 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -71,6 +71,7 @@ async function getQuote( } interface QuoteOptionsBase { + tenantId: string walletAddressId: string receiver: string method: 'ilp' @@ -99,6 +100,14 @@ async function createQuote( callName: 'QuoteService:create', description: 'Time to create a quote' }) + // TODO + // const tenant = await deps.tenantService.get( + // options.tenantId + // ) + // if (!tenant) { + // stopTimer() + // return QuoteError.UnknownTenant + // } if (options.debitAmount && options.receiveAmount) { stopTimer() return QuoteError.InvalidAmount @@ -164,6 +173,7 @@ async function createQuote( paymentMethod, { quoteId, + tenantId: options.tenantId, walletAddress, receiver, receiveAmount: options.receiveAmount, @@ -189,6 +199,7 @@ async function createQuote( const createdQuote = await Quote.query(trx) .insertAndFetch({ id: quoteId, + tenantId: options.tenantId, walletAddressId: options.walletAddressId, assetId: walletAddress.assetId, receiver: options.receiver, @@ -251,7 +262,10 @@ export async function resolveReceiver( deps: ServiceDependencies, options: CreateQuoteOptions ): Promise { - const receiver = await deps.receiverService.get(options.receiver) + const receiver = await deps.receiverService.get( + options.receiver, + options.tenantId + ) if (!receiver) { deps.logger.info( { receiver: options.receiver }, diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index ed3d05ebd7..bd09505c4a 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -80,6 +80,7 @@ describe('Receiver Service', (): void => { describe('get', () => { describe('local incoming payment', () => { test('resolves local incoming payment', async () => { + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const walletAddress = await createWalletAddress(deps, { mockServerPort: Config.openPaymentsPort }) @@ -93,7 +94,7 @@ describe('Receiver Service', (): void => { }) await expect( - receiverService.get(incomingPayment.getUrl(walletAddress)) + receiverService.get(incomingPayment.getUrl(walletAddress), tenantId) ).resolves.toEqual({ assetCode: incomingPayment.receivedAmount.assetCode, assetScale: incomingPayment.receivedAmount.assetScale, @@ -191,6 +192,7 @@ describe('Receiver Service', (): void => { describe('remote incoming payment', () => { test('gets receiver from remote incoming payment', async () => { const mockedIncomingPayment = mockIncomingPaymentWithPaymentMethods() + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' jest .spyOn(incomingPaymentService, 'get') @@ -201,7 +203,7 @@ describe('Receiver Service', (): void => { .mockResolvedValueOnce(mockedIncomingPayment) await expect( - receiverService.get(mockedIncomingPayment.id) + receiverService.get(mockedIncomingPayment.id, tenantId) ).resolves.toEqual({ assetCode: mockedIncomingPayment.receivedAmount.assetCode, assetScale: mockedIncomingPayment.receivedAmount.assetScale, @@ -235,6 +237,7 @@ describe('Receiver Service', (): void => { test('returns undefined if could not get remote incoming payment', async () => { const mockedIncomingPayment = mockIncomingPaymentWithPaymentMethods() + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const localIncomingPaymentServiceGetSpy = jest .spyOn(incomingPaymentService, 'get') @@ -245,7 +248,7 @@ describe('Receiver Service', (): void => { .mockResolvedValueOnce(RemoteIncomingPaymentError.InvalidGrant) await expect( - receiverService.get(mockedIncomingPayment.id) + receiverService.get(mockedIncomingPayment.id, tenantId) ).resolves.toBeUndefined() expect(localIncomingPaymentServiceGetSpy).toHaveBeenCalledTimes(1) expect(remoteIncomingPaymentServiceGetSpy).toHaveBeenCalledTimes(1) @@ -255,6 +258,7 @@ describe('Receiver Service', (): void => { const mockedIncomingPayment = mockIncomingPaymentWithPaymentMethods({ completed: true // cannot get receiver with a completed incoming payment }) + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const localIncomingPaymentServiceGetSpy = jest .spyOn(incomingPaymentService, 'get') @@ -265,7 +269,7 @@ describe('Receiver Service', (): void => { .mockResolvedValueOnce(mockedIncomingPayment) await expect( - receiverService.get(mockedIncomingPayment.id) + receiverService.get(mockedIncomingPayment.id, tenantId) ).resolves.toBeUndefined() expect(localIncomingPaymentServiceGetSpy).toHaveBeenCalledTimes(1) expect(remoteIncomingPaymentServiceGetSpy).toHaveBeenCalledTimes(1) @@ -415,7 +419,6 @@ describe('Receiver Service', (): void => { incomingPaymentService, 'create' ) - const receiver = await receiverService.create({ walletAddressUrl: walletAddress.id, incomingAmount, diff --git a/packages/backend/src/open_payments/receiver/service.ts b/packages/backend/src/open_payments/receiver/service.ts index d93c902fda..772b49e013 100644 --- a/packages/backend/src/open_payments/receiver/service.ts +++ b/packages/backend/src/open_payments/receiver/service.ts @@ -25,7 +25,7 @@ interface CreateReceiverArgs { // A receiver is resolved from an incoming payment export interface ReceiverService { - get(url: string): Promise + get(url: string, tenantId: string): Promise create(args: CreateReceiverArgs): Promise } @@ -52,7 +52,7 @@ export async function createReceiverService( } return { - get: (url) => getReceiver(deps, url), + get: (url, tenantId) => getReceiver(deps, url, tenantId), create: (url) => createReceiver(deps, url) } } @@ -136,7 +136,8 @@ async function createLocalIncomingPayment( async function getReceiver( deps: ServiceDependencies, - url: string + url: string, + tenantId: string ): Promise { const stopTimer = deps.telemetry.startTimer('getReceiver', { callName: 'ReceiverService:get' @@ -148,7 +149,11 @@ async function getReceiver( return receiver } - const remoteIncomingPayment = await getRemoteIncomingPayment(deps, url) + const remoteIncomingPayment = await getRemoteIncomingPayment( + deps, + url, + tenantId + ) if (remoteIncomingPayment) { const receiver = new Receiver(remoteIncomingPayment, false) return receiver @@ -220,10 +225,13 @@ export async function getLocalIncomingPayment( async function getRemoteIncomingPayment( deps: ServiceDependencies, - url: string + url: string, + tenantId: string ): Promise { - const incomingPaymentOrError = - await deps.remoteIncomingPaymentService.get(url) + const incomingPaymentOrError = await deps.remoteIncomingPaymentService.get( + url, + tenantId + ) if (isRemoteIncomingPaymentError(incomingPaymentOrError)) { return undefined diff --git a/packages/backend/src/open_payments/wallet_address/model.test.ts b/packages/backend/src/open_payments/wallet_address/model.test.ts index 6aaeb79184..ee8d1ae346 100644 --- a/packages/backend/src/open_payments/wallet_address/model.test.ts +++ b/packages/backend/src/open_payments/wallet_address/model.test.ts @@ -247,6 +247,7 @@ export const getRouteTests = ({ createModel, testGet: async ({ id, walletAddressId, client }, expectedMatch) => { const walletAddress = await getWalletAddress() + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' walletAddress.id = walletAddressId const ctx = setup({ reqOpts: { @@ -255,7 +256,8 @@ export const getRouteTests = ({ url: `${urlPath}/${id}` }, params: { - id + id, + tenantId }, walletAddress, client, diff --git a/packages/backend/src/open_payments/wallet_address/model.ts b/packages/backend/src/open_payments/wallet_address/model.ts index 81dd603a1d..959ffb0441 100644 --- a/packages/backend/src/open_payments/wallet_address/model.ts +++ b/packages/backend/src/open_payments/wallet_address/model.ts @@ -180,6 +180,7 @@ export interface GetOptions { id: string client?: string walletAddressId?: string + tenantId?: string } export interface ListOptions { diff --git a/packages/backend/src/payment-method/handler/service.test.ts b/packages/backend/src/payment-method/handler/service.test.ts index 254963967c..a15892d060 100644 --- a/packages/backend/src/payment-method/handler/service.test.ts +++ b/packages/backend/src/payment-method/handler/service.test.ts @@ -52,6 +52,7 @@ describe('PaymentMethodHandlerService', (): void => { const options: StartQuoteOptions = { quoteId: uuid(), + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress, receiver: await createReceiver(deps, walletAddress), debitAmount: { @@ -79,6 +80,7 @@ describe('PaymentMethodHandlerService', (): void => { }) const options: StartQuoteOptions = { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress, receiver: await createReceiver(deps, walletAddress), debitAmount: { @@ -113,6 +115,7 @@ describe('PaymentMethodHandlerService', (): void => { receivingWalletAddress: walletAddress, method: 'ilp', quoteOptions: { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', debitAmount: { assetCode: walletAddress.asset.code, assetScale: walletAddress.asset.scale, @@ -147,6 +150,7 @@ describe('PaymentMethodHandlerService', (): void => { receivingWalletAddress: walletAddress, method: 'ilp', quoteOptions: { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', debitAmount: { assetCode: walletAddress.asset.code, assetScale: walletAddress.asset.scale, diff --git a/packages/backend/src/payment-method/handler/service.ts b/packages/backend/src/payment-method/handler/service.ts index 281007bee9..88a6f899ef 100644 --- a/packages/backend/src/payment-method/handler/service.ts +++ b/packages/backend/src/payment-method/handler/service.ts @@ -9,6 +9,7 @@ import { Transaction } from 'objection' export interface StartQuoteOptions { quoteId?: string + tenantId?: string walletAddress: WalletAddress debitAmount?: Amount receiveAmount?: Amount diff --git a/packages/backend/src/payment-method/ilp/service.test.ts b/packages/backend/src/payment-method/ilp/service.test.ts index b2f37b7025..47f6c4d3e9 100644 --- a/packages/backend/src/payment-method/ilp/service.test.ts +++ b/packages/backend/src/payment-method/ilp/service.test.ts @@ -95,6 +95,7 @@ describe('IlpPaymentService', (): void => { const options: StartQuoteOptions = { quoteId: uuid(), + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']), debitAmount: { @@ -118,6 +119,7 @@ describe('IlpPaymentService', (): void => { const quoteId = uuid() const options: StartQuoteOptions = { quoteId, + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']), debitAmount: { @@ -180,6 +182,7 @@ describe('IlpPaymentService', (): void => { const quoteId = uuid() const options: StartQuoteOptions = { quoteId, + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']), debitAmount: { @@ -298,6 +301,7 @@ describe('IlpPaymentService', (): void => { const options: StartQuoteOptions = { quoteId: uuid(), + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']), debitAmount: { @@ -336,6 +340,7 @@ describe('IlpPaymentService', (): void => { const options: StartQuoteOptions = { quoteId: uuid(), + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD'], { incomingAmount @@ -397,6 +402,7 @@ describe('IlpPaymentService', (): void => { const options: StartQuoteOptions = { quoteId: uuid(), + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']) } @@ -427,6 +433,7 @@ describe('IlpPaymentService', (): void => { const options: StartQuoteOptions = { quoteId: uuid(), + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD'], { incomingAmount: { @@ -467,6 +474,7 @@ describe('IlpPaymentService', (): void => { const options: StartQuoteOptions = { quoteId: uuid(), + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']) } @@ -528,6 +536,7 @@ describe('IlpPaymentService', (): void => { const options: StartQuoteOptions = { quoteId: uuid(), + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress: sendingWalletAddress, receiver: await createReceiver(deps, receivingWalletAddress), receiveAmount: { @@ -588,6 +597,7 @@ describe('IlpPaymentService', (): void => { const options: StartQuoteOptions = { quoteId: uuid(), + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddress: sendingWalletAddress, receiver: await createReceiver(deps, receivingWalletAddress), debitAmount: { @@ -665,6 +675,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -695,6 +706,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', exchangeRate: 1, debitAmount: { value: 100n, @@ -745,6 +757,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -785,6 +798,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -825,6 +839,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -862,6 +877,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 94cff4100e..b71728b46b 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -34,6 +34,7 @@ describe('LocalPaymentService', (): void => { let accountingService: AccountingService let incomingPaymentService: IncomingPaymentService + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const exchangeRatesUrl = 'https://example-rates.com' const assetMap: Record = {} @@ -149,6 +150,7 @@ describe('LocalPaymentService', (): void => { test('returns all fields correctly', async (): Promise => { const options: StartQuoteOptions = { + tenantId, walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']), debitAmount: { @@ -236,6 +238,7 @@ describe('LocalPaymentService', (): void => { } const options: StartQuoteOptions = { + tenantId, walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD'], { incomingAmount @@ -282,6 +285,7 @@ describe('LocalPaymentService', (): void => { const sendingWalletAddress = walletAddressMap[debitAssetCode] const options: StartQuoteOptions = { + tenantId, walletAddress: sendingWalletAddress, receiver: await createReceiver(deps, receivingWalletAddress), receiveAmount: { @@ -338,6 +342,7 @@ describe('LocalPaymentService', (): void => { const sendingWalletAddress = walletAddressMap[debitAssetCode] const options: StartQuoteOptions = { + tenantId, walletAddress: sendingWalletAddress, receiver: await createReceiver(deps, receivingWalletAddress), debitAmount: { @@ -395,6 +400,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -425,6 +431,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -462,6 +469,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -501,6 +509,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -540,6 +549,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -579,6 +589,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -619,6 +630,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, diff --git a/packages/backend/src/shared/pagination.test.ts b/packages/backend/src/shared/pagination.test.ts index 18be4c5541..8bdbf60346 100644 --- a/packages/backend/src/shared/pagination.test.ts +++ b/packages/backend/src/shared/pagination.test.ts @@ -73,6 +73,7 @@ describe('Pagination', (): void => { }) describe('getPageInfo', (): void => { describe('wallet address resources', (): void => { + let tenantId: string let defaultWalletAddress: WalletAddress let secondaryWalletAddress: WalletAddress let debitAmount: Amount @@ -82,6 +83,7 @@ describe('Pagination', (): void => { outgoingPaymentService = await deps.use('outgoingPaymentService') quoteService = await deps.use('quoteService') + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const asset = await createAsset(deps) defaultWalletAddress = await createWalletAddress(deps, { assetId: asset.id @@ -171,6 +173,7 @@ describe('Pagination', (): void => { const paymentIds: string[] = [] for (let i = 0; i < num; i++) { const payment = await createOutgoingPayment(deps, { + tenantId, walletAddressId: defaultWalletAddress.id, receiver: secondaryWalletAddress.url, method: 'ilp', @@ -228,6 +231,7 @@ describe('Pagination', (): void => { const quoteIds: string[] = [] for (let i = 0; i < num; i++) { const quote = await createQuote(deps, { + tenantId, walletAddressId: defaultWalletAddress.id, receiver: secondaryWalletAddress.url, debitAmount, diff --git a/packages/backend/src/tests/combinedPayment.ts b/packages/backend/src/tests/combinedPayment.ts index 504aceeb97..180392ff17 100644 --- a/packages/backend/src/tests/combinedPayment.ts +++ b/packages/backend/src/tests/combinedPayment.ts @@ -39,6 +39,7 @@ export async function createCombinedPayment( ): Promise { const sendAsset = await createAsset(deps) const receiveAsset = await createAsset(deps) + const tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' const sendWalletAddressId = ( await createWalletAddress(deps, { assetId: sendAsset.id }) ).id @@ -53,6 +54,7 @@ export async function createCombinedPayment( walletAddressId: receiveWalletAddress.id }) : await createOutgoingPayment(deps, { + tenantId, walletAddressId: sendWalletAddressId, method: 'ilp', receiver: `${Config.openPaymentsUrl}/${uuid()}`, diff --git a/packages/backend/src/tests/outgoingPayment.ts b/packages/backend/src/tests/outgoingPayment.ts index 451f33e6bb..8c1a98c053 100644 --- a/packages/backend/src/tests/outgoingPayment.ts +++ b/packages/backend/src/tests/outgoingPayment.ts @@ -24,6 +24,7 @@ export async function createOutgoingPayment( options: CreateTestQuoteAndOutgoingPaymentOptions ): Promise { const quoteOptions: CreateTestQuoteOptions = { + tenantId: options.tenantId, walletAddressId: options.walletAddressId, client: options.client, receiver: options.receiver, @@ -85,7 +86,7 @@ interface CreateOutgoingPaymentWithReceiverArgs { quoteOptions?: Partial< Pick< CreateTestQuoteAndOutgoingPaymentOptions, - 'debitAmount' | 'receiveAmount' | 'exchangeRate' + 'debitAmount' | 'receiveAmount' | 'exchangeRate' | 'tenantId' > > sendingWalletAddress: WalletAddress @@ -130,6 +131,9 @@ export async function createOutgoingPaymentWithReceiver( ) const outgoingPayment = await createOutgoingPayment(deps, { + //TODO + tenantId: + args.quoteOptions?.tenantId || '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddressId: args.sendingWalletAddress.id, method: args.method, receiver: receiver.incomingPayment!.id!, diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index 26c0928619..b8d5f72160 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -57,6 +57,7 @@ export function mockQuote( export async function createQuote( deps: IocContract, { + tenantId, walletAddressId, receiver: receiverUrl, debitAmount, @@ -69,6 +70,18 @@ export async function createQuote( exchangeRate = 0.5 }: CreateTestQuoteOptions ): Promise { + if (!tenantId) { + throw new Error('Invalid tenant id') + } + // TODO remove this + const knex = await deps.use('knex') + const tenantResult = await knex.raw('SELECT * FROM tenants WHERE id = ?', [ + tenantId + ]) + + if (!tenantResult.rows.length) { + throw new Error(`Tenant with id ${tenantId} does not exist.`) + } const walletAddressService = await deps.use('walletAddressService') const walletAddress = await walletAddressService.get(walletAddressId) if (!walletAddress) { @@ -86,7 +99,7 @@ export async function createQuote( let receiveAsset: AssetOptions | undefined if (validDestination) { const receiverService = await deps.use('receiverService') - const receiver = await receiverService.get(receiverUrl) + const receiver = await receiverService.get(receiverUrl, tenantId) if (!receiver) { throw new Error('receiver not found') } @@ -178,6 +191,7 @@ export async function createQuote( return Quote.query() .insertAndFetch({ id: quoteId, + tenantId, walletAddressId, assetId: walletAddress.assetId, receiver: receiverUrl, diff --git a/packages/backend/src/tests/tableManager.ts b/packages/backend/src/tests/tableManager.ts index 26f07d5d2d..90b950acbd 100644 --- a/packages/backend/src/tests/tableManager.ts +++ b/packages/backend/src/tests/tableManager.ts @@ -11,6 +11,8 @@ export async function truncateTable( export async function truncateTables( knex: Knex, ignoreTables = [ + // TODO do not ignore tenants table + 'tenants', 'knex_migrations', 'knex_migrations_lock', 'knex_migrations_backend', diff --git a/packages/backend/src/webhook/service.test.ts b/packages/backend/src/webhook/service.test.ts index bb33fdec72..2562618c01 100644 --- a/packages/backend/src/webhook/service.test.ts +++ b/packages/backend/src/webhook/service.test.ts @@ -109,6 +109,7 @@ describe('Webhook Service', (): void => { }) describe('Get Webhook Event by account id and types', (): void => { + let tenantId: string let walletAddressIn: WalletAddress let walletAddressOut: WalletAddress let incomingPaymentIds: string[] @@ -116,6 +117,7 @@ describe('Webhook Service', (): void => { let events: WebhookEvent[] = [] beforeEach(async (): Promise => { + tenantId = '8e1db008-ab2f-4f1d-8c44-593354084100' walletAddressIn = await createWalletAddress(deps) walletAddressOut = await createWalletAddress(deps) incomingPaymentIds = [ @@ -134,6 +136,7 @@ describe('Webhook Service', (): void => { ( await createOutgoingPayment(deps, { method: 'ilp', + tenantId, walletAddressId: walletAddressOut.id, receiver: '', validDestination: false @@ -142,6 +145,7 @@ describe('Webhook Service', (): void => { ( await createOutgoingPayment(deps, { method: 'ilp', + tenantId, walletAddressId: walletAddressOut.id, receiver: '', validDestination: false diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 4bc2279d6f..5ba8a6d3ff 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -337,6 +337,8 @@ export type CreateQuoteInput = { receiveAmount?: InputMaybe; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['input']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['String']['input']; /** Unique identifier of the wallet address under which the quote will be created. */ walletAddressId: Scalars['String']['input']; }; @@ -1142,7 +1144,7 @@ export type Query = { peerByAddressAndAsset?: Maybe; /** Fetch a paginated list of peers. */ peers: PeersConnection; - /** Fetch an Open Payments quote by its ID. */ + /** Fetch an Open Payments quote by its ID and tenant ID. */ quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; @@ -1235,11 +1237,13 @@ export type QueryPeersArgs = { export type QueryQuoteArgs = { id: Scalars['String']['input']; + tenantId: Scalars['String']['input']; }; export type QueryReceiverArgs = { id: Scalars['String']['input']; + tenantId: Scalars['String']['input']; }; @@ -1287,6 +1291,8 @@ export type Quote = { receiveAmount: Amount; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['output']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['ID']['output']; /** Unique identifier of the wallet address under which the quote was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -2307,8 +2313,8 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; peerByAddressAndAsset?: Resolver, ParentType, ContextType, RequireFields>; peers?: Resolver>; - quote?: Resolver, ParentType, ContextType, RequireFields>; - receiver?: Resolver, ParentType, ContextType, RequireFields>; + quote?: Resolver, ParentType, ContextType, RequireFields>; + receiver?: Resolver, ParentType, ContextType, RequireFields>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; @@ -2323,6 +2329,7 @@ export type QuoteResolvers; receiveAmount?: Resolver; receiver?: Resolver; + tenantId?: Resolver; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 9437d6ec24..915af77536 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -337,6 +337,8 @@ export type CreateQuoteInput = { receiveAmount?: InputMaybe; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['input']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['String']['input']; /** Unique identifier of the wallet address under which the quote will be created. */ walletAddressId: Scalars['String']['input']; }; @@ -1142,7 +1144,7 @@ export type Query = { peerByAddressAndAsset?: Maybe; /** Fetch a paginated list of peers. */ peers: PeersConnection; - /** Fetch an Open Payments quote by its ID. */ + /** Fetch an Open Payments quote by its ID and tenant ID. */ quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; @@ -1235,11 +1237,13 @@ export type QueryPeersArgs = { export type QueryQuoteArgs = { id: Scalars['String']['input']; + tenantId: Scalars['String']['input']; }; export type QueryReceiverArgs = { id: Scalars['String']['input']; + tenantId: Scalars['String']['input']; }; @@ -1287,6 +1291,8 @@ export type Quote = { receiveAmount: Amount; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['output']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['ID']['output']; /** Unique identifier of the wallet address under which the quote was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -2307,8 +2313,8 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; peerByAddressAndAsset?: Resolver, ParentType, ContextType, RequireFields>; peers?: Resolver>; - quote?: Resolver, ParentType, ContextType, RequireFields>; - receiver?: Resolver, ParentType, ContextType, RequireFields>; + quote?: Resolver, ParentType, ContextType, RequireFields>; + receiver?: Resolver, ParentType, ContextType, RequireFields>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; @@ -2323,6 +2329,7 @@ export type QuoteResolvers; receiveAmount?: Resolver; receiver?: Resolver; + tenantId?: Resolver; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index d83df3e03f..af93067250 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -334,6 +334,7 @@ describe('Integration tests', (): void => { const receiver = await createReceiver(createReceiverInput) const quote = await createQuote({ + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddressId: senderWalletAddressId, receiver: receiver.id }) @@ -385,6 +386,7 @@ describe('Integration tests', (): void => { assert(receiver.incomingAmount) const quote = await createQuote({ + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddressId: senderWalletAddressId, receiver: receiver.id }) @@ -478,6 +480,7 @@ describe('Integration tests', (): void => { const receiver = await createReceiver(createReceiverInput) const quote = await createQuote({ + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddressId: senderWalletAddressId, receiver: receiver.id }) @@ -522,6 +525,7 @@ describe('Integration tests', (): void => { const senderWalletAddressId = senderWalletAddress.walletAddressID const createQuoteInput = { + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddressId: senderWalletAddressId, receiver: receiver.id, debitAmount: { @@ -590,6 +594,7 @@ describe('Integration tests', (): void => { assert(receiver.incomingAmount) const quote = await createQuote({ + tenantId: '8e1db008-ab2f-4f1d-8c44-593354084100', walletAddressId: senderWalletAddressId, receiver: receiver.id }) diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 9437d6ec24..915af77536 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -337,6 +337,8 @@ export type CreateQuoteInput = { receiveAmount?: InputMaybe; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['input']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['String']['input']; /** Unique identifier of the wallet address under which the quote will be created. */ walletAddressId: Scalars['String']['input']; }; @@ -1142,7 +1144,7 @@ export type Query = { peerByAddressAndAsset?: Maybe; /** Fetch a paginated list of peers. */ peers: PeersConnection; - /** Fetch an Open Payments quote by its ID. */ + /** Fetch an Open Payments quote by its ID and tenant ID. */ quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; @@ -1235,11 +1237,13 @@ export type QueryPeersArgs = { export type QueryQuoteArgs = { id: Scalars['String']['input']; + tenantId: Scalars['String']['input']; }; export type QueryReceiverArgs = { id: Scalars['String']['input']; + tenantId: Scalars['String']['input']; }; @@ -1287,6 +1291,8 @@ export type Quote = { receiveAmount: Amount; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['output']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['ID']['output']; /** Unique identifier of the wallet address under which the quote was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -2307,8 +2313,8 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; peerByAddressAndAsset?: Resolver, ParentType, ContextType, RequireFields>; peers?: Resolver>; - quote?: Resolver, ParentType, ContextType, RequireFields>; - receiver?: Resolver, ParentType, ContextType, RequireFields>; + quote?: Resolver, ParentType, ContextType, RequireFields>; + receiver?: Resolver, ParentType, ContextType, RequireFields>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; @@ -2323,6 +2329,7 @@ export type QuoteResolvers; receiveAmount?: Resolver; receiver?: Resolver; + tenantId?: Resolver; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; };