@classytic/primitives 0.25.0 → 0.26.0
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 +17 -0
- package/dist/deadline.d.mts +16 -0
- package/dist/deadline.mjs +31 -0
- package/dist/item-facts.d.mts +73 -0
- package/dist/item-facts.mjs +53 -0
- package/dist/item-identity.d.mts +10 -20
- package/dist/monetization.d.mts +40 -1
- package/dist/monetization.mjs +46 -1
- package/dist/stock-level.d.mts +59 -0
- package/dist/stock-level.mjs +27 -0
- package/package.json +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
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.0] — 2026-08-27
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`./deadline` subpath** — `Deadline` value type + helpers for date-bounded
|
|
11
|
+
obligations (reservations, SLA windows, contract terms).
|
|
12
|
+
- **`./stock-level` subpath** — canonical stock-level representation and helpers.
|
|
13
|
+
- **`./item-facts` subpath** — cross-kernel item fact contract (weight, dimensions,
|
|
14
|
+
hazmat flags) that catalog declares and logistics/inventory reads.
|
|
15
|
+
- **`StockKey` branded type** (`./item-identity`) — prevents a bare merchandising
|
|
16
|
+
label or ObjectId being passed where a quant key is expected; the only source is
|
|
17
|
+
`stockKeyOf()` / `requireStockKey()`. Return types of both functions updated.
|
|
18
|
+
- **Upfront collection policy** (`./monetization`) — `UpfrontCollectionPolicy`
|
|
19
|
+
(`'full_payment' | 'deposit' | 'deferred'`) + `UPFRONT_COLLECTION_POLICIES`
|
|
20
|
+
const. Cross-kernel: catalog declares the policy, order turns it into tenders,
|
|
21
|
+
revenue settles them.
|
|
22
|
+
|
|
6
23
|
## [0.25.0] - 2026-08-24
|
|
7
24
|
|
|
8
25
|
### Added
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/composition/deadline.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Race a promise against a deadline without leaking the loser.
|
|
4
|
+
*
|
|
5
|
+
* A call that can hang — no throw, no response, the underlying layer just
|
|
6
|
+
* never settles — must not block its caller forever. Nothing here cancels
|
|
7
|
+
* the original promise; it keeps running after the deadline, so its eventual
|
|
8
|
+
* settlement is swallowed rather than surfaced a second time to a caller
|
|
9
|
+
* that already moved on.
|
|
10
|
+
*/
|
|
11
|
+
declare class DeadlineExceededError extends Error {
|
|
12
|
+
constructor(label: string, timeoutMs: number);
|
|
13
|
+
}
|
|
14
|
+
declare function withDeadline<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { DeadlineExceededError, withDeadline };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/composition/deadline.ts
|
|
2
|
+
/**
|
|
3
|
+
* Race a promise against a deadline without leaking the loser.
|
|
4
|
+
*
|
|
5
|
+
* A call that can hang — no throw, no response, the underlying layer just
|
|
6
|
+
* never settles — must not block its caller forever. Nothing here cancels
|
|
7
|
+
* the original promise; it keeps running after the deadline, so its eventual
|
|
8
|
+
* settlement is swallowed rather than surfaced a second time to a caller
|
|
9
|
+
* that already moved on.
|
|
10
|
+
*/
|
|
11
|
+
var DeadlineExceededError = class extends Error {
|
|
12
|
+
constructor(label, timeoutMs) {
|
|
13
|
+
super(`${label} timed out after ${timeoutMs}ms`);
|
|
14
|
+
this.name = "DeadlineExceededError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function withDeadline(promise, timeoutMs, label) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const timer = setTimeout(() => reject(new DeadlineExceededError(label, timeoutMs)), timeoutMs);
|
|
20
|
+
timer.unref?.();
|
|
21
|
+
promise.then((value) => {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
resolve(value);
|
|
24
|
+
}, (err) => {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
reject(err);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { DeadlineExceededError, withDeadline };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ShelfLifePolicy } from "./shelf-life.mjs";
|
|
2
|
+
//#region src/inventory/item-facts.d.ts
|
|
3
|
+
/** Coarse lot/serial tracking. `trackingPolicy.mode` wins when both are set. */
|
|
4
|
+
type TrackingMode = "none" | "lot" | "serial";
|
|
5
|
+
/** The stock facts an Item contributes. Persistence and classification are the owning package's. */
|
|
6
|
+
interface ItemStockSource {
|
|
7
|
+
skuRef: string;
|
|
8
|
+
sku: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
/** The canonical STOCK unit — what quants and moves are balanced in. */
|
|
11
|
+
uom: string;
|
|
12
|
+
trackingMode: TrackingMode;
|
|
13
|
+
trackingPolicy?: ShelfLifePolicy | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Variable-weight item (deli, meat, loose produce): a measured weight is
|
|
16
|
+
* captured per received lot while the piece count stays the stock dimension.
|
|
17
|
+
*/
|
|
18
|
+
catchWeight?: boolean | undefined;
|
|
19
|
+
weightUom?: string | undefined;
|
|
20
|
+
weight?: number | undefined;
|
|
21
|
+
volume?: number | undefined;
|
|
22
|
+
barcode?: string[] | undefined;
|
|
23
|
+
/** Retired items are not stockable. Independent of whether a Product sells. */
|
|
24
|
+
isActive: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** The merchandising side. Every stock fact is optional — the Item owns those. */
|
|
27
|
+
interface ProductFacet {
|
|
28
|
+
skuRef: string;
|
|
29
|
+
sku: string;
|
|
30
|
+
displayName: string;
|
|
31
|
+
uom?: string | undefined;
|
|
32
|
+
trackingMode?: TrackingMode | undefined;
|
|
33
|
+
trackingPolicy?: ShelfLifePolicy | undefined;
|
|
34
|
+
catchWeight?: boolean | undefined;
|
|
35
|
+
weightUom?: string | undefined;
|
|
36
|
+
weight?: number | undefined;
|
|
37
|
+
volume?: number | undefined;
|
|
38
|
+
barcode?: string[] | undefined;
|
|
39
|
+
isActive?: boolean | undefined;
|
|
40
|
+
}
|
|
41
|
+
/** The resolved answer. `@classytic/flow`'s `SkuDetails` IS this type. */
|
|
42
|
+
interface ItemStockFacts {
|
|
43
|
+
skuRef: string;
|
|
44
|
+
sku: string;
|
|
45
|
+
displayName: string;
|
|
46
|
+
trackingMode: TrackingMode;
|
|
47
|
+
trackingPolicy?: ShelfLifePolicy;
|
|
48
|
+
uom: string;
|
|
49
|
+
catchWeight?: boolean;
|
|
50
|
+
weightUom?: string;
|
|
51
|
+
weight?: number;
|
|
52
|
+
volume?: number;
|
|
53
|
+
barcode?: string[];
|
|
54
|
+
isActive: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Merge an Item with its Product facet.
|
|
58
|
+
*
|
|
59
|
+
* - **Neither** ⇒ `null`. An unknown SKU is ordinary (a deleted line), not an error.
|
|
60
|
+
* - **Product only** ⇒ the product's own facts — every deployment before an Item exists.
|
|
61
|
+
* - **Item only** ⇒ the item's facts. A raw material nobody sells.
|
|
62
|
+
* - **Both** ⇒ stock facts from the Item, name and label from the Product.
|
|
63
|
+
*
|
|
64
|
+
* `isActive` follows the ITEM when one exists: a product withdrawn from sale is
|
|
65
|
+
* still stock on a shelf, whereas a retired item is not stockable at all. Taking
|
|
66
|
+
* the product's flag would make unlisting a product silently orphan its stock.
|
|
67
|
+
*/
|
|
68
|
+
declare function resolveStockFacts(input: {
|
|
69
|
+
item?: ItemStockSource | null | undefined;
|
|
70
|
+
product?: ProductFacet | null | undefined;
|
|
71
|
+
}): ItemStockFacts | null;
|
|
72
|
+
//#endregion
|
|
73
|
+
export { ItemStockFacts, ItemStockSource, ProductFacet, TrackingMode, resolveStockFacts };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/inventory/item-facts.ts
|
|
2
|
+
/** Unit assumed when neither side declares one. A deployment-wide guess, and a bad one — set a real `uom` on the Item. */
|
|
3
|
+
const FALLBACK_UOM = "unit";
|
|
4
|
+
const defined = (key, value) => value === void 0 ? {} : { [key]: value };
|
|
5
|
+
/**
|
|
6
|
+
* Merge an Item with its Product facet.
|
|
7
|
+
*
|
|
8
|
+
* - **Neither** ⇒ `null`. An unknown SKU is ordinary (a deleted line), not an error.
|
|
9
|
+
* - **Product only** ⇒ the product's own facts — every deployment before an Item exists.
|
|
10
|
+
* - **Item only** ⇒ the item's facts. A raw material nobody sells.
|
|
11
|
+
* - **Both** ⇒ stock facts from the Item, name and label from the Product.
|
|
12
|
+
*
|
|
13
|
+
* `isActive` follows the ITEM when one exists: a product withdrawn from sale is
|
|
14
|
+
* still stock on a shelf, whereas a retired item is not stockable at all. Taking
|
|
15
|
+
* the product's flag would make unlisting a product silently orphan its stock.
|
|
16
|
+
*/
|
|
17
|
+
function resolveStockFacts(input) {
|
|
18
|
+
const { item, product } = input;
|
|
19
|
+
if (!item && !product) return null;
|
|
20
|
+
if (!item) {
|
|
21
|
+
const p = product;
|
|
22
|
+
return {
|
|
23
|
+
skuRef: p.skuRef,
|
|
24
|
+
sku: p.sku,
|
|
25
|
+
displayName: p.displayName,
|
|
26
|
+
uom: p.uom ?? FALLBACK_UOM,
|
|
27
|
+
trackingMode: p.trackingMode ?? "none",
|
|
28
|
+
...defined("trackingPolicy", p.trackingPolicy),
|
|
29
|
+
...defined("catchWeight", p.catchWeight),
|
|
30
|
+
...defined("weightUom", p.weightUom),
|
|
31
|
+
...defined("weight", p.weight),
|
|
32
|
+
...defined("volume", p.volume),
|
|
33
|
+
...defined("barcode", p.barcode),
|
|
34
|
+
isActive: p.isActive ?? true
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
skuRef: item.skuRef,
|
|
39
|
+
sku: product?.sku ?? item.sku,
|
|
40
|
+
displayName: product?.displayName ?? item.displayName,
|
|
41
|
+
uom: item.uom,
|
|
42
|
+
trackingMode: item.trackingMode,
|
|
43
|
+
...defined("trackingPolicy", item.trackingPolicy),
|
|
44
|
+
...defined("catchWeight", item.catchWeight),
|
|
45
|
+
...defined("weightUom", item.weightUom),
|
|
46
|
+
...defined("weight", item.weight),
|
|
47
|
+
...defined("volume", item.volume),
|
|
48
|
+
...defined("barcode", item.barcode),
|
|
49
|
+
isActive: item.isActive
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { resolveStockFacts };
|
package/dist/item-identity.d.mts
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
|
+
import { t as Brand } from "./brand-Lj_75Y_r.mjs";
|
|
1
2
|
//#region src/identity/item-identity.d.ts
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
+
* A resolved STOCK key — the string quants, moves and reservations are keyed by.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Use `stockKeyOf` for anything that reserves or moves stock, `skuDisplay` for
|
|
11
|
-
* anything a human reads. Never hand-roll either: both fall back, in OPPOSITE
|
|
12
|
-
* directions, and getting one backwards type-checks and returns a plausible
|
|
13
|
-
* string.
|
|
14
|
-
*
|
|
15
|
-
* - stock key = `skuRef ?? sku` — absent `skuRef` means a pre-split producer
|
|
16
|
-
* whose `sku` still IS the key.
|
|
17
|
-
* - display = `skuLabel ?? sku ?? skuRef` — the ref last, and flagged, because
|
|
18
|
-
* it can be an opaque id with no merchandising meaning.
|
|
19
|
-
*
|
|
20
|
-
* Pinned by `tests/unit/item-identity.test.ts`.
|
|
6
|
+
* Branded so a port that moves stock cannot accept a bare string: the only way
|
|
7
|
+
* to obtain one is {@link stockKeyOf} / {@link requireStockKey}, which apply the
|
|
8
|
+
* `skuRef ?? sku` rule. A merchandising label passed where a stock key belongs
|
|
9
|
+
* finds no quant and reports "out of stock" against a full shelf.
|
|
21
10
|
*/
|
|
11
|
+
type StockKey = Brand<string, "StockKey">;
|
|
22
12
|
/** Identifier fields a record may carry — each shape populates a different subset. */
|
|
23
13
|
interface SkuFields {
|
|
24
14
|
/** Merchandising label — mutable, human-facing. */
|
|
@@ -40,12 +30,12 @@ interface SkuFields {
|
|
|
40
30
|
*/
|
|
41
31
|
declare function stockKeyOf(item: SkuFields, fallback?: string | {
|
|
42
32
|
toString(): string;
|
|
43
|
-
}):
|
|
33
|
+
}): StockKey | undefined;
|
|
44
34
|
/**
|
|
45
35
|
* {@link stockKeyOf} that THROWS. Use wherever the key feeds a stock movement:
|
|
46
36
|
* a reservation on `undefined` returns "out of stock" instead of failing.
|
|
47
37
|
*/
|
|
48
|
-
declare function requireStockKey(item: SkuFields, context?: string):
|
|
38
|
+
declare function requireStockKey(item: SkuFields, context?: string): StockKey;
|
|
49
39
|
/** What a surface should render for an item, and whether it had a real label. */
|
|
50
40
|
interface SkuDisplay {
|
|
51
41
|
/** Best human-facing value. `undefined` = no identifier at all; render a dash. */
|
|
@@ -68,4 +58,4 @@ declare function skuDisplay(item: SkuFields): SkuDisplay;
|
|
|
68
58
|
*/
|
|
69
59
|
declare function looksLikeOpaqueId(value: string | undefined): boolean;
|
|
70
60
|
//#endregion
|
|
71
|
-
export { SkuDisplay, SkuFields, looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyOf };
|
|
61
|
+
export { SkuDisplay, SkuFields, StockKey, looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyOf };
|
package/dist/monetization.d.mts
CHANGED
|
@@ -22,6 +22,45 @@ declare const MONETIZATION_KINDS: readonly ["free", "one_time", "subscription",
|
|
|
22
22
|
type MonetizationKind = (typeof MONETIZATION_KINDS)[number];
|
|
23
23
|
/** True when `value` is one of the canonical kinds. */
|
|
24
24
|
declare function isMonetizationKind(value: unknown): value is MonetizationKind;
|
|
25
|
+
/**
|
|
26
|
+
* HOW MUCH of a price is collected at the point of sale.
|
|
27
|
+
*
|
|
28
|
+
* Distinct from every neighbouring vocabulary, which is why it is its own:
|
|
29
|
+
*
|
|
30
|
+
* - order's `PAYMENT_TERMS` (`prepaid` | `collect_on_delivery`) says WHEN.
|
|
31
|
+
* - `PaymentAllocationStatus` (`partial`, `allocated`…) says how much of a
|
|
32
|
+
* RECEIVED payment has been applied to what it settles.
|
|
33
|
+
* - revenue's `PARTIALLY_REFUNDED` is a transaction's own state.
|
|
34
|
+
*
|
|
35
|
+
* None of them answers "what do we charge this customer right now", and a host
|
|
36
|
+
* that guesses charges either nothing or everything — opposite answers to the
|
|
37
|
+
* same question.
|
|
38
|
+
*
|
|
39
|
+
* It lives here, beside {@link MONETIZATION_KINDS}, for the reason stated in
|
|
40
|
+
* catalog's barrel: monetization is a CROSS-KERNEL contract. Catalog declares
|
|
41
|
+
* the policy on a product, order turns it into tenders, revenue settles them.
|
|
42
|
+
* Three packages reading one definition, none re-exporting it.
|
|
43
|
+
*
|
|
44
|
+
* - `full_payment` — the whole price, before the sale completes.
|
|
45
|
+
* - `deposit` — a fixed amount now, the balance outstanding on the
|
|
46
|
+
* order and settled later by another tender. One provider
|
|
47
|
+
* line instead of two, which matters when card fees are
|
|
48
|
+
* charged per transaction.
|
|
49
|
+
* - `pay_later` — nothing now. The order still records what is owed.
|
|
50
|
+
*
|
|
51
|
+
* ABSENCE means `full_payment` — the behaviour before this existed — so no
|
|
52
|
+
* stored record changes meaning when a build that knows this field ships.
|
|
53
|
+
*/
|
|
54
|
+
declare const UPFRONT_COLLECTION_MODES: readonly ["full_payment", "deposit", "pay_later"];
|
|
55
|
+
type UpfrontCollectionMode = (typeof UPFRONT_COLLECTION_MODES)[number];
|
|
56
|
+
/**
|
|
57
|
+
* Narrow an unknown value to a collection mode.
|
|
58
|
+
*
|
|
59
|
+
* Readers MUST treat `false` as UNKNOWN, never as `full_payment`: a record
|
|
60
|
+
* written by a build that knows a mode this one does not has not said "charge
|
|
61
|
+
* everything", and assuming so takes money nobody agreed to.
|
|
62
|
+
*/
|
|
63
|
+
declare function isUpfrontCollectionMode(value: unknown): value is UpfrontCollectionMode;
|
|
25
64
|
/** A quantity-based price break. `price` (fixed override) XOR `discountPercent`. */
|
|
26
65
|
interface PriceTier {
|
|
27
66
|
/** Minimum quantity to qualify. */
|
|
@@ -191,4 +230,4 @@ declare function toRevenueMonetizationType(kind: MonetizationKind): RevenueMonet
|
|
|
191
230
|
/** Revenue wire type → canonical kind (`purchase` → `one_time`). */
|
|
192
231
|
declare function fromRevenueMonetizationType(type: RevenueMonetizationType): MonetizationKind;
|
|
193
232
|
//#endregion
|
|
194
|
-
export { BundleMonetization, DurationUnit, FreeMonetization, MONETIZATION_KINDS, Monetization, MonetizationKind, MoneyByCurrency, OneTimeMonetization, OneTimePricing, PriceTier, RevenueMonetizationType, SubscriptionMonetization, SubscriptionPlan, UsageMonetization, UsageRating, UsageTier, compareAtPriceOf, fromRevenueMonetizationType, isBundleMonetization, isFreeMonetization, isMonetizationKind, isOneTimeMonetization, isSubscriptionMonetization, isUsageMonetization, monetizationKindOf, toRevenueMonetizationType, unitPriceOf };
|
|
233
|
+
export { BundleMonetization, DurationUnit, FreeMonetization, MONETIZATION_KINDS, Monetization, MonetizationKind, MoneyByCurrency, OneTimeMonetization, OneTimePricing, PriceTier, RevenueMonetizationType, SubscriptionMonetization, SubscriptionPlan, UPFRONT_COLLECTION_MODES, UpfrontCollectionMode, UsageMonetization, UsageRating, UsageTier, compareAtPriceOf, fromRevenueMonetizationType, isBundleMonetization, isFreeMonetization, isMonetizationKind, isOneTimeMonetization, isSubscriptionMonetization, isUpfrontCollectionMode, isUsageMonetization, monetizationKindOf, toRevenueMonetizationType, unitPriceOf };
|
package/dist/monetization.mjs
CHANGED
|
@@ -21,6 +21,51 @@ const MONETIZATION_KIND_SET = new Set(MONETIZATION_KINDS);
|
|
|
21
21
|
function isMonetizationKind(value) {
|
|
22
22
|
return typeof value === "string" && MONETIZATION_KIND_SET.has(value);
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* HOW MUCH of a price is collected at the point of sale.
|
|
26
|
+
*
|
|
27
|
+
* Distinct from every neighbouring vocabulary, which is why it is its own:
|
|
28
|
+
*
|
|
29
|
+
* - order's `PAYMENT_TERMS` (`prepaid` | `collect_on_delivery`) says WHEN.
|
|
30
|
+
* - `PaymentAllocationStatus` (`partial`, `allocated`…) says how much of a
|
|
31
|
+
* RECEIVED payment has been applied to what it settles.
|
|
32
|
+
* - revenue's `PARTIALLY_REFUNDED` is a transaction's own state.
|
|
33
|
+
*
|
|
34
|
+
* None of them answers "what do we charge this customer right now", and a host
|
|
35
|
+
* that guesses charges either nothing or everything — opposite answers to the
|
|
36
|
+
* same question.
|
|
37
|
+
*
|
|
38
|
+
* It lives here, beside {@link MONETIZATION_KINDS}, for the reason stated in
|
|
39
|
+
* catalog's barrel: monetization is a CROSS-KERNEL contract. Catalog declares
|
|
40
|
+
* the policy on a product, order turns it into tenders, revenue settles them.
|
|
41
|
+
* Three packages reading one definition, none re-exporting it.
|
|
42
|
+
*
|
|
43
|
+
* - `full_payment` — the whole price, before the sale completes.
|
|
44
|
+
* - `deposit` — a fixed amount now, the balance outstanding on the
|
|
45
|
+
* order and settled later by another tender. One provider
|
|
46
|
+
* line instead of two, which matters when card fees are
|
|
47
|
+
* charged per transaction.
|
|
48
|
+
* - `pay_later` — nothing now. The order still records what is owed.
|
|
49
|
+
*
|
|
50
|
+
* ABSENCE means `full_payment` — the behaviour before this existed — so no
|
|
51
|
+
* stored record changes meaning when a build that knows this field ships.
|
|
52
|
+
*/
|
|
53
|
+
const UPFRONT_COLLECTION_MODES = [
|
|
54
|
+
"full_payment",
|
|
55
|
+
"deposit",
|
|
56
|
+
"pay_later"
|
|
57
|
+
];
|
|
58
|
+
const UPFRONT_COLLECTION_MODE_SET = new Set(UPFRONT_COLLECTION_MODES);
|
|
59
|
+
/**
|
|
60
|
+
* Narrow an unknown value to a collection mode.
|
|
61
|
+
*
|
|
62
|
+
* Readers MUST treat `false` as UNKNOWN, never as `full_payment`: a record
|
|
63
|
+
* written by a build that knows a mode this one does not has not said "charge
|
|
64
|
+
* everything", and assuming so takes money nobody agreed to.
|
|
65
|
+
*/
|
|
66
|
+
function isUpfrontCollectionMode(value) {
|
|
67
|
+
return typeof value === "string" && UPFRONT_COLLECTION_MODE_SET.has(value);
|
|
68
|
+
}
|
|
24
69
|
const isFreeMonetization = (m) => m.type === "free";
|
|
25
70
|
const isOneTimeMonetization = (m) => m.type === "one_time";
|
|
26
71
|
const isSubscriptionMonetization = (m) => m.type === "subscription";
|
|
@@ -76,4 +121,4 @@ function fromRevenueMonetizationType(type) {
|
|
|
76
121
|
return "one_time";
|
|
77
122
|
}
|
|
78
123
|
//#endregion
|
|
79
|
-
export { MONETIZATION_KINDS, compareAtPriceOf, fromRevenueMonetizationType, isBundleMonetization, isFreeMonetization, isMonetizationKind, isOneTimeMonetization, isSubscriptionMonetization, isUsageMonetization, monetizationKindOf, toRevenueMonetizationType, unitPriceOf };
|
|
124
|
+
export { MONETIZATION_KINDS, UPFRONT_COLLECTION_MODES, compareAtPriceOf, fromRevenueMonetizationType, isBundleMonetization, isFreeMonetization, isMonetizationKind, isOneTimeMonetization, isSubscriptionMonetization, isUpfrontCollectionMode, isUsageMonetization, monetizationKindOf, toRevenueMonetizationType, unitPriceOf };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { t as Brand } from "./brand-Lj_75Y_r.mjs";
|
|
2
|
+
//#region src/inventory/stock-level.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Stock quantities as NOMINAL types, so "how many are there" and "how many may
|
|
5
|
+
* I still promise" cannot be passed for one another.
|
|
6
|
+
*
|
|
7
|
+
* They are different questions with the same JavaScript type, and every layer
|
|
8
|
+
* re-derived the relationship between them: the flow ledger, the catalog
|
|
9
|
+
* bridge, the quant invariants, and the storefront cache each knew that
|
|
10
|
+
* available is on-hand minus reserved, and one of them still shipped on-hand
|
|
11
|
+
* into a field named `totalAvailable` — so the shop advertised units already
|
|
12
|
+
* held in someone else's cart and checkout refused the sale it had promised.
|
|
13
|
+
*
|
|
14
|
+
* Same fix as money's `MinorUnits<C>` brand, and for the same reason: a unit
|
|
15
|
+
* confusion that a reviewer must notice becomes one the compiler refuses.
|
|
16
|
+
* Zero runtime cost — the brands erase at compile time.
|
|
17
|
+
*/
|
|
18
|
+
/** Units physically present. INCLUDES units promised to open carts. */
|
|
19
|
+
type OnHand = Brand<number, "OnHand">;
|
|
20
|
+
/** Units held against an owner (cart, order, move group) — present, not free. */
|
|
21
|
+
type Reserved = Brand<number, "Reserved">;
|
|
22
|
+
/**
|
|
23
|
+
* Units that may still be promised to a NEW buyer: `onHand - reserved`.
|
|
24
|
+
*
|
|
25
|
+
* Constructible only via {@link sellable}, which is what makes passing on-hand
|
|
26
|
+
* where sellable is required a type error rather than an oversell.
|
|
27
|
+
*/
|
|
28
|
+
type Sellable = Brand<number, "Sellable">;
|
|
29
|
+
/** What a location holds, as the ledger records it. */
|
|
30
|
+
interface StockLevel {
|
|
31
|
+
readonly onHand: OnHand;
|
|
32
|
+
readonly reserved: Reserved;
|
|
33
|
+
}
|
|
34
|
+
declare const onHand: (n: number) => OnHand;
|
|
35
|
+
declare const reserved: (n: number) => Reserved;
|
|
36
|
+
/**
|
|
37
|
+
* The ONE definition of sellable stock.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately NOT clamped at zero. Writing stock off below what is already
|
|
40
|
+
* reserved (goods damaged after being promised) is a real state, not an error —
|
|
41
|
+
* you cannot un-promise a cart. A negative reads as OVERSOLD, and every
|
|
42
|
+
* consumer already treats `<= 0` as unsellable; clamping would erase the only
|
|
43
|
+
* signal operations has that the condition exists.
|
|
44
|
+
*/
|
|
45
|
+
declare const sellable: (level: StockLevel) => Sellable;
|
|
46
|
+
/** `true` when more is promised than is physically present. */
|
|
47
|
+
declare const isOversold: (level: StockLevel) => boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Build a level from a ledger row, whatever it calls its fields.
|
|
50
|
+
*
|
|
51
|
+
* A quant reports all three; `available` is IGNORED on purpose and recomputed,
|
|
52
|
+
* so a source whose stored total has drifted cannot import that drift.
|
|
53
|
+
*/
|
|
54
|
+
declare const levelOf: (row: {
|
|
55
|
+
quantityOnHand?: number;
|
|
56
|
+
quantityReserved?: number;
|
|
57
|
+
}) => StockLevel;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { OnHand, Reserved, Sellable, StockLevel, isOversold, levelOf, onHand, reserved, sellable };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/inventory/stock-level.ts
|
|
2
|
+
const onHand = (n) => n;
|
|
3
|
+
const reserved = (n) => n;
|
|
4
|
+
/**
|
|
5
|
+
* The ONE definition of sellable stock.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately NOT clamped at zero. Writing stock off below what is already
|
|
8
|
+
* reserved (goods damaged after being promised) is a real state, not an error —
|
|
9
|
+
* you cannot un-promise a cart. A negative reads as OVERSOLD, and every
|
|
10
|
+
* consumer already treats `<= 0` as unsellable; clamping would erase the only
|
|
11
|
+
* signal operations has that the condition exists.
|
|
12
|
+
*/
|
|
13
|
+
const sellable = (level) => level.onHand - level.reserved;
|
|
14
|
+
/** `true` when more is promised than is physically present. */
|
|
15
|
+
const isOversold = (level) => sellable(level) < 0;
|
|
16
|
+
/**
|
|
17
|
+
* Build a level from a ledger row, whatever it calls its fields.
|
|
18
|
+
*
|
|
19
|
+
* A quant reports all three; `available` is IGNORED on purpose and recomputed,
|
|
20
|
+
* so a source whose stored total has drifted cannot import that drift.
|
|
21
|
+
*/
|
|
22
|
+
const levelOf = (row) => ({
|
|
23
|
+
onHand: onHand(row.quantityOnHand ?? 0),
|
|
24
|
+
reserved: reserved(row.quantityReserved ?? 0)
|
|
25
|
+
});
|
|
26
|
+
//#endregion
|
|
27
|
+
export { isOversold, levelOf, onHand, reserved, sellable };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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",
|
|
@@ -185,6 +185,10 @@
|
|
|
185
185
|
"types": "./dist/mixin.d.mts",
|
|
186
186
|
"default": "./dist/mixin.mjs"
|
|
187
187
|
},
|
|
188
|
+
"./deadline": {
|
|
189
|
+
"types": "./dist/deadline.d.mts",
|
|
190
|
+
"default": "./dist/deadline.mjs"
|
|
191
|
+
},
|
|
188
192
|
"./sla-policy": {
|
|
189
193
|
"types": "./dist/sla-policy.d.mts",
|
|
190
194
|
"default": "./dist/sla-policy.mjs"
|
|
@@ -213,6 +217,14 @@
|
|
|
213
217
|
"./item-identity": {
|
|
214
218
|
"types": "./dist/item-identity.d.mts",
|
|
215
219
|
"default": "./dist/item-identity.mjs"
|
|
220
|
+
},
|
|
221
|
+
"./stock-level": {
|
|
222
|
+
"types": "./dist/stock-level.d.mts",
|
|
223
|
+
"default": "./dist/stock-level.mjs"
|
|
224
|
+
},
|
|
225
|
+
"./item-facts": {
|
|
226
|
+
"types": "./dist/item-facts.d.mts",
|
|
227
|
+
"default": "./dist/item-facts.mjs"
|
|
216
228
|
}
|
|
217
229
|
},
|
|
218
230
|
"files": [
|