@nuskin/cs-product-lib 1.7.3-bundleonly-members.1 → 1.8.0-bundleonly-members.2
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/package.json +1 -1
- package/src/csProduct.js +132 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuskin/cs-product-lib",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0-bundleonly-members.2",
|
|
4
4
|
"description": "Library for pulling and parsing product content (and applying business logic) retrieved from Contentstack",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
package/src/csProduct.js
CHANGED
|
@@ -42,6 +42,34 @@ const validateConfig = (props) => {
|
|
|
42
42
|
/* istanbul ignore next */
|
|
43
43
|
const csProduct = (props) => {
|
|
44
44
|
const DEV_ENVIRONMENT = 'public-dev'
|
|
45
|
+
// CS-native sellable gate for the bypassProductStatus path -- mirrors
|
|
46
|
+
// content-validator ValidationSteps (csStatus === 'sellable' || 'bundleOnly').
|
|
47
|
+
const CS_SELLABLE_STATUSES = ['sellable', 'bundleOnly']
|
|
48
|
+
|
|
49
|
+
// Kit-member dedup tiebreak (order-independent). A kit member SKU can match
|
|
50
|
+
// more than one product entry (e.g. a market-localized product AND an older
|
|
51
|
+
// en-only promo product that still carries the same variant). Prefer the
|
|
52
|
+
// entry whose own locale matches the requested market locale; if that does
|
|
53
|
+
// not disambiguate, prefer the newer publish, then a stable uid -- so the
|
|
54
|
+
// kept entry never depends on query/array order.
|
|
55
|
+
const kitMemberLocaleRank = (member, requestedLocale) => {
|
|
56
|
+
const memberLocale = member?.csAtts?.locale
|
|
57
|
+
return memberLocale && requestedLocale &&
|
|
58
|
+
memberLocale.toLowerCase() === requestedLocale.toLowerCase()
|
|
59
|
+
? 1
|
|
60
|
+
: 0
|
|
61
|
+
}
|
|
62
|
+
const isPreferredKitMember = (candidate, current, requestedLocale) => {
|
|
63
|
+
if (!current) return true
|
|
64
|
+
const candRank = kitMemberLocaleRank(candidate, requestedLocale)
|
|
65
|
+
const currRank = kitMemberLocaleRank(current, requestedLocale)
|
|
66
|
+
if (candRank !== currRank) return candRank > currRank
|
|
67
|
+
const candTime = Date.parse(candidate?.csAtts?.updatedAt) || 0
|
|
68
|
+
const currTime = Date.parse(current?.csAtts?.updatedAt) || 0
|
|
69
|
+
if (candTime !== currTime) return candTime > currTime
|
|
70
|
+
return `${candidate?.id}` < `${current?.id}`
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
const VARIANTS = 'variants'
|
|
46
74
|
const VARIANT = 'variant'
|
|
47
75
|
const KEY_INGREDIENTS = 'ingredients.key_ingredients'
|
|
@@ -108,7 +136,7 @@ const csProduct = (props) => {
|
|
|
108
136
|
}
|
|
109
137
|
}
|
|
110
138
|
|
|
111
|
-
const removeInvalidVariants = async (products = [], locale, translations, keepBundleOnly = false) => {
|
|
139
|
+
const removeInvalidVariants = async (products = [], locale, translations, keepBundleOnly = false, bypassProductStatus = false) => {
|
|
112
140
|
const validSkus = []
|
|
113
141
|
for (const product of products) {
|
|
114
142
|
for (const variant of product?.variants || []) {
|
|
@@ -125,13 +153,35 @@ const csProduct = (props) => {
|
|
|
125
153
|
let status = {}
|
|
126
154
|
let marketData = {}
|
|
127
155
|
if (validSkus?.length > 0) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
156
|
+
if (bypassProductStatus) {
|
|
157
|
+
// Equinox markets: derive status from the CS entry instead of
|
|
158
|
+
// calling product-status. Live availability/quantity is
|
|
159
|
+
// intentionally not available on this path. Temporary until
|
|
160
|
+
// cs-product-graphql is retired.
|
|
161
|
+
status = {}
|
|
162
|
+
for (const product of products) {
|
|
163
|
+
for (const variant of product?.variants || []) {
|
|
164
|
+
const best = helpers?.getBestSkuData(variant)
|
|
165
|
+
if (best?.sku) {
|
|
166
|
+
const csStatus = helpers.mapCsStatus(variant)
|
|
167
|
+
status[best.sku] = {
|
|
168
|
+
status: { retail: csStatus.retail, wholesale: csStatus.wholesale },
|
|
169
|
+
csStatus: csStatus.status,
|
|
170
|
+
isBackordered: false,
|
|
171
|
+
backorderedAvailableDate: null
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
status = await getProductStatus(
|
|
178
|
+
validSkus,
|
|
179
|
+
locale,
|
|
180
|
+
homeUrl,
|
|
181
|
+
awsBase,
|
|
182
|
+
clientId
|
|
183
|
+
)
|
|
184
|
+
}
|
|
135
185
|
const localeSplit = locale.split('_')
|
|
136
186
|
let market =
|
|
137
187
|
localeSplit?.length > 1 ? localeSplit[1].toUpperCase() : 'XX'
|
|
@@ -156,7 +206,12 @@ const csProduct = (props) => {
|
|
|
156
206
|
const varStatus = status[bestSkuData.sku]
|
|
157
207
|
const isBundleOnly =
|
|
158
208
|
bestSkuData?.status_selections?.status === 'bundleOnly'
|
|
159
|
-
|
|
209
|
+
const passesBypassGate =
|
|
210
|
+
bypassProductStatus &&
|
|
211
|
+
CS_SELLABLE_STATUSES.includes(varStatus?.csStatus)
|
|
212
|
+
const passesStatusGate =
|
|
213
|
+
!bypassProductStatus && varStatus && varStatus.status.retail
|
|
214
|
+
if (passesStatusGate || passesBypassGate || (keepBundleOnly && isBundleOnly)) {
|
|
160
215
|
variant.sap_data.sku_data = [bestSkuData]
|
|
161
216
|
validVariants.push({
|
|
162
217
|
...variant,
|
|
@@ -256,46 +311,63 @@ const csProduct = (props) => {
|
|
|
256
311
|
})
|
|
257
312
|
}
|
|
258
313
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
314
|
+
// Clone a product and keep only the variant matching the given sku.
|
|
315
|
+
const selectVariantProduct = (product, variantSku) => {
|
|
316
|
+
const productSelect = JSON.parse(JSON.stringify(product))
|
|
317
|
+
productSelect.variants = (productSelect?.variants || []).filter(
|
|
318
|
+
(variant) => variant.sku === variantSku
|
|
319
|
+
)
|
|
320
|
+
return productSelect
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Build the kit's required-product list, deduped by sku+quantity. A member
|
|
324
|
+
// sku can match more than one product entry; keep the preferred one
|
|
325
|
+
// (locale-native over fallback) via isPreferredKitMember, order-independent.
|
|
326
|
+
const dedupeKitMembers = (products, variantSkus, kitVariants, requestedLocale) => {
|
|
327
|
+
const bestBySkuQuantity = new Map()
|
|
328
|
+
for (const product of products || []) {
|
|
270
329
|
for (const variantSku of variantSkus) {
|
|
271
|
-
const productSelect =
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (!isNullOrEmpty(productSelect?.variants)) {
|
|
279
|
-
requiredProducts.push({
|
|
280
|
-
product: [productSelect],
|
|
281
|
-
quantity: skuKit[0].variants.find(v=>v.sku === variantSku).quantity
|
|
282
|
-
})
|
|
330
|
+
const productSelect = selectVariantProduct(product, variantSku)
|
|
331
|
+
if (isNullOrEmpty(productSelect?.variants)) continue
|
|
332
|
+
const quantity = kitVariants.find((v) => v.sku === variantSku)?.quantity
|
|
333
|
+
const dedupKey = `${variantSku}:${quantity}`
|
|
334
|
+
const current = bestBySkuQuantity.get(dedupKey)
|
|
335
|
+
if (isPreferredKitMember(productSelect, current?.product?.[0], requestedLocale)) {
|
|
336
|
+
bestBySkuQuantity.set(dedupKey, { product: [productSelect], quantity })
|
|
283
337
|
}
|
|
284
338
|
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
339
|
+
}
|
|
340
|
+
return [...bestBySkuQuantity.values()]
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const convertKitToRequiredProducts = async (skuKit, locale, bypassProductStatus = false) => {
|
|
344
|
+
let productBundle = {}
|
|
345
|
+
const variantSkus = []
|
|
346
|
+
for (let kitItem of skuKit[0].variants) {
|
|
347
|
+
const skuData = helpers.getBestSkuData((kitItem.variant || [])[0])
|
|
348
|
+
variantSkus.push((skuData || {}).sku)
|
|
349
|
+
kitItem.sku = (skuData || {}).sku
|
|
350
|
+
}
|
|
351
|
+
const products = await getProductsBySku(variantSkus, locale, true, bypassProductStatus)
|
|
352
|
+
const requiredProducts = dedupeKitMembers(
|
|
353
|
+
products,
|
|
354
|
+
variantSkus,
|
|
355
|
+
skuKit[0].variants,
|
|
356
|
+
badLocaleMap(locale)
|
|
357
|
+
)
|
|
358
|
+
const discounts = skuKit[0].discounts || {}
|
|
359
|
+
const dangerousGoods = requiredProducts.some(({ product }) => {
|
|
360
|
+
const [requiredProduct] = product || []
|
|
361
|
+
const [variant] = requiredProduct?.variants || []
|
|
362
|
+
return variant?.dangerousGoods === true
|
|
363
|
+
})
|
|
364
|
+
productBundle.required_products = requiredProducts
|
|
365
|
+
productBundle.charge_shipping = skuKit[0].charge_shipping
|
|
366
|
+
productBundle.cvdis = discounts.cvDiscount || skuKit[0].cvdis
|
|
367
|
+
productBundle.dangerous_goods = dangerousGoods
|
|
368
|
+
productBundle.foreign_limit = skuKit[0].foreign_limit
|
|
369
|
+
productBundle.grpdis = discounts.grpDiscount || skuKit[0].grpdis
|
|
370
|
+
productBundle.is_exclusive = skuKit[0].is_exclusive
|
|
299
371
|
productBundle.bundle_id = skuKit[0].legacy_sku || skuKit[0].kit_id
|
|
300
372
|
productBundle.locale = skuKit[0].locale
|
|
301
373
|
productBundle.point_discount = skuKit[0].point_discount
|
|
@@ -316,14 +388,15 @@ const csProduct = (props) => {
|
|
|
316
388
|
return [productBundle]
|
|
317
389
|
}
|
|
318
390
|
|
|
319
|
-
const formatKit = async (products, locale, translations) => {
|
|
391
|
+
const formatKit = async (products, locale, translations, bypassProductStatus = false) => {
|
|
320
392
|
for (let p = 0; p < products.length; p++) {
|
|
321
393
|
let noFormat = false
|
|
322
394
|
if (products[p]?.sku_kit?.length > 0) {
|
|
323
395
|
products[p].type = 'kit'
|
|
324
396
|
products[p].product_bundle = await convertKitToRequiredProducts(
|
|
325
397
|
products[p].sku_kit,
|
|
326
|
-
locale
|
|
398
|
+
locale,
|
|
399
|
+
bypassProductStatus
|
|
327
400
|
)
|
|
328
401
|
noFormat = true
|
|
329
402
|
}
|
|
@@ -439,8 +512,8 @@ const csProduct = (props) => {
|
|
|
439
512
|
* @deprecated use getProductsByUID instead
|
|
440
513
|
* @returns
|
|
441
514
|
*/
|
|
442
|
-
const getBundle = async (id, locale) => {
|
|
443
|
-
const bundles = await getBundles(id, locale)
|
|
515
|
+
const getBundle = async (id, locale, bypassProductStatus = false) => {
|
|
516
|
+
const bundles = await getBundles(id, locale, bypassProductStatus)
|
|
444
517
|
return bundles && bundles[0] ? bundles[0] : null
|
|
445
518
|
}
|
|
446
519
|
|
|
@@ -464,7 +537,7 @@ const csProduct = (props) => {
|
|
|
464
537
|
* @deprecated use getProductsByUID instead
|
|
465
538
|
* @returns
|
|
466
539
|
*/
|
|
467
|
-
const getBundles = async (ids, locale) => {
|
|
540
|
+
const getBundles = async (ids, locale, bypassProductStatus = false) => {
|
|
468
541
|
locale = badLocaleMap(locale)
|
|
469
542
|
if (!Array.isArray(ids)) {
|
|
470
543
|
ids = [ids]
|
|
@@ -480,9 +553,11 @@ const csProduct = (props) => {
|
|
|
480
553
|
const products = await queryProducts(query, locale)
|
|
481
554
|
if (Array.isArray(products) && products.length > 0) {
|
|
482
555
|
const translations = await getTranslations(locale)
|
|
483
|
-
await formatKit(products, locale, translations)
|
|
484
|
-
await removeInvalidVariants(products, locale, translations)
|
|
485
|
-
const status =
|
|
556
|
+
await formatKit(products, locale, translations, bypassProductStatus)
|
|
557
|
+
await removeInvalidVariants(products, locale, translations, false, bypassProductStatus)
|
|
558
|
+
const status = bypassProductStatus
|
|
559
|
+
? {}
|
|
560
|
+
: await getProductStatus(getAllBundleIds(products), helpers?.reorderLocale(locale), homeUrl, awsBase, clientId)
|
|
486
561
|
return formatProductWithSchema(products, status)
|
|
487
562
|
}
|
|
488
563
|
const errorResponse = products.response || {}
|
|
@@ -490,7 +565,7 @@ const csProduct = (props) => {
|
|
|
490
565
|
return [{error: {message: products.message, name: products.name, status: errorResponse.status, errors: [`${responseData.error_code}: ${responseData.error_message}`]}}]
|
|
491
566
|
}
|
|
492
567
|
|
|
493
|
-
const getProductsBySku = async (skus, locale, keepBundleOnly = false) => {
|
|
568
|
+
const getProductsBySku = async (skus, locale, keepBundleOnly = false, bypassProductStatus = false) => {
|
|
494
569
|
locale = badLocaleMap(locale)
|
|
495
570
|
if (!Array.isArray(skus)) {
|
|
496
571
|
skus = [skus]
|
|
@@ -516,8 +591,8 @@ const csProduct = (props) => {
|
|
|
516
591
|
return bundleProducts
|
|
517
592
|
}
|
|
518
593
|
const translations = await getTranslations(locale)
|
|
519
|
-
await formatKit(products, locale, translations)
|
|
520
|
-
await removeInvalidVariants(products, locale, translations, keepBundleOnly)
|
|
594
|
+
await formatKit(products, locale, translations, bypassProductStatus)
|
|
595
|
+
await removeInvalidVariants(products, locale, translations, keepBundleOnly, bypassProductStatus)
|
|
521
596
|
const formattedProducts = formatProductWithSchema(products)
|
|
522
597
|
for (const product of formattedProducts) {
|
|
523
598
|
product.skus = skus
|