@salesforce/retail-react-app 7.1.0-preview.1 → 8.0.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +8 -2
  2. package/app/components/_app/index.jsx +9 -7
  3. package/app/components/_app/index.test.js +2 -2
  4. package/app/components/_app-config/index.jsx +9 -3
  5. package/app/components/drawer-menu/drawer-menu.jsx +3 -1
  6. package/app/components/footer/index.jsx +3 -1
  7. package/app/components/header/index.jsx +3 -1
  8. package/app/components/header/index.test.js +2 -2
  9. package/app/components/island/README.md +1 -1
  10. package/app/components/island/index.jsx +3 -1
  11. package/app/components/island/index.test.js +94 -5
  12. package/app/components/item-variant/item-attributes.jsx +12 -3
  13. package/app/components/multiship/multiship-order-summary.jsx +137 -0
  14. package/app/components/multiship/multiship-order-summary.test.js +121 -0
  15. package/app/components/order-summary/index.jsx +2 -4
  16. package/app/components/pickup-or-delivery/index.jsx +80 -0
  17. package/app/components/pickup-or-delivery/index.test.jsx +182 -0
  18. package/app/components/product-item/index.jsx +26 -16
  19. package/app/components/product-item/index.test.js +29 -2
  20. package/app/components/product-item-list/index.jsx +10 -0
  21. package/app/components/product-item-list/index.test.jsx +14 -0
  22. package/app/components/product-view/index.jsx +9 -6
  23. package/app/components/product-view/index.test.js +25 -21
  24. package/app/components/quantity-picker/index.test.jsx +12 -12
  25. package/app/components/reset-password/index.test.js +1 -1
  26. package/app/components/shared/ui/AlertDescription/index.jsx +8 -0
  27. package/app/components/shared/ui/index.jsx +1 -0
  28. package/app/components/store-display/index.jsx +28 -4
  29. package/app/components/store-display/index.test.js +71 -0
  30. package/app/components/store-locator/form.test.jsx +16 -4
  31. package/app/components/store-locator/list.jsx +9 -4
  32. package/app/components/toggle-card/index.jsx +14 -0
  33. package/app/components/unavailable-product-confirmation-modal/index.jsx +19 -5
  34. package/app/components/unavailable-product-confirmation-modal/index.test.js +122 -1
  35. package/app/constants.js +20 -6
  36. package/app/contexts/store-locator-provider.jsx +7 -1
  37. package/app/contexts/store-locator-provider.test.jsx +36 -1
  38. package/app/hooks/use-address-form.js +155 -0
  39. package/app/hooks/use-address-form.test.js +501 -0
  40. package/app/hooks/use-auth-modal.js +2 -6
  41. package/app/hooks/use-current-basket.js +71 -2
  42. package/app/hooks/use-current-basket.test.js +37 -1
  43. package/app/hooks/use-dnt-notification.js +4 -4
  44. package/app/hooks/use-dnt-notification.test.js +5 -5
  45. package/app/hooks/use-item-shipment-management.js +233 -0
  46. package/app/hooks/use-item-shipment-management.test.js +696 -0
  47. package/app/hooks/use-multiship.js +589 -0
  48. package/app/hooks/use-multiship.test.js +776 -0
  49. package/app/hooks/use-pickup-shipment.js +70 -106
  50. package/app/hooks/use-pickup-shipment.test.js +345 -209
  51. package/app/hooks/use-product-address-assignment.js +280 -0
  52. package/app/hooks/use-product-address-assignment.test.js +414 -0
  53. package/app/hooks/use-product-inventory.js +100 -0
  54. package/app/hooks/use-product-inventory.test.js +254 -0
  55. package/app/hooks/use-shipment-operations.js +168 -0
  56. package/app/hooks/use-shipment-operations.test.js +385 -0
  57. package/app/hooks/use-store-locator.js +24 -2
  58. package/app/hooks/use-store-locator.test.jsx +109 -1
  59. package/app/pages/account/index.test.js +1 -1
  60. package/app/pages/account/profile.test.js +0 -2
  61. package/app/pages/cart/index.jsx +397 -157
  62. package/app/pages/cart/index.test.js +353 -2
  63. package/app/pages/cart/partials/bonus-products-title.jsx +10 -8
  64. package/app/pages/cart/partials/cart-secondary-button-group.test.js +1 -1
  65. package/app/pages/cart/partials/order-type-display.jsx +68 -0
  66. package/app/pages/cart/partials/order-type-display.test.js +241 -0
  67. package/app/pages/checkout/confirmation.jsx +79 -158
  68. package/app/pages/checkout/index.jsx +34 -9
  69. package/app/pages/checkout/index.test.js +245 -118
  70. package/app/pages/checkout/partials/contact-info.jsx +2 -6
  71. package/app/pages/checkout/partials/contact-info.test.js +93 -7
  72. package/app/pages/checkout/partials/payment.jsx +19 -5
  73. package/app/pages/checkout/partials/pickup-address.jsx +340 -70
  74. package/app/pages/checkout/partials/pickup-address.test.js +1075 -82
  75. package/app/pages/checkout/partials/product-shipping-address-card.jsx +382 -0
  76. package/app/pages/checkout/partials/shipment-details.jsx +209 -0
  77. package/app/pages/checkout/partials/shipment-details.test.js +246 -0
  78. package/app/pages/checkout/partials/shipping-address.jsx +156 -68
  79. package/app/pages/checkout/partials/shipping-address.test.js +673 -0
  80. package/app/pages/checkout/partials/shipping-method-options.jsx +180 -0
  81. package/app/pages/checkout/partials/shipping-methods.jsx +403 -0
  82. package/app/pages/checkout/partials/shipping-methods.test.js +472 -0
  83. package/app/pages/checkout/partials/shipping-multi-address.jsx +259 -0
  84. package/app/pages/checkout/partials/shipping-multi-address.test.js +2088 -0
  85. package/app/pages/checkout/partials/shipping-product-cards.jsx +101 -0
  86. package/app/pages/checkout/util/checkout-context.js +25 -18
  87. package/app/pages/login/index.jsx +2 -6
  88. package/app/pages/product-detail/index.jsx +96 -81
  89. package/app/pages/product-detail/index.test.js +103 -19
  90. package/app/pages/product-list/index.jsx +3 -1
  91. package/app/pages/product-list/partials/inventory-filter.jsx +18 -21
  92. package/app/pages/product-list/partials/inventory-filter.test.js +15 -17
  93. package/app/pages/product-list/partials/selected-refinements.jsx +3 -1
  94. package/app/ssr.js +1 -1
  95. package/app/static/translations/compiled/en-GB.json +316 -30
  96. package/app/static/translations/compiled/en-US.json +316 -30
  97. package/app/static/translations/compiled/en-XA.json +673 -75
  98. package/app/utils/address-utils.js +112 -0
  99. package/app/utils/address-utils.test.js +484 -0
  100. package/app/utils/product-utils.js +17 -5
  101. package/app/utils/product-utils.test.js +17 -8
  102. package/app/utils/sfdc-user-agent-utils.js +32 -0
  103. package/app/utils/sfdc-user-agent-utils.test.js +82 -0
  104. package/app/utils/shipment-utils.js +196 -0
  105. package/app/utils/shipment-utils.test.js +458 -0
  106. package/app/utils/test-utils.js +4 -4
  107. package/app/utils/utils.js +6 -1
  108. package/config/default.js +4 -1
  109. package/config/mocks/default.js +3 -1
  110. package/package.json +9 -9
  111. package/translations/en-GB.json +127 -10
  112. package/translations/en-US.json +127 -10
  113. package/app/pages/checkout/partials/shipping-options.jsx +0 -269
@@ -0,0 +1,280 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import {useState, useEffect, useCallback, useMemo, useRef} from 'react'
8
+ import {nanoid} from 'nanoid'
9
+ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
10
+ import {
11
+ areAddressesEqual,
12
+ isAddressEmpty
13
+ } from '@salesforce/retail-react-app/app/utils/address-utils'
14
+ import {isPickupMethod} from '@salesforce/retail-react-app/app/utils/shipment-utils'
15
+
16
+ /**
17
+ * Managing address selection state with product delivery items
18
+ */
19
+ export const useProductAddressAssignment = (basket) => {
20
+ const {data: customer} = useCurrentCustomer()
21
+
22
+ const deliveryItems = useMemo(() => {
23
+ return (
24
+ basket?.productItems?.filter((item) => {
25
+ const shipment = basket?.shipments?.find((s) => s.shipmentId === item.shipmentId)
26
+ return !isPickupMethod(shipment?.shippingMethod)
27
+ }) || []
28
+ )
29
+ }, [basket?.productItems, basket?.shipments])
30
+
31
+ const [guestAddresses, setGuestAddresses] = useState([])
32
+ const [selectedGuestAddresses, setSelectedGuestAddresses] = useState({})
33
+ const [selectedRegisteredUserAddresses, setSelectedRegisteredUserAddresses] = useState({})
34
+
35
+ // track if already initialized addresses to prevent infinite loops
36
+ const hasInitialized = useRef(false)
37
+
38
+ const availableAddresses = useMemo(() => {
39
+ if (customer?.isGuest) {
40
+ return guestAddresses
41
+ }
42
+ return customer?.addresses || []
43
+ }, [customer, guestAddresses])
44
+
45
+ // initialize address selections -registered users
46
+ useEffect(() => {
47
+ if (
48
+ customer?.customerId &&
49
+ customer?.isRegistered &&
50
+ deliveryItems?.length > 0 &&
51
+ availableAddresses.length > 0
52
+ ) {
53
+ const initialSelected = {}
54
+
55
+ const existingShipments =
56
+ basket?.shipments?.filter(
57
+ (shipment) =>
58
+ shipment.shippingAddress && !isPickupMethod(shipment?.shippingMethod)
59
+ ) || []
60
+
61
+ if (existingShipments.length > 0) {
62
+ deliveryItems.forEach((item) => {
63
+ const addressKey = item.itemId
64
+ const shipment = existingShipments.find((s) => s.shipmentId === item.shipmentId)
65
+
66
+ if (shipment && shipment.shippingAddress) {
67
+ const matchingAddress = availableAddresses.find((addr) =>
68
+ areAddressesEqual(addr, shipment.shippingAddress)
69
+ )
70
+
71
+ if (matchingAddress) {
72
+ initialSelected[addressKey] = matchingAddress.addressId
73
+ } else if (availableAddresses.length > 0) {
74
+ // fall back to first customer address if no match
75
+ initialSelected[addressKey] = availableAddresses[0].addressId
76
+ }
77
+ } else {
78
+ // set default for items that don't have a address assignment yet
79
+ if (availableAddresses.length > 0) {
80
+ const defaultAddress =
81
+ availableAddresses.find((addr) => addr.preferred) ||
82
+ availableAddresses[0]
83
+ if (defaultAddress) {
84
+ initialSelected[addressKey] = defaultAddress.addressId
85
+ }
86
+ }
87
+ }
88
+ })
89
+ } else if (availableAddresses.length > 0) {
90
+ // Fall back to customer addresses if no existing shipments
91
+ deliveryItems.forEach((item) => {
92
+ const addressKey = item.itemId
93
+ // preferred address or use first address as default
94
+ const defaultAddress =
95
+ availableAddresses.find((addr) => addr.preferred) || availableAddresses[0]
96
+ if (defaultAddress) {
97
+ initialSelected[addressKey] = defaultAddress.addressId
98
+ }
99
+ })
100
+ }
101
+
102
+ // Only update if we have new selections and they're different from current
103
+ if (Object.keys(initialSelected).length > 0) {
104
+ setSelectedRegisteredUserAddresses((prev) => {
105
+ const newState = {...prev}
106
+ let hasChanges = false
107
+
108
+ deliveryItems.forEach((item) => {
109
+ const addressKey = item.itemId
110
+ if (!prev[addressKey] && initialSelected[addressKey]) {
111
+ newState[addressKey] = initialSelected[addressKey]
112
+ hasChanges = true
113
+ }
114
+ })
115
+
116
+ return hasChanges ? newState : prev
117
+ })
118
+ }
119
+ }
120
+ }, [
121
+ customer?.customerId,
122
+ customer?.isGuest,
123
+ availableAddresses.length,
124
+ basket?.shipments?.length,
125
+ deliveryItems?.length
126
+ ])
127
+
128
+ // initialize address selections -guest
129
+ useEffect(() => {
130
+ if (customer?.isGuest && deliveryItems?.length > 0 && !hasInitialized.current) {
131
+ const existingShipments =
132
+ basket?.shipments?.filter(
133
+ (shipment) =>
134
+ shipment.shippingAddress && !isPickupMethod(shipment?.shippingMethod)
135
+ ) || []
136
+
137
+ if (existingShipments.length > 0) {
138
+ const newGuestAddresses = []
139
+ const newSelectedAddresses = {}
140
+
141
+ deliveryItems.forEach((item) => {
142
+ const addressKey = item.itemId
143
+ const shipment = existingShipments.find((s) => s.shipmentId === item.shipmentId)
144
+
145
+ if (shipment && !isAddressEmpty(shipment.shippingAddress)) {
146
+ const existingAddress = guestAddresses.find((addr) =>
147
+ areAddressesEqual(addr, shipment.shippingAddress)
148
+ )
149
+
150
+ if (existingAddress) {
151
+ newSelectedAddresses[addressKey] = existingAddress.addressId
152
+ } else {
153
+ const addressId = `guest_${item.itemId}`
154
+ const address = {
155
+ addressId,
156
+ firstName: shipment.shippingAddress.firstName,
157
+ lastName: shipment.shippingAddress.lastName,
158
+ address1: shipment.shippingAddress.address1,
159
+ city: shipment.shippingAddress.city,
160
+ stateCode: shipment.shippingAddress.stateCode,
161
+ postalCode: shipment.shippingAddress.postalCode,
162
+ countryCode: shipment.shippingAddress.countryCode,
163
+ phone: shipment.shippingAddress.phone,
164
+ isGuestAddress: true,
165
+ originalShipmentId: shipment.shipmentId
166
+ }
167
+
168
+ newGuestAddresses.push(address)
169
+ newSelectedAddresses[addressKey] = addressId
170
+ }
171
+ }
172
+ })
173
+
174
+ // update state if we have new addresses/selections
175
+ if (newGuestAddresses.length > 0) {
176
+ setGuestAddresses((prev) => {
177
+ const allAddresses = [...prev, ...newGuestAddresses]
178
+ const uniqueAddresses = []
179
+
180
+ allAddresses.forEach((addr) => {
181
+ const isDuplicate = uniqueAddresses.some((existingAddr) =>
182
+ areAddressesEqual(addr, existingAddr)
183
+ )
184
+
185
+ if (!isDuplicate) {
186
+ uniqueAddresses.push(addr)
187
+ }
188
+ })
189
+
190
+ return uniqueAddresses
191
+ })
192
+ }
193
+ if (Object.keys(newSelectedAddresses).length > 0) {
194
+ setSelectedGuestAddresses((prev) => ({...prev, ...newSelectedAddresses}))
195
+ }
196
+ hasInitialized.current = true
197
+ }
198
+ }
199
+ }, [customer?.isGuest, basket?.productItems?.length, basket?.shipments?.length])
200
+
201
+ const selectedAddresses = customer?.isGuest
202
+ ? selectedGuestAddresses
203
+ : selectedRegisteredUserAddresses
204
+
205
+ const setAddressesForItems = useCallback(
206
+ (itemIds, addressId) => {
207
+ const itemIdArray = Array.isArray(itemIds) ? itemIds : [itemIds]
208
+
209
+ if (customer?.isGuest) {
210
+ setSelectedGuestAddresses((prev) => {
211
+ const newState = {...prev}
212
+ if (addressId === '') {
213
+ // Remove selections for specified items
214
+ itemIdArray.forEach((itemId) => {
215
+ delete newState[itemId]
216
+ })
217
+ } else {
218
+ // Set selections for specified items
219
+ itemIdArray.forEach((itemId) => {
220
+ newState[itemId] = addressId
221
+ })
222
+ }
223
+ return newState
224
+ })
225
+ } else {
226
+ setSelectedRegisteredUserAddresses((prev) => {
227
+ const newState = {...prev}
228
+ if (addressId === '') {
229
+ // Remove selections for specified items
230
+ itemIdArray.forEach((itemId) => {
231
+ delete newState[itemId]
232
+ })
233
+ } else {
234
+ // Set selections for specified items
235
+ itemIdArray.forEach((itemId) => {
236
+ newState[itemId] = addressId
237
+ })
238
+ }
239
+ return newState
240
+ })
241
+ }
242
+ },
243
+ [customer?.isGuest]
244
+ )
245
+
246
+ const addGuestAddress = useCallback((address) => {
247
+ const newAddress = {
248
+ ...address,
249
+ addressId: `guest_${nanoid()}`,
250
+ isGuestAddress: true
251
+ }
252
+ setGuestAddresses((prev) => [...prev, newAddress])
253
+ return newAddress
254
+ }, [])
255
+
256
+ const itemAddressMap = useMemo(() => {
257
+ const map = {}
258
+ deliveryItems.forEach((item) => {
259
+ const addressId = selectedAddresses[item.itemId]
260
+ const address = availableAddresses.find((addr) => addr.addressId === addressId)
261
+ if (address) {
262
+ map[item.itemId] = address
263
+ }
264
+ })
265
+ return map
266
+ }, [deliveryItems, selectedAddresses, availableAddresses])
267
+
268
+ const allItemsHaveAddresses = useMemo(() => {
269
+ return deliveryItems.every((item) => itemAddressMap[item.itemId])
270
+ }, [deliveryItems, itemAddressMap])
271
+
272
+ return {
273
+ availableAddresses: availableAddresses || [],
274
+ selectedAddresses: selectedAddresses || {},
275
+ addGuestAddress,
276
+ setAddressesForItems,
277
+ deliveryItems: deliveryItems || [],
278
+ allItemsHaveAddresses
279
+ }
280
+ }
@@ -0,0 +1,414 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+
8
+ import {renderHook, act} from '@testing-library/react'
9
+ import {useProductAddressAssignment} from '@salesforce/retail-react-app/app/hooks/use-product-address-assignment'
10
+
11
+ // Mock dependencies
12
+ jest.mock('@salesforce/retail-react-app/app/hooks/use-current-customer')
13
+
14
+ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
15
+
16
+ const mockUseCurrentCustomer = useCurrentCustomer
17
+
18
+ const mockBasket = {
19
+ basketId: 'basket-1',
20
+ productItems: [
21
+ {itemId: 'item-1', productId: 'product-1', shipmentId: 'shipment-1'},
22
+ {itemId: 'item-2', productId: 'product-2', shipmentId: 'shipment-2'}
23
+ ],
24
+ shipments: [
25
+ {
26
+ shipmentId: 'shipment-1',
27
+ shippingMethod: {id: 'delivery-method-1'},
28
+ shippingAddress: {
29
+ firstName: 'John',
30
+ lastName: 'Doe',
31
+ address1: '123 Test St',
32
+ city: 'Test City',
33
+ stateCode: 'CA',
34
+ postalCode: '12345',
35
+ countryCode: 'US',
36
+ phone: '1234567890'
37
+ }
38
+ },
39
+ {
40
+ shipmentId: 'shipment-2',
41
+ shippingMethod: {id: 'delivery-method-2'},
42
+ shippingAddress: {
43
+ firstName: 'Jane',
44
+ lastName: 'Smith',
45
+ address1: '456 Another St',
46
+ city: 'Another City',
47
+ stateCode: 'NY',
48
+ postalCode: '67890',
49
+ countryCode: 'US',
50
+ phone: '0987654321'
51
+ }
52
+ }
53
+ ]
54
+ }
55
+
56
+ const mockCustomer = {
57
+ customerId: 'customer-1',
58
+ isGuest: false,
59
+ isRegistered: true,
60
+ addresses: [
61
+ {
62
+ addressId: 'addr-1',
63
+ firstName: 'John',
64
+ lastName: 'Doe',
65
+ address1: '123 Test St',
66
+ city: 'Test City',
67
+ stateCode: 'CA',
68
+ postalCode: '12345',
69
+ countryCode: 'US',
70
+ phone: '1234567890',
71
+ preferred: true
72
+ },
73
+ {
74
+ addressId: 'addr-2',
75
+ firstName: 'Jane',
76
+ lastName: 'Smith',
77
+ address1: '456 Another St',
78
+ city: 'Another City',
79
+ stateCode: 'NY',
80
+ postalCode: '67890',
81
+ countryCode: 'US',
82
+ phone: '0987654321',
83
+ preferred: false
84
+ }
85
+ ]
86
+ }
87
+
88
+ const mockGuestCustomer = {
89
+ customerId: 'guest-1',
90
+ isGuest: true,
91
+ addresses: []
92
+ }
93
+
94
+ describe('useProductAddressAssignment', () => {
95
+ beforeEach(() => {
96
+ jest.clearAllMocks()
97
+
98
+ mockUseCurrentCustomer.mockReturnValue({
99
+ data: mockCustomer,
100
+ isLoading: false
101
+ })
102
+ })
103
+
104
+ describe('deliveryItems filtering', () => {
105
+ test('should filter out pickup shipments', () => {
106
+ const pickupBasket = {
107
+ ...mockBasket,
108
+ shipments: mockBasket.shipments.map((s) => ({
109
+ ...s,
110
+ shippingMethod: {...s.shippingMethod, c_storePickupEnabled: true}
111
+ }))
112
+ }
113
+
114
+ const {result} = renderHook(() => useProductAddressAssignment(pickupBasket))
115
+
116
+ expect(result.current.deliveryItems).toHaveLength(0)
117
+ })
118
+
119
+ test('should include delivery shipments', () => {
120
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
121
+
122
+ expect(result.current.deliveryItems).toHaveLength(2)
123
+ expect(result.current.deliveryItems[0].itemId).toBe('item-1')
124
+ expect(result.current.deliveryItems[1].itemId).toBe('item-2')
125
+ })
126
+
127
+ test('should handle empty basket', () => {
128
+ const emptyBasket = {...mockBasket, productItems: []}
129
+ const {result} = renderHook(() => useProductAddressAssignment(emptyBasket))
130
+
131
+ expect(result.current.deliveryItems).toHaveLength(0)
132
+ })
133
+ })
134
+
135
+ describe('availableAddresses', () => {
136
+ test('should return customer addresses for registered users', () => {
137
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
138
+
139
+ expect(result.current.availableAddresses).toHaveLength(2)
140
+ expect(result.current.availableAddresses[0].addressId).toBe('addr-1')
141
+ expect(result.current.availableAddresses[1].addressId).toBe('addr-2')
142
+ })
143
+
144
+ test('should return guest addresses for guest users from existing shipments', () => {
145
+ mockUseCurrentCustomer.mockReturnValue({
146
+ data: mockGuestCustomer,
147
+ isLoading: false
148
+ })
149
+
150
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
151
+
152
+ expect(result.current.availableAddresses).toHaveLength(2)
153
+ expect(result.current.availableAddresses[0].isGuestAddress).toBe(true)
154
+ })
155
+ })
156
+
157
+ describe('registered user address initialization', () => {
158
+ test('should initialize addresses from existing shipments', () => {
159
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
160
+
161
+ expect(result.current.selectedAddresses['item-1']).toBe('addr-1')
162
+ expect(result.current.selectedAddresses['item-2']).toBe('addr-2')
163
+ })
164
+
165
+ test('should fall back to preferred address when no shipment match', () => {
166
+ const basketWithoutShipments = {
167
+ ...mockBasket,
168
+ shipments: []
169
+ }
170
+
171
+ const {result} = renderHook(() => useProductAddressAssignment(basketWithoutShipments))
172
+
173
+ expect(result.current.selectedAddresses['item-1']).toBe('addr-1')
174
+ expect(result.current.selectedAddresses['item-2']).toBe('addr-1')
175
+ })
176
+
177
+ test('should use first address when no preferred address exists', () => {
178
+ const customerWithoutPreferred = {
179
+ ...mockCustomer,
180
+ addresses: mockCustomer.addresses.map((addr) => ({...addr, preferred: false}))
181
+ }
182
+
183
+ mockUseCurrentCustomer.mockReturnValue({
184
+ data: customerWithoutPreferred,
185
+ isLoading: false
186
+ })
187
+
188
+ const basketWithoutShipments = {
189
+ ...mockBasket,
190
+ shipments: []
191
+ }
192
+
193
+ const {result} = renderHook(() => useProductAddressAssignment(basketWithoutShipments))
194
+
195
+ expect(result.current.selectedAddresses['item-1']).toBe('addr-1')
196
+ expect(result.current.selectedAddresses['item-2']).toBe('addr-1')
197
+ })
198
+ })
199
+
200
+ describe('guest user address initialization', () => {
201
+ test('should initialize guest addresses from existing shipments', () => {
202
+ mockUseCurrentCustomer.mockReturnValue({
203
+ data: mockGuestCustomer,
204
+ isLoading: false
205
+ })
206
+
207
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
208
+
209
+ expect(result.current.availableAddresses).toHaveLength(2)
210
+ expect(result.current.selectedAddresses['item-1']).toMatch(/^guest_/)
211
+ expect(result.current.selectedAddresses['item-2']).toMatch(/^guest_/)
212
+ })
213
+
214
+ test('should prevent duplicate guest addresses', () => {
215
+ mockUseCurrentCustomer.mockReturnValue({
216
+ data: mockGuestCustomer,
217
+ isLoading: false
218
+ })
219
+
220
+ const basketWithDuplicateAddresses = {
221
+ ...mockBasket,
222
+ shipments: [
223
+ mockBasket.shipments[0],
224
+ {
225
+ ...mockBasket.shipments[1],
226
+ shippingAddress: mockBasket.shipments[0].shippingAddress
227
+ }
228
+ ]
229
+ }
230
+
231
+ const {result} = renderHook(() =>
232
+ useProductAddressAssignment(basketWithDuplicateAddresses)
233
+ )
234
+
235
+ expect(result.current.availableAddresses).toHaveLength(1)
236
+ })
237
+
238
+ test('should only initialize once', () => {
239
+ mockUseCurrentCustomer.mockReturnValue({
240
+ data: mockGuestCustomer,
241
+ isLoading: false
242
+ })
243
+
244
+ const {result, rerender} = renderHook(() => useProductAddressAssignment(mockBasket))
245
+
246
+ const initialAddressCount = result.current.availableAddresses.length
247
+
248
+ rerender()
249
+
250
+ expect(result.current.availableAddresses).toHaveLength(initialAddressCount)
251
+ })
252
+ })
253
+
254
+ describe('addGuestAddress', () => {
255
+ test('should add new guest address with unique ID', () => {
256
+ mockUseCurrentCustomer.mockReturnValue({
257
+ data: mockGuestCustomer,
258
+ isLoading: false
259
+ })
260
+
261
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
262
+
263
+ const newAddress = {
264
+ firstName: 'New',
265
+ lastName: 'User',
266
+ address1: '789 New St',
267
+ city: 'New City',
268
+ stateCode: 'TX',
269
+ postalCode: '55555',
270
+ countryCode: 'US',
271
+ phone: '5555555555'
272
+ }
273
+
274
+ act(() => {
275
+ const addedAddress = result.current.addGuestAddress(newAddress)
276
+ expect(addedAddress.addressId).toMatch(/^guest_/)
277
+ expect(addedAddress.isGuestAddress).toBe(true)
278
+ })
279
+
280
+ expect(result.current.availableAddresses).toHaveLength(3)
281
+ const addedAddress = result.current.availableAddresses.find(
282
+ (addr) => addr.firstName === 'New'
283
+ )
284
+ expect(addedAddress).toBeDefined()
285
+ expect(addedAddress.firstName).toBe('New')
286
+ })
287
+ })
288
+
289
+ describe('setAddressesForItems', () => {
290
+ test('should set addresses for registered users', () => {
291
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
292
+
293
+ act(() => {
294
+ result.current.setAddressesForItems(['item-1'], 'addr-2')
295
+ })
296
+
297
+ expect(result.current.selectedAddresses['item-1']).toBe('addr-2')
298
+ })
299
+
300
+ test('should set addresses for guest users', () => {
301
+ mockUseCurrentCustomer.mockReturnValue({
302
+ data: mockGuestCustomer,
303
+ isLoading: false
304
+ })
305
+
306
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
307
+
308
+ act(() => {
309
+ result.current.setAddressesForItems(['item-1'], 'guest_new')
310
+ })
311
+
312
+ expect(result.current.selectedAddresses['item-1']).toBe('guest_new')
313
+ })
314
+
315
+ test('should handle multiple items', () => {
316
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
317
+
318
+ act(() => {
319
+ result.current.setAddressesForItems(['item-1', 'item-2'], 'addr-2')
320
+ })
321
+
322
+ expect(result.current.selectedAddresses['item-1']).toBe('addr-2')
323
+ expect(result.current.selectedAddresses['item-2']).toBe('addr-2')
324
+ })
325
+
326
+ test('should clear addresses when empty string provided', () => {
327
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
328
+
329
+ act(() => {
330
+ result.current.setAddressesForItems(['item-1'], '')
331
+ })
332
+
333
+ expect(result.current.selectedAddresses['item-1']).toBeUndefined()
334
+ })
335
+ })
336
+
337
+ describe('address selection state', () => {
338
+ test('should have selected addresses for items', () => {
339
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
340
+
341
+ expect(result.current.selectedAddresses['item-1']).toBeDefined()
342
+ expect(result.current.selectedAddresses['item-1']).toBe('addr-1')
343
+ })
344
+
345
+ test('should handle items without addresses', () => {
346
+ const basketWithoutAddresses = {
347
+ ...mockBasket,
348
+ shipments: []
349
+ }
350
+
351
+ const {result} = renderHook(() => useProductAddressAssignment(basketWithoutAddresses))
352
+
353
+ expect(result.current.selectedAddresses['item-1']).toBeDefined()
354
+ })
355
+ })
356
+
357
+ describe('allItemsHaveAddresses', () => {
358
+ test('should return true when all items have addresses', () => {
359
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
360
+
361
+ expect(result.current.allItemsHaveAddresses).toBe(true)
362
+ })
363
+
364
+ test('should return true when items have addresses from customer data', () => {
365
+ const basketWithoutAddresses = {
366
+ ...mockBasket,
367
+ shipments: []
368
+ }
369
+
370
+ const {result} = renderHook(() => useProductAddressAssignment(basketWithoutAddresses))
371
+
372
+ expect(result.current.allItemsHaveAddresses).toBe(true)
373
+ })
374
+ })
375
+
376
+ describe('edge cases', () => {
377
+ test('should handle null basket', () => {
378
+ const {result} = renderHook(() => useProductAddressAssignment(null))
379
+
380
+ expect(result.current.deliveryItems).toHaveLength(0)
381
+ expect(result.current.availableAddresses).toHaveLength(2)
382
+ expect(result.current.selectedAddresses).toEqual({})
383
+ expect(result.current.allItemsHaveAddresses).toBe(true)
384
+ })
385
+
386
+ test('should handle undefined customer', () => {
387
+ mockUseCurrentCustomer.mockReturnValue({
388
+ data: undefined,
389
+ isLoading: false
390
+ })
391
+
392
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
393
+
394
+ expect(result.current.availableAddresses).toHaveLength(0)
395
+ })
396
+
397
+ test('should handle empty addresses array', () => {
398
+ const customerWithoutAddresses = {
399
+ ...mockCustomer,
400
+ addresses: []
401
+ }
402
+
403
+ mockUseCurrentCustomer.mockReturnValue({
404
+ data: customerWithoutAddresses,
405
+ isLoading: false
406
+ })
407
+
408
+ const {result} = renderHook(() => useProductAddressAssignment(mockBasket))
409
+
410
+ expect(result.current.availableAddresses).toHaveLength(0)
411
+ expect(result.current.allItemsHaveAddresses).toBe(false)
412
+ })
413
+ })
414
+ })