@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.7.3-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,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
- status = await getProductStatus(
129
- validSkus,
130
- locale,
131
- homeUrl,
132
- awsBase,
133
- clientId
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
- if ((varStatus && varStatus.status.retail) || (keepBundleOnly && isBundleOnly)) {
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
- const convertKitToRequiredProducts = async (skuKit, locale) => {
260
- let productBundle = {}
261
- const variantSkus = []
262
- for (let kitItem of skuKit[0].variants) {
263
- const skuData = helpers.getBestSkuData((kitItem.variant || [])[0])
264
- variantSkus.push((skuData || {}).sku)
265
- kitItem.sku = (skuData || {}).sku
266
- }
267
- const products = await getProductsBySku(variantSkus, locale, true)
268
- const requiredProducts = []
269
- for (let i = 0; i < products?.length; i++) {
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 = JSON.parse(JSON.stringify(products[i]))
272
- for (let v = 0; v < productSelect?.variants?.length; v++) {
273
- if (productSelect.variants[v].sku !== variantSku) {
274
- productSelect.variants.splice(v, 1)
275
- v--
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
- const discounts = skuKit[0].discounts || {}
287
- const dangerousGoods = requiredProducts.some(({ product }) => {
288
- const [requiredProduct] = product || []
289
- const [variant] = requiredProduct?.variants || []
290
- return variant?.dangerousGoods === true
291
- })
292
- productBundle.required_products = requiredProducts
293
- productBundle.charge_shipping = skuKit[0].charge_shipping
294
- productBundle.cvdis = discounts.cvDiscount || skuKit[0].cvdis
295
- productBundle.dangerous_goods = dangerousGoods
296
- productBundle.foreign_limit = skuKit[0].foreign_limit
297
- productBundle.grpdis = discounts.grpDiscount || skuKit[0].grpdis
298
- productBundle.is_exclusive = skuKit[0].is_exclusive
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 = await getProductStatus(getAllBundleIds(products), helpers?.reorderLocale(locale), homeUrl, awsBase, clientId)
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