Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: payment method handler service (quoting) #1974

Merged
merged 9 commits into from
Oct 6, 2023
4 changes: 4 additions & 0 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ import { AutoPeeringService } from './payment-method/ilp/auto-peering/service'
import { AutoPeeringRoutes } from './payment-method/ilp/auto-peering/routes'
import { Rafiki as ConnectorApp } from './payment-method/ilp/connector/core'
import { AxiosInstance } from 'axios'
import { PaymentMethodHandlerService } from './payment-method/handler/service'
import { IlpPaymentService } from './payment-method/ilp/service'

export interface AppContextData {
logger: Logger
Expand Down Expand Up @@ -225,6 +227,8 @@ export interface AppServices {
autoPeeringRoutes: Promise<AutoPeeringRoutes>
connectorApp: Promise<ConnectorApp>
tigerbeetle: Promise<TigerbeetleClient>
paymentMethodHandlerService: Promise<PaymentMethodHandlerService>
ilpPaymentService: Promise<IlpPaymentService>
}

export type AppContainer = IocContract<AppServices>
Expand Down
20 changes: 20 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import { createFeeService } from './fee/service'
import { createAutoPeeringService } from './payment-method/ilp/auto-peering/service'
import { createAutoPeeringRoutes } from './payment-method/ilp/auto-peering/routes'
import axios from 'axios'
import { createIlpPaymentService } from './payment-method/ilp/service'
import { createPaymentMethodHandlerService } from './payment-method/handler/service'

BigInt.prototype.toJSON = function () {
return this.toString()
Expand Down Expand Up @@ -432,6 +434,24 @@ export function initIocContainer(
})
})

container.singleton('ilpPaymentService', async (deps) => {
return createIlpPaymentService({
logger: await deps.use('logger'),
knex: await deps.use('knex'),
config: await deps.use('config'),
makeIlpPlugin: await deps.use('makeIlpPlugin'),
ratesService: await deps.use('ratesService')
})
})

container.singleton('paymentMethodHandlerService', async (deps) => {
return createPaymentMethodHandlerService({
logger: await deps.use('logger'),
knex: await deps.use('knex'),
ilpPaymentService: await deps.use('ilpPaymentService')
})
})
Comment on lines +447 to +453
Copy link
Member

Choose a reason for hiding this comment

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

I understand this is a minor part of integrating different payment methods into Rafiki, but I'm currently a bit confused. Wouldn't it be more appropriate to have this as factory that is instantiated at runtime based on the payment method?

What I mean by that:

export async function createPaymentMethodHandlerService({
    logger,
    knex,
-   ilpPaymentService
+   paymentMethod // ('ilp', '...');
}: ServiceDependencies): Promise<PaymentMethodHandlerService> {
    const log = logger.child({
        service: "PaymentMethodHandlerService",
    });
    const deps: ServiceDependencies = {
        logger: log,
        knex,
        ilpPaymentService,
    };
   
+   const paymentMethod = new Payment(paymentMethod)

    return {
-        getQuote: (method, quoteOptions) =>
-           getPaymentMethodService(deps, method).getQuote(quoteOptions),
+        getQuote: (quoteOptions) =>
+           paymentMethod.getQuote(quoteOptions),
    };
}

Copy link
Contributor

Choose a reason for hiding this comment

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

To try and build on this idea, maybe it could take in an object whose keys are the names of supported payment methods, and the values are its services. So getQuote would then look like:

/*
  PaymentMethods: {
    ilp: IlpPaymentService
    ...other future payment method services
  }

  paymentMethods: PaymentMethods
*/

getQuote: (method, quoteOptions) => paymentMethods[method].getQuote(quoteOptions)

Copy link
Contributor Author

@mkurapov mkurapov Oct 4, 2023

Choose a reason for hiding this comment

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

@raducristianpopa the PaymentMethodHandlerService is our "factory" in this case already, but instead of a new instance of a class we're just returning the service/singleton for the specific payment method that extends from PaymentMethodService. I could rename PaymentMethodHandlerService to PaymentMethodFactory if that seems better?

@njlie yeah I think I'll change it to the suggestion since the types make it sort of "safe" and prevent passing in an invalid payment method 👍


Comment on lines +437 to +454
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not used yet, but will be in the next PR

return container
}

Expand Down
93 changes: 93 additions & 0 deletions packages/backend/src/payment-method/handler/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
PaymentMethod,
PaymentMethodHandlerService,
StartQuoteOptions
} from './service'
import { initIocContainer } from '../../'
import { createTestApp, TestContainer } from '../../tests/app'
import { Config } from '../../config/app'
import { IocContract } from '@adonisjs/fold'
import { AppServices } from '../../app'
import { createAsset } from '../../tests/asset'
import { createPaymentPointer } from '../../tests/paymentPointer'

import { createReceiver } from '../../tests/receiver'
import { IlpPaymentService } from '../ilp/service'
import { truncateTables } from '../../tests/tableManager'

describe('PaymentMethodHandlerService', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let paymentMethodHandlerService: PaymentMethodHandlerService
let ilpPaymentService: IlpPaymentService

beforeAll(async (): Promise<void> => {
deps = initIocContainer(Config)
appContainer = await createTestApp(deps)

paymentMethodHandlerService = await deps.use('paymentMethodHandlerService')
ilpPaymentService = await deps.use('ilpPaymentService')
})

afterEach(async (): Promise<void> => {
jest.restoreAllMocks()
await truncateTables(appContainer.knex)
})

afterAll(async (): Promise<void> => {
await appContainer.shutdown()
})

describe('getQuote', (): void => {
test('calls ilpPaymentService for ILP payment type', async (): Promise<void> => {
const asset = await createAsset(deps)
const paymentPointer = await createPaymentPointer(deps, {
assetId: asset.id
})

const options: StartQuoteOptions = {
paymentPointer,
receiver: await createReceiver(deps, paymentPointer),
debitAmount: {
assetCode: 'USD',
assetScale: 2,
value: 100n
}
}

const ilpPaymentServiceGetQuoteSpy = jest.spyOn(
ilpPaymentService,
'getQuote'
)

await paymentMethodHandlerService.getQuote('ILP', options)

expect(ilpPaymentServiceGetQuoteSpy).toHaveBeenCalledWith(options)
})

test('throws if invalid payment method', async (): Promise<void> => {
const asset = await createAsset(deps, {
code: 'USD',
scale: 2
})

const paymentPointer = await createPaymentPointer(deps, {
assetId: asset.id
})

const options: StartQuoteOptions = {
paymentPointer,
receiver: await createReceiver(deps, paymentPointer),
debitAmount: {
assetCode: 'USD',
assetScale: 2,
value: 100n
}
}

expect(() =>
paymentMethodHandlerService.getQuote('' as PaymentMethod, options)
).toThrow('Payment method not supported')
})
})
})
68 changes: 68 additions & 0 deletions packages/backend/src/payment-method/handler/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Amount } from '../../open_payments/amount'
import { PaymentPointer } from '../../open_payments/payment_pointer/model'
import { Receiver } from '../../open_payments/receiver/model'
import { BaseService } from '../../shared/baseService'
import { IlpPaymentService } from '../ilp/service'

export interface StartQuoteOptions {
paymentPointer: PaymentPointer
debitAmount?: Amount
receiveAmount?: Amount
receiver: Receiver
}

export interface PaymentQuote {
paymentPointer: PaymentPointer
receiver: Receiver
debitAmount: Amount
receiveAmount: Amount
additionalFields: Record<string, unknown>
}

export interface PaymentMethodService {
getQuote(quoteOptions: StartQuoteOptions): Promise<PaymentQuote>
}
Comment on lines +7 to +24
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interfaces that will need to be satisfied for all payment method handlers


export type PaymentMethod = 'ILP'

export interface PaymentMethodHandlerService {
getQuote(
method: PaymentMethod,
quoteOptions: StartQuoteOptions
): Promise<PaymentQuote>
}

interface ServiceDependencies extends BaseService {
ilpPaymentService: IlpPaymentService
}

export async function createPaymentMethodHandlerService({
logger,
knex,
ilpPaymentService
}: ServiceDependencies): Promise<PaymentMethodHandlerService> {
const log = logger.child({
service: 'PaymentMethodHandlerService'
})
const deps: ServiceDependencies = {
logger: log,
knex,
ilpPaymentService
}

return {
getQuote: (method, quoteOptions) =>
getPaymentMethodService(deps, method).getQuote(quoteOptions)
}
}

function getPaymentMethodService(
deps: ServiceDependencies,
method: PaymentMethod
): PaymentMethodService {
if (method === 'ILP') {
return deps.ilpPaymentService
}

throw new Error('Payment method not supported')
}
Loading