diff --git a/changelog/kreykjalin-8868-ece-cart-and-checkout-page b/changelog/kreykjalin-8868-ece-cart-and-checkout-page new file mode 100644 index 00000000000..bdb4df869e3 --- /dev/null +++ b/changelog/kreykjalin-8868-ece-cart-and-checkout-page @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add support for ECE elements on the Shortcode Cart and Checkout pages diff --git a/client/express-checkout/blocks/hooks/use-express-checkout.js b/client/express-checkout/blocks/hooks/use-express-checkout.js index 090e3f82ac5..e01c5eaf5bc 100644 --- a/client/express-checkout/blocks/hooks/use-express-checkout.js +++ b/client/express-checkout/blocks/hooks/use-express-checkout.js @@ -40,7 +40,7 @@ export const useExpressCheckout = ( { }; const abortPayment = ( onConfirmEvent, message ) => { - onConfirmEvent.paymentFailed( 'fail' ); + onConfirmEvent.paymentFailed( { reason: 'fail' } ); setExpressPaymentError( message ); }; diff --git a/client/express-checkout/event-handlers.js b/client/express-checkout/event-handlers.js index 79e40789a4b..cd45606992b 100644 --- a/client/express-checkout/event-handlers.js +++ b/client/express-checkout/event-handlers.js @@ -9,34 +9,42 @@ import { import { getErrorMessageFromNotice } from './utils/index'; export const shippingAddressChangeHandler = async ( api, event, elements ) => { - const response = await api.expressCheckoutECECalculateShippingOptions( - normalizeShippingAddress( event.address ) - ); + try { + const response = await api.expressCheckoutECECalculateShippingOptions( + normalizeShippingAddress( event.address ) + ); - if ( response.result === 'success' ) { - elements.update( { - amount: response.total.amount, - } ); - event.resolve( { - shippingRates: response.shipping_options, - lineItems: normalizeLineItems( response.displayItems ), - } ); - } else { + if ( response.result === 'success' ) { + elements.update( { + amount: response.total.amount, + } ); + event.resolve( { + shippingRates: response.shipping_options, + lineItems: normalizeLineItems( response.displayItems ), + } ); + } else { + event.reject(); + } + } catch ( e ) { event.reject(); } }; export const shippingRateChangeHandler = async ( api, event, elements ) => { - const response = await api.paymentRequestUpdateShippingDetails( - event.shippingRate - ); + try { + const response = await api.paymentRequestUpdateShippingDetails( + event.shippingRate + ); - if ( response.result === 'success' ) { - elements.update( { amount: response.total.amount } ); - event.resolve( { - lineItems: normalizeLineItems( response.displayItems ), - } ); - } else { + if ( response.result === 'success' ) { + elements.update( { amount: response.total.amount } ); + event.resolve( { + lineItems: normalizeLineItems( response.displayItems ), + } ); + } else { + event.reject(); + } + } catch ( e ) { event.reject(); } }; @@ -88,6 +96,6 @@ export const onConfirmHandler = async ( completePayment( redirectUrl ); } } catch ( e ) { - return abortPayment( event, error.message ); + return abortPayment( event, e.message ); } }; diff --git a/client/express-checkout/index.js b/client/express-checkout/index.js index 6a5aa3c40ef..abcf1102b46 100644 --- a/client/express-checkout/index.js +++ b/client/express-checkout/index.js @@ -81,7 +81,7 @@ jQuery( ( $ ) => { * @param {string} message Error message to display. */ abortPayment: ( payment, message ) => { - payment.paymentFailed(); + payment.paymentFailed( { reason: 'fail' } ); wcpayECE.unblock(); $( '.woocommerce-error' ).remove(); diff --git a/client/express-checkout/test/event-handlers.js b/client/express-checkout/test/event-handlers.js new file mode 100644 index 00000000000..338ca2a3da1 --- /dev/null +++ b/client/express-checkout/test/event-handlers.js @@ -0,0 +1,437 @@ +/** + * Internal dependencies + */ +import { + shippingAddressChangeHandler, + shippingRateChangeHandler, + onConfirmHandler, +} from '../event-handlers'; +import { + normalizeLineItems, + normalizeShippingAddress, + normalizeOrderData, +} from '../utils'; + +describe( 'Express checkout event handlers', () => { + describe( 'shippingAddressChangeHandler', () => { + let api; + let event; + let elements; + + beforeEach( () => { + api = { + expressCheckoutECECalculateShippingOptions: jest.fn(), + }; + event = { + address: { + recipient: 'John Doe', + addressLine: [ '123 Main St' ], + city: 'New York', + state: 'NY', + country: 'US', + postal_code: '10001', + }, + resolve: jest.fn(), + reject: jest.fn(), + }; + elements = { + update: jest.fn(), + }; + } ); + + afterEach( () => { + jest.clearAllMocks(); + } ); + + test( 'should handle successful response', async () => { + const response = { + result: 'success', + total: { amount: 1000 }, + shipping_options: [ + { id: 'option_1', label: 'Standard Shipping' }, + ], + displayItems: [ { label: 'Sample Item', amount: 500 } ], + }; + + api.expressCheckoutECECalculateShippingOptions.mockResolvedValue( + response + ); + + await shippingAddressChangeHandler( api, event, elements ); + + const expectedNormalizedAddress = normalizeShippingAddress( + event.address + ); + expect( + api.expressCheckoutECECalculateShippingOptions + ).toHaveBeenCalledWith( expectedNormalizedAddress ); + + const expectedNormalizedLineItems = normalizeLineItems( + response.displayItems + ); + expect( elements.update ).toHaveBeenCalledWith( { amount: 1000 } ); + expect( event.resolve ).toHaveBeenCalledWith( { + shippingRates: response.shipping_options, + lineItems: expectedNormalizedLineItems, + } ); + expect( event.reject ).not.toHaveBeenCalled(); + } ); + + test( 'should handle unsuccessful response', async () => { + const response = { + result: 'error', + }; + + api.expressCheckoutECECalculateShippingOptions.mockResolvedValue( + response + ); + + await shippingAddressChangeHandler( api, event, elements ); + + const expectedNormalizedAddress = normalizeShippingAddress( + event.address + ); + expect( + api.expressCheckoutECECalculateShippingOptions + ).toHaveBeenCalledWith( expectedNormalizedAddress ); + expect( elements.update ).not.toHaveBeenCalled(); + expect( event.resolve ).not.toHaveBeenCalled(); + expect( event.reject ).toHaveBeenCalled(); + } ); + + test( 'should handle API call failure', async () => { + api.expressCheckoutECECalculateShippingOptions.mockRejectedValue( + new Error( 'API error' ) + ); + + await shippingAddressChangeHandler( api, event, elements ); + + const expectedNormalizedAddress = normalizeShippingAddress( + event.address + ); + expect( + api.expressCheckoutECECalculateShippingOptions + ).toHaveBeenCalledWith( expectedNormalizedAddress ); + expect( elements.update ).not.toHaveBeenCalled(); + expect( event.resolve ).not.toHaveBeenCalled(); + expect( event.reject ).toHaveBeenCalled(); + } ); + } ); + + describe( 'shippingRateChangeHandler', () => { + let api; + let event; + let elements; + + beforeEach( () => { + api = { + paymentRequestUpdateShippingDetails: jest.fn(), + }; + event = { + shippingRate: { + id: 'rate_1', + label: 'Standard Shipping', + amount: 500, + }, + resolve: jest.fn(), + reject: jest.fn(), + }; + elements = { + update: jest.fn(), + }; + } ); + + afterEach( () => { + jest.clearAllMocks(); + } ); + + test( 'should handle successful response', async () => { + const response = { + result: 'success', + total: { amount: 1500 }, + displayItems: [ { label: 'Sample Item', amount: 1000 } ], + }; + + api.paymentRequestUpdateShippingDetails.mockResolvedValue( + response + ); + + await shippingRateChangeHandler( api, event, elements ); + + const expectedNormalizedLineItems = normalizeLineItems( + response.displayItems + ); + expect( + api.paymentRequestUpdateShippingDetails + ).toHaveBeenCalledWith( event.shippingRate ); + expect( elements.update ).toHaveBeenCalledWith( { amount: 1500 } ); + expect( event.resolve ).toHaveBeenCalledWith( { + lineItems: expectedNormalizedLineItems, + } ); + expect( event.reject ).not.toHaveBeenCalled(); + } ); + + test( 'should handle unsuccessful response', async () => { + const response = { + result: 'error', + }; + + api.paymentRequestUpdateShippingDetails.mockResolvedValue( + response + ); + + await shippingRateChangeHandler( api, event, elements ); + + expect( + api.paymentRequestUpdateShippingDetails + ).toHaveBeenCalledWith( event.shippingRate ); + expect( elements.update ).not.toHaveBeenCalled(); + expect( event.resolve ).not.toHaveBeenCalled(); + expect( event.reject ).toHaveBeenCalled(); + } ); + + test( 'should handle API call failure', async () => { + api.paymentRequestUpdateShippingDetails.mockRejectedValue( + new Error( 'API error' ) + ); + + await shippingRateChangeHandler( api, event, elements ); + + expect( + api.paymentRequestUpdateShippingDetails + ).toHaveBeenCalledWith( event.shippingRate ); + expect( elements.update ).not.toHaveBeenCalled(); + expect( event.resolve ).not.toHaveBeenCalled(); + expect( event.reject ).toHaveBeenCalled(); + } ); + } ); + + describe( 'onConfirmHandler', () => { + let api; + let stripe; + let elements; + let completePayment; + let abortPayment; + let event; + + beforeEach( () => { + api = { + expressCheckoutECECreateOrder: jest.fn(), + confirmIntent: jest.fn(), + }; + stripe = { + createPaymentMethod: jest.fn(), + }; + elements = { + submit: jest.fn(), + }; + completePayment = jest.fn(); + abortPayment = jest.fn(); + event = { + billingDetails: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + organization: 'Some Company', + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + phone: '(123) 456-7890', + }, + shippingAddress: { + name: 'John Doe', + organization: 'Some Company', + address: { + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + }, + shippingRate: { id: 'rate_1' }, + expressPaymentType: 'express', + }; + global.window.wcpayFraudPreventionToken = 'token123'; + } ); + + afterEach( () => { + jest.clearAllMocks(); + } ); + + test( 'should abort payment if elements.submit fails', async () => { + elements.submit.mockResolvedValue( { + error: { message: 'Submit error' }, + } ); + + await onConfirmHandler( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + expect( elements.submit ).toHaveBeenCalled(); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Submit error' + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + + test( 'should abort payment if stripe.createPaymentMethod fails', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + error: { message: 'Payment method error' }, + } ); + + await onConfirmHandler( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + expect( elements.submit ).toHaveBeenCalled(); + expect( stripe.createPaymentMethod ).toHaveBeenCalledWith( { + elements, + } ); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Payment method error' + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + + test( 'should abort payment if expressCheckoutECECreateOrder fails', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECECreateOrder.mockResolvedValue( { + result: 'error', + messages: 'Order creation error', + } ); + + await onConfirmHandler( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + const expectedOrderData = normalizeOrderData( event, 'pm_123' ); + expect( api.expressCheckoutECECreateOrder ).toHaveBeenCalledWith( + expectedOrderData + ); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Order creation error' + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + + test( 'should complete payment if confirmationRequest is true', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECECreateOrder.mockResolvedValue( { + result: 'success', + redirect: 'https://example.com/redirect', + } ); + api.confirmIntent.mockReturnValue( true ); + + await onConfirmHandler( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( completePayment ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( abortPayment ).not.toHaveBeenCalled(); + } ); + + test( 'should complete payment if confirmationRequest returns a redirect URL', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECECreateOrder.mockResolvedValue( { + result: 'success', + redirect: 'https://example.com/redirect', + } ); + api.confirmIntent.mockResolvedValue( + 'https://example.com/confirmation_redirect' + ); + + await onConfirmHandler( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( completePayment ).toHaveBeenCalledWith( + 'https://example.com/confirmation_redirect' + ); + expect( abortPayment ).not.toHaveBeenCalled(); + } ); + + test( 'should abort payment if confirmIntent throws an error', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECECreateOrder.mockResolvedValue( { + result: 'success', + redirect: 'https://example.com/redirect', + } ); + api.confirmIntent.mockRejectedValue( + new Error( 'Intent confirmation error' ) + ); + + await onConfirmHandler( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Intent confirmation error' + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + } ); +} ); diff --git a/client/express-checkout/utils/index.ts b/client/express-checkout/utils/index.ts index eb6f359b98b..b10d80b0960 100644 --- a/client/express-checkout/utils/index.ts +++ b/client/express-checkout/utils/index.ts @@ -3,7 +3,7 @@ export * from './normalize'; /** * An /incomplete/ representation of the data that is loaded into the frontend for the Express Checkout. */ -interface WCPayExpressCheckoutParams { +export interface WCPayExpressCheckoutParams { ajax_url: string; /** diff --git a/client/express-checkout/utils/normalize.js b/client/express-checkout/utils/normalize.js index 365e1553397..576a2109ea3 100644 --- a/client/express-checkout/utils/normalize.js +++ b/client/express-checkout/utils/normalize.js @@ -12,6 +12,7 @@ export const normalizeLineItems = ( displayItems ) => { ( { ...displayItem, name: displayItem.label, + amount: displayItem?.amount ?? displayItem?.value, } ) ); }; @@ -20,7 +21,7 @@ export const normalizeLineItems = ( displayItems ) => { * Normalize order data from Stripe's object to the expected format for WC. * * @param {Object} event Stripe's event object. - * @param {Object} paymentMethodId Stripe's payment method id. + * @param {string} paymentMethodId Stripe's payment method id. * * @return {Object} Order object in the format WooCommerce expects. */ @@ -32,14 +33,14 @@ export const normalizeOrderData = ( event, paymentMethodId ) => { const fraudPreventionTokenValue = window.wcpayFraudPreventionToken ?? ''; const phone = - event?.billingDetails?.phone ?? - event?.payerPhone?.replace( '/[() -]/g', '' ) ?? + event?.billingDetails?.phone?.replace( /[() -]/g, '' ) ?? + event?.payerPhone?.replace( /[() -]/g, '' ) ?? ''; return { billing_first_name: name?.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '', - billing_last_name: name?.split( ' ' )?.slice( 1 )?.join( ' ' ) || '-', + billing_last_name: name?.split( ' ' )?.slice( 1 )?.join( ' ' ) ?? '-', billing_company: billing?.organization ?? '', billing_email: email ?? event?.payerEmail ?? '', billing_phone: phone, diff --git a/client/express-checkout/utils/test/index.ts b/client/express-checkout/utils/test/index.ts new file mode 100644 index 00000000000..e1e61edf988 --- /dev/null +++ b/client/express-checkout/utils/test/index.ts @@ -0,0 +1,44 @@ +/** + * Internal dependencies + */ +import { + WCPayExpressCheckoutParams, + getErrorMessageFromNotice, + getExpressCheckoutData, +} from '../index'; + +describe( 'Express checkout utils', () => { + test( 'getExpressCheckoutData returns null for missing option', () => { + expect( + getExpressCheckoutData( + // Force wrong usage, just in case this is called from JS with incorrect params. + 'does-not-exist' as keyof WCPayExpressCheckoutParams + ) + ).toBeNull(); + } ); + + test( 'getExpressCheckoutData returns correct value for present option', () => { + // We don't care that the implementation is partial for the purposes of the test, so + // the type assertion is fine. + window.wcpayExpressCheckoutParams = { + ajax_url: 'test', + } as WCPayExpressCheckoutParams; + + expect( getExpressCheckoutData( 'ajax_url' ) ).toBe( 'test' ); + } ); + + test( 'getErrorMessageFromNotice strips formatting', () => { + const notice = '
Error: Payment failed.
'; + expect( getErrorMessageFromNotice( notice ) ).toBe( + 'Error: Payment failed.' + ); + } ); + + test( 'getErrorMessageFromNotice strips scripts', () => { + const notice = + 'Error: Payment failed.
'; + expect( getErrorMessageFromNotice( notice ) ).toBe( + 'Error: Payment failed.alert("hello")' + ); + } ); +} ); diff --git a/client/express-checkout/utils/test/normalize.js b/client/express-checkout/utils/test/normalize.js new file mode 100644 index 00000000000..6bd88b47b0b --- /dev/null +++ b/client/express-checkout/utils/test/normalize.js @@ -0,0 +1,416 @@ +/** + * Internal dependencies + */ +import { + normalizeLineItems, + normalizeOrderData, + normalizeShippingAddress, +} from '../normalize'; + +describe( 'Express checkout normalization', () => { + describe( 'normalizeLineItems', () => { + test( 'normalizes blocks array properly', () => { + const displayItems = [ + { + label: 'Item 1', + value: 100, + }, + { + label: 'Item 2', + value: 200, + }, + { + label: 'Item 3', + valueWithTax: 300, + value: 200, + }, + ]; + + // Extra items in the array are expected since they're not stripped. + const expected = [ + { + name: 'Item 1', + label: 'Item 1', + amount: 100, + value: 100, + }, + { + name: 'Item 2', + label: 'Item 2', + amount: 200, + value: 200, + }, + { + name: 'Item 3', + label: 'Item 3', + amount: 200, + valueWithTax: 300, + value: 200, + }, + ]; + + expect( normalizeLineItems( displayItems ) ).toStrictEqual( + expected + ); + } ); + + test( 'normalizes shortcode array properly', () => { + const displayItems = [ + { + label: 'Item 1', + amount: 100, + }, + { + label: 'Item 2', + amount: 200, + }, + { + label: 'Item 3', + amount: 300, + }, + ]; + + const expected = [ + { + name: 'Item 1', + label: 'Item 1', + amount: 100, + }, + { + name: 'Item 2', + label: 'Item 2', + amount: 200, + }, + { + name: 'Item 3', + label: 'Item 3', + amount: 300, + }, + ]; + + expect( normalizeLineItems( displayItems ) ).toStrictEqual( + expected + ); + } ); + } ); + + describe( 'normalizeOrderData', () => { + afterEach( () => { + // Clear any changes to the fraud prevention token. + delete window.wcpayFraudPreventionToken; + } ); + + test( 'should normalize order data with complete event and paymentMethodId', () => { + window.wcpayFraudPreventionToken = 'token123'; + + const event = { + billingDetails: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + organization: 'Some Company', + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + phone: '(123) 456-7890', + }, + shippingAddress: { + name: 'John Doe', + organization: 'Some Company', + address: { + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + }, + shippingRate: { id: 'rate_1' }, + expressPaymentType: 'express', + }; + + const paymentMethodId = 'pm_123456'; + + const expectedNormalizedData = { + billing_first_name: 'John', + billing_last_name: 'Doe', + billing_company: 'Some Company', + billing_email: 'john.doe@example.com', + billing_phone: '1234567890', + billing_country: 'US', + billing_address_1: '123 Main St', + billing_address_2: 'Apt 4B', + billing_city: 'New York', + billing_state: 'NY', + billing_postcode: '10001', + shipping_first_name: 'John', + shipping_last_name: 'Doe', + shipping_company: 'Some Company', + shipping_phone: '1234567890', + shipping_country: 'US', + shipping_address_1: '123 Main St', + shipping_address_2: 'Apt 4B', + shipping_city: 'New York', + shipping_state: 'NY', + shipping_postcode: '10001', + shipping_method: [ 'rate_1' ], + order_comments: '', + payment_method: 'woocommerce_payments', + ship_to_different_address: 1, + terms: 1, + 'wcpay-payment-method': paymentMethodId, + payment_request_type: 'express', + express_payment_type: 'express', + 'wcpay-fraud-prevention-token': 'token123', + }; + + expect( normalizeOrderData( event, paymentMethodId ) ).toEqual( + expectedNormalizedData + ); + } ); + + test( 'should normalize order data with missing optional event fields', () => { + const event = {}; + const paymentMethodId = 'pm_123456'; + + const expectedNormalizedData = { + billing_first_name: '', + billing_last_name: '-', + billing_company: '', + billing_email: '', + billing_phone: '', + billing_country: '', + billing_address_1: '', + billing_address_2: '', + billing_city: '', + billing_state: '', + billing_postcode: '', + shipping_first_name: '', + shipping_last_name: '', + shipping_company: '', + shipping_phone: '', + shipping_country: '', + shipping_address_1: '', + shipping_address_2: '', + shipping_city: '', + shipping_state: '', + shipping_postcode: '', + shipping_method: [ null ], + order_comments: '', + payment_method: 'woocommerce_payments', + ship_to_different_address: 1, + terms: 1, + 'wcpay-payment-method': paymentMethodId, + payment_request_type: undefined, + express_payment_type: undefined, + 'wcpay-fraud-prevention-token': '', + }; + + expect( normalizeOrderData( event, paymentMethodId ) ).toEqual( + expectedNormalizedData + ); + } ); + + test( 'should normalize order data with minimum required fields', () => { + const event = { + billingDetails: { + name: 'John', + }, + }; + const paymentMethodId = 'pm_123456'; + + const expectedNormalizedData = { + billing_first_name: 'John', + billing_last_name: '', + billing_company: '', + billing_email: '', + billing_phone: '', + billing_country: '', + billing_address_1: '', + billing_address_2: '', + billing_city: '', + billing_state: '', + billing_postcode: '', + shipping_first_name: '', + shipping_last_name: '', + shipping_company: '', + shipping_phone: '', + shipping_country: '', + shipping_address_1: '', + shipping_address_2: '', + shipping_city: '', + shipping_state: '', + shipping_postcode: '', + shipping_method: [ null ], + order_comments: '', + payment_method: 'woocommerce_payments', + ship_to_different_address: 1, + terms: 1, + 'wcpay-payment-method': paymentMethodId, + payment_request_type: undefined, + express_payment_type: undefined, + 'wcpay-fraud-prevention-token': '', + }; + + expect( normalizeOrderData( event, paymentMethodId ) ).toEqual( + expectedNormalizedData + ); + } ); + } ); + + describe( 'normalizeShippingAddress', () => { + test( 'should normalize shipping address with all fields present', () => { + const shippingAddress = { + recipient: 'John Doe', + addressLine: [ '123 Main St', 'Apt 4B' ], + city: 'New York', + state: 'NY', + country: 'US', + postal_code: '10001', + }; + + const expectedNormalizedAddress = { + first_name: 'John', + last_name: 'Doe', + company: '', + address_1: '123 Main St', + address_2: 'Apt 4B', + city: 'New York', + state: 'NY', + country: 'US', + postcode: '10001', + }; + + expect( normalizeShippingAddress( shippingAddress ) ).toEqual( + expectedNormalizedAddress + ); + } ); + + test( 'should normalize shipping address with only recipient name', () => { + const shippingAddress = { + recipient: 'John', + }; + + const expectedNormalizedAddress = { + first_name: 'John', + last_name: '', + company: '', + address_1: '', + address_2: '', + city: '', + state: '', + country: '', + postcode: '', + }; + + expect( normalizeShippingAddress( shippingAddress ) ).toEqual( + expectedNormalizedAddress + ); + } ); + + test( 'should normalize shipping address with missing recipient name', () => { + const shippingAddress = { + addressLine: [ '123 Main St' ], + city: 'New York', + state: 'NY', + country: 'US', + postal_code: '10001', + }; + + const expectedNormalizedAddress = { + first_name: '', + last_name: '', + company: '', + address_1: '123 Main St', + address_2: '', + city: 'New York', + state: 'NY', + country: 'US', + postcode: '10001', + }; + + expect( normalizeShippingAddress( shippingAddress ) ).toEqual( + expectedNormalizedAddress + ); + } ); + + test( 'should normalize shipping address with empty addressLine', () => { + const shippingAddress = { + recipient: 'John Doe', + addressLine: [], + city: 'New York', + state: 'NY', + country: 'US', + postal_code: '10001', + }; + + const expectedNormalizedAddress = { + first_name: 'John', + last_name: 'Doe', + company: '', + address_1: '', + address_2: '', + city: 'New York', + state: 'NY', + country: 'US', + postcode: '10001', + }; + + expect( normalizeShippingAddress( shippingAddress ) ).toEqual( + expectedNormalizedAddress + ); + } ); + + test( 'should normalize an empty shipping address', () => { + const shippingAddress = {}; + + const expectedNormalizedAddress = { + first_name: '', + last_name: '', + company: '', + address_1: '', + address_2: '', + city: '', + state: '', + country: '', + postcode: '', + }; + + expect( normalizeShippingAddress( shippingAddress ) ).toEqual( + expectedNormalizedAddress + ); + } ); + + test( 'should normalize a shipping address with a multi-word recipient name', () => { + const shippingAddress = { + recipient: 'John Doe Smith', + addressLine: [ '123 Main St', 'Apt 4B' ], + city: 'New York', + state: 'NY', + country: 'US', + postal_code: '10001', + }; + + const expectedNormalizedAddress = { + first_name: 'John', + last_name: 'Doe Smith', + company: '', + address_1: '123 Main St', + address_2: 'Apt 4B', + city: 'New York', + state: 'NY', + country: 'US', + postcode: '10001', + }; + + expect( normalizeShippingAddress( shippingAddress ) ).toEqual( + expectedNormalizedAddress + ); + } ); + } ); +} );