Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createScriptLoader } from '@bigcommerce/script-loader';
import { noop } from 'lodash';

import {
BillingAddress,
Expand Down Expand Up @@ -1933,6 +1934,113 @@ describe('StripeOCSPaymentStrategy', () => {
});
});

describe('asyncPaymentValidation', () => {
const mockPaymentMethodWithFlag = (asyncPaymentValidation: boolean) => {
jest.spyOn(
paymentIntegrationService.getState(),
'getPaymentMethodOrThrow',
).mockReturnValue({
...getStripeOCSMock(),
initializationData: {
...getStripeOCSMock().initializationData,
asyncPaymentValidation,
},
});
};

const mockStripeCheckoutWithConfirm = (confirmFn: jest.Mock) => {
jest.spyOn(stripeScriptLoader, 'getStripeCheckout').mockReturnValue(
Promise.resolve({
...getStripeCheckoutInstanceMock(),
loadActions: () =>
Promise.resolve({
type: StripeLoadActionsResultType.SUCCESS,
actions: {
...getStripeCheckoutSessionActionsMock(),
confirm: confirmFn,
},
}),
}),
);
};

it('resolves without waiting for the second submitPayment response when flag is true', async () => {
mockPaymentMethodWithFlag(true);
mockFirstPaymentRequest(errorResponse);
jest.spyOn(paymentIntegrationService, 'submitPayment').mockReturnValueOnce(
new Promise<never>(noop),
);
confirmPaymentMock = jest.fn().mockResolvedValue({
session: {
id: 'paymentIntentId',
status: {
paymentStatus: StripeCheckoutSessionPaymentStatus.UnPaid,
},
},
});
mockStripeCheckoutWithConfirm(confirmPaymentMock);

await stripeCSPaymentStrategy.initialize(stripeOptions);

await expect(
stripeCSPaymentStrategy.execute(getStripeOCSOrderRequestBodyMock(methodId)),
).resolves.not.toThrow();

expect(paymentIntegrationService.submitPayment).toHaveBeenCalledTimes(2);
});

it('resolves and ignores error when second submitPayment fails and order already paid on the stripe side', async () => {
mockPaymentMethodWithFlag(true);
mockFirstPaymentRequest(errorResponse);
mockFirstPaymentRequest(new Error('second submitPayment error'));
confirmPaymentMock = jest.fn().mockResolvedValue({
session: {
id: 'paymentIntentId',
status: {
paymentStatus: StripeCheckoutSessionPaymentStatus.Paid,
},
},
});
mockStripeCheckoutWithConfirm(confirmPaymentMock);

await stripeCSPaymentStrategy.initialize(stripeOptions);

await expect(
stripeCSPaymentStrategy.execute(getStripeOCSOrderRequestBodyMock(methodId)),
).resolves.not.toThrow();

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

expect(paymentIntegrationService.submitPayment).toHaveBeenCalledTimes(2);
expect(
stripeIntegrationService.throwPaymentConfirmationProceedMessage,
).not.toHaveBeenCalled();
});

it('waits for the second submitPayment response when flag is false', async () => {
mockPaymentMethodWithFlag(false);
mockFirstPaymentRequest(errorResponse);
mockFirstPaymentRequest(new Error('second submitPayment error'));
confirmPaymentMock = jest.fn().mockResolvedValue({
session: {
id: 'paymentIntentId',
status: {
paymentStatus: StripeCheckoutSessionPaymentStatus.UnPaid,
},
},
});
mockStripeCheckoutWithConfirm(confirmPaymentMock);

await stripeCSPaymentStrategy.initialize(stripeOptions);

await expect(
stripeCSPaymentStrategy.execute(getStripeOCSOrderRequestBodyMock(methodId)),
).rejects.toThrow('second submitPayment error');

expect(paymentIntegrationService.submitPayment).toHaveBeenCalledTimes(2);
});
});

describe('vaulting via selected payment method', () => {
const setupStripeCheckoutWithEvent = (
eventValue:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cloneDeep, merge } from 'lodash';
import { cloneDeep, merge, noop } from 'lodash';

import {
InvalidArgumentError,
Expand Down Expand Up @@ -368,7 +368,8 @@ export default class StripeCSPaymentStrategy implements PaymentStrategy {
const { initializationData } = this.paymentIntegrationService
.getState()
.getPaymentMethodOrThrow<StripeInitializationData>(methodId, gatewayId);
const { sendSecondPaymentRequestOnStripeError } = initializationData || {};
const { sendSecondPaymentRequestOnStripeError, asyncPaymentValidation } =
initializationData || {};

if (stripeError || !stripeCheckoutSession) {
if (sendSecondPaymentRequestOnStripeError) {
Expand All @@ -395,6 +396,15 @@ export default class StripeCSPaymentStrategy implements PaymentStrategy {
throw new PaymentMethodFailedError(stripeError?.message);
}

if (asyncPaymentValidation) {
// INFO: await is skipped and errors are ignored here intentionally, because the payment
// is already confirmed on the Stripe side, so the order status will be updated
// by webhooks even if this request fails.
this.paymentIntegrationService.submitPayment(paymentPayload).catch(noop);

return;
Comment thread
PavlenkoM marked this conversation as resolved.
}

try {
return await this.paymentIntegrationService.submitPayment(paymentPayload);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createScriptLoader } from '@bigcommerce/script-loader';
import { noop } from 'lodash';

import {
InvalidArgumentError,
Expand Down Expand Up @@ -1362,6 +1363,105 @@ describe('StripeOCSPaymentStrategy', () => {
);
});
});

describe('asyncPaymentValidation', () => {
const mockPaymentMethodWithFlag = (asyncPaymentValidation: boolean) => {
jest.spyOn(
paymentIntegrationService.getState(),
'getPaymentMethodOrThrow',
).mockReturnValue({
...getStripeOCSMock(),
initializationData: {
...getStripeOCSMock().initializationData,
asyncPaymentValidation,
},
});
};

const mockStripeClientWithConfirm = (confirmFn: jest.Mock) => {
stripeUPEJsMock = {
...getStripeJsMock(),
confirmPayment: confirmFn,
retrievePaymentIntent: jest.fn(),
};
jest.spyOn(stripeScriptLoader, 'getStripeClient').mockImplementation(
jest.fn(() => Promise.resolve(stripeUPEJsMock)),
);
};

it('resolves without waiting for the second submitPayment response when flag is true', async () => {
mockPaymentMethodWithFlag(true);
mockFirstPaymentRequest(errorResponse);
jest.spyOn(paymentIntegrationService, 'submitPayment').mockReturnValueOnce(
new Promise<never>(noop),
);
confirmPaymentMock = jest.fn().mockResolvedValue({
paymentIntent: {
id: 'paymentIntentId',
client_secret: 'paymentIntentClientSecret',
},
});
mockStripeClientWithConfirm(confirmPaymentMock);

await stripeOCSPaymentStrategy.initialize(stripeOptions);

await expect(
stripeOCSPaymentStrategy.execute(getStripeOCSOrderRequestBodyMock()),
).resolves.not.toThrow();

expect(paymentIntegrationService.submitPayment).toHaveBeenCalledTimes(2);
});

it('resolves and ignores error when second submitPayment request fails', async () => {
mockPaymentMethodWithFlag(true);
mockFirstPaymentRequest(errorResponse);
mockFirstPaymentRequest(new Error('second submitPayment error'));
confirmPaymentMock = jest.fn().mockResolvedValue({
paymentIntent: {
id: 'paymentIntentId',
client_secret: 'paymentIntentClientSecret',
},
});
mockStripeClientWithConfirm(confirmPaymentMock);

await stripeOCSPaymentStrategy.initialize(stripeOptions);

await expect(
stripeOCSPaymentStrategy.execute(getStripeOCSOrderRequestBodyMock()),
).resolves.not.toThrow();

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

expect(paymentIntegrationService.submitPayment).toHaveBeenCalledTimes(2);
expect(
stripeIntegrationService.throwPaymentConfirmationProceedMessage,
).not.toHaveBeenCalled();
});

it('waits for the second submitPayment response when flag is false', async () => {
mockPaymentMethodWithFlag(false);
mockFirstPaymentRequest(errorResponse);
mockFirstPaymentRequest(new Error('second submitPayment error'));
confirmPaymentMock = jest.fn().mockResolvedValue({
paymentIntent: {
id: 'paymentIntentId',
client_secret: 'paymentIntentClientSecret',
},
});
mockStripeClientWithConfirm(confirmPaymentMock);

await stripeOCSPaymentStrategy.initialize(stripeOptions);

await expect(
stripeOCSPaymentStrategy.execute(getStripeOCSOrderRequestBodyMock()),
).rejects.toThrow(PaymentMethodFailedError);

expect(paymentIntegrationService.submitPayment).toHaveBeenCalledTimes(2);
expect(
stripeIntegrationService.throwPaymentConfirmationProceedMessage,
).toHaveBeenCalled();
});
});
});

describe('#vaulted instruments', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { merge } from 'lodash';
import { merge, noop } from 'lodash';

import {
InvalidArgumentError,
Expand Down Expand Up @@ -313,13 +313,13 @@ export default class StripeOCSPaymentStrategy implements PaymentStrategy {
paymentIntentClientSecret || token,
paymentMethodOptions,
);
const { initializationData } = this.paymentIntegrationService
.getState()
.getPaymentMethodOrThrow<StripeInitializationData>(methodId, gatewayId);
const { sendSecondPaymentRequestOnStripeError, asyncPaymentValidation } =
initializationData || {};

if (stripeError || !paymentIntent) {
const { initializationData } = this.paymentIntegrationService
.getState()
.getPaymentMethodOrThrow<StripeInitializationData>(methodId, gatewayId);
const { sendSecondPaymentRequestOnStripeError } = initializationData || {};

if (sendSecondPaymentRequestOnStripeError) {
// INFO: even in case when stripe payment confirmation was declined
// we need to send submitPayment request to update status of checkout session on BE side.
Expand All @@ -342,6 +342,15 @@ export default class StripeOCSPaymentStrategy implements PaymentStrategy {
this.stripeIntegrationService.throwStripeError(stripeError);
}

if (asyncPaymentValidation) {
// INFO: await is skipped and errors are ignored here intentionally, because the payment
// is already confirmed on the Stripe side, so the order status will be updated
// by webhooks even if this request fails.
this.paymentIntegrationService.submitPayment(paymentPayload).catch(noop);

return;
}

try {
return await this.paymentIntegrationService.submitPayment(paymentPayload);
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions packages/stripe-utils/src/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,7 @@ export interface StripeInitializationData {
sendSecondPaymentRequestOnStripeError?: boolean;
adaptivePricingEnabled?: boolean;
hasSectionOnTopOfPaymentsList?: boolean;
asyncPaymentValidation?: boolean;
}

export interface StripeElementUpdateOptions {
Expand Down