Skip to content

Commit

Permalink
update: tokenized ECE behavior w/ totals discrepancies (#10311)
Browse files Browse the repository at this point in the history
  • Loading branch information
frosso authored Feb 9, 2025
1 parent af5914b commit 1d51c6f
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 3 deletions.
4 changes: 4 additions & 0 deletions changelog/fix-tokenized-ece-rounding-of-itemized-items
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: update

update: tokenized ECE to exclude itemized items on rounding discrepancies of totals.
19 changes: 18 additions & 1 deletion client/express-checkout/blocks/hooks/use-express-checkout.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,27 @@ export const useExpressCheckout = ( {
}
}

const lineItems = normalizeLineItems( billing.cartTotalItems );
const totalAmountOfLineItems = lineItems.reduce(
( acc, lineItem ) => {
return acc + lineItem.amount;
},
0
);

const options = {
business: {
name: getExpressCheckoutData( 'store_name' ),
},
lineItems: normalizeLineItems( billing?.cartTotalItems ),
// if the `billing.cartTotal.value` is less than the total of `lineItems`, Stripe throws an error
// it can sometimes happen that the total is _slightly_ less, due to rounding errors on individual items/taxes/shipping
// (or with the `woocommerce_tax_round_at_subtotal` setting).
// if that happens, let's just not return any of the line items.
// This way, just the total amount will be displayed to the customer.
lineItems:
billing.cartTotal.value < totalAmountOfLineItems
? []
: lineItems,
emailRequired: true,
shippingAddressRequired,
phoneNumberRequired:
Expand All @@ -115,6 +131,7 @@ export const useExpressCheckout = ( {
[
onClick,
billing.cartTotalItems,
billing.cartTotal.value,
shippingData.needsShipping,
shippingData.shippingRates,
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,155 @@ describe( 'useExpressCheckout', () => {
global.jQuery = jQueryMock;
} );

it( 'should provide the line items', () => {
const onClickMock = jest.fn();
const event = { resolve: jest.fn() };
const { result } = renderHook( () =>
useExpressCheckout( {
billing: {
cartTotalItems: [
{
key: 'total_items',
label: 'Subtotal:',
value: 4000,
valueWithTax: 4330,
},
{
key: 'total_fees',
label: 'Fees:',
value: 0,
valueWithTax: 0,
},
{
key: 'total_discount',
label: 'Discount:',
value: 0,
valueWithTax: 0,
},
{
key: 'total_tax',
label: 'Taxes:',
value: 330,
valueWithTax: 330,
},
{
key: 'total_shipping',
label: 'Shipping:',
value: 0,
valueWithTax: 0,
},
],
cartTotal: {
label: 'Total',
value: 4330,
},
},
shippingData: {
needsShipping: false,
shippingRates: [],
},
onClick: onClickMock,
onClose: {},
setExpressPaymentError: {},
} )
);

expect( onClickMock ).not.toHaveBeenCalled();

result.current.onButtonClick( event );

expect( event.resolve ).toHaveBeenCalledWith(
expect.objectContaining( {
lineItems: [
{ amount: 4000, name: 'Subtotal:' },
{ amount: 0, name: 'Fees:' },
{ amount: -0, name: 'Discount:' },
{ amount: 330, name: 'Taxes:' },
{ amount: 0, name: 'Shipping:' },
],
} )
);
expect( onClickMock ).toHaveBeenCalled();
} );

it( "should not provide the line items if the totals don't match", () => {
const onClickMock = jest.fn();
const event = { resolve: jest.fn() };
const { result } = renderHook( () =>
useExpressCheckout( {
billing: {
cartTotalItems: [
{
key: 'total_items',
label: 'Subtotal:',
value: 4000,
valueWithTax: 4330,
},
{
key: 'total_fees',
label: 'Fees:',
value: 0,
valueWithTax: 0,
},
{
key: 'total_discount',
label: 'Discount:',
value: 0,
valueWithTax: 0,
},
{
key: 'total_tax',
label: 'Taxes:',
value: 330,
valueWithTax: 330,
},
{
key: 'total_shipping',
label: 'Shipping:',
value: 0,
valueWithTax: 0,
},
],
cartTotal: {
label: 'Total',
// simulating a total amount that is lower than the sum of the values of `cartTotalItems`
// this scenario happens with the Gift Cards plugin.
value: 400,
},
},
shippingData: {
needsShipping: false,
shippingRates: [],
},
onClick: onClickMock,
onClose: {},
setExpressPaymentError: {},
} )
);

expect( onClickMock ).not.toHaveBeenCalled();

result.current.onButtonClick( event );

expect( event.resolve ).toHaveBeenCalledWith(
expect.objectContaining( {
lineItems: [],
} )
);
expect( onClickMock ).toHaveBeenCalled();
} );

it( 'should provide no shipping rates when not required on click', () => {
const onClickMock = jest.fn();
const event = { resolve: jest.fn() };
const { result } = renderHook( () =>
useExpressCheckout( {
billing: {
cartTotalItems: [],
cartTotal: {
label: 'Total',
value: 448,
},
},
shippingData: {
needsShipping: false,
Expand Down Expand Up @@ -76,6 +218,10 @@ describe( 'useExpressCheckout', () => {
useExpressCheckout( {
billing: {
cartTotalItems: [],
cartTotal: {
label: 'Total',
value: 448,
},
},
shippingData: {
needsShipping: true,
Expand Down Expand Up @@ -119,6 +265,10 @@ describe( 'useExpressCheckout', () => {
useExpressCheckout( {
billing: {
cartTotalItems: [],
cartTotal: {
label: 'Total',
value: 448,
},
},
shippingData: {
needsShipping: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,25 @@ export const useExpressCheckout = ( {
}
}

const lineItems = normalizeLineItems( billing.cartTotalItems );
const totalAmountOfLineItems = lineItems.reduce(
( acc, lineItem ) => acc + lineItem.amount,
0
);

const options = {
business: {
name: getExpressCheckoutData( 'store_name' ),
},
lineItems: normalizeLineItems( billing?.cartTotalItems ),
// if the `billing.cartTotal.value` is less than the total of `lineItems`, Stripe throws an error
// it can sometimes happen that the total is _slightly_ less, due to rounding errors on individual items/taxes/shipping
// (or with the `woocommerce_tax_round_at_subtotal` setting).
// if that happens, let's just not return any of the line items.
// This way, just the total amount will be displayed to the customer.
lineItems:
billing.cartTotal.value < totalAmountOfLineItems
? []
: lineItems,
emailRequired: true,
shippingAddressRequired,
phoneNumberRequired:
Expand All @@ -115,6 +129,7 @@ export const useExpressCheckout = ( {
[
onClick,
billing.cartTotalItems,
billing.cartTotal.value,
shippingData.needsShipping,
shippingData.shippingRates,
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,26 @@ describe( 'wc-to-stripe transformers', () => {
},
],
shipping_rates: [],
totals: {},
totals: {
currency_code: 'USD',
currency_decimal_separator: '.',
currency_minor_unit: 2,
currency_prefix: '$',
currency_suffix: '',
currency_symbol: '$',
currency_thousand_separator: ',',
tax_lines: [],
total_discount: '0',
total_discount_tax: '0',
total_fees: '0',
total_fees_tax: '0',
total_items: '6000',
total_items_tax: '0',
total_price: '6000',
total_shipping: '0',
total_shipping_tax: '0',
total_tax: '0',
},
} )
).toStrictEqual( [
{ amount: 4500, name: 'Physical subscription' },
Expand Down Expand Up @@ -207,6 +226,113 @@ describe( 'wc-to-stripe transformers', () => {
} )
).toStrictEqual( [] );
} );

it( 'does not return line items when there is a discrepancy with the totals', () => {
expect(
transformCartDataForDisplayItems( {
items: [
{
key: '6fd9b4da889ae534ceae47561b939f24',
id: 214,
type: 'simple',
quantity: 2,
name: 'Deposit',
variation: [],
item_data: [
{
name: 'Payment Plan',
value: 'Deposit 30',
display: '',
},
{
key: 'Payable In Total',
value: '&#36;45.00 payable over 20 days',
},
],
prices: {
price: '4500',
regular_price: '5000',
sale_price: '4500',
price_range: null,
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
raw_prices: {
precision: 6,
price: '45000000',
regular_price: '50000000',
sale_price: '45000000',
},
},
totals: {
line_subtotal: '1350',
line_subtotal_tax: 0,
line_total: '4500',
line_total_tax: '388',
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
catalog_visibility: 'visible',
extensions: {
'woocommerce-deposits': {
is_deposit: true,
has_payment_plan: true,
plan_schedule: [
{
schedule_id: '2',
schedule_index: '0',
plan_id: '2',
amount: '30',
interval_amount: '0',
interval_unit: '0',
},
{
schedule_id: '3',
schedule_index: '1',
plan_id: '2',
amount: '70',
interval_amount: '20',
interval_unit: 'day',
},
],
},
bundles: [],
},
},
],
shipping_rates: [],
totals: {
total_items: '0',
total_items_tax: '0',
total_fees: '0',
total_fees_tax: '0',
total_discount: '0',
total_discount_tax: '0',
total_shipping: '0',
total_shipping_tax: '0',
total_price: '0',
total_tax: '0',
tax_lines: [],
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
},
} )
).toStrictEqual( [] );
} );
} );

describe( 'transformPrice', () => {
Expand Down
Loading

0 comments on commit 1d51c6f

Please sign in to comment.