diff --git a/packages/template-retail-react-app/app/hooks/use-pickup-shipment.js b/packages/template-retail-react-app/app/hooks/use-pickup-shipment.js index aa12e1753d..f3fb2e4c96 100644 --- a/packages/template-retail-react-app/app/hooks/use-pickup-shipment.js +++ b/packages/template-retail-react-app/app/hooks/use-pickup-shipment.js @@ -5,15 +5,31 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import {useShopperBasketsMutation} from '@salesforce/commerce-sdk-react' +import { + useShopperBasketsMutation, + useShippingMethodsForShipment +} from '@salesforce/commerce-sdk-react' /** * Custom hook to handle pickup in store shipment configuration * @returns {Object} Object containing helper functions for pickup shipment management */ -export const usePickupShipment = () => { +export const usePickupShipment = (basket) => { const updateShipmentForBasketMutation = useShopperBasketsMutation('updateShipmentForBasket') + // Hook for shipping methods - we'll use refetch when needed + const {refetch: refetchShippingMethods} = useShippingMethodsForShipment( + { + parameters: { + basketId: basket?.basketId, + shipmentId: 'me' + } + }, + { + enabled: false // Disable automatic fetching, we'll fetch manually when needed + } + ) + /** * Gets the shipping method ID for pickup in store * @param {Object} shippingMethods - The shipping methods for the shipment @@ -169,9 +185,72 @@ export const usePickupShipment = () => { }) } + /** + * Configure shipping method based on pickup selection + * @param {Object} basketResponse - The basket response from adding items + * @param {Array} productItems - Array of product items that were added + * @param {boolean} hasAnyPickupSelected - Whether any items have pickup selected + * @param {Object} selectedStore - The selected store information + * @returns {Promise} + */ + const configureShippingMethodIfNeeded = async ( + basketResponse, + productItems, + hasAnyPickupSelected, + selectedStore + ) => { + if (!basketResponse?.basketId || !basketResponse.shipments.length) { + return + } + + const currentShippingMethod = basketResponse.shipments[0].shippingMethod + const isCurrentlyPickup = isCurrentShippingMethodPickup(currentShippingMethod) + + // Only configure if there's a mismatch between pickup selection and current method + if ( + (hasAnyPickupSelected && !isCurrentlyPickup) || + (!hasAnyPickupSelected && isCurrentlyPickup) + ) { + // Clear shipping address when there's a mismatch by updating shipment without shippingAddress + await updateShipmentForBasketMutation.mutateAsync({ + parameters: { + basketId: basketResponse.basketId, + shipmentId: 'me' + }, + body: { + shippingAddress: {} + } + }) + + // Fetch shipping methods to get available options + const {data: fetchedShippingMethods} = await refetchShippingMethods() + + if (hasAnyPickupSelected && !isCurrentlyPickup) { + // Configure pickup shipment if pickup is selected but current method is not pickup + const pickupShippingMethodId = getPickupShippingMethodId(fetchedShippingMethods) + await configurePickupShipment( + basketResponse.basketId, + productItems, + selectedStore, + { + pickupShippingMethodId + } + ) + } else if (!hasAnyPickupSelected && isCurrentlyPickup) { + // Configure regular shipping if pickup is not selected but current method is pickup + const defaultShippingMethodId = getDefaultShippingMethodId(fetchedShippingMethods) + await configureRegularShippingMethod( + basketResponse.basketId, + defaultShippingMethodId + ) + } + } + } + return { configurePickupShipment, configureRegularShippingMethod, + configureShippingMethodIfNeeded, hasPickupItems, addInventoryIdsToPickupItems, getPickupShippingMethodId, diff --git a/packages/template-retail-react-app/app/hooks/use-pickup-shipment.test.js b/packages/template-retail-react-app/app/hooks/use-pickup-shipment.test.js index a29064ce3b..b96d126b99 100644 --- a/packages/template-retail-react-app/app/hooks/use-pickup-shipment.test.js +++ b/packages/template-retail-react-app/app/hooks/use-pickup-shipment.test.js @@ -14,6 +14,9 @@ jest.mock('@salesforce/commerce-sdk-react', () => ({ useShopperBasketsMutation: jest.fn(() => ({ mutateAsync: jest.fn(), isLoading: false + })), + useShippingMethodsForShipment: jest.fn(() => ({ + refetch: jest.fn() })) })) @@ -607,4 +610,318 @@ describe('usePickupShipment', () => { ).rejects.toThrow('Mutation failed') }) }) + + describe('configureShippingMethodIfNeeded', () => { + let mockMutateAsync + let mockRefetchShippingMethods + + beforeEach(() => { + mockMutateAsync = jest.fn().mockResolvedValue({}) + mockRefetchShippingMethods = jest.fn().mockResolvedValue({ + data: { + applicableShippingMethods: [ + { + id: 'standard-shipping' + }, + { + id: 'pickup-method-123', + c_storePickupEnabled: true + } + ], + defaultShippingMethodId: 'standard-shipping' + } + }) + + jest.clearAllMocks() + + // Get the mocked module and update the mock to include mutateAsync and refetch + const commerceSdkMock = jest.requireMock('@salesforce/commerce-sdk-react') + commerceSdkMock.useShopperBasketsMutation.mockReturnValue({ + mutateAsync: mockMutateAsync, + isLoading: false + }) + commerceSdkMock.useShippingMethodsForShipment.mockReturnValue({ + refetch: mockRefetchShippingMethods + }) + }) + + test('configures pickup shipment when pickup selected but current method is not pickup', async () => { + const {result} = renderHook(() => usePickupShipment()) + + const basketResponse = { + basketId: 'basket-123', + shipments: [ + { + shippingMethod: { + id: 'standard-shipping' + } + } + ] + } + const productItems = [{productId: 'product-1', inventoryId: 'inv-1', quantity: 1}] + const hasAnyPickupSelected = true + const selectedStore = {id: 'store-1', inventoryId: 'inv-1'} + + await result.current.configureShippingMethodIfNeeded( + basketResponse, + productItems, + hasAnyPickupSelected, + selectedStore + ) + + // Should clear shipping address first + expect(mockMutateAsync).toHaveBeenNthCalledWith(1, { + parameters: { + basketId: 'basket-123', + shipmentId: 'me' + }, + body: { + shippingAddress: {} + } + }) + + // Should fetch shipping methods + expect(mockRefetchShippingMethods).toHaveBeenCalled() + + // Should configure pickup shipment (second call) + expect(mockMutateAsync).toHaveBeenNthCalledWith(2, { + parameters: { + basketId: 'basket-123', + shipmentId: 'me' + }, + body: { + shippingMethod: { + id: 'pickup-method-123' + }, + c_fromStoreId: 'store-1' + } + }) + }) + + test('configures regular shipping when pickup not selected but current method is pickup', async () => { + const {result} = renderHook(() => usePickupShipment()) + + const basketResponse = { + basketId: 'basket-123', + shipments: [ + { + shippingMethod: { + id: 'pickup-method-123', + c_storePickupEnabled: true + } + } + ] + } + const productItems = [{productId: 'product-1', quantity: 1}] + const hasAnyPickupSelected = false + const selectedStore = {id: 'store-1', inventoryId: 'inv-1'} + + await result.current.configureShippingMethodIfNeeded( + basketResponse, + productItems, + hasAnyPickupSelected, + selectedStore + ) + + // Should clear shipping address first + expect(mockMutateAsync).toHaveBeenNthCalledWith(1, { + parameters: { + basketId: 'basket-123', + shipmentId: 'me' + }, + body: { + shippingAddress: {} + } + }) + + // Should fetch shipping methods + expect(mockRefetchShippingMethods).toHaveBeenCalled() + + // Should configure regular shipping method (second call) + expect(mockMutateAsync).toHaveBeenNthCalledWith(2, { + parameters: { + basketId: 'basket-123', + shipmentId: 'me' + }, + body: { + shippingMethod: { + id: 'standard-shipping' + } + } + }) + }) + + test('does not configure shipping when pickup selection matches current method', async () => { + const {result} = renderHook(() => usePickupShipment()) + + const basketResponse = { + basketId: 'basket-123', + shipments: [ + { + shippingMethod: { + id: 'pickup-method-123', + c_storePickupEnabled: true + } + } + ] + } + const productItems = [{productId: 'product-1', quantity: 1}] + const hasAnyPickupSelected = true // Pickup selected and current method is pickup + const selectedStore = {id: 'store-1', inventoryId: 'inv-1'} + + await result.current.configureShippingMethodIfNeeded( + basketResponse, + productItems, + hasAnyPickupSelected, + selectedStore + ) + + expect(mockMutateAsync).not.toHaveBeenCalled() + expect(mockRefetchShippingMethods).not.toHaveBeenCalled() + }) + + test('does not configure shipping when no pickup selected and current method is not pickup', async () => { + const {result} = renderHook(() => usePickupShipment()) + + const basketResponse = { + basketId: 'basket-123', + shipments: [ + { + shippingMethod: { + id: 'standard-shipping', + c_storePickupEnabled: false + } + } + ] + } + const productItems = [{productId: 'product-1', quantity: 1}] + const hasAnyPickupSelected = false // No pickup selected and current method is not pickup + const selectedStore = {id: 'store-1', inventoryId: 'inv-1'} + + await result.current.configureShippingMethodIfNeeded( + basketResponse, + productItems, + hasAnyPickupSelected, + selectedStore + ) + + expect(mockMutateAsync).not.toHaveBeenCalled() + expect(mockRefetchShippingMethods).not.toHaveBeenCalled() + }) + + test('handles case when no pickup shipping method is found', async () => { + mockRefetchShippingMethods.mockResolvedValue({ + data: { + applicableShippingMethods: [ + { + id: 'standard-shipping', + c_storePickupEnabled: false + } + ], + defaultShippingMethodId: 'standard-shipping' + } + }) + + const {result} = renderHook(() => usePickupShipment()) + + const basketResponse = { + basketId: 'basket-123', + shipments: [ + { + shippingMethod: { + id: 'standard-shipping', + c_storePickupEnabled: false + } + } + ] + } + const productItems = [{productId: 'product-1', inventoryId: 'inv-1', quantity: 1}] + const hasAnyPickupSelected = true + const selectedStore = {id: 'store-1', inventoryId: 'inv-1'} + + await result.current.configureShippingMethodIfNeeded( + basketResponse, + productItems, + hasAnyPickupSelected, + selectedStore + ) + + // Should still clear shipping address and fetch methods + expect(mockMutateAsync).toHaveBeenNthCalledWith(1, { + parameters: { + basketId: 'basket-123', + shipmentId: 'me' + }, + body: { + shippingAddress: {} + } + }) + expect(mockRefetchShippingMethods).toHaveBeenCalled() + + // Should configure pickup with null shipping method ID (which will use default) + expect(mockMutateAsync).toHaveBeenNthCalledWith(2, { + parameters: { + basketId: 'basket-123', + shipmentId: 'me' + }, + body: { + shippingMethod: { + id: null + }, + c_fromStoreId: 'store-1' + } + }) + }) + + test('handles case when no default shipping method is found', async () => { + mockRefetchShippingMethods.mockResolvedValue({ + data: { + applicableShippingMethods: [ + { + id: 'pickup-method-123', + c_storePickupEnabled: true + } + ] + // No defaultShippingMethodId + } + }) + + const {result} = renderHook(() => usePickupShipment()) + + const basketResponse = { + basketId: 'basket-123', + shipments: [ + { + shippingMethod: { + id: 'pickup-method-123', + c_storePickupEnabled: true + } + } + ] + } + const productItems = [{productId: 'product-1', quantity: 1}] + const hasAnyPickupSelected = false + const selectedStore = {id: 'store-1', inventoryId: 'inv-1'} + + await result.current.configureShippingMethodIfNeeded( + basketResponse, + productItems, + hasAnyPickupSelected, + selectedStore + ) + + // Should configure regular shipping with null shipping method ID + expect(mockMutateAsync).toHaveBeenNthCalledWith(2, { + parameters: { + basketId: 'basket-123', + shipmentId: 'me' + }, + body: { + shippingMethod: { + id: null + } + } + }) + }) + }) }) diff --git a/packages/template-retail-react-app/app/pages/checkout/partials/pickup-address.jsx b/packages/template-retail-react-app/app/pages/checkout/partials/pickup-address.jsx index df77abfeaa..d97831cbb4 100644 --- a/packages/template-retail-react-app/app/pages/checkout/partials/pickup-address.jsx +++ b/packages/template-retail-react-app/app/pages/checkout/partials/pickup-address.jsx @@ -30,6 +30,7 @@ export default function PickupAddress() { const {data: basket} = useCurrentBasket() const selectedShippingAddress = basket?.shipments && basket?.shipments[0]?.shippingAddress + const isAddressFilled = selectedShippingAddress?.address1 && selectedShippingAddress?.city // Check if basket is a pickup order const isPickupOrder = basket?.shipments?.[0]?.shippingMethod?.c_storePickupEnabled === true @@ -113,7 +114,7 @@ export default function PickupAddress() { )} - {selectedShippingAddress && ( + {isAddressFilled && ( - {selectedShippingAddress && ( + {isAddressFilled && ( diff --git a/packages/template-retail-react-app/app/pages/checkout/util/checkout-context.js b/packages/template-retail-react-app/app/pages/checkout/util/checkout-context.js index 566e29fb8f..f47e58eb90 100644 --- a/packages/template-retail-react-app/app/pages/checkout/util/checkout-context.js +++ b/packages/template-retail-react-app/app/pages/checkout/util/checkout-context.js @@ -39,7 +39,7 @@ export const CheckoutProvider = ({children}) => { if (customer.isGuest && !basket.customerInfo?.email) { step = STEPS.CONTACT_INFO - } else if (!basket.shipments[0]?.shippingAddress) { + } else if (!basket.shipments[0]?.shippingAddress?.address1) { // Check if it's a pickup order const isPickupOrder = basket?.shipments[0]?.shippingMethod?.c_storePickupEnabled === true diff --git a/packages/template-retail-react-app/app/pages/product-detail/index.jsx b/packages/template-retail-react-app/app/pages/product-detail/index.jsx index 490ec07129..f2ab299f1e 100644 --- a/packages/template-retail-react-app/app/pages/product-detail/index.jsx +++ b/packages/template-retail-react-app/app/pages/product-detail/index.jsx @@ -23,8 +23,7 @@ import { useShopperCustomersMutation, useShopperBasketsMutation, useCustomerId, - useShopperBasketsMutationHelper, - useShippingMethodsForShipment + useShopperBasketsMutationHelper } from '@salesforce/commerce-sdk-react' // Hooks @@ -96,27 +95,11 @@ const ProductDetail = () => { const selectedInventoryId = selectedStore?.inventoryId || null const { - configurePickupShipment, - configureRegularShippingMethod, addInventoryIdsToPickupItems, - getPickupShippingMethodId, - getDefaultShippingMethodId, + configureShippingMethodIfNeeded, isCurrentShippingMethodPickup, hasPickupItems - } = usePickupShipment() - - // Hook for shipping methods - we'll use refetch when needed - const {refetch: refetchShippingMethods} = useShippingMethodsForShipment( - { - parameters: { - basketId: basket?.basketId, - shipmentId: 'me' - } - }, - { - enabled: false // Disable automatic fetching, we'll fetch manually when needed - } - ) + } = usePickupShipment(basket) /*************************** Product Detail and Category ********************/ const {productId} = useParams() @@ -415,37 +398,12 @@ const ProductDetail = () => { const basketResponse = await addItemToNewOrExistingBasket(productItems) // Configure shipping method based on pickup selection - if (basketResponse?.basketId && basketResponse.shipments.length > 0) { - const currentShippingMethod = basketResponse.shipments[0].shippingMethod - const isCurrentlyPickup = isCurrentShippingMethodPickup(currentShippingMethod) - - if (hasAnyPickupSelected && !isCurrentlyPickup) { - // Fetch shipping methods to get available options - const {data: fetchedShippingMethods} = await refetchShippingMethods() - - // Configure pickup shipment if pickup is selected but current method is not pickup - const pickupShippingMethodId = getPickupShippingMethodId(fetchedShippingMethods) - await configurePickupShipment( - basketResponse.basketId, - productItems, - selectedStore, - { - pickupShippingMethodId - } - ) - } else if (!hasAnyPickupSelected && isCurrentlyPickup) { - // Fetch shipping methods to get available options - const {data: fetchedShippingMethods} = await refetchShippingMethods() - - // Configure regular shipping if pickup is not selected but current method is pickup - const defaultShippingMethodId = - getDefaultShippingMethodId(fetchedShippingMethods) - await configureRegularShippingMethod( - basketResponse.basketId, - defaultShippingMethodId - ) - } - } + await configureShippingMethodIfNeeded( + basketResponse, + productItems, + hasAnyPickupSelected, + selectedStore + ) const productItemsForEinstein = productSelectionValues.map( ({product, variant, quantity}) => ({ @@ -626,26 +584,12 @@ const ProductDetail = () => { } // Configure shipping method based on pickup selection - if (res.basketId && res.shipments.length > 0) { - const currentShippingMethod = res.shipments[0].shippingMethod - const isCurrentlyPickup = isCurrentShippingMethodPickup(currentShippingMethod) - - // Fetch shipping methods to get available options - const {data: fetchedShippingMethods} = await refetchShippingMethods() - - if (hasAnyPickupSelected && !isCurrentlyPickup) { - // Configure pickup shipment if pickup is selected but current method is not pickup - const pickupShippingMethodId = getPickupShippingMethodId(fetchedShippingMethods) - await configurePickupShipment(res.basketId, productItems, selectedStore, { - pickupShippingMethodId - }) - } else if (!hasAnyPickupSelected && isCurrentlyPickup) { - // Configure regular shipping if pickup is not selected but current method is pickup - const defaultShippingMethodId = - getDefaultShippingMethodId(fetchedShippingMethods) - await configureRegularShippingMethod(res.basketId, defaultShippingMethodId) - } - } + await configureShippingMethodIfNeeded( + res, + productItems, + hasAnyPickupSelected, + selectedStore + ) einstein.sendAddToCart(productItems) // Open modal with itemsAdded and selectedQuantity for bundles