Skip to content

Commit ee87641

Browse files
author
FranciscoOnt
committed
feat(checkout): INT-1780 Enable card vaulting for barclaycard
1 parent 431b67a commit ee87641

8 files changed

+274
-5
lines changed

src/payment/create-payment-strategy-registry.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { AdyenV2PaymentStrategy, AdyenV2ScriptLoader } from './strategies/adyenv
2222
import { AffirmPaymentStrategy, AffirmScriptLoader } from './strategies/affirm';
2323
import { AfterpayPaymentStrategy, AfterpayScriptLoader } from './strategies/afterpay';
2424
import { AmazonPayPaymentStrategy, AmazonPayScriptLoader } from './strategies/amazon-pay';
25+
import { BarclaycardPaymentStrategy } from './strategies/barclaycard';
2526
import { createBraintreePaymentProcessor, createBraintreeVisaCheckoutPaymentProcessor, BraintreeCreditCardPaymentStrategy, BraintreePaypalPaymentStrategy, BraintreeScriptLoader, BraintreeSDKCreator, BraintreeVisaCheckoutPaymentStrategy, VisaCheckoutScriptLoader } from './strategies/braintree';
2627
import { CardinalClient, CardinalScriptLoader, CardinalThreeDSecureFlow } from './strategies/cardinal';
2728
import { ChasePayPaymentStrategy, ChasePayScriptLoader } from './strategies/chasepay';
@@ -365,5 +366,13 @@ export default function createPaymentStrategyRegistry(
365366
)
366367
);
367368

369+
registry.register(PaymentStrategyType.BARCLAYCARD, () =>
370+
new BarclaycardPaymentStrategy(
371+
store,
372+
orderActionCreator,
373+
paymentActionCreator
374+
)
375+
);
376+
368377
return registry;
369378
}

src/payment/instrument/supported-payment-instruments.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ const supportedInstruments: SupportedInstruments = {
4949
provider: 'paymetric',
5050
method: 'card',
5151
},
52+
barclaycard: {
53+
provider: 'barclaycard',
54+
method: 'card',
55+
},
5256
};
5357

5458
export default supportedInstruments;

src/payment/payment-action-creator.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ describe('PaymentActionCreator', () => {
106106
describe('#initializeOffsitePayment()', () => {
107107
it('dispatches actions to data store', async () => {
108108
const payment = getPayment();
109-
const actions = await from(paymentActionCreator.initializeOffsitePayment(payment.methodId, payment.gatewayId)(store))
109+
const actions = await from(paymentActionCreator.initializeOffsitePayment(payment.methodId, payment.gatewayId, payment.paymentData)(store))
110110
.pipe(toArray())
111111
.toPromise();
112112

@@ -124,7 +124,7 @@ describe('PaymentActionCreator', () => {
124124

125125
const errorHandler = jest.fn(action => of(action));
126126
const payment = getPayment();
127-
const actions = await from(paymentActionCreator.initializeOffsitePayment(payment.methodId, payment.gatewayId)(store))
127+
const actions = await from(paymentActionCreator.initializeOffsitePayment(payment.methodId, payment.gatewayId, payment.paymentData)(store))
128128
.pipe(
129129
catchError(errorHandler),
130130
toArray()

src/payment/payment-action-creator.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { InternalCheckoutSelectors } from '../checkout';
66
import { throwErrorAction } from '../common/error';
77
import { OrderActionCreator } from '../order';
88

9-
import Payment from './payment';
9+
import Payment, { PaymentInstrument } from './payment';
1010
import { InitializeOffsitePaymentAction, PaymentActionType, SubmitPaymentAction } from './payment-actions';
1111
import PaymentRequestSender from './payment-request-sender';
1212
import PaymentRequestTransformer from './payment-request-transformer';
@@ -37,10 +37,11 @@ export default class PaymentActionCreator {
3737

3838
initializeOffsitePayment(
3939
methodId: string,
40-
gatewayId?: string
40+
gatewayId?: string,
41+
paymentData: PaymentInstrument = {}
4142
): ThunkAction<InitializeOffsitePaymentAction, InternalCheckoutSelectors> {
4243
return store => {
43-
const payload = this._paymentRequestTransformer.transform({ gatewayId, methodId }, store.getState());
44+
const payload = this._paymentRequestTransformer.transform({ gatewayId, methodId, paymentData }, store.getState());
4445

4546
return concat(
4647
of(createAction(PaymentActionType.InitializeOffsitePaymentRequested)),

src/payment/payment-strategy-type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ enum PaymentStrategyType {
2727
STRIPE_GOOGLE_PAY = 'googlepaystripe',
2828
ZIP = 'zip',
2929
CONVERGE = 'converge',
30+
BARCLAYCARD = 'barclaycard',
3031
}
3132

3233
export default PaymentStrategyType;
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { createClient as createPaymentClient } from '@bigcommerce/bigpay-client';
2+
import { createAction } from '@bigcommerce/data-store';
3+
import { createRequestSender } from '@bigcommerce/request-sender';
4+
import { createScriptLoader } from '@bigcommerce/script-loader';
5+
import { merge } from 'lodash';
6+
import { of, Observable } from 'rxjs';
7+
8+
import { createCheckoutStore, CheckoutRequestSender, CheckoutStore, CheckoutValidator } from '../../../checkout';
9+
import { getCheckoutStoreState } from '../../../checkout/checkouts.mock';
10+
import { FinalizeOrderAction, OrderActionCreator, OrderActionType, OrderRequestBody, OrderRequestSender, SubmitOrderAction } from '../../../order';
11+
import { OrderFinalizationNotRequiredError } from '../../../order/errors';
12+
import { getIncompleteOrder, getOrderRequestBody, getSubmittedOrder } from '../../../order/internal-orders.mock';
13+
import { getOrder } from '../../../order/orders.mock';
14+
import { createSpamProtection, SpamProtectionActionCreator } from '../../../order/spam-protection';
15+
import PaymentActionCreator from '../../payment-action-creator';
16+
import { InitializeOffsitePaymentAction, PaymentActionType, SubmitPaymentAction } from '../../payment-actions';
17+
import { PaymentRequestOptions } from '../../payment-request-options';
18+
import PaymentRequestSender from '../../payment-request-sender';
19+
import PaymentRequestTransformer from '../../payment-request-transformer';
20+
import * as paymentStatusTypes from '../../payment-status-types';
21+
import { getVaultedInstrument } from '../../payments.mock';
22+
23+
import BarclaycardPaymentStrategy from './barclaycard-payment-strategy';
24+
25+
describe('BarclaycardPaymentStrategy', () => {
26+
let finalizeOrderAction: Observable<FinalizeOrderAction>;
27+
let initializeOffsitePaymentAction: Observable<InitializeOffsitePaymentAction>;
28+
let orderActionCreator: OrderActionCreator;
29+
let paymentActionCreator: PaymentActionCreator;
30+
let options: PaymentRequestOptions;
31+
let payload: OrderRequestBody;
32+
let store: CheckoutStore;
33+
let strategy: BarclaycardPaymentStrategy;
34+
let submitOrderAction: Observable<SubmitOrderAction>;
35+
let submitPaymentAction: Observable<SubmitPaymentAction>;
36+
37+
beforeEach(() => {
38+
store = createCheckoutStore(getCheckoutStoreState());
39+
orderActionCreator = new OrderActionCreator(
40+
new OrderRequestSender(createRequestSender()),
41+
new CheckoutValidator(new CheckoutRequestSender(createRequestSender())),
42+
new SpamProtectionActionCreator(createSpamProtection(createScriptLoader()))
43+
);
44+
paymentActionCreator = new PaymentActionCreator(
45+
new PaymentRequestSender(createPaymentClient()),
46+
orderActionCreator,
47+
new PaymentRequestTransformer()
48+
);
49+
finalizeOrderAction = of(createAction(OrderActionType.FinalizeOrderRequested));
50+
initializeOffsitePaymentAction = of(createAction(PaymentActionType.InitializeOffsitePaymentRequested));
51+
submitOrderAction = of(createAction(OrderActionType.SubmitOrderRequested));
52+
submitPaymentAction = of(createAction(PaymentActionType.SubmitPaymentRequested));
53+
54+
options = { methodId: 'card', gatewayId: 'barclaycard' };
55+
payload = merge(getOrderRequestBody(), {
56+
payment: {
57+
methodId: options.methodId,
58+
gatewayId: options.gatewayId,
59+
paymentData: {
60+
shouldSaveInstrument: false,
61+
},
62+
},
63+
});
64+
65+
jest.spyOn(store, 'dispatch');
66+
67+
jest.spyOn(orderActionCreator, 'finalizeOrder')
68+
.mockReturnValue(finalizeOrderAction);
69+
70+
jest.spyOn(orderActionCreator, 'submitOrder')
71+
.mockReturnValue(submitOrderAction);
72+
73+
jest.spyOn(paymentActionCreator, 'submitPayment')
74+
.mockReturnValue(submitPaymentAction);
75+
76+
jest.spyOn(paymentActionCreator, 'initializeOffsitePayment')
77+
.mockReturnValue(initializeOffsitePaymentAction);
78+
79+
strategy = new BarclaycardPaymentStrategy(store, orderActionCreator, paymentActionCreator);
80+
});
81+
82+
it('submits order with payment payload', async () => {
83+
await strategy.execute(payload, options);
84+
85+
expect(orderActionCreator.submitOrder).toHaveBeenCalledWith(payload, options);
86+
expect(store.dispatch).toHaveBeenCalledWith(submitOrderAction);
87+
});
88+
89+
it('initializes offsite payment flow', async () => {
90+
await strategy.execute(payload, options);
91+
92+
expect(paymentActionCreator.initializeOffsitePayment).toHaveBeenCalled();
93+
expect(store.dispatch).toHaveBeenCalledWith(initializeOffsitePaymentAction);
94+
});
95+
96+
it('submits payment with a vaulted instrument', async () => {
97+
const payload = {
98+
...getOrderRequestBody(),
99+
payment: {
100+
methodId: 'card',
101+
gatewayId: 'barclaycard',
102+
paymentData: getVaultedInstrument(),
103+
},
104+
};
105+
106+
await strategy.execute(payload, options);
107+
108+
expect(orderActionCreator.submitOrder).toHaveBeenCalledWith(payload, options);
109+
expect(paymentActionCreator.submitPayment).toHaveBeenCalled();
110+
expect(store.dispatch).toHaveBeenCalledWith(submitOrderAction);
111+
expect(store.dispatch).toHaveBeenCalledWith(submitPaymentAction);
112+
});
113+
114+
it('finalizes order if order is created and payment is acknowledged', async () => {
115+
const state = store.getState();
116+
117+
jest.spyOn(state.order, 'getOrder')
118+
.mockReturnValue(getOrder());
119+
120+
jest.spyOn(state.payment, 'getPaymentStatus')
121+
.mockReturnValue(paymentStatusTypes.ACKNOWLEDGE);
122+
123+
await strategy.finalize(options);
124+
125+
expect(orderActionCreator.finalizeOrder).toHaveBeenCalledWith(getOrder().orderId, options);
126+
expect(store.dispatch).toHaveBeenCalledWith(finalizeOrderAction);
127+
});
128+
129+
it('finalizes order if order is created and payment is finalized', async () => {
130+
const state = store.getState();
131+
132+
jest.spyOn(state.order, 'getOrder')
133+
.mockReturnValue(getOrder());
134+
135+
jest.spyOn(state.payment, 'getPaymentStatus')
136+
.mockReturnValue(paymentStatusTypes.FINALIZE);
137+
138+
await strategy.finalize(options);
139+
140+
expect(orderActionCreator.finalizeOrder).toHaveBeenCalledWith(getOrder().orderId, options);
141+
expect(store.dispatch).toHaveBeenCalledWith(finalizeOrderAction);
142+
});
143+
144+
it('does not finalize order if order is not created', async () => {
145+
const state = store.getState();
146+
147+
jest.spyOn(state.order, 'getOrder').mockReturnValue(getIncompleteOrder());
148+
149+
try {
150+
await strategy.finalize();
151+
} catch (error) {
152+
expect(error).toBeInstanceOf(OrderFinalizationNotRequiredError);
153+
expect(orderActionCreator.finalizeOrder).not.toHaveBeenCalled();
154+
expect(store.dispatch).not.toHaveBeenCalledWith(finalizeOrderAction);
155+
}
156+
});
157+
158+
it('does not finalize order if order is not finalized or acknowledged', async () => {
159+
const state = store.getState();
160+
161+
jest.spyOn(state.order, 'getOrder').mockReturnValue(merge({}, getSubmittedOrder(), {
162+
payment: {
163+
status: paymentStatusTypes.INITIALIZE,
164+
},
165+
}));
166+
167+
try {
168+
await strategy.finalize();
169+
} catch (error) {
170+
expect(error).toBeInstanceOf(OrderFinalizationNotRequiredError);
171+
expect(orderActionCreator.finalizeOrder).not.toHaveBeenCalled();
172+
expect(store.dispatch).not.toHaveBeenCalledWith(finalizeOrderAction);
173+
}
174+
});
175+
176+
it('throws error if unable to finalize due to missing data', async () => {
177+
const state = store.getState();
178+
179+
jest.spyOn(state.order, 'getOrder')
180+
.mockReturnValue(null);
181+
182+
try {
183+
await strategy.finalize();
184+
} catch (error) {
185+
expect(error).toBeInstanceOf(OrderFinalizationNotRequiredError);
186+
}
187+
});
188+
189+
it('returns checkout state', async () => {
190+
const output = await strategy.execute(getOrderRequestBody());
191+
192+
expect(output).toEqual(store.getState());
193+
});
194+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { CheckoutStore, InternalCheckoutSelectors } from '../../../checkout';
2+
import { OrderActionCreator, OrderRequestBody } from '../../../order';
3+
import { OrderFinalizationNotRequiredError } from '../../../order/errors';
4+
import { PaymentArgumentInvalidError } from '../../errors';
5+
import isVaultedInstrument from '../../is-vaulted-instrument';
6+
import PaymentActionCreator from '../../payment-action-creator';
7+
import { PaymentRequestOptions } from '../../payment-request-options';
8+
import * as paymentStatusTypes from '../../payment-status-types';
9+
import PaymentStrategy from '../payment-strategy';
10+
11+
export default class BarclaycardPaymentStrategy implements PaymentStrategy {
12+
constructor(
13+
private _store: CheckoutStore,
14+
private _orderActionCreator: OrderActionCreator,
15+
private _paymentActionCreator: PaymentActionCreator
16+
) { }
17+
18+
async execute(payload: OrderRequestBody, options?: PaymentRequestOptions): Promise<InternalCheckoutSelectors> {
19+
const { payment } = payload;
20+
const paymentData = payment && payment.paymentData;
21+
22+
if (!payment) {
23+
throw new PaymentArgumentInvalidError(['payment']);
24+
}
25+
26+
await this._store.dispatch(this._orderActionCreator.submitOrder(payload, options));
27+
28+
if (paymentData && isVaultedInstrument(paymentData)) {
29+
return this._store.dispatch(this._paymentActionCreator.submitPayment({ ...payment, paymentData }));
30+
}
31+
32+
return await this._store.dispatch(this._paymentActionCreator.initializeOffsitePayment(
33+
payment.methodId,
34+
payment.gatewayId,
35+
{
36+
...paymentData,
37+
}));
38+
}
39+
40+
finalize(options?: PaymentRequestOptions): Promise<InternalCheckoutSelectors> {
41+
const state = this._store.getState();
42+
const order = state.order.getOrder();
43+
const status = state.payment.getPaymentStatus();
44+
45+
if (order && (status === paymentStatusTypes.ACKNOWLEDGE || status === paymentStatusTypes.FINALIZE)) {
46+
return this._store.dispatch(this._orderActionCreator.finalizeOrder(order.orderId, options));
47+
}
48+
49+
return Promise.reject(new OrderFinalizationNotRequiredError());
50+
}
51+
52+
initialize(): Promise<InternalCheckoutSelectors> {
53+
return Promise.resolve(this._store.getState());
54+
}
55+
56+
deinitialize(): Promise<InternalCheckoutSelectors> {
57+
return Promise.resolve(this._store.getState());
58+
}
59+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as BarclaycardPaymentStrategy } from './barclaycard-payment-strategy';

0 commit comments

Comments
 (0)