From 606d2159eb01778dc5bed5d1d6c89ccabb5af29d Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Thu, 4 Sep 2025 12:29:39 +0300 Subject: [PATCH 1/8] Added bruno script + added missing env variables to local docker containers --- .../POS Service APIs/Initiate Payment.bru | 24 +++++++++++++++++++ localenv/cloud-nine-wallet/docker-compose.yml | 5 ++++ localenv/happy-life-bank/docker-compose.yml | 5 ++++ packages/point-of-sale/src/payments/routes.ts | 1 + .../point-of-sale/src/payments/service.ts | 2 +- 5 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru diff --git a/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru b/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru new file mode 100644 index 0000000000..93e383ddd5 --- /dev/null +++ b/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru @@ -0,0 +1,24 @@ +meta { + name: Initiate Payment + type: http + seq: 3 +} + +post { + url: http://localhost:3008/payment + body: json + auth: inherit +} + +body:json { + { + "card": { + "walletAddress": "http://localhost:3000/accounts/gfranklin", + "trasactionCounter": 1, + "expiry": "2025-09-13T13:00:00Z" + }, + "signature": "signature", + "value": 1, + "merchantWalletAddress": "http://localhost:3000/accounts/gfranklin" + } +} diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 47de97e5a9..274361c086 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -23,6 +23,7 @@ services: LOG_LEVEL: debug CARD_SERVICE_PORT: 3007 DATABASE_URL: postgresql://cloud_nine_wallet_card_service:cloud_nine_wallet_card_service@shared-database/cloud_nine_wallet_card_service + GRAPHQL_URL: http://cloud-nine-wallet-backend:3001/graphql depends_on: - shared-database healthcheck: @@ -55,6 +56,10 @@ services: LOG_LEVEL: debug PORT: 3008 DATABASE_URL: postgresql://cloud_nine_wallet_point_of_sale:cloud_nine_wallet_point_of_sale@shared-database/cloud_nine_wallet_point_of_sale + TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787 + TENANT_SECRET: secret + GRAPHQL_URL: http://cloud-nine-wallet-backend:3001/graphql + WEBHOOK_SIGNATURE_SECRET: webhook_secret depends_on: - shared-database healthcheck: diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 61f4edd002..7508a65a0b 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -23,6 +23,7 @@ services: LOG_LEVEL: debug CARD_SERVICE_PORT: 4007 DATABASE_URL: postgresql://happy_life_bank_card_service:happy_life_bank_card_service@shared-database/happy_life_bank_card_service + GRAPHQL_URL: http://happy-life-bank-backend:3001/graphql depends_on: - shared-database - cloud-nine-wallet-card-service @@ -56,6 +57,10 @@ services: LOG_LEVEL: debug PORT: 4008 DATABASE_URL: postgresql://happy_life_bank_point_of_sale:happy_life_bank_point_of_sale@shared-database/happy_life_bank_point_of_sale + TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d + TENANT_SECRET: secret + GRAPHQL_URL: http://happy-life-bank-backend:3001/graphql + WEBHOOK_SIGNATURE_SECRET: webhook_secret depends_on: - shared-database - cloud-nine-wallet-point-of-sale diff --git a/packages/point-of-sale/src/payments/routes.ts b/packages/point-of-sale/src/payments/routes.ts index 0f1db5cea2..0b2df8fe07 100644 --- a/packages/point-of-sale/src/payments/routes.ts +++ b/packages/point-of-sale/src/payments/routes.ts @@ -101,6 +101,7 @@ async function payment( ctx.body = result ctx.status = 200 } catch (err) { + deps.logger.debug(err) if (err instanceof IncomingPaymentEventTimeoutError) webhookWaitMap.delete(err.incomingPaymentId) const { body, status } = handlePaymentError(err) diff --git a/packages/point-of-sale/src/payments/service.ts b/packages/point-of-sale/src/payments/service.ts index 06179314e3..7a67cf632a 100644 --- a/packages/point-of-sale/src/payments/service.ts +++ b/packages/point-of-sale/src/payments/service.ts @@ -101,7 +101,7 @@ async function getWalletAddress( ): Promise { const config: AxiosRequestConfig = { headers: { - 'Content-Type': 'application/json' + Accept: 'application/json' } } const { data: walletAddress } = await deps.axios.get< From 14728c470363b47aad1c3b820666c51b96aefd14 Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Thu, 4 Sep 2025 12:55:17 +0300 Subject: [PATCH 2/8] Added missing env variables for card-service --- localenv/cloud-nine-wallet/docker-compose.yml | 3 +++ localenv/happy-life-bank/docker-compose.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 274361c086..ae1a71e30f 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -24,6 +24,9 @@ services: CARD_SERVICE_PORT: 3007 DATABASE_URL: postgresql://cloud_nine_wallet_card_service:cloud_nine_wallet_card_service@shared-database/cloud_nine_wallet_card_service GRAPHQL_URL: http://cloud-nine-wallet-backend:3001/graphql + TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787 + TENANT_SECRET: tenant_secret + TENANT_SIGNATURE_VERSION: tenant_signature_version depends_on: - shared-database healthcheck: diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 7508a65a0b..ec80e7a0b3 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -24,6 +24,9 @@ services: CARD_SERVICE_PORT: 4007 DATABASE_URL: postgresql://happy_life_bank_card_service:happy_life_bank_card_service@shared-database/happy_life_bank_card_service GRAPHQL_URL: http://happy-life-bank-backend:3001/graphql + TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d + TENANT_SECRET: tenant_secret + TENANT_SIGNATURE_VERSION: tenant_signature_version depends_on: - shared-database - cloud-nine-wallet-card-service From b824561bd7f299470b9c247195a4225e81e7b159 Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Mon, 8 Sep 2025 13:53:48 +0300 Subject: [PATCH 3/8] Sent tenant id to backend for now, updated bruno req, fixed createIncPayment mutation --- .../POS Service APIs/Initiate Payment.bru | 2 +- packages/backend/src/app.ts | 2 +- packages/point-of-sale/src/payments/routes.ts | 4 ++- .../point-of-sale/src/payments/service.ts | 31 +++++++++++++------ 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru b/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru index 93e383ddd5..ca9b88b372 100644 --- a/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru +++ b/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru @@ -13,7 +13,7 @@ post { body:json { { "card": { - "walletAddress": "http://localhost:3000/accounts/gfranklin", + "walletAddress": "http://cloud-nine-wallet-backend/accounts/gfranklin", "trasactionCounter": 1, "expiry": "2025-09-13T13:00:00Z" }, diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 9952324002..b2b97353e4 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -454,7 +454,7 @@ export class App { // For tests, we still need to get the tenant in the middleware, but // we don't need to verify the signature nor prevent replay attacks koa.use( - this.config.env !== 'test' + this.config.env !== 'test' && this.config.env !== 'development' ? tenantSignatureMiddleware : testTenantSignatureMiddleware ) diff --git a/packages/point-of-sale/src/payments/routes.ts b/packages/point-of-sale/src/payments/routes.ts index 0b2df8fe07..e3a834ee47 100644 --- a/packages/point-of-sale/src/payments/routes.ts +++ b/packages/point-of-sale/src/payments/routes.ts @@ -62,6 +62,7 @@ async function payment( ctx: PaymentContext ): Promise { const body = ctx.request.body + const tenantId = ctx.request.header['tenant-id'] as string | undefined try { const walletAddress = await deps.paymentService.getWalletAddress( body.card.walletAddress @@ -73,7 +74,8 @@ async function payment( } const incomingPayment = await deps.paymentService.createIncomingPayment( walletAddress.id, - incomingAmount + incomingAmount, + tenantId ) const deferred = new Deferred() webhookWaitMap.setWithExpiry( diff --git a/packages/point-of-sale/src/payments/service.ts b/packages/point-of-sale/src/payments/service.ts index 7a67cf632a..fc050c142f 100644 --- a/packages/point-of-sale/src/payments/service.ts +++ b/packages/point-of-sale/src/payments/service.ts @@ -4,8 +4,8 @@ import { IAppConfig } from '../config/app' import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { AmountInput, - CreateIncomingPaymentInput, IncomingPayment, + MutationCreateIncomingPaymentArgs, type Mutation } from '../graphql/generated/graphql' import { FnWithDeps } from '../shared/types' @@ -37,7 +37,8 @@ export type WalletAddress = OpenPaymentsWalletAddress & { export type PaymentService = { createIncomingPayment: ( walletAddressId: string, - incomingAmount: AmountInput + incomingAmount: AmountInput, + tenantId?: string ) => Promise getWalletAddress: (walletAddressUrl: string) => Promise } @@ -66,19 +67,28 @@ export function createPaymentService( const createIncomingPayment: FnWithDeps< ServiceDependencies, PaymentService['createIncomingPayment'] -> = async (deps, walletAddressId, incomingAmount) => { +> = async (deps, walletAddressId, incomingAmount, tenantId) => { const client = deps.apolloClient const { data } = await client.mutate< Mutation['createIncomingPayment'], - CreateIncomingPaymentInput + MutationCreateIncomingPaymentArgs >({ mutation: CREATE_INCOMING_PAYMENT, variables: { - walletAddressId, - incomingAmount, - idempotencyKey: v4(), - isCardPayment: true - } + input: { + walletAddressId, + incomingAmount, + idempotencyKey: v4(), + isCardPayment: true + } + }, + ...(tenantId && { + context: { + headers: { + 'tenant-id': tenantId + } + } + }) }) const incomingPayment = data?.payment @@ -101,7 +111,8 @@ async function getWalletAddress( ): Promise { const config: AxiosRequestConfig = { headers: { - Accept: 'application/json' + Accept: 'application/json', + host: 'cloud-nine-wallet-backend' } } const { data: walletAddress } = await deps.axios.get< From 77b3c62e5de6cc541fef74a729cf64a2a889f3b1 Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Mon, 8 Sep 2025 13:58:37 +0300 Subject: [PATCH 4/8] Removed unnecessary header --- packages/point-of-sale/src/payments/service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/point-of-sale/src/payments/service.ts b/packages/point-of-sale/src/payments/service.ts index fc050c142f..498e687194 100644 --- a/packages/point-of-sale/src/payments/service.ts +++ b/packages/point-of-sale/src/payments/service.ts @@ -111,8 +111,7 @@ async function getWalletAddress( ): Promise { const config: AxiosRequestConfig = { headers: { - Accept: 'application/json', - host: 'cloud-nine-wallet-backend' + Accept: 'application/json' } } const { data: walletAddress } = await deps.axios.get< From 2c17a92a308b8c7742e217a08d364d9f7e2506cc Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Mon, 8 Sep 2025 14:19:28 +0300 Subject: [PATCH 5/8] Fixed a test after mutation update --- packages/point-of-sale/src/payments/service.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/point-of-sale/src/payments/service.test.ts b/packages/point-of-sale/src/payments/service.test.ts index 4e2619ad86..c1ce419ab8 100644 --- a/packages/point-of-sale/src/payments/service.test.ts +++ b/packages/point-of-sale/src/payments/service.test.ts @@ -68,11 +68,13 @@ describe('createPaymentService', () => { expect(mockApolloClient.mutate).toHaveBeenCalledWith( expect.objectContaining({ variables: expect.objectContaining({ - walletAddressId, - incomingAmount, - idempotencyKey: expect.any(String), - isCardPayment: true, - expiresAt + input: expect.objectContaining({ + expiresAt, + idempotencyKey: expect.any(String), + incomingAmount, + isCardPayment: true, + walletAddressId + }) }) }) ) From e08e90035b2f9b782c056460ad006825ceefcebd Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Mon, 8 Sep 2025 14:20:29 +0300 Subject: [PATCH 6/8] Updated generated graphql files --- packages/card-service/src/graphql/generated/graphql.ts | 2 +- packages/point-of-sale/src/graphql/generated/graphql.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/card-service/src/graphql/generated/graphql.ts b/packages/card-service/src/graphql/generated/graphql.ts index 9e61a154ae..7879596da0 100644 --- a/packages/card-service/src/graphql/generated/graphql.ts +++ b/packages/card-service/src/graphql/generated/graphql.ts @@ -277,7 +277,7 @@ export type CreateOutgoingPaymentFromIncomingPaymentInput = { /** Used for the card service to provide the card expiry and signature */ cardDetails?: InputMaybe; /** Amount to send (fixed send). */ - debitAmount: AmountInput; + debitAmount?: InputMaybe; /** Unique key to ensure duplicate or retried requests are processed only once. For more information, refer to [idempotency](https://rafiki.dev/apis/graphql/admin-api-overview/#idempotency). */ idempotencyKey?: InputMaybe; /** Incoming payment URL to create the outgoing payment from. */ diff --git a/packages/point-of-sale/src/graphql/generated/graphql.ts b/packages/point-of-sale/src/graphql/generated/graphql.ts index 29990c0832..a4712e74d0 100644 --- a/packages/point-of-sale/src/graphql/generated/graphql.ts +++ b/packages/point-of-sale/src/graphql/generated/graphql.ts @@ -277,7 +277,7 @@ export type CreateOutgoingPaymentFromIncomingPaymentInput = { /** Used for the card service to provide the card expiry and signature */ cardDetails?: InputMaybe; /** Amount to send (fixed send). */ - debitAmount: AmountInput; + debitAmount?: InputMaybe; /** Unique key to ensure duplicate or retried requests are processed only once. For more information, refer to [idempotency](https://rafiki.dev/apis/graphql/admin-api-overview/#idempotency). */ idempotencyKey?: InputMaybe; /** Incoming payment URL to create the outgoing payment from. */ From 49e5ae31866aad84fe5782bf2683855d2a41f4d4 Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Wed, 10 Sep 2025 14:27:14 +0300 Subject: [PATCH 7/8] Fixed connection between payment service calls --- .../POS Service APIs/Initiate Payment.bru | 4 +- localenv/cloud-nine-wallet/docker-compose.yml | 3 +- localenv/happy-life-bank/docker-compose.yml | 3 +- .../src/card-service-client/client.test.ts | 17 ++++--- .../src/card-service-client/client.ts | 14 +++--- .../point-of-sale/src/payments/routes.test.ts | 3 ++ packages/point-of-sale/src/payments/routes.ts | 38 ++++++++------ .../src/payments/service.test.ts | 46 ++++++++++++++--- .../point-of-sale/src/payments/service.ts | 50 +++++++++++++------ 9 files changed, 126 insertions(+), 52 deletions(-) diff --git a/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru b/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru index ca9b88b372..9a13c092cd 100644 --- a/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru +++ b/bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru @@ -5,7 +5,7 @@ meta { } post { - url: http://localhost:3008/payment + url: http://localhost:4008/payment body: json auth: inherit } @@ -19,6 +19,6 @@ body:json { }, "signature": "signature", "value": 1, - "merchantWalletAddress": "http://localhost:3000/accounts/gfranklin" + "merchantWalletAddress": "http://happy-life-bank-backend/accounts/pfry" } } diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index ae1a71e30f..36a4a38eab 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -11,6 +11,7 @@ services: - rafiki ports: - '3007:3007' + - '9234:9229' volumes: - type: bind source: ../../packages/card-service/src @@ -26,7 +27,7 @@ services: GRAPHQL_URL: http://cloud-nine-wallet-backend:3001/graphql TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787 TENANT_SECRET: tenant_secret - TENANT_SIGNATURE_VERSION: tenant_signature_version + TENANT_SIGNATURE_VERSION: 1 depends_on: - shared-database healthcheck: diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index ec80e7a0b3..b9dd11c3ba 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -26,7 +26,7 @@ services: GRAPHQL_URL: http://happy-life-bank-backend:3001/graphql TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d TENANT_SECRET: tenant_secret - TENANT_SIGNATURE_VERSION: tenant_signature_version + TENANT_SIGNATURE_VERSION: 1 depends_on: - shared-database - cloud-nine-wallet-card-service @@ -48,6 +48,7 @@ services: - rafiki ports: - '4008:4008' + - '9233:9229' volumes: - type: bind source: ../../packages/point-of-sale/src diff --git a/packages/point-of-sale/src/card-service-client/client.test.ts b/packages/point-of-sale/src/card-service-client/client.test.ts index 50c99ea56f..10d0a4e5ef 100644 --- a/packages/point-of-sale/src/card-service-client/client.test.ts +++ b/packages/point-of-sale/src/card-service-client/client.test.ts @@ -9,6 +9,7 @@ import nock from 'nock' import { HttpStatusCode } from 'axios' import { initIocContainer } from '..' import { Config } from '../config/app' +import { faker } from '@faker-js/faker' describe('CardServiceClient', () => { const CARD_SERVICE_URL = 'http://card-service.com' @@ -38,9 +39,7 @@ describe('CardServiceClient', () => { date: new Date(), signature: '', card: { - walletAddress: { - cardService: CARD_SERVICE_URL - }, + walletAddress: faker.internet.url(), trasactionCounter: 1, expiry: new Date(new Date().getDate() + 1) }, @@ -61,13 +60,17 @@ describe('CardServiceClient', () => { nock(CARD_SERVICE_URL) .post('/payment') .reply(response.code, createPaymentResponse(response.result)) - expect(await client.sendPayment(options)).toBe(response.result) + expect(await client.sendPayment(CARD_SERVICE_URL, options)).toBe( + response.result + ) }) }) test('throws when there is no payload data', async () => { nock(CARD_SERVICE_URL).post('/payment').reply(HttpStatusCode.Ok, undefined) - await expect(client.sendPayment(options)).rejects.toMatchObject({ + await expect( + client.sendPayment(CARD_SERVICE_URL, options) + ).rejects.toMatchObject({ status: HttpStatusCode.NotFound, message: 'No payment information was received' }) @@ -77,7 +80,9 @@ describe('CardServiceClient', () => { nock(CARD_SERVICE_URL) .post('/payment') .reply(HttpStatusCode.ServiceUnavailable, 'Something went wrong') - await expect(client.sendPayment(options)).rejects.toMatchObject({ + await expect( + client.sendPayment(CARD_SERVICE_URL, options) + ).rejects.toMatchObject({ status: HttpStatusCode.ServiceUnavailable, message: 'Something went wrong' }) diff --git a/packages/point-of-sale/src/card-service-client/client.ts b/packages/point-of-sale/src/card-service-client/client.ts index ed8b316964..2894041070 100644 --- a/packages/point-of-sale/src/card-service-client/client.ts +++ b/packages/point-of-sale/src/card-service-client/client.ts @@ -11,9 +11,7 @@ import { BaseService } from '../shared/baseService' interface Card { trasactionCounter: number expiry: Date - // TODO: replace with WalletAddress from payment service - // eslint-disable-next-line @typescript-eslint/no-explicit-any - walletAddress: any + walletAddress: string } export interface PaymentOptions { @@ -30,7 +28,7 @@ export interface PaymentOptions { } export interface CardServiceClient { - sendPayment(options: PaymentOptions): Promise + sendPayment(cardServiceUrl: string, options: PaymentOptions): Promise } interface ServiceDependencies extends BaseService { @@ -70,12 +68,14 @@ export async function createCardServiceClient({ axios } return { - sendPayment: (options) => sendPayment(deps, options) + sendPayment: (cardServiceUrl, options) => + sendPayment(deps, cardServiceUrl, options) } } async function sendPayment( deps: ServiceDependencies, + cardServiceUrl: string, options: PaymentOptions ): Promise { try { @@ -88,9 +88,8 @@ async function sendPayment( ...options, requestId: uuid() } - const cardServiceUrl = options.card.walletAddress.cardService const response = await deps.axios.post( - `${cardServiceUrl}/payment`, + `${cardServiceUrl + (cardServiceUrl.endsWith('/') ? 'payment' : '/payment')}`, requestBody, config ) @@ -103,6 +102,7 @@ async function sendPayment( } return payment.result } catch (error) { + deps.logger.debug(error) if (error instanceof CardServiceClientError) throw error if (error instanceof AxiosError) { diff --git a/packages/point-of-sale/src/payments/routes.test.ts b/packages/point-of-sale/src/payments/routes.test.ts index e9d867c72f..aaf4081848 100644 --- a/packages/point-of-sale/src/payments/routes.test.ts +++ b/packages/point-of-sale/src/payments/routes.test.ts @@ -165,6 +165,9 @@ describe('Payment Routes', () => { }, state: IncomingPaymentState.Pending }) + jest + .spyOn(paymentService, 'getWalletAddressIdByUrl') + .mockResolvedValueOnce(faker.internet.url()) } }) }) diff --git a/packages/point-of-sale/src/payments/routes.ts b/packages/point-of-sale/src/payments/routes.ts index e3a834ee47..13221dc998 100644 --- a/packages/point-of-sale/src/payments/routes.ts +++ b/packages/point-of-sale/src/payments/routes.ts @@ -62,7 +62,6 @@ async function payment( ctx: PaymentContext ): Promise { const body = ctx.request.body - const tenantId = ctx.request.header['tenant-id'] as string | undefined try { const walletAddress = await deps.paymentService.getWalletAddress( body.card.walletAddress @@ -72,10 +71,17 @@ async function payment( assetScale: walletAddress.assetScale, value: body.value } + // TODO: in the future we need to find a way to make it work in local playground + const walletAddressUrl = body.merchantWalletAddress.replace( + /^http:/, + 'https:' + ) + const walletAddressId = + await deps.paymentService.getWalletAddressIdByUrl(walletAddressUrl) + const incomingPayment = await deps.paymentService.createIncomingPayment( - walletAddress.id, - incomingAmount, - tenantId + walletAddressId, + incomingAmount ) const deferred = new Deferred() webhookWaitMap.setWithExpiry( @@ -83,17 +89,21 @@ async function payment( deferred, deps.config.webhookTimeoutMs ) - const result = await deps.cardServiceClient.sendPayment({ - merchantWalletAddress: body.merchantWalletAddress, - incomingPaymentUrl: incomingPayment.url, - date: new Date(), - signature: body.signature, - card: body.card, - incomingAmount: { - ...incomingAmount, - value: incomingAmount.value.toString() + + const result = await deps.cardServiceClient.sendPayment( + walletAddress.cardService, + { + merchantWalletAddress: body.merchantWalletAddress, + incomingPaymentUrl: incomingPayment.url, + date: new Date(), + signature: body.signature, + card: body.card, + incomingAmount: { + ...incomingAmount, + value: incomingAmount.value.toString() + } } - }) + ) if (result !== Result.APPROVED) throw new InvalidCardPaymentError(result) const event = await waitForIncomingPaymentEvent(deps.config, deferred) diff --git a/packages/point-of-sale/src/payments/service.test.ts b/packages/point-of-sale/src/payments/service.test.ts index c1ce419ab8..6c036ad302 100644 --- a/packages/point-of-sale/src/payments/service.test.ts +++ b/packages/point-of-sale/src/payments/service.test.ts @@ -43,9 +43,11 @@ describe('createPaymentService', () => { const expectedUrl = 'https://api.example.com/incoming-payments/abc123' mockApolloClient.mutate = jest.fn().mockResolvedValue({ data: { - payment: { - id: uuid, - url: expectedUrl + createIncomingPayment: { + payment: { + id: uuid, + url: expectedUrl + } } } }) @@ -81,9 +83,9 @@ describe('createPaymentService', () => { }) it('should throw and log error if payment creation fails (no id)', async () => { - mockApolloClient.mutate = jest - .fn() - .mockResolvedValue({ data: { payment: undefined } }) + mockApolloClient.mutate = jest.fn().mockResolvedValue({ + data: { createIncomingPayment: { payment: undefined } } + }) const service = createPaymentService(deps) const walletAddressId = 'wallet-123' const incomingAmount: AmountInput = { @@ -161,3 +163,35 @@ describe('getWalletAddress', () => { ) }) }) + +describe('getWalletAddressByUrl', () => { + let service: PaymentService + const WALLET_ADDRESS_URL = 'https://api.example.com/wallet-address' + + beforeAll(() => { + service = createPaymentService(deps) + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + test('should obtain wallet address id successfully', async () => { + const id = uuid() + mockApolloClient.query = jest.fn().mockResolvedValue({ + data: { walletAddressByUrl: { id } } + }) + const walletAddressId = + await service.getWalletAddressIdByUrl(WALLET_ADDRESS_URL) + expect(walletAddressId).toBe(id) + }) + + test('should throw when no wallet address was found', async () => { + mockApolloClient.query = jest.fn().mockResolvedValue({ + data: { walletAddressByUrl: undefined } + }) + await expect( + service.getWalletAddressIdByUrl(WALLET_ADDRESS_URL) + ).rejects.toThrow('Wallet address not found') + }) +}) diff --git a/packages/point-of-sale/src/payments/service.ts b/packages/point-of-sale/src/payments/service.ts index f45ae7c6ad..04f78a528e 100644 --- a/packages/point-of-sale/src/payments/service.ts +++ b/packages/point-of-sale/src/payments/service.ts @@ -1,11 +1,13 @@ import { Logger } from 'pino' import { CREATE_INCOMING_PAYMENT } from '../graphql/mutations/createIncomingPayment' import { IAppConfig } from '../config/app' -import { ApolloClient, NormalizedCacheObject } from '@apollo/client' +import { ApolloClient, NormalizedCacheObject, gql } from '@apollo/client' import { AmountInput, IncomingPayment, MutationCreateIncomingPaymentArgs, + Query, + QueryWalletAddressByUrlArgs, type Mutation } from '../graphql/generated/graphql' import { FnWithDeps } from '../shared/types' @@ -37,10 +39,10 @@ export type WalletAddress = OpenPaymentsWalletAddress & { export type PaymentService = { createIncomingPayment: ( walletAddressId: string, - incomingAmount: AmountInput, - tenantId?: string + incomingAmount: AmountInput ) => Promise getWalletAddress: (walletAddressUrl: string) => Promise + getWalletAddressIdByUrl: (walletAddressUrl: string) => Promise } export function createPaymentService( @@ -60,20 +62,22 @@ export function createPaymentService( incomingAmount: AmountInput ) => createIncomingPayment(deps, walletAddressId, incomingAmount), getWalletAddress: (walletAddressUrl: string) => - getWalletAddress(deps, walletAddressUrl) + getWalletAddress(deps, walletAddressUrl), + getWalletAddressIdByUrl: (walletAddressUrl: string) => + getWalletAddressIdByUrl(deps, walletAddressUrl) } } const createIncomingPayment: FnWithDeps< ServiceDependencies, PaymentService['createIncomingPayment'] -> = async (deps, walletAddressId, incomingAmount, tenantId) => { +> = async (deps, walletAddressId, incomingAmount) => { const client = deps.apolloClient const expiresAt = new Date( Date.now() + deps.config.incomingPaymentExpiryMs ).toISOString() const { data } = await client.mutate< - Mutation['createIncomingPayment'], + Mutation, MutationCreateIncomingPaymentArgs >({ mutation: CREATE_INCOMING_PAYMENT, @@ -85,17 +89,10 @@ const createIncomingPayment: FnWithDeps< isCardPayment: true, expiresAt } - }, - ...(tenantId && { - context: { - headers: { - 'tenant-id': tenantId - } - } - }) + } }) - const incomingPayment = data?.payment + const incomingPayment = data?.createIncomingPayment?.payment if (!incomingPayment) { deps.logger.error( { walletAddressId }, @@ -132,3 +129,26 @@ async function getWalletAddress( } return walletAddress as WalletAddress } + +async function getWalletAddressIdByUrl( + deps: ServiceDependencies, + walletAddressUrl: string +): Promise { + const client = deps.apolloClient + const { data } = await client.query({ + variables: { + url: walletAddressUrl + }, + query: gql` + query getWalletAddressByUrl($url: String!) { + walletAddressByUrl(url: $url) { + id + } + } + ` + }) + if (!data?.walletAddressByUrl) { + throw new Error('Wallet address not found') + } + return data.walletAddressByUrl.id +} From 91402d1e4262cea0e61a382728078b90ec432d8c Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Wed, 17 Sep 2025 09:20:29 +0300 Subject: [PATCH 8/8] Fixed env variables and mutation type for creating inc payment --- localenv/cloud-nine-wallet/docker-compose.yml | 4 ++-- localenv/happy-life-bank/docker-compose.yml | 4 ++-- packages/backend/src/app.ts | 2 +- .../point-of-sale/src/payments/routes.test.ts | 12 +----------- packages/point-of-sale/src/payments/service.ts | 16 ++++++++++------ 5 files changed, 16 insertions(+), 22 deletions(-) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 36a4a38eab..dd3e66e7e9 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -26,7 +26,7 @@ services: DATABASE_URL: postgresql://cloud_nine_wallet_card_service:cloud_nine_wallet_card_service@shared-database/cloud_nine_wallet_card_service GRAPHQL_URL: http://cloud-nine-wallet-backend:3001/graphql TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787 - TENANT_SECRET: tenant_secret + TENANT_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= TENANT_SIGNATURE_VERSION: 1 depends_on: - shared-database @@ -61,7 +61,7 @@ services: PORT: 3008 DATABASE_URL: postgresql://cloud_nine_wallet_point_of_sale:cloud_nine_wallet_point_of_sale@shared-database/cloud_nine_wallet_point_of_sale TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787 - TENANT_SECRET: secret + TENANT_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= GRAPHQL_URL: http://cloud-nine-wallet-backend:3001/graphql WEBHOOK_SIGNATURE_SECRET: webhook_secret depends_on: diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index b9dd11c3ba..f23524a3d7 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -25,7 +25,7 @@ services: DATABASE_URL: postgresql://happy_life_bank_card_service:happy_life_bank_card_service@shared-database/happy_life_bank_card_service GRAPHQL_URL: http://happy-life-bank-backend:3001/graphql TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d - TENANT_SECRET: tenant_secret + TENANT_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= TENANT_SIGNATURE_VERSION: 1 depends_on: - shared-database @@ -62,7 +62,7 @@ services: PORT: 4008 DATABASE_URL: postgresql://happy_life_bank_point_of_sale:happy_life_bank_point_of_sale@shared-database/happy_life_bank_point_of_sale TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d - TENANT_SECRET: secret + TENANT_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= GRAPHQL_URL: http://happy-life-bank-backend:3001/graphql WEBHOOK_SIGNATURE_SECRET: webhook_secret depends_on: diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index b2b97353e4..9952324002 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -454,7 +454,7 @@ export class App { // For tests, we still need to get the tenant in the middleware, but // we don't need to verify the signature nor prevent replay attacks koa.use( - this.config.env !== 'test' && this.config.env !== 'development' + this.config.env !== 'test' ? tenantSignatureMiddleware : testTenantSignatureMiddleware ) diff --git a/packages/point-of-sale/src/payments/routes.test.ts b/packages/point-of-sale/src/payments/routes.test.ts index aaf4081848..cffcc5e5e5 100644 --- a/packages/point-of-sale/src/payments/routes.test.ts +++ b/packages/point-of-sale/src/payments/routes.test.ts @@ -10,7 +10,6 @@ import { PaymentService } from './service' import { CardServiceClient, Result } from '../card-service-client/client' import { createContext } from '../tests/context' import { CardServiceClientError } from '../card-service-client/errors' -import { IncomingPaymentState } from '../graphql/generated/graphql' import { webhookWaitMap } from '../webhook-handlers/request-map' import { faker } from '@faker-js/faker' import { withConfigOverride } from '../tests/helpers' @@ -154,16 +153,7 @@ describe('Payment Routes', () => { .spyOn(paymentService, 'createIncomingPayment') .mockResolvedValueOnce({ id: 'incoming-payment-url', - url: faker.internet.url(), - createdAt: new Date().toString(), - walletAddressId: v4(), - expiresAt: new Date(Date.now() + 30000).toString(), - receivedAmount: { - assetCode: 'USD', - assetScale: 2, - value: BigInt(0) - }, - state: IncomingPaymentState.Pending + url: faker.internet.url() }) jest .spyOn(paymentService, 'getWalletAddressIdByUrl') diff --git a/packages/point-of-sale/src/payments/service.ts b/packages/point-of-sale/src/payments/service.ts index 04f78a528e..f64d8f2bca 100644 --- a/packages/point-of-sale/src/payments/service.ts +++ b/packages/point-of-sale/src/payments/service.ts @@ -4,11 +4,10 @@ import { IAppConfig } from '../config/app' import { ApolloClient, NormalizedCacheObject, gql } from '@apollo/client' import { AmountInput, - IncomingPayment, + CreateIncomingPayment, MutationCreateIncomingPaymentArgs, Query, - QueryWalletAddressByUrlArgs, - type Mutation + QueryWalletAddressByUrlArgs } from '../graphql/generated/graphql' import { FnWithDeps } from '../shared/types' import { v4 } from 'uuid' @@ -36,11 +35,16 @@ export type WalletAddress = OpenPaymentsWalletAddress & { cardService: string } +export type CreatedIncomingPayment = { + id: string + url: string +} + export type PaymentService = { createIncomingPayment: ( walletAddressId: string, incomingAmount: AmountInput - ) => Promise + ) => Promise getWalletAddress: (walletAddressUrl: string) => Promise getWalletAddressIdByUrl: (walletAddressUrl: string) => Promise } @@ -77,7 +81,7 @@ const createIncomingPayment: FnWithDeps< Date.now() + deps.config.incomingPaymentExpiryMs ).toISOString() const { data } = await client.mutate< - Mutation, + CreateIncomingPayment, MutationCreateIncomingPaymentArgs >({ mutation: CREATE_INCOMING_PAYMENT, @@ -92,7 +96,7 @@ const createIncomingPayment: FnWithDeps< } }) - const incomingPayment = data?.createIncomingPayment?.payment + const incomingPayment = data?.createIncomingPayment.payment if (!incomingPayment) { deps.logger.error( { walletAddressId },