Skip to content

Commit 4ba60df

Browse files
authored
@W-20073841: Fixed intermittent issues with bonus product modal selection and add to cart (#3445)
* fixed issues on add to cart and showing bonus product modal * Lint error
1 parent f88dea1 commit 4ba60df

File tree

5 files changed

+130
-13
lines changed

5 files changed

+130
-13
lines changed

packages/template-retail-react-app/app/components/bonus-product-view-modal/index.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,11 @@ const BonusProductViewModal = ({
170170
// addItemToNewOrExistingBasket returns the basket directly
171171
const updatedBasket = result
172172

173-
// Check if there are still remaining bonus products available
174-
const hasRemainingBonusProducts = checkForRemainingBonusProducts(updatedBasket)
173+
// Check if there are still remaining bonus products available for THIS promotion
174+
const hasRemainingBonusProducts = checkForRemainingBonusProducts(
175+
updatedBasket,
176+
promotionId
177+
)
175178

176179
if (hasRemainingBonusProducts && onReturnToSelection) {
177180
// Return to SelectBonusProductModal if there are remaining bonus products

packages/template-retail-react-app/app/components/bonus-product-view-modal/index.test.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,10 +320,11 @@ describe('BonusProductViewModal - Return to Selection Flow', () => {
320320
const user = userEvent.setup()
321321

322322
// Mock successful add to cart with remaining bonus products
323+
// The promotionId must match what's passed to BonusProductViewModal
323324
const updatedBasket = {
324325
bonusDiscountLineItems: [
325-
{id: 'bonus-1', maxBonusItems: 2},
326-
{id: 'bonus-2', maxBonusItems: 1}
326+
{id: 'bonus-1', promotionId: 'test-promo', maxBonusItems: 2},
327+
{id: 'bonus-2', promotionId: 'test-promo', maxBonusItems: 1}
327328
],
328329
productItems: [
329330
{
@@ -362,10 +363,11 @@ describe('BonusProductViewModal - Return to Selection Flow', () => {
362363
const user = userEvent.setup()
363364

364365
// Mock successful add to cart with no remaining bonus products
366+
// All bonusDiscountLineItems for this promotion are fully allocated
365367
const updatedBasket = {
366368
bonusDiscountLineItems: [
367-
{id: 'bonus-1', maxBonusItems: 2},
368-
{id: 'bonus-2', maxBonusItems: 1}
369+
{id: 'bonus-1', promotionId: 'test-promo', maxBonusItems: 2},
370+
{id: 'bonus-2', promotionId: 'test-promo', maxBonusItems: 1}
369371
],
370372
productItems: [
371373
{

packages/template-retail-react-app/app/components/bonus-product-view-modal/utils.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,21 @@ export const createGetRemainingBonusQuantity = (
8383
* This function examines the bonus discount line items to see if any still have capacity.
8484
*
8585
* @param {Object} updatedBasket - The updated basket object after adding items
86+
* @param {string} promotionId - Optional promotion ID to check only bonusDiscountLineItems for a specific promotion
8687
* @returns {boolean} - True if there are remaining bonus products available, false otherwise
8788
*/
88-
export const checkForRemainingBonusProducts = (updatedBasket) => {
89+
export const checkForRemainingBonusProducts = (updatedBasket, promotionId) => {
8990
if (!updatedBasket?.bonusDiscountLineItems) {
9091
return false
9192
}
9293

94+
// Filter bonus discount line items by promotionId if provided
95+
const bonusItemsToCheck = promotionId
96+
? updatedBasket.bonusDiscountLineItems.filter((item) => item.promotionId === promotionId)
97+
: updatedBasket.bonusDiscountLineItems
98+
9399
// Check if any bonus discount line items still have available capacity
94-
return updatedBasket.bonusDiscountLineItems.some((discountItem) => {
100+
return bonusItemsToCheck.some((discountItem) => {
95101
const maxBonusItems = discountItem.maxBonusItems || 0
96102

97103
// Calculate how many bonus products are already in cart for this specific discount item

packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: BSD-3-Clause
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import React, {useContext, useState, useEffect} from 'react'
7+
import React, {useContext, useState, useEffect, useMemo} from 'react'
88
import {useLocation} from 'react-router-dom'
99
import PropTypes from 'prop-types'
1010
import {useIntl, FormattedMessage} from 'react-intl'
@@ -91,6 +91,19 @@ export const AddToCartModal = () => {
9191
// Port v4 logic: Check for bonus discount line items and calculate remaining capacity
9292
const {bonusDiscountLineItems = []} = basket || {}
9393

94+
// Build a map of selected bonus products per bonusDiscountLineItem for efficient lookup
95+
// This avoids repeated filtering through productItems for each bonusDiscountLineItem
96+
const bonusSelectionMap = useMemo(() => {
97+
const map = {}
98+
basket?.productItems?.forEach((cartItem) => {
99+
if (cartItem.bonusProductLineItem && cartItem.bonusDiscountLineItemId) {
100+
const id = cartItem.bonusDiscountLineItemId
101+
map[id] = (map[id] || 0) + (cartItem.quantity || 0)
102+
}
103+
})
104+
return map
105+
}, [basket?.productItems])
106+
94107
if (!isOpen) {
95108
return null
96109
}
@@ -361,11 +374,16 @@ export const AddToCartModal = () => {
361374
ruleBasedQualifyingProductsMap
362375
)
363376

364-
// Find the first bonusDiscountLineItem that matches any of the promotionIds
377+
// Find a bonusDiscountLineItem that has remaining capacity
378+
// This ensures we don't pass a fully-allocated bonusDiscountLineItem to SelectBonusProductsCard
365379
const matchingBonusDiscountLineItem =
366-
basket?.bonusDiscountLineItems?.find((bli) =>
367-
promotionIds.includes(bli.promotionId)
368-
)
380+
basket?.bonusDiscountLineItems?.find((bli) => {
381+
return (
382+
promotionIds.includes(bli.promotionId) &&
383+
(bonusSelectionMap[bli.id] || 0) <
384+
(bli.maxBonusItems || 0)
385+
)
386+
})
369387

370388
// If no matching bonusDiscountLineItem found, don't render
371389
if (!matchingBonusDiscountLineItem) {

packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.test.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,3 +1013,91 @@ test('renders SelectBonusProductsCard with hideSelectionCounter=true in add-to-c
10131013
expect(bonusProductsCard).toBeInTheDocument()
10141014
expect(bonusProductsCard).toHaveAttribute('data-hide-selection-counter', 'true')
10151015
})
1016+
1017+
test('selects bonusDiscountLineItem with remaining capacity when first one is fully allocated', () => {
1018+
// This test covers the specific bug fix where adding the same qualifying product
1019+
// multiple times should find a bonusDiscountLineItem with remaining capacity
1020+
const MOCK_PRODUCT_WITH_BONUS = {
1021+
...MOCK_PRODUCT,
1022+
id: 'test-product-123',
1023+
productPromotions: [
1024+
{
1025+
promotionId: 'promo-123',
1026+
calloutMsg: "Buy one men's suit, get 2 free ties"
1027+
}
1028+
]
1029+
}
1030+
1031+
const MOCK_DATA_WITH_BONUS = {
1032+
product: MOCK_PRODUCT_WITH_BONUS,
1033+
itemsAdded: [
1034+
{
1035+
product: MOCK_PRODUCT_WITH_BONUS,
1036+
variant: MOCK_PRODUCT.variants[0],
1037+
quantity: 1
1038+
}
1039+
]
1040+
}
1041+
1042+
// Mock basket with multiple bonusDiscountLineItems for same promotion
1043+
// The first one is fully allocated (2/2), the second has capacity (0/2)
1044+
const mockBasketWithMultipleBonusItems = {
1045+
data: {
1046+
bonusDiscountLineItems: [
1047+
{
1048+
id: 'bonus-line-item-1',
1049+
promotionId: 'promo-123',
1050+
maxBonusItems: 2
1051+
},
1052+
{
1053+
id: 'bonus-line-item-2',
1054+
promotionId: 'promo-123',
1055+
maxBonusItems: 2
1056+
}
1057+
],
1058+
productItems: [
1059+
// Two bonus products selected for the first bonusDiscountLineItem
1060+
{
1061+
productId: 'bonus-product-1',
1062+
bonusProductLineItem: true,
1063+
bonusDiscountLineItemId: 'bonus-line-item-1',
1064+
quantity: 1
1065+
},
1066+
{
1067+
productId: 'bonus-product-2',
1068+
bonusProductLineItem: true,
1069+
bonusDiscountLineItemId: 'bonus-line-item-1',
1070+
quantity: 1
1071+
}
1072+
// No bonus products selected for bonus-line-item-2 yet
1073+
],
1074+
productSubTotal: 191.99,
1075+
currency: 'USD'
1076+
},
1077+
derivedData: {
1078+
totalItems: 3
1079+
},
1080+
currency: 'USD'
1081+
}
1082+
1083+
mockUseCurrentBasket.mockReturnValue(mockBasketWithMultipleBonusItems)
1084+
1085+
renderWithProviders(
1086+
<AddToCartModalContext.Provider
1087+
value={{
1088+
isOpen: true,
1089+
data: MOCK_DATA_WITH_BONUS,
1090+
onClose: jest.fn()
1091+
}}
1092+
>
1093+
<AddToCartModal />
1094+
</AddToCartModalContext.Provider>
1095+
)
1096+
1097+
// Verify that the SelectBonusProductsCard is still rendered
1098+
// This proves that the component found bonus-line-item-2 (which has capacity)
1099+
// instead of stopping at bonus-line-item-1 (which is fully allocated)
1100+
const bonusProductsCard = screen.getByTestId('select-bonus-products-card')
1101+
expect(bonusProductsCard).toBeInTheDocument()
1102+
expect(bonusProductsCard).toHaveAttribute('data-hide-selection-counter', 'true')
1103+
})

0 commit comments

Comments
 (0)