@classytic/primitives 0.26.1 → 0.26.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/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
3
3
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## 0.26.2
7
+
8
+ ### Added — `stockKeyFor` (`/item-identity`): one stock-key precedence for every package
9
+
10
+ The product-level stock key, with a single precedence:
11
+
12
+ 1. the variant's stamped `skuRef` — assigned once at birth, immutable
13
+ 2. a **sole** variant with no stamp → the product's `_id`, never the label
14
+ (legacy writers derived `variantSku || productId`, and for one variant that
15
+ key IS the `_id`)
16
+ 3. a **sibling** with no stamp → its label, which genuinely is its legacy key
17
+ 4. nothing addressable → the product's `_id`
18
+
19
+ The caller declares only what it HAS — `{ kind: 'sole' | 'label' | 'variant' }`
20
+ — so the sole-vs-sibling decision is internal and a caller cannot make it wrong.
21
+ That choice previously lived at every call site across three packages, in two
22
+ functions with different fallbacks; they disagreed on a sole, unstamped,
23
+ labelled variant, and the read then looked up a key no quant is stored under.
24
+
25
+ It lives here, below every domain, because `@spinekit/order` and
26
+ `@spinekit/catalog` must not import a peer domain — the rule sitting one layer
27
+ too high is exactly why each package re-derived it.
28
+
6
29
  ## 0.26.1
7
30
 
8
31
  - `./testing/stock-key-seam` — new subpath; `scanStockKeySeam`, `assertStockKeySeam`, `takesStockKeyFromLabel`, `STOCK_KEY_SEAM_FIXTURES` for conformance-scanning packages that resolve stock keys
@@ -31,6 +31,50 @@ interface SkuFields {
31
31
  declare function stockKeyOf(item: SkuFields, fallback?: string | {
32
32
  toString(): string;
33
33
  }): StockKey | undefined;
34
+ /** The minimal product shape {@link stockKeyFor} reads. */
35
+ interface StockKeySource {
36
+ _id: string | {
37
+ toString(): string;
38
+ };
39
+ variants?: ReadonlyArray<SkuFields> | null;
40
+ }
41
+ /**
42
+ * WHAT THE CALLER HAS — never what the key should be.
43
+ *
44
+ * - `sole` nothing selected (a simple product, or a whole-product read)
45
+ * - `label` an operator-typed / scanned SKU label
46
+ * - `variant` one variant record in hand, from iterating `variants`
47
+ */
48
+ type StockKeySelector = {
49
+ readonly kind: "sole";
50
+ } | {
51
+ readonly kind: "label";
52
+ readonly sku: string;
53
+ } | {
54
+ readonly kind: "variant";
55
+ readonly variant: SkuFields;
56
+ };
57
+ /**
58
+ * THE product-level stock key. One precedence, for every package.
59
+ *
60
+ * 1. the variant's STAMPED `skuRef` — assigned once at birth, immutable
61
+ * 2. a SOLE variant with no stamp -> the product's `_id`. Never the label:
62
+ * legacy writers derived `variantSku || productId`, and for one variant
63
+ * that key IS the `_id`
64
+ * 3. a SIBLING with no stamp -> its label, which genuinely IS its legacy key
65
+ * 4. nothing addressable -> the product's `_id`
66
+ *
67
+ * The caller declares only what it HAS; the sole-vs-sibling decision is
68
+ * internal, so a caller cannot make it wrong. That choice previously lived at
69
+ * every call site across three packages, in two functions with different
70
+ * fallbacks — they disagreed on a sole, unstamped, labelled variant, and the
71
+ * read then looked up a key no quant is stored under.
72
+ *
73
+ * Lives HERE, below every domain, because `@spinekit/order` and
74
+ * `@spinekit/catalog` must not import a peer domain — the rule being one layer
75
+ * too high is exactly why each re-derived it.
76
+ */
77
+ declare function stockKeyFor(product: StockKeySource, selector: StockKeySelector): StockKey;
34
78
  /**
35
79
  * {@link stockKeyOf} that THROWS. Use wherever the key feeds a stock movement:
36
80
  * a reservation on `undefined` returns "out of stock" instead of failing.
@@ -58,4 +102,4 @@ declare function skuDisplay(item: SkuFields): SkuDisplay;
58
102
  */
59
103
  declare function looksLikeOpaqueId(value: string | undefined): boolean;
60
104
  //#endregion
61
- export { SkuDisplay, SkuFields, StockKey, looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyOf };
105
+ export { SkuDisplay, SkuFields, StockKey, StockKeySelector, StockKeySource, looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyFor, stockKeyOf };
@@ -14,6 +14,49 @@ function stockKeyOf(item, fallback) {
14
14
  return nonEmpty(item.skuRef) ?? nonEmpty(item.sku) ?? (fallback !== void 0 ? String(fallback) : void 0);
15
15
  }
16
16
  /**
17
+ * THE product-level stock key. One precedence, for every package.
18
+ *
19
+ * 1. the variant's STAMPED `skuRef` — assigned once at birth, immutable
20
+ * 2. a SOLE variant with no stamp -> the product's `_id`. Never the label:
21
+ * legacy writers derived `variantSku || productId`, and for one variant
22
+ * that key IS the `_id`
23
+ * 3. a SIBLING with no stamp -> its label, which genuinely IS its legacy key
24
+ * 4. nothing addressable -> the product's `_id`
25
+ *
26
+ * The caller declares only what it HAS; the sole-vs-sibling decision is
27
+ * internal, so a caller cannot make it wrong. That choice previously lived at
28
+ * every call site across three packages, in two functions with different
29
+ * fallbacks — they disagreed on a sole, unstamped, labelled variant, and the
30
+ * read then looked up a key no quant is stored under.
31
+ *
32
+ * Lives HERE, below every domain, because `@spinekit/order` and
33
+ * `@spinekit/catalog` must not import a peer domain — the rule being one layer
34
+ * too high is exactly why each re-derived it.
35
+ */
36
+ function stockKeyFor(product, selector) {
37
+ const productId = String(product._id);
38
+ const variants = product.variants ?? [];
39
+ const sole = variants.length <= 1;
40
+ switch (selector.kind) {
41
+ case "sole":
42
+ if (!sole) return productId;
43
+ return nonEmpty(variants[0]?.skuRef) ?? productId;
44
+ case "label": {
45
+ const matched = variants.find((v) => v.sku === selector.sku);
46
+ const stamp = nonEmpty(matched?.skuRef);
47
+ if (stamp !== void 0) return stamp;
48
+ if (sole) return productId;
49
+ return selector.sku;
50
+ }
51
+ case "variant": {
52
+ const stamp = nonEmpty(selector.variant.skuRef);
53
+ if (stamp !== void 0) return stamp;
54
+ if (sole) return productId;
55
+ return stockKeyOf(selector.variant, productId);
56
+ }
57
+ }
58
+ }
59
+ /**
17
60
  * {@link stockKeyOf} that THROWS. Use wherever the key feeds a stock movement:
18
61
  * a reservation on `undefined` returns "out of stock" instead of failing.
19
62
  */
@@ -47,4 +90,4 @@ function looksLikeOpaqueId(value) {
47
90
  return typeof value === "string" && /^[0-9a-f]{24}$/i.test(value);
48
91
  }
49
92
  //#endregion
50
- export { looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyOf };
93
+ export { looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyFor, stockKeyOf };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/primitives",
3
- "version": "0.26.1",
3
+ "version": "0.26.2",
4
4
  "description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
5
5
  "author": "Classytic",
6
6
  "license": "MIT",