@pradip1995/commerce-core 1.0.0
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/README.md +15 -0
- package/package.json +70 -0
- package/src/analytics/ga4-ecommerce.ts +96 -0
- package/src/config.ts +36 -0
- package/src/constants.tsx +84 -0
- package/src/context/modal-context.tsx +40 -0
- package/src/context/wishlist-context.tsx +96 -0
- package/src/data/cart/abandoned.ts +111 -0
- package/src/data/cart/buyNow.ts +184 -0
- package/src/data/cart/checkout.ts +487 -0
- package/src/data/cart/index.ts +7 -0
- package/src/data/cart/mutations.ts +189 -0
- package/src/data/cart/promotions.ts +121 -0
- package/src/data/cart/region.ts +66 -0
- package/src/data/cart/retrieve.ts +162 -0
- package/src/data/categories.ts +90 -0
- package/src/data/collections.ts +109 -0
- package/src/data/contact.ts +143 -0
- package/src/data/cookies.ts +170 -0
- package/src/data/customer-registration.ts +365 -0
- package/src/data/customer.ts +638 -0
- package/src/data/dynamic-config.ts +420 -0
- package/src/data/fulfillment.ts +95 -0
- package/src/data/guest.ts +357 -0
- package/src/data/locale-actions.ts +74 -0
- package/src/data/locales.ts +28 -0
- package/src/data/newsletter.ts +41 -0
- package/src/data/notifications.ts +22 -0
- package/src/data/onboarding.ts +9 -0
- package/src/data/orders.ts +500 -0
- package/src/data/payment-details.ts +68 -0
- package/src/data/payment.ts +32 -0
- package/src/data/products.ts +424 -0
- package/src/data/regions.ts +64 -0
- package/src/data/returns.ts +305 -0
- package/src/data/reviews.ts +279 -0
- package/src/data/swaps.ts +154 -0
- package/src/data/variants.ts +38 -0
- package/src/data/wishlist.ts +292 -0
- package/src/domain/cart/abandoned-carts.ts +49 -0
- package/src/domain/cart/buy-now.ts +15 -0
- package/src/domain/cart/checkout.ts +25 -0
- package/src/domain/cart/index.ts +8 -0
- package/src/domain/cart/metadata.ts +21 -0
- package/src/domain/cart/payment.ts +21 -0
- package/src/domain/cart/phone.ts +17 -0
- package/src/domain/cart/reorder.ts +19 -0
- package/src/domain/cart/validation.ts +43 -0
- package/src/domain/product/pricing.ts +49 -0
- package/src/domain/product/variant-selection.ts +193 -0
- package/src/firebase.ts +48 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/use-add-to-cart.ts +63 -0
- package/src/hooks/use-cart.ts +132 -0
- package/src/hooks/use-checkout.ts +62 -0
- package/src/hooks/use-in-view.tsx +29 -0
- package/src/hooks/use-product-actions.ts +190 -0
- package/src/hooks/use-product-reviews.ts +18 -0
- package/src/hooks/use-product-variant.ts +142 -0
- package/src/hooks/use-server-action.ts +30 -0
- package/src/hooks/use-toggle-state.tsx +46 -0
- package/src/hooks/use-wishlist.ts +3 -0
- package/src/theme/inline-vars.ts +12 -0
- package/src/types/account.ts +21 -0
- package/src/types/cart.ts +13 -0
- package/src/types/home.ts +52 -0
- package/src/types/layout.ts +29 -0
- package/src/types/product-card.ts +17 -0
- package/src/util/compare-addresses.ts +28 -0
- package/src/util/env.ts +3 -0
- package/src/util/get-locale-header.ts +8 -0
- package/src/util/get-percentage-diff.ts +6 -0
- package/src/util/get-product-price.ts +78 -0
- package/src/util/google-oauth.ts +28 -0
- package/src/util/isEmpty.ts +11 -0
- package/src/util/medusa-error.ts +18 -0
- package/src/util/money.ts +26 -0
- package/src/util/order-status.tsx +179 -0
- package/src/util/product.ts +431 -0
- package/src/util/repeat.ts +5 -0
- package/src/util/returns.ts +71 -0
- package/src/util/sort-products.ts +48 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { HttpTypes } from "@medusajs/types"
|
|
2
|
+
|
|
3
|
+
export const isSimpleProduct = (product: HttpTypes.StoreProduct): boolean => {
|
|
4
|
+
return product.options?.length === 1 && product.options[0].values?.length === 1
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function variantOptionsAsKeymap(
|
|
8
|
+
variantOptions: HttpTypes.StoreProductVariant["options"]
|
|
9
|
+
): Record<string, string> {
|
|
10
|
+
return (
|
|
11
|
+
variantOptions?.reduce((acc: Record<string, string>, option) => {
|
|
12
|
+
if (option.option_id) {
|
|
13
|
+
acc[option.option_id] = option.value
|
|
14
|
+
}
|
|
15
|
+
return acc
|
|
16
|
+
}, {}) ?? {}
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolves which variant should drive the product gallery.
|
|
22
|
+
* Uses the exact variant when all options match; otherwise the first variant
|
|
23
|
+
* that matches whatever options the shopper has already picked (e.g. color only).
|
|
24
|
+
*/
|
|
25
|
+
export function resolveDisplayVariant(
|
|
26
|
+
product: HttpTypes.StoreProduct,
|
|
27
|
+
options: Record<string, string | undefined>
|
|
28
|
+
): HttpTypes.StoreProductVariant | undefined {
|
|
29
|
+
const exactMatch = findExactVariant(product, options)
|
|
30
|
+
|
|
31
|
+
if (exactMatch) {
|
|
32
|
+
return exactMatch
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const matchingVariants = resolveMatchingVariants(product, options)
|
|
36
|
+
|
|
37
|
+
return matchingVariants[0]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findExactVariant(
|
|
41
|
+
product: HttpTypes.StoreProduct,
|
|
42
|
+
options: Record<string, string | undefined>
|
|
43
|
+
): HttpTypes.StoreProductVariant | undefined {
|
|
44
|
+
if (!product.variants?.length) {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return product.variants.find((variant) => {
|
|
49
|
+
const variantOptions = variantOptionsAsKeymap(variant.options)
|
|
50
|
+
|
|
51
|
+
return Object.entries(variantOptions).every(
|
|
52
|
+
([optionId, value]) => options[optionId] === value
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** All variants matching the currently selected options (supports partial selection). */
|
|
58
|
+
export function resolveMatchingVariants(
|
|
59
|
+
product: HttpTypes.StoreProduct,
|
|
60
|
+
options: Record<string, string | undefined>
|
|
61
|
+
): HttpTypes.StoreProductVariant[] {
|
|
62
|
+
const selectedEntries = Object.entries(options).filter(
|
|
63
|
+
(entry): entry is [string, string] => Boolean(entry[1])
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if (!selectedEntries.length || !product.variants?.length) {
|
|
67
|
+
return []
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return product.variants.filter((variant) => {
|
|
71
|
+
const variantOptions = variantOptionsAsKeymap(variant.options)
|
|
72
|
+
|
|
73
|
+
return selectedEntries.every(
|
|
74
|
+
([optionId, value]) => variantOptions[optionId] === value
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function dedupeImages(
|
|
80
|
+
images: HttpTypes.StoreProductImage[]
|
|
81
|
+
): HttpTypes.StoreProductImage[] {
|
|
82
|
+
const seen: ImageMatchKeys = {
|
|
83
|
+
urlKeys: new Set<string>(),
|
|
84
|
+
idKeys: new Set<string>(),
|
|
85
|
+
}
|
|
86
|
+
const deduped: HttpTypes.StoreProductImage[] = []
|
|
87
|
+
|
|
88
|
+
images.forEach((image) => {
|
|
89
|
+
if (!image.url || imageMatchesKeys(image, seen)) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
deduped.push(image)
|
|
94
|
+
addImageToMatchKeys(image, seen)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return deduped
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type ImageMatchKeys = {
|
|
101
|
+
urlKeys: Set<string>
|
|
102
|
+
idKeys: Set<string>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function addImageToMatchKeys(
|
|
106
|
+
image: Pick<HttpTypes.StoreProductImage, "url" | "id"> | undefined | null,
|
|
107
|
+
keys: ImageMatchKeys
|
|
108
|
+
) {
|
|
109
|
+
if (!image?.url) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const cleanUrl = image.url.split("?")[0].toLowerCase().trim()
|
|
114
|
+
const urlPath = cleanUrl.split("/").pop() || ""
|
|
115
|
+
|
|
116
|
+
keys.urlKeys.add(cleanUrl)
|
|
117
|
+
if (urlPath) {
|
|
118
|
+
keys.urlKeys.add(urlPath.toLowerCase())
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (image.id) {
|
|
122
|
+
keys.idKeys.add(image.id)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function imageMatchesKeys(
|
|
127
|
+
image: HttpTypes.StoreProductImage,
|
|
128
|
+
keys: ImageMatchKeys
|
|
129
|
+
): boolean {
|
|
130
|
+
if (!image.url) {
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const cleanUrl = image.url.split("?")[0].toLowerCase().trim()
|
|
135
|
+
const urlPath = cleanUrl.split("/").pop() || ""
|
|
136
|
+
|
|
137
|
+
const urlMatches =
|
|
138
|
+
keys.urlKeys.has(cleanUrl) ||
|
|
139
|
+
(urlPath ? keys.urlKeys.has(urlPath.toLowerCase()) : false)
|
|
140
|
+
const idMatches = image.id ? keys.idKeys.has(image.id) : false
|
|
141
|
+
|
|
142
|
+
return urlMatches || idMatches
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getVariantAssignedImageKeys(
|
|
146
|
+
product: HttpTypes.StoreProduct,
|
|
147
|
+
excludeVariantId?: string
|
|
148
|
+
): ImageMatchKeys {
|
|
149
|
+
const keys: ImageMatchKeys = {
|
|
150
|
+
urlKeys: new Set<string>(),
|
|
151
|
+
idKeys: new Set<string>(),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
product.variants?.forEach((variant) => {
|
|
155
|
+
if (excludeVariantId && variant.id === excludeVariantId) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
variant.images?.forEach((image) => {
|
|
160
|
+
addImageToMatchKeys(image as HttpTypes.StoreProductImage, keys)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
return keys
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getUnsupportedProductImages(
|
|
168
|
+
product: HttpTypes.StoreProduct,
|
|
169
|
+
selectedVariantId: string
|
|
170
|
+
): HttpTypes.StoreProductImage[] {
|
|
171
|
+
const productImages = product.images || []
|
|
172
|
+
if (productImages.length === 0) {
|
|
173
|
+
return []
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const assignedKeys = getVariantAssignedImageKeys(product, selectedVariantId)
|
|
177
|
+
|
|
178
|
+
return productImages.filter((image) => !imageMatchesKeys(image, assignedKeys))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getKeysForImages(images: HttpTypes.StoreProductImage[]): ImageMatchKeys {
|
|
182
|
+
const keys: ImageMatchKeys = {
|
|
183
|
+
urlKeys: new Set<string>(),
|
|
184
|
+
idKeys: new Set<string>(),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
images.forEach((image) => addImageToMatchKeys(image, keys))
|
|
188
|
+
|
|
189
|
+
return keys
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function mergeVariantAndGeneralImages(
|
|
193
|
+
variantImages: HttpTypes.StoreProductImage[],
|
|
194
|
+
generalImages: HttpTypes.StoreProductImage[]
|
|
195
|
+
): HttpTypes.StoreProductImage[] {
|
|
196
|
+
const variantKeys = getKeysForImages(variantImages)
|
|
197
|
+
const merged = [...variantImages]
|
|
198
|
+
|
|
199
|
+
generalImages.forEach((image) => {
|
|
200
|
+
if (!imageMatchesKeys(image, variantKeys)) {
|
|
201
|
+
merged.push(image)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
return merged
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Images assigned to this variant but not to any other variant.
|
|
210
|
+
* Medusa often includes shared product images on every variant; the admin-selected
|
|
211
|
+
* variant image is typically the extra one appended last for that variant.
|
|
212
|
+
*/
|
|
213
|
+
function getVariantExclusiveImages(
|
|
214
|
+
product: HttpTypes.StoreProduct,
|
|
215
|
+
variant: HttpTypes.StoreProductVariant
|
|
216
|
+
): HttpTypes.StoreProductImage[] {
|
|
217
|
+
const variantImages = (variant.images || []) as HttpTypes.StoreProductImage[]
|
|
218
|
+
|
|
219
|
+
if (!variantImages.length) {
|
|
220
|
+
return []
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const otherVariantKeys = getVariantAssignedImageKeys(product, variant.id)
|
|
224
|
+
|
|
225
|
+
return variantImages.filter((image) => !imageMatchesKeys(image, otherVariantKeys))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getPrimaryVariantImage(
|
|
229
|
+
product: HttpTypes.StoreProduct,
|
|
230
|
+
variant: HttpTypes.StoreProductVariant
|
|
231
|
+
): HttpTypes.StoreProductImage | undefined {
|
|
232
|
+
const variantImages = (variant.images || []) as HttpTypes.StoreProductImage[]
|
|
233
|
+
|
|
234
|
+
if (!variantImages.length) {
|
|
235
|
+
return undefined
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const exclusiveImages = getVariantExclusiveImages(product, variant)
|
|
239
|
+
|
|
240
|
+
if (exclusiveImages.length > 0) {
|
|
241
|
+
const exclusiveKeys = getKeysForImages(exclusiveImages)
|
|
242
|
+
let primaryIndex = -1
|
|
243
|
+
|
|
244
|
+
variantImages.forEach((image, index) => {
|
|
245
|
+
if (imageMatchesKeys(image, exclusiveKeys)) {
|
|
246
|
+
primaryIndex = index
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
if (primaryIndex >= 0) {
|
|
251
|
+
return variantImages[primaryIndex]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return exclusiveImages[exclusiveImages.length - 1]
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return variantImages[0]
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function reorderWithPrimaryFirst(
|
|
261
|
+
images: HttpTypes.StoreProductImage[],
|
|
262
|
+
primaryImage?: HttpTypes.StoreProductImage
|
|
263
|
+
): HttpTypes.StoreProductImage[] {
|
|
264
|
+
if (!primaryImage) {
|
|
265
|
+
return images
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const primaryKeys = getKeysForImages([primaryImage])
|
|
269
|
+
const primaryIndex = images.findIndex((image) => imageMatchesKeys(image, primaryKeys))
|
|
270
|
+
|
|
271
|
+
if (primaryIndex <= 0) {
|
|
272
|
+
return images
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const reordered = [...images]
|
|
276
|
+
const [primary] = reordered.splice(primaryIndex, 1)
|
|
277
|
+
|
|
278
|
+
return [primary, ...reordered]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Index of the variant's primary assigned image inside the gallery list.
|
|
283
|
+
* Falls back to 0 when the variant has no dedicated media.
|
|
284
|
+
*/
|
|
285
|
+
export function getDefaultImageIndexForVariant(
|
|
286
|
+
product: HttpTypes.StoreProduct,
|
|
287
|
+
selectedVariantId: string | undefined,
|
|
288
|
+
images: HttpTypes.StoreProductImage[]
|
|
289
|
+
): number {
|
|
290
|
+
if (!images.length) {
|
|
291
|
+
return 0
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!selectedVariantId || !product.variants) {
|
|
295
|
+
return 0
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const variant = product.variants.find((v) => v.id === selectedVariantId)
|
|
299
|
+
const primaryVariantImage = variant
|
|
300
|
+
? getPrimaryVariantImage(product, variant)
|
|
301
|
+
: undefined
|
|
302
|
+
|
|
303
|
+
if (!primaryVariantImage) {
|
|
304
|
+
return 0
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const primaryKeys = getKeysForImages([primaryVariantImage])
|
|
308
|
+
const index = images.findIndex((image) => imageMatchesKeys(image, primaryKeys))
|
|
309
|
+
|
|
310
|
+
return index >= 0 ? index : 0
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Consistently determines which images to show for a product or a specific variant.
|
|
315
|
+
* Used on both server and client to prevent flickering during hydration.
|
|
316
|
+
*/
|
|
317
|
+
export function getImagesForVariant(
|
|
318
|
+
product: HttpTypes.StoreProduct,
|
|
319
|
+
selectedVariantId?: string
|
|
320
|
+
): HttpTypes.StoreProductImage[] {
|
|
321
|
+
// 1. If no variant is selected, show general product images
|
|
322
|
+
if (!selectedVariantId || !product.variants) {
|
|
323
|
+
return product.images || []
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 2. Find the selected variant
|
|
327
|
+
const variant = product.variants.find((v) => v.id === selectedVariantId)
|
|
328
|
+
|
|
329
|
+
// 3. Variant has specific images — primary assigned image first, then the rest
|
|
330
|
+
if (variant?.images && variant.images.length > 0) {
|
|
331
|
+
const variantImages = [...variant.images] as HttpTypes.StoreProductImage[]
|
|
332
|
+
const generalImages = getUnsupportedProductImages(product, selectedVariantId)
|
|
333
|
+
const primaryImage = getPrimaryVariantImage(product, variant)
|
|
334
|
+
const merged = mergeVariantAndGeneralImages(variantImages, generalImages)
|
|
335
|
+
|
|
336
|
+
return reorderWithPrimaryFirst(merged, primaryImage)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 4. No variant images: show product images not assigned to other variants
|
|
340
|
+
const unsupportedImages = getUnsupportedProductImages(product, selectedVariantId)
|
|
341
|
+
if (unsupportedImages.length > 0) {
|
|
342
|
+
return unsupportedImages
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (product.thumbnail) {
|
|
346
|
+
return [
|
|
347
|
+
{
|
|
348
|
+
id: "default-thumb",
|
|
349
|
+
url: product.thumbnail,
|
|
350
|
+
} as HttpTypes.StoreProductImage,
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return []
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Gallery images for the current option selection.
|
|
359
|
+
* - No selection → all product images
|
|
360
|
+
* - Exact variant (color + size, etc.) → that variant's gallery
|
|
361
|
+
* - Partial selection (e.g. color only) → unique images from all matching variants
|
|
362
|
+
*/
|
|
363
|
+
export function getImagesForSelection(
|
|
364
|
+
product: HttpTypes.StoreProduct,
|
|
365
|
+
options: Record<string, string | undefined>
|
|
366
|
+
): HttpTypes.StoreProductImage[] {
|
|
367
|
+
const hasSelection = Object.values(options).some(Boolean)
|
|
368
|
+
|
|
369
|
+
if (!hasSelection) {
|
|
370
|
+
return product.images || []
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const exactVariant = findExactVariant(product, options)
|
|
374
|
+
|
|
375
|
+
if (exactVariant) {
|
|
376
|
+
return getImagesForVariant(product, exactVariant.id)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const matchingVariants = resolveMatchingVariants(product, options)
|
|
380
|
+
|
|
381
|
+
if (!matchingVariants.length) {
|
|
382
|
+
return product.images || []
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const aggregated: HttpTypes.StoreProductImage[] = []
|
|
386
|
+
const exclusivePrimaries: HttpTypes.StoreProductImage[] = []
|
|
387
|
+
|
|
388
|
+
matchingVariants.forEach((variant) => {
|
|
389
|
+
aggregated.push(...getImagesForVariant(product, variant.id))
|
|
390
|
+
|
|
391
|
+
const exclusiveImages = getVariantExclusiveImages(product, variant)
|
|
392
|
+
|
|
393
|
+
if (exclusiveImages.length > 0) {
|
|
394
|
+
const primary = getPrimaryVariantImage(product, variant)
|
|
395
|
+
|
|
396
|
+
if (primary) {
|
|
397
|
+
exclusivePrimaries.push(primary)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
let images = dedupeImages(aggregated)
|
|
403
|
+
const uniquePrimaries = dedupeImages(exclusivePrimaries)
|
|
404
|
+
|
|
405
|
+
uniquePrimaries.forEach((primary) => {
|
|
406
|
+
images = reorderWithPrimaryFirst(images, primary)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
return images
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Default main-image index for the current option selection.
|
|
414
|
+
*/
|
|
415
|
+
export function getDefaultImageIndexForSelection(
|
|
416
|
+
product: HttpTypes.StoreProduct,
|
|
417
|
+
options: Record<string, string | undefined>,
|
|
418
|
+
images: HttpTypes.StoreProductImage[]
|
|
419
|
+
): number {
|
|
420
|
+
if (!images.length) {
|
|
421
|
+
return 0
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const exactVariant = findExactVariant(product, options)
|
|
425
|
+
|
|
426
|
+
if (exactVariant) {
|
|
427
|
+
return getDefaultImageIndexForVariant(product, exactVariant.id, images)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return 0
|
|
431
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { HttpTypes } from "@medusajs/types"
|
|
2
|
+
|
|
3
|
+
export type ItemWithDeliveryStatus = HttpTypes.StoreOrderLineItem & {
|
|
4
|
+
returnable_quantity: number
|
|
5
|
+
delivered_quantity: number
|
|
6
|
+
return_requested_quantity: number
|
|
7
|
+
return_received_quantity: number
|
|
8
|
+
written_off_quantity: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const calculateReturnableQuantity = (
|
|
12
|
+
item: HttpTypes.StoreOrderLineItem
|
|
13
|
+
): number => {
|
|
14
|
+
// In a real implementation, these would come from the item.detail or similar
|
|
15
|
+
// For now, we'll assume we can calculate it from available data or default to quantity
|
|
16
|
+
// Adjust this based on your actual data structure for item details
|
|
17
|
+
|
|
18
|
+
// Note: The actual data structure depends on how Medusa returns order item details
|
|
19
|
+
// Typically it's in item.detail associated with fulfillments/returns
|
|
20
|
+
|
|
21
|
+
// For the purpose of this storefront implementation (assuming standard Medusa):
|
|
22
|
+
// We strictly follow the logic: delivered - requested - received - written_off
|
|
23
|
+
|
|
24
|
+
// Checking if properties exist on item (they might be on a 'detail' object or top level depending on version)
|
|
25
|
+
const anyItem = item as any
|
|
26
|
+
|
|
27
|
+
// Defaulting to item.quantity if details missing (safe fallback for initial dev)
|
|
28
|
+
// BUT per requirements, we must implement the logic.
|
|
29
|
+
|
|
30
|
+
// If the backend provides these fields on the item directly:
|
|
31
|
+
const delivered = anyItem.delivered_quantity ?? anyItem.quantity ?? 0 // Fallback to quantity if assumed delivered
|
|
32
|
+
const requested = anyItem.return_requested_quantity ?? 0
|
|
33
|
+
const received = anyItem.return_received_quantity ?? 0
|
|
34
|
+
const writtenOff = anyItem.written_off_quantity ?? 0
|
|
35
|
+
|
|
36
|
+
// If fulfillment_status is not fulfilled/shipped/partially_shipped, returnable might be 0
|
|
37
|
+
// But strict formula:
|
|
38
|
+
const returnable = Math.max(0, delivered - requested - received - writtenOff)
|
|
39
|
+
|
|
40
|
+
return returnable
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const isItemReturnable = (item: HttpTypes.StoreOrderLineItem): boolean => {
|
|
44
|
+
return calculateReturnableQuantity(item) > 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const hasReturnableItems = (order: HttpTypes.StoreOrder): boolean => {
|
|
48
|
+
if (!order || !order.items) return false
|
|
49
|
+
|
|
50
|
+
// Simple check: if order is canceled, no returns
|
|
51
|
+
if (order.status === "canceled") return false
|
|
52
|
+
|
|
53
|
+
return order.items.some((item) => isItemReturnable(item))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const enhanceItemsWithReturnStatus = (
|
|
57
|
+
items: HttpTypes.StoreOrderLineItem[]
|
|
58
|
+
): ItemWithDeliveryStatus[] => {
|
|
59
|
+
return items.map((item) => {
|
|
60
|
+
const anyItem = item as any
|
|
61
|
+
return {
|
|
62
|
+
...item,
|
|
63
|
+
// Ensure these properties exist
|
|
64
|
+
delivered_quantity: anyItem.delivered_quantity ?? item.quantity, // Optimistic default
|
|
65
|
+
return_requested_quantity: anyItem.return_requested_quantity ?? 0,
|
|
66
|
+
return_received_quantity: anyItem.return_received_quantity ?? 0,
|
|
67
|
+
written_off_quantity: anyItem.written_off_quantity ?? 0,
|
|
68
|
+
returnable_quantity: calculateReturnableQuantity(item),
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { HttpTypes } from "@medusajs/types"
|
|
2
|
+
import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
|
|
3
|
+
|
|
4
|
+
interface MinPricedProduct extends HttpTypes.StoreProduct {
|
|
5
|
+
_minPrice?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Helper function to sort products by price until the store API supports sorting by price
|
|
10
|
+
* @param products
|
|
11
|
+
* @param sortBy
|
|
12
|
+
* @returns products sorted by price
|
|
13
|
+
*/
|
|
14
|
+
export function sortProducts(
|
|
15
|
+
products: HttpTypes.StoreProduct[],
|
|
16
|
+
sortBy: SortOptions
|
|
17
|
+
): HttpTypes.StoreProduct[] {
|
|
18
|
+
let sortedProducts = products as MinPricedProduct[]
|
|
19
|
+
|
|
20
|
+
if (["price_asc", "price_desc"].includes(sortBy)) {
|
|
21
|
+
// Precompute the minimum price for each product
|
|
22
|
+
sortedProducts.forEach((product) => {
|
|
23
|
+
if (product.variants && product.variants.length > 0) {
|
|
24
|
+
product._minPrice = Math.min(
|
|
25
|
+
...product.variants.map(
|
|
26
|
+
(variant) => variant?.calculated_price?.calculated_amount || 0
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
} else {
|
|
30
|
+
product._minPrice = Infinity
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Sort products based on the precomputed minimum prices
|
|
35
|
+
sortedProducts.sort((a, b) => {
|
|
36
|
+
const diff = a._minPrice! - b._minPrice!
|
|
37
|
+
return sortBy === "price_asc" ? diff : -diff
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (sortBy === "created_at") {
|
|
42
|
+
sortedProducts.sort((a, b) => {
|
|
43
|
+
return new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return sortedProducts
|
|
48
|
+
}
|