Skip to content

Commit

Permalink
Add custom payment creation (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
tl-lucas-lima authored Jun 22, 2023
1 parent 52a04ba commit 83130b0
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 11 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,34 @@ To create a payment, you can try and run this command.
$ curl -X POST 'http://localhost:3000/v3/payment/scheme_selection'
```

### [POST] `/v3/payment/create` - Create Custom Payment
This endpoint has a default payment request body, so it behaves like `/v3/payment` if not request body is passed.
The core difference is that you can override the default body request to customise the payment creation.
The request below is used to create a payment in `EURs` with a `Preselected` provider and a `UserSelected` scheme.
Note that the property `type` of every block is required so the service knows what defaults to use.

```json
{
"currency": "EUR",
"payment_method": {
"type": "bank_transfer",
"provider_selection": {
"type": "preselected",
"provider_id": "xs2a-sparkasse",
"scheme_selection": {
"type": "user_selected"
}
},
"beneficiary": {
"type": "external_account",
"account_identifier": {
"type": "iban"
}
}
}
}
```

### [GET] `/v3/payment/{paymentId}` - Get Payment status
Once the payment has been created, you can retrieve its status by using this command.

Expand Down
210 changes: 210 additions & 0 deletions src/controllers/paymentsController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { NextFunction, Request, Response } from 'express';
import {
Beneficiary,
CreatePaymentRequest,
CreatePaymentRequestResponse,
ProviderSelection,
SchemeSelection
} from 'models/v3/payments-api/create_payment';
import AuthenticationClient from 'clients/authentication-client';
import PaymentsClient from 'clients/paymentv3-client';
import config from 'config';
import { HttpException } from 'middleware/errors';
import { PaymentAccountIdentifier, ProviderSelectionFilter } from 'models/v3/payments-api/common';

export class PaymentsController {
private paymentClient = new PaymentsClient(new AuthenticationClient());

createPayment = async (req: Request<Partial<CreatePaymentRequest>>, res: Response, next: NextFunction) => {
const request = this.buildDefaultCreatePaymentRequest(req.body);
try {
const response = await this.paymentClient.initiatePayment(request);
res.status(200).send({
localhost: this.buildHppUrl(response, 'http://localhost:3000'),
hpp_url: this.buildHppUrl(response),
request,
response
});
} catch (error) {
next(error instanceof HttpException ? error : new HttpException(500, 'Failed to initiate payment.'));
}
};

private buildDefaultCreatePaymentRequest = (req: Partial<CreatePaymentRequest>): any => {
if (req?.payment_method != undefined && req?.payment_method?.type != 'bank_transfer') {
throw new Error('Unsuporrted payment method type. Only "bank_transfer" is supported.');
}

return {
amount_in_minor: 1,
currency: req?.currency ?? 'GBP',
user: {
name: 'John Doe',
phone: '+447514983456',
email: '[email protected]',
...req?.user
},
payment_method: {
type: 'bank_transfer', // Always bank transfer
provider_selection: this.overrideProviderSelection(req?.payment_method?.provider_selection),
beneficiary: { ...this.overrideBeneficiary(req?.payment_method?.beneficiary) }
}
};
};

private buildHppUrl = (response: CreatePaymentRequestResponse, baseUrl?: string): string => {
return `${baseUrl ?? config.HPP_URI}/payments#payment_id=${response.id}&resource_token=${
response.resource_token
}&return_uri=${config.REDIRECT_URI}`;
};

private overrideProviderSelection = (providerSelection?: Partial<ProviderSelection>): ProviderSelection => {
if (providerSelection != undefined) {
if (providerSelection?.type == null) {
throw new Error('provider selection type is required');
}

if (providerSelection.type === 'user_selected') {
return {
type: 'user_selected',
...this.overrideProviderSelectionFilter(providerSelection?.filter),
...this.overrideSchemeSelection(providerSelection?.scheme_selection)
};
}

if (providerSelection.type === 'preselected') {
const result: ProviderSelection = {
type: 'preselected',
provider_id: providerSelection?.provider_id ?? 'ob-lloyds',
...this.overrideSchemeSelection(providerSelection?.scheme_selection)
};

if (providerSelection?.scheme_selection == undefined) {
result.scheme_id = providerSelection?.scheme_id ?? 'faster_payments_service';
}

return result;
}
}

return {
type: 'user_selected',
filter: {
release_channel: 'alpha'
}
};
};

private overrideProviderSelectionFilter = (
filter?: Partial<ProviderSelectionFilter>
): { filter?: ProviderSelectionFilter } => {
// Sending a property as null removes it from the request overriding the default values
if (filter === null) {
return {};
}

if (filter != undefined) {
return {
filter: {
...filter,
release_channel: filter?.release_channel ?? 'alpha'
}
};
}

return { filter: { release_channel: 'alpha' } };
};

private overrideSchemeSelection = (
schemeSelection?: Partial<SchemeSelection>
): { scheme_selection?: SchemeSelection } => {
if (schemeSelection?.type == 'instant_only') {
return {
scheme_selection: {
...schemeSelection,
type: 'instant_only'
}
};
}

if (schemeSelection?.type == 'instant_preferred') {
return {
scheme_selection: {
...schemeSelection,
type: 'instant_preferred'
}
};
}

if (schemeSelection?.type == 'user_selected') {
return {
scheme_selection: {
type: 'user_selected'
}
};
}

return {};
};

private overrideBeneficiary = (beneficiary?: Partial<Beneficiary>): Beneficiary => {
if (beneficiary != undefined) {
if (beneficiary?.type == null) {
throw new Error('beneficiary type is required');
}

if (beneficiary.type === 'external_account') {
return {
type: 'external_account',
reference: beneficiary?.reference ?? 'some reference',
account_holder_name: beneficiary?.account_holder_name ?? 'Merry Poppins',
account_identifier: { ...this.overrideAccountIdentifier(beneficiary?.account_identifier) }
};
}
// Add merchant_account if needed
}

// Default to external_account
return {
type: 'external_account',
reference: 'some reference',
account_holder_name: 'Merry Poppins',
account_identifier: {
type: 'sort_code_account_number',
account_number: '12345678',
sort_code: '112233'
}
};
};

private overrideAccountIdentifier = (
accountIdentifier?: Partial<PaymentAccountIdentifier>
): PaymentAccountIdentifier => {
if (accountIdentifier != undefined) {
if (accountIdentifier.type == null) {
throw new Error('account identifier type is required');
}

if (accountIdentifier.type === 'sort_code_account_number') {
return {
type: 'sort_code_account_number',
account_number: accountIdentifier?.account_number ?? '12345678',
sort_code: accountIdentifier?.sort_code ?? '112233'
};
}

if (accountIdentifier.type === 'iban') {
return {
type: 'iban',
iban: accountIdentifier?.iban ?? 'GB33BUKB20201555555555'
};
}
}

return {
type: 'sort_code_account_number',
account_number: '12345678',
sort_code: '112233'
};
};
}
12 changes: 4 additions & 8 deletions src/controllers/paymentsV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@ import AuthenticationClient from 'clients/authentication-client';
import PaymentsClient from 'clients/paymentv3-client';
import { HttpException } from 'middleware/errors';
import config from 'config';
import {
CreatePaymentRequest,
ProviderFilter,
ProviderSelection,
SchemeSelection
} from 'models/v3/payments-api/create_payment';
import { ProviderSelectionFilter } from 'models/v3/payments-api/common';
import { CreatePaymentRequest, ProviderSelection, SchemeSelection } from 'models/v3/payments-api/create_payment';

/**
* Controller for the PaymentsV3 API - Payments.
Expand Down Expand Up @@ -112,7 +108,7 @@ export default class PaymentsV3Controller {
private buildPaymentRequest(currency?: 'EUR'): CreatePaymentRequest {
// Include all providers by default,
// this is particulary useful when testing against embedded flow mock banks on Mobile (xs2a-volksbanken-de-sandbox)
const filter: ProviderFilter = {
const filter: ProviderSelectionFilter = {
release_channel: 'alpha'
};

Expand Down Expand Up @@ -156,7 +152,7 @@ export default class PaymentsV3Controller {
// ob-monzo does not work with preselected
private buildPaymentRequestWithUserSelectedScheme(): CreatePaymentRequest {
const beneficiary = this.getBeneficiary('EUR');
const filter: ProviderFilter = {
const filter: ProviderSelectionFilter = {
release_channel: 'alpha'
};

Expand Down
4 changes: 4 additions & 0 deletions src/models/v3/payments-api/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const providerFilterSchema = z
})
.deepPartial();

export type ProviderSelectionFilter = z.infer<typeof providerFilterSchema>;

export const accountIdentifierScanSchema = z.object({
type: z.literal('sort_code_account_number'),
sort_code: z.string(),
Expand All @@ -59,6 +61,8 @@ export const paymentAccountIdentifierSchema = z.discriminatedUnion('type', [
accountIdentifierIbanSchema
]);

export type PaymentAccountIdentifier = z.infer<typeof paymentAccountIdentifierSchema>;

export const remitterSchema = z.object({
account_holder_name: z.string(),
account_identifier: paymentAccountIdentifierSchema
Expand Down
6 changes: 3 additions & 3 deletions src/models/v3/payments-api/create_payment.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import z from 'zod';
import { currencyCodeSchema, paymentAccountIdentifierSchema, providerFilterSchema } from './common';

export type ProviderFilter = z.infer<typeof providerFilterSchema>;

const beneficiarySchema = z.object({
type: z.literal('external_account'),
account_identifier: paymentAccountIdentifierSchema,
account_holder_name: z.string(),
reference: z.string()
});

export type Beneficiary = z.infer<typeof beneficiarySchema>;

const schemeSelectionIntantOnlySchema = z.object({
type: z.literal('instant_only'),
allow_remitter_fee: z.boolean().optional()
Expand All @@ -34,7 +34,7 @@ export type SchemeSelection = z.infer<typeof schemeSelectionSchema>;

const providerSelectionUserSelectedSchema = z.object({
type: z.literal('user_selected'),
filter: providerFilterSchema,
filter: providerFilterSchema.optional(),
scheme_selection: schemeSelectionSchema.optional()
});

Expand Down
3 changes: 3 additions & 0 deletions src/routes/payments.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import PaymentsV3Controller from 'controllers/paymentsV3';
import { PaymentsController } from 'controllers/paymentsController';
import { Router } from 'express';

const router = Router();
const v3Controller = new PaymentsV3Controller();
const paymentsController = new PaymentsController();

router.post('/v3/payment', v3Controller.createPayment);
router.post('/v3/payment/euro', v3Controller.createEuroPayment);
router.post('/v3/payment/provider', v3Controller.createPaymentWithProvider());
router.post('/v3/payment/scheme_selection', v3Controller.createPaymentWithUserSelectedScheme());
router.get('/v3/payment/:id', v3Controller.getPayment);
router.post('/v3/payment/create', paymentsController.createPayment);

export default router;

0 comments on commit 83130b0

Please sign in to comment.