@nuskin/cs-product-lib 1.8.0-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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/csProduct.js +80 -44
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuskin/cs-product-lib",
3
- "version": "1.8.0-bundleonly-members.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,9 +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').
45
+ // CS-native sellable gate for the bypassProductStatus path -- mirrors
46
+ // content-validator ValidationSteps (csStatus === 'sellable' || 'bundleOnly').
47
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
+
48
73
  const VARIANTS = 'variants'
49
74
  const VARIANT = 'variant'
50
75
  const KEY_INGREDIENTS = 'ingredients.key_ingredients'
@@ -286,52 +311,63 @@ const csProduct = (props) => {
286
311
  })
287
312
  }
288
313
 
289
- const convertKitToRequiredProducts = async (skuKit, locale, bypassProductStatus = false) => {
290
- let productBundle = {}
291
- const variantSkus = []
292
- for (let kitItem of skuKit[0].variants) {
293
- const skuData = helpers.getBestSkuData((kitItem.variant || [])[0])
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 || []) {
329
+ for (const variantSku of variantSkus) {
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 })
337
+ }
338
+ }
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])
294
348
  variantSkus.push((skuData || {}).sku)
295
349
  kitItem.sku = (skuData || {}).sku
296
350
  }
297
351
  const products = await getProductsBySku(variantSkus, locale, true, bypassProductStatus)
298
- const requiredProducts = []
299
- const seenSkuQuantities = new Set()
300
- for (let i = 0; i < products?.length; i++) {
301
- for (const variantSku of variantSkus) {
302
- const productSelect = JSON.parse(JSON.stringify(products[i]))
303
- for (let v = 0; v < productSelect?.variants?.length; v++) {
304
- if (productSelect.variants[v].sku !== variantSku) {
305
- productSelect.variants.splice(v, 1)
306
- v--
307
- }
308
- }
309
- if (!isNullOrEmpty(productSelect?.variants)) {
310
- const quantity = skuKit[0].variants.find(v=>v.sku === variantSku).quantity
311
- const dedupKey = `${variantSku}:${quantity}`
312
- if (!seenSkuQuantities.has(dedupKey)) {
313
- seenSkuQuantities.add(dedupKey)
314
- requiredProducts.push({
315
- product: [productSelect],
316
- quantity
317
- })
318
- }
319
- }
320
- }
321
- }
322
- const discounts = skuKit[0].discounts || {}
323
- const dangerousGoods = requiredProducts.some(({ product }) => {
324
- const [requiredProduct] = product || []
325
- const [variant] = requiredProduct?.variants || []
326
- return variant?.dangerousGoods === true
327
- })
328
- productBundle.required_products = requiredProducts
329
- productBundle.charge_shipping = skuKit[0].charge_shipping
330
- productBundle.cvdis = discounts.cvDiscount || skuKit[0].cvdis
331
- productBundle.dangerous_goods = dangerousGoods
332
- productBundle.foreign_limit = skuKit[0].foreign_limit
333
- productBundle.grpdis = discounts.grpDiscount || skuKit[0].grpdis
334
- productBundle.is_exclusive = skuKit[0].is_exclusive
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
335
371
  productBundle.bundle_id = skuKit[0].legacy_sku || skuKit[0].kit_id
336
372
  productBundle.locale = skuKit[0].locale
337
373
  productBundle.point_discount = skuKit[0].point_discount