@classytic/primitives 0.24.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 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.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
+
23
+ ## [0.25.0] - 2026-08-24
24
+
25
+ ### Added
26
+
27
+ - **`@classytic/primitives/item-identity` subpath** — `SkuFields`, `stockKeyOf`, `skuDisplay`. `sku` is a label; `skuRef` is the stock identity — the two fall back in opposite directions and getting one backwards type-checks while returning a plausible wrong string. `stockKeyOf` resolves `skuRef ?? sku ?? fallback` (the key to reserve, decrement and post against); `skuDisplay` resolves `skuLabel ?? sku ?? skuRef` (what a human reads). Canonicalises the per-package `variantSku || String(productId)` derivations into one place.
28
+
6
29
  ## [0.24.0] - 2026-08-24
7
30
 
8
31
  ### Added — `@classytic/primitives/address`: capture-format system
package/README.md CHANGED
@@ -1,160 +1,160 @@
1
- # @classytic/primitives
2
-
3
- [![Sponsor](https://img.shields.io/github/sponsors/classytic?style=flat-square&label=Sponsor&logo=GitHub&color=EA4AAA)](https://github.com/sponsors/classytic)
4
-
5
- Framework-agnostic TypeScript primitives shared across Classytic packages.
6
- Zero runtime dependencies. ESM only. Node 22+.
7
-
8
- These are the building blocks every Classytic package speaks in common:
9
- `Money`, `DomainEvent`, `OperationContext`, polymorphic references. When a
10
- package duplicates one of these shapes, it drifts — this package exists so it
11
- doesn't.
12
-
13
- > **Canonical-contract relocations in 0.3.0.** Three modules moved out to
14
- > `@classytic/repo-core` so kits and arc see one source of truth instead of
15
- > two:
16
- > - **Pagination** (`OffsetPaginationResult`, `KeysetPaginationResult`,
17
- > `AggregatePaginationResult`, `PaginationResult`, `toCanonicalList()`) →
18
- > `@classytic/repo-core/pagination`.
19
- > - **Tenant config** (`TenantConfig`, `TenantStrategy`, `TenantFieldType`,
20
- > `resolveTenantConfig`, `DEFAULT_TENANT_CONFIG`, `ResolvedTenantConfig`) →
21
- > `@classytic/repo-core/tenant`. Mongokit and sqlitekit's
22
- > `MultiTenantOptions extends Pick<TenantConfig, ...>`.
23
- > - **Error contracts** (`HttpError` throwable, `ErrorContract` wire shape,
24
- > `ErrorDetail`, `ErrorCode`, `ERROR_CODES`, `toErrorContract()`,
25
- > `statusToErrorCode()`) → `@classytic/repo-core/errors`.
26
- > `ArcError implements HttpError`.
27
- >
28
- > Events stay here. `@classytic/primitives/events` remains the canonical
29
- > source for `EventMeta`, `DomainEvent`, `EventHandler`, `EventLogger`,
30
- > `EventTransport`, `DeadLetteredEvent`, `PublishManyResult`, `createEvent`,
31
- > `createChildEvent`, `matchEventPattern`. Arc 2.12 re-exports only the
32
- > runtime `MemoryEventTransport`.
33
-
34
- ## Install
35
-
36
- ```bash
37
- npm install @classytic/primitives
38
- ```
39
-
40
- ## Use
41
-
42
- Prefer **subpath imports** — each concern is its own entry point, types erase
43
- at compile time, runtime helpers are tiny.
44
-
45
- ```ts
46
- import { addMoney, fromMajor, type Money } from '@classytic/primitives/money';
47
- import { createEvent, type DomainEvent, type EventTransport } from '@classytic/primitives/events';
48
- import type { OperationContext } from '@classytic/primitives/context';
49
- import { allocate, type SplitResult } from '@classytic/primitives/split-allocation';
50
- // Tenant config, error contracts, and pagination now live in repo-core:
51
- import { resolveTenantConfig, type TenantConfig } from '@classytic/repo-core/tenant';
52
- import type { HttpError, ErrorContract } from '@classytic/repo-core/errors';
53
- import type {
54
- OffsetPaginationResult, KeysetPaginationResult, AggregatePaginationResult,
55
- } from '@classytic/repo-core/pagination';
56
- ```
57
-
58
- There is intentionally **no root barrel** — a barrel forces the Node ESM
59
- loader and TypeScript compiler to walk every module on a single convenience
60
- import, which slows CI and pulls unused code into the consumer's graph. Use
61
- subpath imports only.
62
-
63
- ## What's inside
64
-
65
- | Subpath | Ships |
66
- |---|---|
67
- | `/money` | `Money`, `fromMajor`, `toMajor`, `addMoney`, `subtractMoney`, `multiplyMoney`, `sumMoney`, `equalsMoney`, `compareMoney`, `negateMoney`, `absMoney`, `isMoney`, `CurrencyMismatchError` |
68
- | `/currency` | `CurrencyCode` brand, `CURRENCIES`, `MINOR_UNIT_FACTOR`, `minorUnitFactor`, `isKnownCurrency`, `toCurrencyCode`, `currencyCode`, `isCurrencyCode`, `InvalidCurrencyCodeError`, `CurrencyMismatchError`, `FxSnapshot`, `convertWithSnapshot`, `reverseWithSnapshot`, `FxRounding` |
69
- | `/address` | `Address`, `ContactAddress`, `GeoPoint`, `GeoJsonPoint`, `toGeoJsonPoint`, `fromGeoJsonPoint` |
70
- | `/period` | `DateRange`, `Period`, `isDateRange`, `isWithin`, `rangeDurationMs`, `rangesOverlap`, `DateRangeError` — **plus timezone-aware resolution**: `resolvePeriod`, `resolveDay`, `resolveMonth`, `resolveQuarter`, `resolveYear`, `resolveDateSpan`, `dayStart`, `periodOf`, `parsePeriod`, `formatPeriod`, `granularityOf`, `periodTimeZone`, `inclusiveEnd`, `PeriodError` |
71
- | `/reference` | `ExternalRef`, `ObjectIdLike`, `IdLike`, `DocumentRef`, `idToString`, `isExternalRef` |
72
- | `/context` | `OperationContext`, `ActorRef` |
73
- | `/events` | `DomainEvent`, `EventMeta`, `EventHandler`, `EventLogger`, `EventTransport`, `DeadLetteredEvent`, `PublishManyResult`, `createEvent`, `createChildEvent`, `matchEventPattern` |
74
- | `/result` | `Result<T, E>`, `ok`, `err`, `isOk`, `isErr`, `mapResult`, `mapError`, `unwrap` |
75
- | `/brand` | `Brand<T, B>`, `Prettify`, `DeepPartial`, `DeepReadonly`, `RequireKeys`, `OptionalKeys`, `Nullable`, `KeysMatching`, `NonEmptyArray` |
76
- | `/split-allocation` | `allocate`, `isBalanced`, `SplitAllocationError`, `SplitMethod`, `SplitSubject`, `SplitPart`, `SplitResult` |
77
- | `/idempotency` | `decideClaim`, `completeClaim`, `renewLease`, `identityKey`, `sameIdentity`, `fingerprintRequest`, `newLeaseToken`, `isLeaseExpired`, `holdsLease`, `isInFlight`, `DEFAULT_LEASE_MS`, `IdempotencyClaim`, `ClaimOutcome`, `ClaimResult`, `ClaimState`, `IdempotencyIdentity`, `IdempotencyError` |
78
- | `/approval` | `createChain`, `applyDecision`, `skipStep`, `nextPendingStep`, `pendingSteps`, `isApproved`, `assertApproved`, `assertApprovedIfPresent`, `isRejected`, `isPending`, `decisionCount`, `ApprovalError`, `ApprovalChain`, `ApprovalStep`, `ApprovalDecision`, `Approver`, `ApprovalThreshold` |
79
- | `/cadence` | `nextOccurrence`, `occurrencesBetween`, `validateCadence`, `CadenceError`, `Cadence`, `DailyCadence`, `WeeklyCadence`, `MonthlyCadence`, `YearlyCadence`, `CronCadence`, `IsoWeekday` |
80
- | `/hold` | `addHold`, `resolveHold`, `activeHolds`, `resolvedHolds`, `isOnHold`, `hasActiveHoldOfCode`, `HoldError`, `HoldReason`, `HoldActor` |
81
- | `/sla` | `breachedAt`, `remainingMs`, `elapsedMs`, `isBreached`, `consumedFraction`, `validateSLA`, `SLAError`, `SLA`, `BreachPolicy` |
82
-
83
- ## Design
84
-
85
- ### Money — integer minor units, always
86
-
87
- ```ts
88
- import { fromMajor, addMoney, toMajor } from '@classytic/primitives/money';
89
-
90
- const subtotal = fromMajor(19.99, 'USD'); // { amount: 1999, currency: 'USD' }
91
- const shipping = fromMajor(5.00, 'USD'); // { amount: 500, currency: 'USD' }
92
- const total = addMoney(subtotal, shipping); // { amount: 2499, currency: 'USD' }
93
- toMajor(total); // 24.99
94
- ```
95
-
96
- `Money.amount` is always an integer in the currency's minor unit — `1` = one
97
- cent for USD, one yen for JPY, one fils for KWD. Mixing currencies throws
98
- `CurrencyMismatchError`. This eliminates the float-rounding drift that happens
99
- when every package rolls its own `{ amount: number, currency: string }`.
100
-
101
- ### DomainEvent — structurally identical to Arc
102
-
103
- ```ts
104
- import { createEvent, type EventTransport } from '@classytic/primitives/events';
105
-
106
- const event = createEvent('order:placed', { orderId: 'ord_123' }, {
107
- organizationId: ctx.organizationId,
108
- userId: ctx.actorId,
109
- correlationId: ctx.correlationId,
110
- });
111
-
112
- await transport.publish(event);
113
- ```
114
-
115
- Shape matches `@classytic/arc`'s `DomainEvent` exactly — any Arc transport
116
- (memory, Redis pub/sub, Redis Streams, Kafka) plugs into a package expecting
117
- this interface without adapters. See PACKAGE_RULES.md §11.
118
-
119
- ### TenantConfig — moved to `@classytic/repo-core/tenant`
120
-
121
- ```ts
122
- import { resolveTenantConfig } from '@classytic/repo-core/tenant';
123
-
124
- const tenant = resolveTenantConfig(config.tenant);
125
- // { enabled: true, tenantField: 'organizationId', fieldType: 'objectId',
126
- // ref: 'organization', contextKey: 'organizationId', required: true }
127
- ```
128
-
129
- Mongokit's `MultiTenantOptions` and sqlitekit's equivalent both
130
- `extends Pick<TenantConfig, ...>` from repo-core — pass the resolved config
131
- straight into `multiTenantPlugin(resolved)`.
132
-
133
- ### ExternalRef — polymorphic, framework-free
134
-
135
- ```ts
136
- interface ExternalRef {
137
- sourceId: string;
138
- sourceModel: string;
139
- }
140
- ```
141
-
142
- For references that cross trust boundaries (another package, another service,
143
- Stripe, Postgres) — paired with a host-implemented `SourceBridge`. Works for
144
- ObjectId, UUID, Stripe ID, anything. See PACKAGE_RULES.md §7.
145
-
146
- ## What's not here (on purpose)
147
-
148
- - **Mongoose types** — use `ObjectIdLike` from `/reference`. Any Mongoose
149
- `Types.ObjectId` satisfies it structurally.
150
- - **Transaction / Order / Cart shapes** — those are domain types, not
151
- primitives. They live in their respective packages.
152
- - **Error subclasses** — throw plain `Error`s; hosts serialize them into
153
- `ErrorContract` (from `@classytic/repo-core/errors`) at the HTTP boundary
154
- via `toErrorContract(err)`.
155
- - **Runtime currency database** — ship a whitelist at the app layer if you
156
- need one. `MINOR_UNIT_FACTOR` covers common cases.
157
-
158
- ## License
159
-
160
- MIT © Classytic
1
+ # @classytic/primitives
2
+
3
+ [![Sponsor](https://img.shields.io/github/sponsors/classytic?style=flat-square&label=Sponsor&logo=GitHub&color=EA4AAA)](https://github.com/sponsors/classytic)
4
+
5
+ Framework-agnostic TypeScript primitives shared across Classytic packages.
6
+ Zero runtime dependencies. ESM only. Node 22+.
7
+
8
+ These are the building blocks every Classytic package speaks in common:
9
+ `Money`, `DomainEvent`, `OperationContext`, polymorphic references. When a
10
+ package duplicates one of these shapes, it drifts — this package exists so it
11
+ doesn't.
12
+
13
+ > **Canonical-contract relocations in 0.3.0.** Three modules moved out to
14
+ > `@classytic/repo-core` so kits and arc see one source of truth instead of
15
+ > two:
16
+ > - **Pagination** (`OffsetPaginationResult`, `KeysetPaginationResult`,
17
+ > `AggregatePaginationResult`, `PaginationResult`, `toCanonicalList()`) →
18
+ > `@classytic/repo-core/pagination`.
19
+ > - **Tenant config** (`TenantConfig`, `TenantStrategy`, `TenantFieldType`,
20
+ > `resolveTenantConfig`, `DEFAULT_TENANT_CONFIG`, `ResolvedTenantConfig`) →
21
+ > `@classytic/repo-core/tenant`. Mongokit and sqlitekit's
22
+ > `MultiTenantOptions extends Pick<TenantConfig, ...>`.
23
+ > - **Error contracts** (`HttpError` throwable, `ErrorContract` wire shape,
24
+ > `ErrorDetail`, `ErrorCode`, `ERROR_CODES`, `toErrorContract()`,
25
+ > `statusToErrorCode()`) → `@classytic/repo-core/errors`.
26
+ > `ArcError implements HttpError`.
27
+ >
28
+ > Events stay here. `@classytic/primitives/events` remains the canonical
29
+ > source for `EventMeta`, `DomainEvent`, `EventHandler`, `EventLogger`,
30
+ > `EventTransport`, `DeadLetteredEvent`, `PublishManyResult`, `createEvent`,
31
+ > `createChildEvent`, `matchEventPattern`. Arc 2.12 re-exports only the
32
+ > runtime `MemoryEventTransport`.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ npm install @classytic/primitives
38
+ ```
39
+
40
+ ## Use
41
+
42
+ Prefer **subpath imports** — each concern is its own entry point, types erase
43
+ at compile time, runtime helpers are tiny.
44
+
45
+ ```ts
46
+ import { addMoney, fromMajor, type Money } from '@classytic/primitives/money';
47
+ import { createEvent, type DomainEvent, type EventTransport } from '@classytic/primitives/events';
48
+ import type { OperationContext } from '@classytic/primitives/context';
49
+ import { allocate, type SplitResult } from '@classytic/primitives/split-allocation';
50
+ // Tenant config, error contracts, and pagination now live in repo-core:
51
+ import { resolveTenantConfig, type TenantConfig } from '@classytic/repo-core/tenant';
52
+ import type { HttpError, ErrorContract } from '@classytic/repo-core/errors';
53
+ import type {
54
+ OffsetPaginationResult, KeysetPaginationResult, AggregatePaginationResult,
55
+ } from '@classytic/repo-core/pagination';
56
+ ```
57
+
58
+ There is intentionally **no root barrel** — a barrel forces the Node ESM
59
+ loader and TypeScript compiler to walk every module on a single convenience
60
+ import, which slows CI and pulls unused code into the consumer's graph. Use
61
+ subpath imports only.
62
+
63
+ ## What's inside
64
+
65
+ | Subpath | Ships |
66
+ |---|---|
67
+ | `/money` | `Money`, `fromMajor`, `toMajor`, `addMoney`, `subtractMoney`, `multiplyMoney`, `sumMoney`, `equalsMoney`, `compareMoney`, `negateMoney`, `absMoney`, `isMoney`, `CurrencyMismatchError` |
68
+ | `/currency` | `CurrencyCode` brand, `CURRENCIES`, `MINOR_UNIT_FACTOR`, `minorUnitFactor`, `isKnownCurrency`, `toCurrencyCode`, `currencyCode`, `isCurrencyCode`, `InvalidCurrencyCodeError`, `CurrencyMismatchError`, `FxSnapshot`, `convertWithSnapshot`, `reverseWithSnapshot`, `FxRounding` |
69
+ | `/address` | `Address`, `ContactAddress`, `GeoPoint`, `GeoJsonPoint`, `toGeoJsonPoint`, `fromGeoJsonPoint` |
70
+ | `/period` | `DateRange`, `Period`, `isDateRange`, `isWithin`, `rangeDurationMs`, `rangesOverlap`, `DateRangeError` — **plus timezone-aware resolution**: `resolvePeriod`, `resolveDay`, `resolveMonth`, `resolveQuarter`, `resolveYear`, `resolveDateSpan`, `dayStart`, `periodOf`, `parsePeriod`, `formatPeriod`, `granularityOf`, `periodTimeZone`, `inclusiveEnd`, `PeriodError` |
71
+ | `/reference` | `ExternalRef`, `ObjectIdLike`, `IdLike`, `DocumentRef`, `idToString`, `isExternalRef` |
72
+ | `/context` | `OperationContext`, `ActorRef` |
73
+ | `/events` | `DomainEvent`, `EventMeta`, `EventHandler`, `EventLogger`, `EventTransport`, `DeadLetteredEvent`, `PublishManyResult`, `createEvent`, `createChildEvent`, `matchEventPattern` |
74
+ | `/result` | `Result<T, E>`, `ok`, `err`, `isOk`, `isErr`, `mapResult`, `mapError`, `unwrap` |
75
+ | `/brand` | `Brand<T, B>`, `Prettify`, `DeepPartial`, `DeepReadonly`, `RequireKeys`, `OptionalKeys`, `Nullable`, `KeysMatching`, `NonEmptyArray` |
76
+ | `/split-allocation` | `allocate`, `isBalanced`, `SplitAllocationError`, `SplitMethod`, `SplitSubject`, `SplitPart`, `SplitResult` |
77
+ | `/idempotency` | `decideClaim`, `completeClaim`, `renewLease`, `identityKey`, `sameIdentity`, `fingerprintRequest`, `newLeaseToken`, `isLeaseExpired`, `holdsLease`, `isInFlight`, `DEFAULT_LEASE_MS`, `IdempotencyClaim`, `ClaimOutcome`, `ClaimResult`, `ClaimState`, `IdempotencyIdentity`, `IdempotencyError` |
78
+ | `/approval` | `createChain`, `applyDecision`, `skipStep`, `nextPendingStep`, `pendingSteps`, `isApproved`, `assertApproved`, `assertApprovedIfPresent`, `isRejected`, `isPending`, `decisionCount`, `ApprovalError`, `ApprovalChain`, `ApprovalStep`, `ApprovalDecision`, `Approver`, `ApprovalThreshold` |
79
+ | `/cadence` | `nextOccurrence`, `occurrencesBetween`, `validateCadence`, `CadenceError`, `Cadence`, `DailyCadence`, `WeeklyCadence`, `MonthlyCadence`, `YearlyCadence`, `CronCadence`, `IsoWeekday` |
80
+ | `/hold` | `addHold`, `resolveHold`, `activeHolds`, `resolvedHolds`, `isOnHold`, `hasActiveHoldOfCode`, `HoldError`, `HoldReason`, `HoldActor` |
81
+ | `/sla` | `breachedAt`, `remainingMs`, `elapsedMs`, `isBreached`, `consumedFraction`, `validateSLA`, `SLAError`, `SLA`, `BreachPolicy` |
82
+
83
+ ## Design
84
+
85
+ ### Money — integer minor units, always
86
+
87
+ ```ts
88
+ import { fromMajor, addMoney, toMajor } from '@classytic/primitives/money';
89
+
90
+ const subtotal = fromMajor(19.99, 'USD'); // { amount: 1999, currency: 'USD' }
91
+ const shipping = fromMajor(5.00, 'USD'); // { amount: 500, currency: 'USD' }
92
+ const total = addMoney(subtotal, shipping); // { amount: 2499, currency: 'USD' }
93
+ toMajor(total); // 24.99
94
+ ```
95
+
96
+ `Money.amount` is always an integer in the currency's minor unit — `1` = one
97
+ cent for USD, one yen for JPY, one fils for KWD. Mixing currencies throws
98
+ `CurrencyMismatchError`. This eliminates the float-rounding drift that happens
99
+ when every package rolls its own `{ amount: number, currency: string }`.
100
+
101
+ ### DomainEvent — structurally identical to Arc
102
+
103
+ ```ts
104
+ import { createEvent, type EventTransport } from '@classytic/primitives/events';
105
+
106
+ const event = createEvent('order:placed', { orderId: 'ord_123' }, {
107
+ organizationId: ctx.organizationId,
108
+ userId: ctx.actorId,
109
+ correlationId: ctx.correlationId,
110
+ });
111
+
112
+ await transport.publish(event);
113
+ ```
114
+
115
+ Shape matches `@classytic/arc`'s `DomainEvent` exactly — any Arc transport
116
+ (memory, Redis pub/sub, Redis Streams, Kafka) plugs into a package expecting
117
+ this interface without adapters. See PACKAGE_RULES.md §11.
118
+
119
+ ### TenantConfig — moved to `@classytic/repo-core/tenant`
120
+
121
+ ```ts
122
+ import { resolveTenantConfig } from '@classytic/repo-core/tenant';
123
+
124
+ const tenant = resolveTenantConfig(config.tenant);
125
+ // { enabled: true, tenantField: 'organizationId', fieldType: 'objectId',
126
+ // ref: 'organization', contextKey: 'organizationId', required: true }
127
+ ```
128
+
129
+ Mongokit's `MultiTenantOptions` and sqlitekit's equivalent both
130
+ `extends Pick<TenantConfig, ...>` from repo-core — pass the resolved config
131
+ straight into `multiTenantPlugin(resolved)`.
132
+
133
+ ### ExternalRef — polymorphic, framework-free
134
+
135
+ ```ts
136
+ interface ExternalRef {
137
+ sourceId: string;
138
+ sourceModel: string;
139
+ }
140
+ ```
141
+
142
+ For references that cross trust boundaries (another package, another service,
143
+ Stripe, Postgres) — paired with a host-implemented `SourceBridge`. Works for
144
+ ObjectId, UUID, Stripe ID, anything. See PACKAGE_RULES.md §7.
145
+
146
+ ## What's not here (on purpose)
147
+
148
+ - **Mongoose types** — use `ObjectIdLike` from `/reference`. Any Mongoose
149
+ `Types.ObjectId` satisfies it structurally.
150
+ - **Transaction / Order / Cart shapes** — those are domain types, not
151
+ primitives. They live in their respective packages.
152
+ - **Error subclasses** — throw plain `Error`s; hosts serialize them into
153
+ `ErrorContract` (from `@classytic/repo-core/errors`) at the HTTP boundary
154
+ via `toErrorContract(err)`.
155
+ - **Runtime currency database** — ship a whitelist at the app layer if you
156
+ need one. `MINOR_UNIT_FACTOR` covers common cases.
157
+
158
+ ## License
159
+
160
+ MIT © Classytic
@@ -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 };
@@ -0,0 +1,61 @@
1
+ import { t as Brand } from "./brand-Lj_75Y_r.mjs";
2
+ //#region src/identity/item-identity.d.ts
3
+ /**
4
+ * A resolved STOCK key — the string quants, moves and reservations are keyed by.
5
+ *
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.
10
+ */
11
+ type StockKey = Brand<string, "StockKey">;
12
+ /** Identifier fields a record may carry — each shape populates a different subset. */
13
+ interface SkuFields {
14
+ /** Merchandising label — mutable, human-facing. */
15
+ sku?: string | undefined;
16
+ /** Stock/ledger identity — immutable, keys quants and moves. */
17
+ skuRef?: string | undefined;
18
+ /** Label resolved server-side for a read model keyed by `skuRef`. */
19
+ skuLabel?: string | undefined;
20
+ }
21
+ /**
22
+ * The STOCK key to reserve, decrement and post against: `skuRef ?? sku`,
23
+ * falling back to `fallback` (typically the owning entity's `_id`) when the
24
+ * record carries neither — the shape every "variant, else the product"
25
+ * ledger key already re-derives by hand across three packages.
26
+ *
27
+ * `undefined` = the record carries neither AND no `fallback` was given —
28
+ * malformed, not untracked. Use {@link requireStockKey} where that must stop
29
+ * the write.
30
+ */
31
+ declare function stockKeyOf(item: SkuFields, fallback?: string | {
32
+ toString(): string;
33
+ }): StockKey | undefined;
34
+ /**
35
+ * {@link stockKeyOf} that THROWS. Use wherever the key feeds a stock movement:
36
+ * a reservation on `undefined` returns "out of stock" instead of failing.
37
+ */
38
+ declare function requireStockKey(item: SkuFields, context?: string): StockKey;
39
+ /** What a surface should render for an item, and whether it had a real label. */
40
+ interface SkuDisplay {
41
+ /** Best human-facing value. `undefined` = no identifier at all; render a dash. */
42
+ label: string | undefined;
43
+ /** The stock identity, for subtext or copy-to-clipboard. */
44
+ ref: string | undefined;
45
+ /**
46
+ * TRUE when `label` is really the `ref` (nothing resolved). Style it as an id
47
+ * — monospace, muted — never as a SKU.
48
+ */
49
+ isRefFallback: boolean;
50
+ }
51
+ /** What to SHOW: `skuLabel ?? sku ?? skuRef`. The ref is returned alongside,
52
+ * never dropped — it is the join key an operator quotes. */
53
+ declare function skuDisplay(item: SkuFields): SkuDisplay;
54
+ /**
55
+ * Looks like an opaque id (24-char hex) rather than a merchandising code.
56
+ * PRESENTATION HINT ONLY — never branch stock logic on it; a deployment may key
57
+ * quants by any stable string.
58
+ */
59
+ declare function looksLikeOpaqueId(value: string | undefined): boolean;
60
+ //#endregion
61
+ export { SkuDisplay, SkuFields, StockKey, looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyOf };
@@ -0,0 +1,50 @@
1
+ //#region src/identity/item-identity.ts
2
+ const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value : void 0;
3
+ /**
4
+ * The STOCK key to reserve, decrement and post against: `skuRef ?? sku`,
5
+ * falling back to `fallback` (typically the owning entity's `_id`) when the
6
+ * record carries neither — the shape every "variant, else the product"
7
+ * ledger key already re-derives by hand across three packages.
8
+ *
9
+ * `undefined` = the record carries neither AND no `fallback` was given —
10
+ * malformed, not untracked. Use {@link requireStockKey} where that must stop
11
+ * the write.
12
+ */
13
+ function stockKeyOf(item, fallback) {
14
+ return nonEmpty(item.skuRef) ?? nonEmpty(item.sku) ?? (fallback !== void 0 ? String(fallback) : void 0);
15
+ }
16
+ /**
17
+ * {@link stockKeyOf} that THROWS. Use wherever the key feeds a stock movement:
18
+ * a reservation on `undefined` returns "out of stock" instead of failing.
19
+ */
20
+ function requireStockKey(item, context) {
21
+ const key = stockKeyOf(item);
22
+ if (key === void 0) throw new Error(`stock key missing: neither skuRef nor sku is set${context ? ` (${context})` : ""}`);
23
+ return key;
24
+ }
25
+ /** What to SHOW: `skuLabel ?? sku ?? skuRef`. The ref is returned alongside,
26
+ * never dropped — it is the join key an operator quotes. */
27
+ function skuDisplay(item) {
28
+ const ref = nonEmpty(item.skuRef);
29
+ const label = nonEmpty(item.skuLabel) ?? nonEmpty(item.sku);
30
+ if (label !== void 0) return {
31
+ label,
32
+ ref,
33
+ isRefFallback: false
34
+ };
35
+ return {
36
+ label: ref,
37
+ ref,
38
+ isRefFallback: ref !== void 0
39
+ };
40
+ }
41
+ /**
42
+ * Looks like an opaque id (24-char hex) rather than a merchandising code.
43
+ * PRESENTATION HINT ONLY — never branch stock logic on it; a deployment may key
44
+ * quants by any stable string.
45
+ */
46
+ function looksLikeOpaqueId(value) {
47
+ return typeof value === "string" && /^[0-9a-f]{24}$/i.test(value);
48
+ }
49
+ //#endregion
50
+ export { looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyOf };
@@ -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 };
@@ -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.24.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"
@@ -209,6 +213,18 @@
209
213
  "./subject": {
210
214
  "types": "./dist/subject.d.mts",
211
215
  "default": "./dist/subject.mjs"
216
+ },
217
+ "./item-identity": {
218
+ "types": "./dist/item-identity.d.mts",
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"
212
228
  }
213
229
  },
214
230
  "files": [