diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md index c1297f009d..5179238b37 100644 --- a/packages/template-retail-react-app/CHANGELOG.md +++ b/packages/template-retail-react-app/CHANGELOG.md @@ -1,4 +1,5 @@ ## v10.1.0-dev +- [Bugfix] Sync `amount` on the basket payment instrument before placing the order so the persisted `PaymentTransaction` matches `orderTotal`. - [Feature] Add cancel order modal on order detail page. Registered users with OMS enabled can cancel orders that are not yet shipped. Modal includes reason code selection and inline success/error feedback. Gated behind `app.oms.enabled` config flag. [#3861](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3861) - [Bugfix] Render the search refinements panels open during server-side rendering. ChakraUI v2's Accordion forces every item closed in SSR (its open state depends on a post-mount layout effect that never runs on the server), so the panels opened only after hydration — a late relayout that could become the PLP largest-contentful-paint element. Replaced the Accordion with a controlled disclosure that honors its open state server-side. - [Bugfix] Memoize the search refinements panel so it stops re-rendering on every parent render when the filter set is unchanged, improving PLP render stability. [#3855](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3855) diff --git a/packages/template-retail-react-app/app/pages/checkout/index.jsx b/packages/template-retail-react-app/app/pages/checkout/index.jsx index b26f61f4eb..cf33281546 100644 --- a/packages/template-retail-react-app/app/pages/checkout/index.jsx +++ b/packages/template-retail-react-app/app/pages/checkout/index.jsx @@ -64,6 +64,9 @@ const Checkout = () => { const {confirmingBasket} = useSFPayments() const [isLoading, setIsLoading] = useState(false) const {mutateAsync: createOrder} = useShopperOrdersMutation('createOrder') + const {mutateAsync: updatePaymentInstrumentInBasket} = useShopperBasketsMutation( + 'updatePaymentInstrumentInBasket' + ) const {passwordless = {}, social = {}} = getConfig().app.login || {} const idps = social?.idps const isSocialEnabled = !!social?.enabled @@ -117,6 +120,23 @@ const Checkout = () => { } const doCreateOrder = async () => { + // Sync payment instrument amount to current orderTotal right before placing + // the order. The amount stamped at addPaymentInstrumentToBasket time can go + // stale if the basket changes (promo, shipping method, items) afterward. + const appliedPayment = basket?.paymentInstruments?.[0] + if ( + appliedPayment?.paymentInstrumentId && + basket?.orderTotal != null && + appliedPayment.amount !== basket.orderTotal + ) { + await updatePaymentInstrumentInBasket({ + parameters: { + basketId: basket.basketId, + paymentInstrumentId: appliedPayment.paymentInstrumentId + }, + body: {amount: basket.orderTotal} + }) + } return await createOrder({ body: {basketId: basket.basketId} }) diff --git a/packages/template-retail-react-app/app/pages/checkout/index.test.js b/packages/template-retail-react-app/app/pages/checkout/index.test.js index 3f3901b78f..b188319f5e 100644 --- a/packages/template-retail-react-app/app/pages/checkout/index.test.js +++ b/packages/template-retail-react-app/app/pages/checkout/index.test.js @@ -1327,6 +1327,7 @@ describe('Checkout error display and submitOrder', () => { ] let orderPostCalled = false + let updatePaymentInstrumentCalled = false global.server.use( rest.post('*/orders', (req, res, ctx) => { orderPostCalled = true @@ -1340,6 +1341,10 @@ describe('Checkout error display and submitOrder', () => { }), rest.get('*/baskets', (req, res, ctx) => { return res(ctx.json({baskets: [currentBasket], total: 1})) + }), + rest.patch('*/baskets/*/payment-instruments/*', (req, res, ctx) => { + updatePaymentInstrumentCalled = true + return res(ctx.json(currentBasket)) }) ) @@ -1364,6 +1369,9 @@ describe('Checkout error display and submitOrder', () => { await waitFor(() => { expect(orderPostCalled).toBe(true) }) + // The fixture's payment instrument has amount=0 while orderTotal is non-zero, + // so doCreateOrder should have synced the amount before placing the order. + expect(updatePaymentInstrumentCalled).toBe(true) expect( screen.queryByText(/An unexpected error occurred during checkout/i) ).not.toBeInTheDocument()