Skip to content

Commit

Permalink
refactor(checkout): PI-1548 moved Converge payment strategy to separa…
Browse files Browse the repository at this point in the history
…te package
  • Loading branch information
vitalii-koshovyi committed Jul 10, 2024
1 parent 4fdf761 commit 5f87917
Show file tree
Hide file tree
Showing 19 changed files with 424 additions and 307 deletions.
39 changes: 39 additions & 0 deletions packages/converge-integration/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/consistent-type-assertions": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off"
}
},
{
"files": ["*.spec.ts"],
"rules": {
"jest/valid-expect": "off",
"jest/no-if": "off",
"@typescript-eslint/await-thenable": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"jest/no-conditional-expect": "off",
"jest/no-test-return-statement": "off",
"@typescript-eslint/no-shadow": "off"
}
},
{
"files": ["*.mock.ts"],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}
]
}
17 changes: 17 additions & 0 deletions packages/converge-integration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# converge-integration
[npx jest --clearCache ](https://developer.elavon.com/products/converge/v1)
This library was generated with [Nx](https://nx.dev).
# converge-integration

This package contains the integration layer for the [Converge® is Elavon's ](https://developer.elavon.com/products/converge/v1) provider.

For additional information on Converge®, please refer to [Converge® documentation](https://developer.elavon.com/products/converge/v1).

## Running unit tests

Run `nx test converge-integration` to execute the unit tests via [Jest](https://jestjs.io).

## Running lint

Run `nx lint converge-integration` to execute the lint via [ESLint](https://eslint.org/).

16 changes: 16 additions & 0 deletions packages/converge-integration/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
displayName: 'converge-integration',
preset: '../../jest.preset.js',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
diagnostics: false,
},
},
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/packages/converge-integration',
setupFilesAfterEnv: ['../../jest-setup.js'],
};
22 changes: 22 additions & 0 deletions packages/converge-integration/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"root": "packages/converge-integration",
"sourceRoot": "packages/converge-integration/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["packages/converge-integration/**/*.ts"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/packages/converge-integration"],
"options": {
"jestConfig": "packages/converge-integration/jest.config.js"
}
}
},
"tags": ["scope:integration"]
}
188 changes: 188 additions & 0 deletions packages/converge-integration/src/converge-payment-strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { createAction, createErrorAction } from '@bigcommerce/data-store';
import { createFormPoster, FormPoster } from '@bigcommerce/form-poster';
import { merge, noop, omit } from 'lodash';
import { Observable, of } from 'rxjs';

import { CreditCardPaymentStrategy } from '@bigcommerce/checkout-sdk/credit-card-integration';
import {
FinalizeOrderAction,
OrderActionType,
OrderFinalizationNotRequiredError,
PaymentActionType,
PaymentIntegrationService,
PaymentStatusTypes,
RequestError,
SubmitOrderAction,
SubmitPaymentAction,
} from '@bigcommerce/checkout-sdk/payment-integration-api';
import {
getErrorPaymentResponseBody,
getOrder,
getOrderRequestBody,
getPaymentMethod,
getResponse,
PaymentIntegrationServiceMock,
} from '@bigcommerce/checkout-sdk/payment-integrations-test-utils';

import ConvergePaymentStrategy from './converge-payment-strategy';

describe('ConvergeaymentStrategy', () => {
let paymentIntegrationService: PaymentIntegrationService;
let finalizeOrderAction: Observable<FinalizeOrderAction>;
let formPoster: FormPoster;
let strategy: ConvergePaymentStrategy;
let submitOrderAction: Observable<SubmitOrderAction>;
let submitPaymentAction: Observable<SubmitPaymentAction>;

beforeEach(() => {
paymentIntegrationService = new PaymentIntegrationServiceMock();
formPoster = createFormPoster();

finalizeOrderAction = of(createAction(OrderActionType.FinalizeOrderRequested));
submitOrderAction = of(createAction(OrderActionType.SubmitOrderRequested));
submitPaymentAction = of(createAction(PaymentActionType.SubmitPaymentRequested));

jest.spyOn(formPoster, 'postForm').mockImplementation((_url, _data, callback = noop) =>
callback(),
);

jest.spyOn(paymentIntegrationService.getState(), 'getPaymentMethodOrThrow').mockReturnValue(
merge(getPaymentMethod(), { config: { isHostedFormEnabled: true } }),
);

jest.spyOn(paymentIntegrationService, 'finalizeOrder').mockReturnValue(finalizeOrderAction);

jest.spyOn(paymentIntegrationService, 'submitOrder').mockReturnValue(submitOrderAction);

jest.spyOn(paymentIntegrationService, 'submitPayment').mockReturnValue(submitPaymentAction);

strategy = new ConvergePaymentStrategy(paymentIntegrationService, formPoster);
});

afterEach(() => {
jest.clearAllMocks();
});

it('submits order without payment data', async () => {
const payload = getOrderRequestBody();
const options = { methodId: 'converge' };

await strategy.execute(payload, options);

expect(paymentIntegrationService.submitOrder).toHaveBeenCalledWith(
omit(payload, 'payment'),
options,
);
});

it('submits payment separately', async () => {
const payload = getOrderRequestBody();
const options = { methodId: 'converge' };

await strategy.execute(payload, options);

expect(paymentIntegrationService.submitPayment).toHaveBeenCalledWith(payload.payment);
});

it('posts 3ds data to Converge if 3ds is enabled', async () => {
const error = new RequestError(
getResponse({
...getErrorPaymentResponseBody(),
errors: [{ code: 'three_d_secure_required' }],
three_ds_result: {
acs_url: 'https://acs/url',
callback_url: 'https://callback/url',
payer_auth_request: 'payer_auth_request',
merchant_data: 'merchant_data',
},
status: 'error',
}),
);

jest.spyOn(paymentIntegrationService, 'submitPayment').mockImplementation(() => {
throw error;
});

strategy = new ConvergePaymentStrategy(paymentIntegrationService, formPoster);

strategy.execute(getOrderRequestBody());

await new Promise((resolve) => process.nextTick(resolve));

expect(formPoster.postForm).toHaveBeenCalledWith('https://acs/url', {
PaReq: 'payer_auth_request',
TermUrl: 'https://callback/url',
MD: 'merchant_data',
});
});

it('does not post 3ds data to Converge if 3ds is not enabled', async () => {
const response = new RequestError(getResponse(getErrorPaymentResponseBody()));

jest.spyOn(paymentIntegrationService, 'submitPayment').mockImplementation(() => {
throw response;
});

try {
await strategy.execute(getOrderRequestBody());
} catch (error) {
expect(error).toBeInstanceOf(RequestError);
expect(formPoster.postForm).not.toHaveBeenCalled();
}
});

it('finalizes order if order is created and payment is finalized', async () => {
const state = paymentIntegrationService.getState();

jest.spyOn(state, 'getOrder').mockReturnValue(getOrder());

jest.spyOn(state, 'getPaymentStatus').mockReturnValue(PaymentStatusTypes.FINALIZE);

await strategy.finalize();

expect(paymentIntegrationService.finalizeOrder).toHaveBeenCalled();
});

it('does not finalize order if order is not created', async () => {
const state = paymentIntegrationService.getState();

jest.spyOn(state, 'getOrder').mockReturnValue(null);

try {
await strategy.finalize();
} catch (error) {
expect(paymentIntegrationService.finalizeOrder).not.toHaveBeenCalled();

expect(error).toBeInstanceOf(OrderFinalizationNotRequiredError);
}
});

it('does not finalize order if order is not finalized', async () => {
const state = paymentIntegrationService.getState();

jest.spyOn(state, 'getPaymentStatus').mockReturnValue(PaymentStatusTypes.INITIALIZE);

try {
await strategy.finalize();
} catch (error) {
expect(paymentIntegrationService.finalizeOrder).not.toHaveBeenCalled();
expect(error).toBeInstanceOf(OrderFinalizationNotRequiredError);
}
});

it('throws error if order is missing', async () => {
const state = paymentIntegrationService.getState();

jest.spyOn(state, 'getOrder').mockReturnValue(null);

try {
await strategy.finalize();
} catch (error) {
expect(error).toBeInstanceOf(OrderFinalizationNotRequiredError);
}
});

it('is special type of credit card strategy', () => {
expect(strategy).toBeInstanceOf(CreditCardPaymentStrategy);
});
});
54 changes: 54 additions & 0 deletions packages/converge-integration/src/converge-payment-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { FormPoster } from '@bigcommerce/form-poster';
import { some } from 'lodash';

import { CreditCardPaymentStrategy } from '@bigcommerce/checkout-sdk/credit-card-integration';
import {
isRequestError,
OrderFinalizationNotRequiredError,
OrderRequestBody,
PaymentIntegrationService,
PaymentRequestOptions,
PaymentStatusTypes,
} from '@bigcommerce/checkout-sdk/payment-integration-api';

export default class ConvergePaymentStrategy extends CreditCardPaymentStrategy {
constructor(
paymentIntegrationService: PaymentIntegrationService,
protected formPoster: FormPoster,
) {
super(paymentIntegrationService);
}
async execute(payload: OrderRequestBody, options?: PaymentRequestOptions): Promise<void> {
try {
await super.execute(payload, options);
} catch (error) {
if (
!isRequestError(error) ||
!some(error.body.errors, { code: 'three_d_secure_required' })
) {
return Promise.reject(error);
}

return new Promise(() => {
this.formPoster.postForm(error.body.three_ds_result.acs_url, {
PaReq: error.body.three_ds_result.payer_auth_request,
TermUrl: error.body.three_ds_result.callback_url,
MD: error.body.three_ds_result.merchant_data,
});
});
}
}

async finalize(options?: PaymentRequestOptions): Promise<void> {
const state = this._paymentIntegrationService.getState();
const order = state.getOrder();

if (order && state.getPaymentStatus() === PaymentStatusTypes.FINALIZE) {
await this._paymentIntegrationService.finalizeOrder(options);

return;
}

return Promise.reject(new OrderFinalizationNotRequiredError());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PaymentIntegrationService } from '@bigcommerce/checkout-sdk/payment-integration-api';
import { PaymentIntegrationServiceMock } from '@bigcommerce/checkout-sdk/payment-integrations-test-utils';

import ConvergePaymentStrategy from './converge-payment-strategy';
import createConvergePaymentStrategy from './create-converge-payment-strategy';

describe('createConvergePaymentStrategy', () => {
let paymentIntegrationService: PaymentIntegrationService;

beforeEach(() => {
paymentIntegrationService = new PaymentIntegrationServiceMock();
});

it('instantiates Converge payment strategy', () => {
const strategy = createConvergePaymentStrategy(paymentIntegrationService);

expect(strategy).toBeInstanceOf(ConvergePaymentStrategy);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createFormPoster } from '@bigcommerce/form-poster';

import {
PaymentStrategyFactory,
toResolvableModule,
} from '@bigcommerce/checkout-sdk/payment-integration-api';

import ConvergePaymentStrategy from './converge-payment-strategy';

const createConvergePaymentStrategy: PaymentStrategyFactory<ConvergePaymentStrategy> = (
paymentIntegrationService,
) => {
return new ConvergePaymentStrategy(paymentIntegrationService, createFormPoster());
};

export default toResolvableModule(createConvergePaymentStrategy, [{ id: 'converge' }]);
1 change: 1 addition & 0 deletions packages/converge-integration/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as createConvergePaymentStrategy } from './create-converge-payment-strategy';
Loading

0 comments on commit 5f87917

Please sign in to comment.