@classytic/primitives 0.26.1 → 0.26.3

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,40 @@
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.3
7
+
8
+ ### Added — `PaymentProviderPort.wantsRawBody`
9
+
10
+ Optional `readonly wantsRawBody?: boolean` on the payment gateway port.
11
+ Providers that verify an HMAC signature (Stripe signs the bytes it transmitted)
12
+ must declare `true` — the transport then hands the raw body rather than the
13
+ parsed object. Absent / `false` preserves the historical behaviour (parsed
14
+ body), which is correct for manual, cash and terminal providers that sign
15
+ nothing.
16
+
17
+ ## 0.26.2
18
+
19
+ ### Added — `stockKeyFor` (`/item-identity`): one stock-key precedence for every package
20
+
21
+ The product-level stock key, with a single precedence:
22
+
23
+ 1. the variant's stamped `skuRef` — assigned once at birth, immutable
24
+ 2. a **sole** variant with no stamp → the product's `_id`, never the label
25
+ (legacy writers derived `variantSku || productId`, and for one variant that
26
+ key IS the `_id`)
27
+ 3. a **sibling** with no stamp → its label, which genuinely is its legacy key
28
+ 4. nothing addressable → the product's `_id`
29
+
30
+ The caller declares only what it HAS — `{ kind: 'sole' | 'label' | 'variant' }`
31
+ — so the sole-vs-sibling decision is internal and a caller cannot make it wrong.
32
+ That choice previously lived at every call site across three packages, in two
33
+ functions with different fallbacks; they disagreed on a sole, unstamped,
34
+ labelled variant, and the read then looked up a key no quant is stored under.
35
+
36
+ It lives here, below every domain, because `@spinekit/order` and
37
+ `@spinekit/catalog` must not import a peer domain — the rule sitting one layer
38
+ too high is exactly why each package re-derived it.
39
+
6
40
  ## 0.26.1
7
41
 
8
42
  - `./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 };
@@ -277,6 +277,25 @@ interface PaymentProviderPort {
277
277
  * without this cannot be auto-reconciled — the engine then RETAINS the reservation.
278
278
  */
279
279
  getRefundStatus?(query: RefundStatusQuery, command?: PaymentCommandContext): Promise<PaymentResult>;
280
+ /**
281
+ * Does this provider need the EXACT transmitted bytes, rather than the
282
+ * parsed body?
283
+ *
284
+ * `true` for anything that verifies an HMAC signature — Stripe signs the
285
+ * bytes it sent, and a re-serialized object differs in key order, spacing and
286
+ * unicode escaping, so a check against the parsed form can never pass.
287
+ *
288
+ * Declared by the provider rather than decided by the transport, because the
289
+ * transport cannot know. Handing raw bytes to every provider breaks the ones
290
+ * that read `payload.sessionId` — they get a string and silently see
291
+ * `undefined`, so the webhook resolves to no transaction and the payment
292
+ * never applies. Handing the parsed body to every provider breaks signature
293
+ * verification. Only the provider knows which it is.
294
+ *
295
+ * Absent means `false`: the historical behaviour, and correct for manual,
296
+ * cash and terminal providers that sign nothing.
297
+ */
298
+ readonly wantsRawBody?: boolean;
280
299
  handleWebhook(payload: unknown, headers?: Record<string, string>): Promise<WebhookEvent>;
281
300
  /**
282
301
  * Reject a forged webhook. Returning `true` unconditionally is only acceptable for a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/primitives",
3
- "version": "0.26.1",
3
+ "version": "0.26.3",
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",