Skip to content

Commit

Permalink
Merge branch 'develop' into fix/currencies-list-and-translatable-strings
Browse files Browse the repository at this point in the history
  • Loading branch information
frosso authored Aug 14, 2024
2 parents 1d2a0ff + 61ae351 commit 6bcd6b3
Show file tree
Hide file tree
Showing 12 changed files with 381 additions and 25 deletions.
4 changes: 4 additions & 0 deletions changelog/fix-9177-handle-payment-method-errors-in-backend
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: fix

If a payment method fails to be created in the frontend during checkout, forward the errors to the server so it can be recorded in an order.
23 changes: 21 additions & 2 deletions client/checkout/blocks/payment-processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { useCustomerData } from './utils';
import enableStripeLinkPaymentMethod from 'wcpay/checkout/stripe-link';
import { getUPEConfig } from 'wcpay/utils/checkout';
import { validateElements } from 'wcpay/checkout/classic/payment-processing';
import { PAYMENT_METHOD_ERROR } from 'wcpay/checkout/constants';

const getBillingDetails = ( billingData ) => {
return {
Expand Down Expand Up @@ -193,8 +194,26 @@ const PaymentProcessor = ( {

if ( result.error ) {
return {
type: 'error',
message: result.error.message,
// We return a `success` type even when there's an error since we want the checkout request to go
// through, so we can have this attempt recorded in an Order.
type: 'success',
meta: {
paymentMethodData: {
payment_method:
upeMethods[ paymentMethodId ],
'wcpay-payment-method': PAYMENT_METHOD_ERROR,
'wcpay-payment-method-error-code':
result.error.code,
'wcpay-payment-method-error-decline-code':
result.error.decline_code,
'wcpay-payment-method-error-message':
result.error.message,
'wcpay-payment-method-error-type':
result.error.type,
'wcpay-fraud-prevention-token': getFraudPreventionToken(),
'wcpay-fingerprint': fingerprint,
},
},
};
}

Expand Down
67 changes: 63 additions & 4 deletions client/checkout/blocks/test/payment-processor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useEffect } from 'react';
*/
import PaymentProcessor from '../payment-processor';
import { PaymentElement } from '@stripe/react-stripe-js';
import { PAYMENT_METHOD_ERROR } from 'wcpay/checkout/constants';

jest.mock( 'wcpay/checkout/classic/payment-processing', () => ( {
validateElements: jest.fn().mockResolvedValue(),
Expand Down Expand Up @@ -153,10 +154,15 @@ describe( 'PaymentProcessor', () => {
expect( mockCreatePaymentMethod ).not.toHaveBeenCalled();
} );

it( 'should return an error when createPaymentMethod fails', async () => {
it( 'should return success with the error data when createPaymentMethod fails', async () => {
let onPaymentSetupCallback;
mockCreatePaymentMethod = jest.fn().mockResolvedValue( {
error: { message: 'Error creating payment method' },
error: {
code: 'code',
decline_code: 'decline_code',
message: 'Error creating payment method',
type: 'card_error',
},
} );

act( () => {
Expand All @@ -179,8 +185,61 @@ describe( 'PaymentProcessor', () => {

expect( mockCreatePaymentMethod ).not.toHaveBeenCalled();
expect( await onPaymentSetupCallback() ).toEqual( {
type: 'error',
message: 'Error creating payment method',
type: 'success',
meta: {
paymentMethodData: {
payment_method: 'woocommerce_payments',
'wcpay-payment-method': PAYMENT_METHOD_ERROR,
'wcpay-payment-method-error-code': 'code',
'wcpay-payment-method-error-decline-code': 'decline_code',
'wcpay-payment-method-error-message':
'Error creating payment method',
'wcpay-payment-method-error-type': 'card_error',
'wcpay-fraud-prevention-token': '',
'wcpay-fingerprint': '',
},
},
} );
expect( mockCreatePaymentMethod ).toHaveBeenCalled();
} );

it( 'should return success when there are no failures', async () => {
let onPaymentSetupCallback;
mockCreatePaymentMethod = jest.fn().mockResolvedValue( {
paymentMethod: {
id: 'paymentMethodId',
},
} );

act( () => {
render(
<PaymentProcessor
activePaymentMethod="woocommerce_payments"
api={ mockApi }
paymentMethodId="card"
emitResponse={ {} }
eventRegistration={ {
onPaymentSetup: ( callback ) =>
( onPaymentSetupCallback = callback ),
} }
fingerprint=""
shouldSavePayment={ false }
upeMethods={ { card: 'woocommerce_payments' } }
/>
);
} );

expect( mockCreatePaymentMethod ).not.toHaveBeenCalled();
expect( await onPaymentSetupCallback() ).toEqual( {
type: 'success',
meta: {
paymentMethodData: {
payment_method: 'woocommerce_payments',
'wcpay-payment-method': 'paymentMethodId',
'wcpay-fraud-prevention-token': '',
'wcpay-fingerprint': '',
},
},
} );
expect( mockCreatePaymentMethod ).toHaveBeenCalled();
} );
Expand Down
39 changes: 26 additions & 13 deletions client/checkout/classic/payment-processing.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import {
appendFraudPreventionTokenInputToForm,
appendPaymentMethodIdToForm,
appendPaymentMethodErrorDataToForm,
getPaymentMethodTypes,
getSelectedUPEGatewayPaymentMethod,
getTerms,
Expand All @@ -29,7 +30,8 @@ import enableStripeLinkPaymentMethod, {
import {
SHORTCODE_SHIPPING_ADDRESS_FIELDS,
SHORTCODE_BILLING_ADDRESS_FIELDS,
} from '../constants';
PAYMENT_METHOD_ERROR,
} from 'wcpay/checkout/constants';

// It looks like on file import there are some side effects. Should probably be fixed.
const gatewayUPEComponents = {};
Expand Down Expand Up @@ -192,14 +194,7 @@ function createStripePaymentMethod(

return api
.getStripeForUPE( paymentMethodType )
.createPaymentMethod( { elements, params: params } )
.then( ( paymentMethod ) => {
if ( paymentMethod.error ) {
throw paymentMethod.error;
}

return paymentMethod;
} );
.createPaymentMethod( { elements, params: params } );
}

/**
Expand Down Expand Up @@ -563,11 +558,20 @@ export const processPayment = (
$form,
paymentMethodType
);

if ( paymentMethodObject.error ) {
appendPaymentMethodIdToForm( $form, PAYMENT_METHOD_ERROR );
appendPaymentMethodErrorDataToForm(
$form,
paymentMethodObject.error
);
} else {
appendPaymentMethodIdToForm(
$form,
paymentMethodObject.paymentMethod.id
);
}
appendFingerprintInputToForm( $form, fingerprint );
appendPaymentMethodIdToForm(
$form,
paymentMethodObject.paymentMethod.id
);
appendFraudPreventionTokenInputToForm( $form );
await additionalActionsHandler(
paymentMethodObject.paymentMethod,
Expand All @@ -587,6 +591,15 @@ export const processPayment = (
return false;
};

/**
* Used only for testing, resets the hasCheckoutCompleted value.
*
* @return {void}
*/
export function __resetHasCheckoutCompleted() {
hasCheckoutCompleted = false;
}

/**
* Used only for testing, resets the gatewayUPEComponents internal cache of elements for a given property.
*
Expand Down
147 changes: 144 additions & 3 deletions client/checkout/classic/test/payment-processing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ import {
processPayment,
renderTerms,
__resetGatewayUPEComponentsElement,
__resetHasCheckoutCompleted,
} from '../payment-processing';
import { getAppearance } from '../../upe-styles';
import { getUPEConfig } from 'wcpay/utils/checkout';
import { getFingerprint } from 'wcpay/checkout/utils/fingerprint';
import {
getFingerprint,
appendFingerprintInputToForm,
} from 'wcpay/checkout/utils/fingerprint';
import showErrorCheckout from 'wcpay/checkout/utils/show-error-checkout';
import { waitFor } from '@testing-library/react';
import { getSelectedUPEGatewayPaymentMethod } from 'wcpay/checkout/utils/upe';
import {
appendPaymentMethodErrorDataToForm,
appendPaymentMethodIdToForm,
getSelectedUPEGatewayPaymentMethod,
} from 'wcpay/checkout/utils/upe';
import { PAYMENT_METHOD_ERROR } from 'wcpay/checkout/constants';

jest.mock( '../../upe-styles' );

Expand Down Expand Up @@ -58,6 +67,7 @@ jest.mock( 'wcpay/utils/checkout', () => ( {

jest.mock( 'wcpay/checkout/utils/fingerprint', () => ( {
getFingerprint: jest.fn(),
appendFingerprintInputToForm: jest.fn(),
} ) );

jest.mock( 'wcpay/checkout/utils/show-error-checkout', () => jest.fn() );
Expand Down Expand Up @@ -87,7 +97,11 @@ const mockElements = jest.fn( () => ( {
submit: mockSubmit,
} ) );

const mockCreatePaymentMethod = jest.fn().mockResolvedValue( {} );
const mockCreatePaymentMethod = jest.fn().mockResolvedValue( {
paymentMethod: {
id: 'paymentMethodId',
},
} );

const apiMock = {
saveUPEAppearance: jest.fn().mockResolvedValue( {} ),
Expand Down Expand Up @@ -386,6 +400,7 @@ describe( 'Payment processing', () => {

document.body.removeChild( element );
} );
__resetHasCheckoutCompleted();
jest.clearAllMocks();
} );

Expand Down Expand Up @@ -619,6 +634,132 @@ describe( 'Payment processing', () => {
} );
} );

test( 'Payment processing adds the payment information to the form', async () => {
setupBillingDetailsFields();
getFingerprint.mockImplementation( () => {
return { visitorId: 'fingerprint' };
} );

const mockDomElement = document.createElement( 'div' );
mockDomElement.dataset.paymentMethodType = 'card';

await mountStripePaymentElement( apiMock, mockDomElement );

const checkoutForm = {
submit: jest.fn(),
addClass: jest.fn( () => ( {
block: jest.fn(),
} ) ),
removeClass: jest.fn( () => ( {
unblock: jest.fn(),
submit: checkoutForm.submit,
} ) ),
attr: jest.fn().mockReturnValue( 'checkout' ),
};

mockCreatePaymentMethod.mockReturnValue( {
paymentMethod: {
id: 'paymentMethodId',
},
} );

await processPayment( apiMock, checkoutForm, 'card' );
// Wait for promises to resolve.
await new Promise( ( resolve ) => setImmediate( resolve ) );

expect( mockCreatePaymentMethod ).toHaveBeenCalledWith( {
elements: expect.any( Object ),
params: {
billing_details: expect.objectContaining( {
name: 'John Doe',
email: '[email protected]',
phone: '555-1234',
address: expect.any( Object ),
} ),
},
} );

expect( appendPaymentMethodIdToForm ).toHaveBeenCalledWith(
checkoutForm,
'paymentMethodId'
);

expect( appendFingerprintInputToForm ).toHaveBeenCalledWith(
checkoutForm,
'fingerprint'
);

expect( checkoutForm.submit ).toHaveBeenCalled();
} );

test( 'Payment processing adds the error information if payment method fails to be created', async () => {
setupBillingDetailsFields();
getFingerprint.mockImplementation( () => {
return { visitorId: 'fingerprint' };
} );

const mockDomElement = document.createElement( 'div' );
mockDomElement.dataset.paymentMethodType = 'card';

await mountStripePaymentElement( apiMock, mockDomElement );

const checkoutForm = {
submit: jest.fn(),
addClass: jest.fn( () => ( {
block: jest.fn(),
} ) ),
removeClass: jest.fn( () => ( {
unblock: jest.fn(),
submit: checkoutForm.submit,
} ) ),
attr: jest.fn().mockReturnValue( 'checkout' ),
};

const errorData = {
code: 'code',
decline_code: 'decline_code',
message: 'message',
type: 'type',
};

mockCreatePaymentMethod.mockReturnValue( {
error: errorData,
} );

await processPayment( apiMock, checkoutForm, 'card' );
// Wait for promises to resolve.
await new Promise( ( resolve ) => setImmediate( resolve ) );

expect( mockCreatePaymentMethod ).toHaveBeenCalledWith( {
elements: expect.any( Object ),
params: {
billing_details: expect.objectContaining( {
name: 'John Doe',
email: '[email protected]',
phone: '555-1234',
address: expect.any( Object ),
} ),
},
} );

expect( appendPaymentMethodIdToForm ).toHaveBeenCalledWith(
checkoutForm,
PAYMENT_METHOD_ERROR
);

expect( appendPaymentMethodErrorDataToForm ).toHaveBeenCalledWith(
checkoutForm,
errorData
);

expect( appendFingerprintInputToForm ).toHaveBeenCalledWith(
checkoutForm,
'fingerprint'
);

expect( checkoutForm.submit ).toHaveBeenCalled();
} );

function setupBillingDetailsFields() {
// Create DOM elements for the test
const firstNameInput = document.createElement( 'input' );
Expand Down
Loading

0 comments on commit 6bcd6b3

Please sign in to comment.