@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 +23 -0
- package/README.md +160 -160
- 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 +61 -0
- package/dist/item-identity.mjs +50 -0
- 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 +17 -1
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
|
-
[](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
|
+
[](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 };
|
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"
|
|
@@ -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": [
|