Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import {useCurrency, useDerivedProduct} from '@salesforce/retail-react-app/app/hooks'
import {useAddToCartModalContext} from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal'
import {useBonusProductModalContext} from '@salesforce/retail-react-app/app/hooks/use-bonus-product-modal'
import {useBonusProductSearch} from '@salesforce/retail-react-app/app/hooks/use-bonus-product-search'

// project components
import ImageGallery from '@salesforce/retail-react-app/app/components/image-gallery'
Expand Down Expand Up @@ -141,6 +142,9 @@ const ProductView = forwardRef(
} = useBonusProductModalContext()
const theme = useTheme()
const [showOptionsMessage, toggleShowOptionsMessage] = useState(false)
const [promotionIdToSearch, setPromotionIdToSearch] = useState(null)
const {data: bonusProductSearchResult} = useBonusProductSearch(promotionIdToSearch)

const {
showLoading,
showInventoryMessage,
Expand Down Expand Up @@ -306,10 +310,38 @@ const ProductView = forwardRef(
// Show bonus product modal first if there are bonus items
if (newBonusItems?.length > 0) {
// Update bonusProducts list with the new bonus items
let isRuleBasedPromotion = !newBonusItems.some(
(item) => item.bonusProducts
)
let bonusProductsToShow = []

if (isRuleBasedPromotion) {
setPromotionIdToSearch(newBonusItems[0].promotionId)
let ruleBasedBonusProducts = []
if (bonusProductSearchResult?.hits?.length > 0) {
bonusProductSearchResult.hits.forEach((bonusProduct, index) => {
ruleBasedBonusProducts.push({
productId: bonusProduct.productId,
productName: bonusProduct.productName,
c_productUrl: bonusProduct.c_productUrl
})
})
bonusProductsToShow = ruleBasedBonusProducts
}
} else {
let listBasedBonusProducts = []
newBonusItems[0].bonusProducts.forEach((bonusProduct) => {
listBasedBonusProducts.push({
productId: bonusProduct.productId,
productName: bonusProduct.productName,
title: bonusProduct.title
})
})
bonusProductsToShow = listBasedBonusProducts
}
addBonusProducts(newBonusItems)
onBonusProductModalOpen({
newBonusItems,
allBonusItems: addToCartResponse.bonusDiscountLineItems,
newBonusItems: bonusProductsToShow,
openAddToCartModalIfNeeded: true,
product,
itemsAdded,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const useBonusState = (basket) => {
const [state, setState] = useState({
isOpen: false,
data: {},
bonusProducts: basket?.bonusDiscountLineItems || []
existingBonusProducts: basket?.bonusDiscountLineItems || []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why changing it to existingBonusProducts? What made it different from bonusProd. from this hook POV, it only cares about basket.bonusDiscountLineItems as bonusProd. What is the non-existingBonusProd if we take this name?
So any un-added/un-set items from outside that is not added to cart is irrelavent

})
const {pathname} = useLocation()
const {onOpen: onAddToCartModalOpen} = useAddToCartModalContext()
Expand All @@ -54,10 +54,10 @@ export const useBonusState = (basket) => {
const currentBonusItems = basket?.bonusDiscountLineItems || []
setState((prev) => {
// Only update if the bonus items have actually changed
if (JSON.stringify(prev.bonusProducts) !== JSON.stringify(currentBonusItems)) {
if (JSON.stringify(prev.existingBonusProducts) !== JSON.stringify(currentBonusItems)) {
return {
...prev,
bonusProducts: currentBonusItems
existingBonusProducts: currentBonusItems
}
}
return prev
Expand All @@ -66,18 +66,18 @@ export const useBonusState = (basket) => {

const addBonusProducts = (newBonusItems) => {
setState((prev) => {
const updatedBonusProducts = [...prev.bonusProducts, ...newBonusItems]
const updatedBonusProducts = [...prev.existingBonusProducts, ...newBonusItems]
return {
...prev,
bonusProducts: updatedBonusProducts
existingBonusProducts: updatedBonusProducts
}
})
}

return {
isOpen: state.isOpen,
data: state.data,
bonusProducts: state.bonusProducts,
bonusProducts: state.existingBonusProducts,
addBonusProducts,
onClose: () => {
setState((prev) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import {HOME_SHOP_PRODUCTS_LIMIT} from '@salesforce/retail-react-app/app/constants'
import {useProductSearch} from '@salesforce/commerce-sdk-react'

export const useBonusProductSearch = (promotionId) => {
const {data: productSearchResult} = useProductSearch({
parameters: {
allImages: true,
allVariationProperties: true,
expand: ['promotions', 'variations', 'prices', 'images', 'custom_properties'],
limit: HOME_SHOP_PRODUCTS_LIMIT,
perPricebook: true,
refine: [`pmid=${promotionId}`, 'htype=master']
}
})
return {
data: productSearchResult
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import {renderHook} from '@testing-library/react'
import {useProductSearch} from '@salesforce/commerce-sdk-react'
import {useBonusProductSearch} from '@salesforce/retail-react-app/../../app/hooks/use-bonus-product-search'
import {HOME_SHOP_PRODUCTS_LIMIT} from '@salesforce/retail-react-app/app/constants'

// Mock the commerce SDK hook
jest.mock('@salesforce/commerce-sdk-react')

describe('useBonusProductSearch', () => {
let mockUseProductSearch

beforeEach(() => {
mockUseProductSearch = {
data: null,
isLoading: false,
error: null
}
useProductSearch.mockReturnValue(mockUseProductSearch)
})

afterEach(() => {
jest.clearAllMocks()
})

test('should call useProductSearch with correct parameters when promotionId is provided', () => {
const promotionId = 'test-promotion-id'

renderHook(() => useBonusProductSearch(promotionId))

expect(useProductSearch).toHaveBeenCalledWith({
parameters: {
allImages: true,
allVariationProperties: true,
expand: ['promotions', 'variations', 'prices', 'images', 'custom_properties'],
limit: HOME_SHOP_PRODUCTS_LIMIT,
perPricebook: true,
refine: [`pmid=${promotionId}`, 'htype=master']
}
})
})

test('should call useProductSearch with null promotionId', () => {
renderHook(() => useBonusProductSearch(null))

expect(useProductSearch).toHaveBeenCalledWith({
parameters: {
allImages: true,
allVariationProperties: true,
expand: ['promotions', 'variations', 'prices', 'images', 'custom_properties'],
limit: HOME_SHOP_PRODUCTS_LIMIT,
perPricebook: true,
refine: ['pmid=null', 'htype=master']
}
})
})

test('should call useProductSearch with undefined promotionId', () => {
renderHook(() => useBonusProductSearch(undefined))

expect(useProductSearch).toHaveBeenCalledWith({
parameters: {
allImages: true,
allVariationProperties: true,
expand: ['promotions', 'variations', 'prices', 'images', 'custom_properties'],
limit: HOME_SHOP_PRODUCTS_LIMIT,
perPricebook: true,
refine: ['pmid=undefined', 'htype=master']
}
})
})

test('should return data from useProductSearch', () => {
const mockData = {
hits: [
{
productId: 'test-product-1',
productName: 'Test Product 1'
},
{
productId: 'test-product-2',
productName: 'Test Product 2'
}
]
}

mockUseProductSearch.data = mockData

const {result} = renderHook(() => useBonusProductSearch('test-promotion'))

expect(result.current.data).toBe(mockData)
})

test('should return null data when useProductSearch returns null', () => {
mockUseProductSearch.data = null

const {result} = renderHook(() => useBonusProductSearch('test-promotion'))

expect(result.current.data).toBeNull()
})

test('should handle empty string promotionId', () => {
renderHook(() => useBonusProductSearch(''))

expect(useProductSearch).toHaveBeenCalledWith({
parameters: {
allImages: true,
allVariationProperties: true,
expand: ['promotions', 'variations', 'prices', 'images', 'custom_properties'],
limit: HOME_SHOP_PRODUCTS_LIMIT,
perPricebook: true,
refine: ['pmid=', 'htype=master']
}
})
})

test('should use correct refine parameters for promotion search', () => {
const promotionId = 'ChoiceOfBonusProdect-ProductLevel-ruleBased'

renderHook(() => useBonusProductSearch(promotionId))

expect(useProductSearch).toHaveBeenCalledWith({
parameters: {
allImages: true,
allVariationProperties: true,
expand: ['promotions', 'variations', 'prices', 'images', 'custom_properties'],
limit: HOME_SHOP_PRODUCTS_LIMIT,
perPricebook: true,
refine: [`pmid=${promotionId}`, 'htype=master']
}
})
})
})
Loading