diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 715dea29d4..d2361867c6 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -23,6 +23,7 @@ services: GRAPHQL_URL: http://happy-life-bank-backend:3001/graphql WEBHOOK_SIGNATURE_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= WEBHOOK_SIGNATURE_VERSION: 1 + USE_HTTP: true depends_on: - shared-database healthcheck: 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 1308b44564..0921c3905a 100644 --- a/packages/point-of-sale/src/card-service-client/client.ts +++ b/packages/point-of-sale/src/card-service-client/client.ts @@ -59,7 +59,7 @@ export async function createCardServiceClient({ axios }: ServiceDependencies): Promise { const log = logger.child({ - service: 'PosDeviceService' + service: 'CardServiceClient' }) const deps: ServiceDependencies = { logger: log, diff --git a/packages/point-of-sale/src/config/app.ts b/packages/point-of-sale/src/config/app.ts index d84b43af19..852bbef729 100644 --- a/packages/point-of-sale/src/config/app.ts +++ b/packages/point-of-sale/src/config/app.ts @@ -37,5 +37,6 @@ export const Config = { webhookSignatureVersion: envInt('WEBHOOK_SIGNATURE_VERSION', 1), webhookSignatureSecret: envString('WEBHOOK_SIGNATURE_SECRET'), webhookTimeoutMs: envInt('WEBHOOK_TIMEOUT_MS', 30000), - incomingPaymentExpiryMs: envInt('INCOMING_PAYMENT_EXPIRY_MS', 10000) + incomingPaymentExpiryMs: envInt('INCOMING_PAYMENT_EXPIRY_MS', 10000), + useHttp: envBool('USE_HTTP', false) } diff --git a/packages/point-of-sale/src/payments/routes.test.ts b/packages/point-of-sale/src/payments/routes.test.ts index 54355b1500..5b940dbf07 100644 --- a/packages/point-of-sale/src/payments/routes.test.ts +++ b/packages/point-of-sale/src/payments/routes.test.ts @@ -28,21 +28,33 @@ describe('Payment Routes', () => { let config: IAppConfig function mockPaymentService() { - jest.spyOn(paymentService, 'getWalletAddress').mockResolvedValueOnce({ - id: 'id', - assetCode: 'USD', - assetScale: 1, - authServer: 'authServer', - resourceServer: 'resourceServer', - cardService: 'cardService' - }) - jest.spyOn(paymentService, 'createIncomingPayment').mockResolvedValueOnce({ - id: 'incoming-payment-url', - url: faker.internet.url() - }) - jest + const getWalletAddressSpy = jest + .spyOn(paymentService, 'getWalletAddress') + .mockResolvedValueOnce({ + id: 'id', + assetCode: 'USD', + assetScale: 1, + authServer: 'authServer', + resourceServer: 'resourceServer', + cardService: 'cardService' + }) + + const createIncomingPaymentSpy = jest + .spyOn(paymentService, 'createIncomingPayment') + .mockResolvedValueOnce({ + id: 'incoming-payment-url', + url: faker.internet.url() + }) + + const getWalletAddressIdByUrlSpy = jest .spyOn(paymentService, 'getWalletAddressIdByUrl') .mockResolvedValueOnce(faker.internet.url()) + + return { + getWalletAddressSpy, + createIncomingPaymentSpy, + getWalletAddressIdByUrlSpy + } } beforeAll(async () => { @@ -159,6 +171,44 @@ describe('Payment Routes', () => { } ) ) + + test( + 'falls back to http for payment request if configured', + withConfigOverride( + () => config, + { useHttp: true }, + async () => { + const senderWalletAddress = 'https://example.com/' + + const ctx = createPaymentContext({ senderWalletAddress }) + + const { getWalletAddressSpy } = mockPaymentService() + jest + .spyOn(cardServiceClient, 'sendPayment') + .mockResolvedValueOnce(Result.APPROVED) + + jest + .spyOn(webhookWaitMap, 'setWithExpiry') + .mockImplementationOnce((key, deferred) => { + deferred.resolve({ + id: v4(), + type: 'incoming_payment.completed', + data: { id: key, completed: true } + }) + return webhookWaitMap + }) + + await paymentRoutes.payment(ctx) + expect(getWalletAddressSpy).toHaveBeenCalledWith( + 'http://example.com/' + ) + expect(ctx.response.body).toEqual({ + result: { code: Result.APPROVED } + }) + expect(ctx.status).toBe(200) + } + ) + ) }) describe('get incoming payments', (): void => { @@ -328,7 +378,7 @@ describe('Payment Routes', () => { }) }) -function createPaymentContext() { +function createPaymentContext(bodyOverrides?: Record) { return createContext({ headers: { Accept: 'application/json' }, method: 'POST', @@ -339,7 +389,8 @@ function createPaymentContext() { receiverWalletAddress: faker.internet.url(), senderWalletAddress: faker.internet.url(), timestamp: new Date().getTime(), - amount: { assetScale: 2, assetCode: 'USD', value: '100' } + amount: { assetScale: 2, assetCode: 'USD', value: '100' }, + ...bodyOverrides } }) } diff --git a/packages/point-of-sale/src/payments/routes.ts b/packages/point-of-sale/src/payments/routes.ts index 35b55e0a2e..da51e6d817 100644 --- a/packages/point-of-sale/src/payments/routes.ts +++ b/packages/point-of-sale/src/payments/routes.ts @@ -117,8 +117,14 @@ async function payment( const body = ctx.request.body let incomingPaymentId: string | undefined try { + const senderWalletAddressUrl = new URL(body.senderWalletAddress) + + if (deps.config.useHttp) { + senderWalletAddressUrl.protocol = 'http:' + } + const senderWalletAddress = await deps.paymentService.getWalletAddress( - body.senderWalletAddress.replace(/^https:/, 'http:') + senderWalletAddressUrl.href ) const receiverWalletAddressId = @@ -126,13 +132,15 @@ async function payment( body.receiverWalletAddress ) - const incomingPayment = await deps.paymentService.createIncomingPayment( - receiverWalletAddressId, - { - ...body.amount, + const incomingPayment = await deps.paymentService.createIncomingPayment({ + walletAddressId: receiverWalletAddressId, + incomingAmount: { + assetCode: body.amount.assetCode, + assetScale: body.amount.assetScale, value: BigInt(body.amount.value) - } - ) + }, + senderWalletAddress: body.senderWalletAddress + }) const deferred = new Deferred() webhookWaitMap.setWithExpiry( incomingPayment.id, diff --git a/packages/point-of-sale/src/payments/service.test.ts b/packages/point-of-sale/src/payments/service.test.ts index 851e781749..2a5c08d545 100644 --- a/packages/point-of-sale/src/payments/service.test.ts +++ b/packages/point-of-sale/src/payments/service.test.ts @@ -64,11 +64,13 @@ describe('createPaymentService', () => { const expiresAt = new Date( now + mockConfig.incomingPaymentExpiryMs ).toISOString() + const senderWalletAddress = faker.internet.url() - const result = await service.createIncomingPayment( + const result = await service.createIncomingPayment({ walletAddressId, - incomingAmount - ) + incomingAmount, + senderWalletAddress + }) expect(result).toEqual({ id: uuid, url: expectedUrl }) expect(mockApolloClient.mutate).toHaveBeenCalledWith( expect.objectContaining({ @@ -78,7 +80,8 @@ describe('createPaymentService', () => { idempotencyKey: expect.any(String), incomingAmount, isCardPayment: true, - walletAddressId + walletAddressId, + senderWalletAddress }) }) }) @@ -97,7 +100,11 @@ describe('createPaymentService', () => { assetScale: 2 } await expect( - service.createIncomingPayment(walletAddressId, incomingAmount) + service.createIncomingPayment({ + walletAddressId, + incomingAmount, + senderWalletAddress: faker.internet.url() + }) ).rejects.toThrow(/Failed to create incoming payment/) expect(mockLogger.error).toHaveBeenCalledWith( { walletAddressId }, diff --git a/packages/point-of-sale/src/payments/service.ts b/packages/point-of-sale/src/payments/service.ts index e5a82825ff..e220f5bedb 100644 --- a/packages/point-of-sale/src/payments/service.ts +++ b/packages/point-of-sale/src/payments/service.ts @@ -11,7 +11,6 @@ import { GetWalletAddress, GetWalletAddressVariables } from '../graphql/generated/graphql' -import { FnWithDeps } from '../shared/types' import { v4 } from 'uuid' import { AxiosInstance, AxiosRequestConfig } from 'axios' import { GET_WALLET_ADDRESS_BY_URL } from '../graphql/queries/getWalletAddress' @@ -44,6 +43,12 @@ export type CreatedIncomingPayment = { url: string } +interface CreateIncomingPaymentArgs { + walletAddressId: string + incomingAmount: AmountInput + senderWalletAddress: string +} + type IncomingPaymentPage = Exclude< Exclude, undefined @@ -54,8 +59,7 @@ export type PaymentService = { options: GetPaymentsQuery ) => Promise createIncomingPayment: ( - walletAddressId: string, - incomingAmount: AmountInput + args: CreateIncomingPaymentArgs ) => Promise getWalletAddress: (walletAddressUrl: string) => Promise getWalletAddressIdByUrl: (walletAddressUrl: string) => Promise @@ -75,10 +79,8 @@ export function createPaymentService( return { getIncomingPayments: (options: GetPaymentsQuery) => getIncomingPayments(deps, options), - createIncomingPayment: ( - walletAddressId: string, - incomingAmount: AmountInput - ) => createIncomingPayment(deps, walletAddressId, incomingAmount), + createIncomingPayment: (args: CreateIncomingPaymentArgs) => + createIncomingPayment(deps, args), getWalletAddress: (walletAddressUrl: string) => getWalletAddress(deps, walletAddressUrl), getWalletAddressIdByUrl: (walletAddressUrl: string) => @@ -107,10 +109,11 @@ async function getIncomingPayments( return data?.walletAddressByUrl?.incomingPayments } -const createIncomingPayment: FnWithDeps< - ServiceDependencies, - PaymentService['createIncomingPayment'] -> = async (deps, walletAddressId, incomingAmount) => { +async function createIncomingPayment( + deps: ServiceDependencies, + args: CreateIncomingPaymentArgs +): Promise { + const { walletAddressId, incomingAmount, senderWalletAddress } = args const client = deps.apolloClient const expiresAt = new Date( Date.now() + deps.config.incomingPaymentExpiryMs @@ -126,7 +129,8 @@ const createIncomingPayment: FnWithDeps< incomingAmount, idempotencyKey: v4(), isCardPayment: true, - expiresAt + expiresAt, + senderWalletAddress } } }) diff --git a/test/testenv/happy-life-bank/docker-compose.yml b/test/testenv/happy-life-bank/docker-compose.yml index 57aeba3f10..9a0ba224fe 100644 --- a/test/testenv/happy-life-bank/docker-compose.yml +++ b/test/testenv/happy-life-bank/docker-compose.yml @@ -109,5 +109,6 @@ services: WEBHOOK_SIGNATURE_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= WEBHOOK_SIGNATURE_VERSION: 1 WEBHOOK_TIMEOUT_MS: 10_000 + USE_HTTP: true depends_on: - shared-database