@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.
- package/CHANGELOG.md +8 -2
- package/app/components/_app/index.jsx +9 -7
- package/app/components/_app/index.test.js +2 -2
- package/app/components/_app-config/index.jsx +9 -3
- package/app/components/drawer-menu/drawer-menu.jsx +3 -1
- package/app/components/footer/index.jsx +3 -1
- package/app/components/header/index.jsx +3 -1
- package/app/components/header/index.test.js +2 -2
- package/app/components/island/README.md +1 -1
- package/app/components/island/index.jsx +3 -1
- package/app/components/island/index.test.js +94 -5
- package/app/components/item-variant/item-attributes.jsx +12 -3
- package/app/components/multiship/multiship-order-summary.jsx +137 -0
- package/app/components/multiship/multiship-order-summary.test.js +121 -0
- package/app/components/order-summary/index.jsx +2 -4
- package/app/components/pickup-or-delivery/index.jsx +80 -0
- package/app/components/pickup-or-delivery/index.test.jsx +182 -0
- package/app/components/product-item/index.jsx +26 -16
- package/app/components/product-item/index.test.js +29 -2
- package/app/components/product-item-list/index.jsx +10 -0
- package/app/components/product-item-list/index.test.jsx +14 -0
- package/app/components/product-view/index.jsx +9 -6
- package/app/components/product-view/index.test.js +25 -21
- package/app/components/quantity-picker/index.test.jsx +12 -12
- package/app/components/reset-password/index.test.js +1 -1
- package/app/components/shared/ui/AlertDescription/index.jsx +8 -0
- package/app/components/shared/ui/index.jsx +1 -0
- package/app/components/store-display/index.jsx +28 -4
- package/app/components/store-display/index.test.js +71 -0
- package/app/components/store-locator/form.test.jsx +16 -4
- package/app/components/store-locator/list.jsx +9 -4
- package/app/components/toggle-card/index.jsx +14 -0
- package/app/components/unavailable-product-confirmation-modal/index.jsx +19 -5
- package/app/components/unavailable-product-confirmation-modal/index.test.js +122 -1
- package/app/constants.js +20 -6
- package/app/contexts/store-locator-provider.jsx +7 -1
- package/app/contexts/store-locator-provider.test.jsx +36 -1
- package/app/hooks/use-address-form.js +155 -0
- package/app/hooks/use-address-form.test.js +501 -0
- package/app/hooks/use-auth-modal.js +2 -6
- package/app/hooks/use-current-basket.js +71 -2
- package/app/hooks/use-current-basket.test.js +37 -1
- package/app/hooks/use-dnt-notification.js +4 -4
- package/app/hooks/use-dnt-notification.test.js +5 -5
- package/app/hooks/use-item-shipment-management.js +233 -0
- package/app/hooks/use-item-shipment-management.test.js +696 -0
- package/app/hooks/use-multiship.js +589 -0
- package/app/hooks/use-multiship.test.js +776 -0
- package/app/hooks/use-pickup-shipment.js +70 -106
- package/app/hooks/use-pickup-shipment.test.js +345 -209
- package/app/hooks/use-product-address-assignment.js +280 -0
- package/app/hooks/use-product-address-assignment.test.js +414 -0
- package/app/hooks/use-product-inventory.js +100 -0
- package/app/hooks/use-product-inventory.test.js +254 -0
- package/app/hooks/use-shipment-operations.js +168 -0
- package/app/hooks/use-shipment-operations.test.js +385 -0
- package/app/hooks/use-store-locator.js +24 -2
- package/app/hooks/use-store-locator.test.jsx +109 -1
- package/app/pages/account/index.test.js +1 -1
- package/app/pages/account/profile.test.js +0 -2
- package/app/pages/cart/index.jsx +397 -157
- package/app/pages/cart/index.test.js +353 -2
- package/app/pages/cart/partials/bonus-products-title.jsx +10 -8
- package/app/pages/cart/partials/cart-secondary-button-group.test.js +1 -1
- package/app/pages/cart/partials/order-type-display.jsx +68 -0
- package/app/pages/cart/partials/order-type-display.test.js +241 -0
- package/app/pages/checkout/confirmation.jsx +79 -158
- package/app/pages/checkout/index.jsx +34 -9
- package/app/pages/checkout/index.test.js +245 -118
- package/app/pages/checkout/partials/contact-info.jsx +2 -6
- package/app/pages/checkout/partials/contact-info.test.js +93 -7
- package/app/pages/checkout/partials/payment.jsx +19 -5
- package/app/pages/checkout/partials/pickup-address.jsx +340 -70
- package/app/pages/checkout/partials/pickup-address.test.js +1075 -82
- package/app/pages/checkout/partials/product-shipping-address-card.jsx +382 -0
- package/app/pages/checkout/partials/shipment-details.jsx +209 -0
- package/app/pages/checkout/partials/shipment-details.test.js +246 -0
- package/app/pages/checkout/partials/shipping-address.jsx +156 -68
- package/app/pages/checkout/partials/shipping-address.test.js +673 -0
- package/app/pages/checkout/partials/shipping-method-options.jsx +180 -0
- package/app/pages/checkout/partials/shipping-methods.jsx +403 -0
- package/app/pages/checkout/partials/shipping-methods.test.js +472 -0
- package/app/pages/checkout/partials/shipping-multi-address.jsx +259 -0
- package/app/pages/checkout/partials/shipping-multi-address.test.js +2088 -0
- package/app/pages/checkout/partials/shipping-product-cards.jsx +101 -0
- package/app/pages/checkout/util/checkout-context.js +25 -18
- package/app/pages/login/index.jsx +2 -6
- package/app/pages/product-detail/index.jsx +96 -81
- package/app/pages/product-detail/index.test.js +103 -19
- package/app/pages/product-list/index.jsx +3 -1
- package/app/pages/product-list/partials/inventory-filter.jsx +18 -21
- package/app/pages/product-list/partials/inventory-filter.test.js +15 -17
- package/app/pages/product-list/partials/selected-refinements.jsx +3 -1
- package/app/ssr.js +1 -1
- package/app/static/translations/compiled/en-GB.json +316 -30
- package/app/static/translations/compiled/en-US.json +316 -30
- package/app/static/translations/compiled/en-XA.json +673 -75
- package/app/utils/address-utils.js +112 -0
- package/app/utils/address-utils.test.js +484 -0
- package/app/utils/product-utils.js +17 -5
- package/app/utils/product-utils.test.js +17 -8
- package/app/utils/sfdc-user-agent-utils.js +32 -0
- package/app/utils/sfdc-user-agent-utils.test.js +82 -0
- package/app/utils/shipment-utils.js +196 -0
- package/app/utils/shipment-utils.test.js +458 -0
- package/app/utils/test-utils.js +4 -4
- package/app/utils/utils.js +6 -1
- package/config/default.js +4 -1
- package/config/mocks/default.js +3 -1
- package/package.json +9 -9
- package/translations/en-GB.json +127 -10
- package/translations/en-US.json +127 -10
- 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
|
+
})
|