Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions bruno/collections/Rafiki/POS Service APIs/Initiate Payment.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
meta {
name: Initiate Payment
type: http
seq: 3
}

post {
url: http://localhost:4008/payment
body: json
auth: inherit
}

body:json {
{
"card": {
"walletAddress": "http://cloud-nine-wallet-backend/accounts/gfranklin",
"trasactionCounter": 1,
"expiry": "2025-09-13T13:00:00Z"
},
"signature": "signature",
"value": 1,
"merchantWalletAddress": "http://happy-life-bank-backend/accounts/pfry"
}
}
9 changes: 9 additions & 0 deletions localenv/cloud-nine-wallet/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ services:
- rafiki
ports:
- '3007:3007'
- '9234:9229'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added for debugging

volumes:
- type: bind
source: ../../packages/card-service/src
Expand All @@ -23,6 +24,10 @@ 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
TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787
TENANT_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
TENANT_SIGNATURE_VERSION: 1
depends_on:
- shared-database
healthcheck:
Expand Down Expand Up @@ -55,6 +60,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: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
GRAPHQL_URL: http://cloud-nine-wallet-backend:3001/graphql
WEBHOOK_SIGNATURE_SECRET: webhook_secret
depends_on:
- shared-database
healthcheck:
Expand Down
9 changes: 9 additions & 0 deletions localenv/happy-life-bank/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ 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
TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d
TENANT_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
TENANT_SIGNATURE_VERSION: 1
depends_on:
- shared-database
- cloud-nine-wallet-card-service
Expand All @@ -44,6 +48,7 @@ services:
- rafiki
ports:
- '4008:4008'
- '9233:9229'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added for debugging

volumes:
- type: bind
source: ../../packages/point-of-sale/src
Expand All @@ -56,6 +61,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: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
GRAPHQL_URL: http://happy-life-bank-backend:3001/graphql
WEBHOOK_SIGNATURE_SECRET: webhook_secret
depends_on:
- shared-database
- cloud-nine-wallet-point-of-sale
Expand Down
17 changes: 11 additions & 6 deletions packages/point-of-sale/src/card-service-client/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
},
Expand All @@ -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'
})
Expand All @@ -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'
})
Expand Down
14 changes: 7 additions & 7 deletions packages/point-of-sale/src/card-service-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -30,7 +28,7 @@ export interface PaymentOptions {
}

export interface CardServiceClient {
sendPayment(options: PaymentOptions): Promise<Result>
sendPayment(cardServiceUrl: string, options: PaymentOptions): Promise<Result>
}

interface ServiceDependencies extends BaseService {
Expand Down Expand Up @@ -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<Result> {
try {
Expand All @@ -88,9 +88,8 @@ async function sendPayment(
...options,
requestId: uuid()
}
const cardServiceUrl = options.card.walletAddress.cardService
const response = await deps.axios.post<PaymentResponse>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will get BAD_REQUEST because we are missing some data that cardService is expecting.

`${cardServiceUrl}/payment`,
`${cardServiceUrl + (cardServiceUrl.endsWith('/') ? 'payment' : '/payment')}`,
requestBody,
config
)
Expand All @@ -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) {
Expand Down
15 changes: 4 additions & 11 deletions packages/point-of-sale/src/payments/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -154,17 +153,11 @@ 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')
.mockResolvedValueOnce(faker.internet.url())
}
})
})
Expand Down
35 changes: 24 additions & 11 deletions packages/point-of-sale/src/payments/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,16 @@ 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,
walletAddressId,
incomingAmount
)
const deferred = new Deferred<WebhookBody>()
Expand All @@ -81,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)
Expand All @@ -101,6 +113,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)
Expand Down
58 changes: 47 additions & 11 deletions packages/point-of-sale/src/payments/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
})
Expand All @@ -68,20 +70,22 @@ 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
})
})
})
)
})

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 = {
Expand Down Expand Up @@ -159,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')
})
})
Loading
Loading