@classytic/primitives 0.29.0 → 0.31.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 +43 -0
- package/dist/approval.d.mts +74 -6
- package/dist/approval.mjs +36 -2
- package/dist/durable-events.d.mts +83 -0
- package/dist/durable-events.mjs +115 -0
- package/dist/events.d.mts +6 -6
- package/dist/events.mjs +5 -6
- package/dist/item-facts.d.mts +19 -0
- package/dist/item-facts.mjs +10 -0
- package/dist/memory-outbox.d.mts +1 -0
- package/dist/memory-outbox.mjs +16 -1
- package/dist/monetization.d.mts +3 -1
- package/dist/monetization.mjs +7 -1
- package/dist/outbox.d.mts +26 -2
- package/dist/outbox.mjs +43 -1
- package/dist/price-basis.d.mts +5 -2
- package/dist/price-basis.mjs +10 -4
- package/dist/quantity.d.mts +38 -0
- package/dist/quantity.mjs +88 -0
- package/dist/rounded-div-D7lyJXt8.d.mts +4 -0
- package/dist/split-allocation.d.mts +20 -2
- package/dist/split-allocation.mjs +57 -1
- package/dist/unit-cost-rate.d.mts +1 -3
- package/dist/usage-pricing.d.mts +53 -0
- package/dist/usage-pricing.mjs +150 -0
- package/package.json +16 -1
package/dist/outbox.d.mts
CHANGED
|
@@ -141,6 +141,23 @@ declare class OutboxOwnershipError extends Error {
|
|
|
141
141
|
readonly currentOwner: string | null;
|
|
142
142
|
constructor(eventId: string, attemptedBy: string, currentOwner: string | null);
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Thrown by `save` when a row with this event id already exists with a DIFFERENT payload.
|
|
146
|
+
* A same-id, same-payload save is an idempotent success; a different payload is two
|
|
147
|
+
* occurrences minted onto one id, and dropping either one loses money silently.
|
|
148
|
+
*/
|
|
149
|
+
declare class OutboxConflictError extends Error {
|
|
150
|
+
readonly name = "OutboxConflictError";
|
|
151
|
+
readonly code = "outbox.conflict";
|
|
152
|
+
readonly eventId: string;
|
|
153
|
+
readonly eventType: string;
|
|
154
|
+
constructor(eventId: string, eventType: string);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Order-independent fingerprint of an event payload as it travels (JSON: `Date` becomes its
|
|
158
|
+
* ISO string, `undefined` drops). Stores compare it on a duplicate id; see {@link OutboxConflictError}.
|
|
159
|
+
*/
|
|
160
|
+
declare function outboxPayloadFingerprint(event: DomainEvent): string;
|
|
144
161
|
/** Thrown when an event missing `type` or `meta.id` is passed to the outbox. */
|
|
145
162
|
declare class InvalidOutboxEventError extends Error {
|
|
146
163
|
readonly name = "InvalidOutboxEventError";
|
|
@@ -235,7 +252,8 @@ interface OutboxStore {
|
|
|
235
252
|
/**
|
|
236
253
|
* Save an event to the outbox (typically called inside a business
|
|
237
254
|
* transaction). MUST reject events missing `type` or `meta.id` — throw
|
|
238
|
-
* rather than persist.
|
|
255
|
+
* rather than persist. A second save of an existing `meta.id` is a no-op when
|
|
256
|
+
* the payload matches, and throws {@link OutboxConflictError} when it does not.
|
|
239
257
|
*/
|
|
240
258
|
save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
|
|
241
259
|
/**
|
|
@@ -267,6 +285,12 @@ interface OutboxStore {
|
|
|
267
285
|
* @throws {@link OutboxOwnershipError} on ownership mismatch
|
|
268
286
|
*/
|
|
269
287
|
acknowledge(eventId: string, options?: OutboxAcknowledgeOptions): Promise<void>;
|
|
288
|
+
/**
|
|
289
|
+
* Mark every listed event delivered in ONE write — what a unit of work's post-commit flush
|
|
290
|
+
* calls, so N delivered events cost one command, not N. Unleased rows only (no fencing);
|
|
291
|
+
* unknown or already-delivered ids are ignored.
|
|
292
|
+
*/
|
|
293
|
+
acknowledgeMany?(eventIds: readonly string[]): Promise<void>;
|
|
270
294
|
/**
|
|
271
295
|
* Record a relay failure. When implemented, the relay calls this instead
|
|
272
296
|
* of stopping the batch — enables retry scheduling and DLQ.
|
|
@@ -335,4 +359,4 @@ interface OutboxStore {
|
|
|
335
359
|
oldestPendingAgeMs?(): Promise<number | null>;
|
|
336
360
|
}
|
|
337
361
|
//#endregion
|
|
338
|
-
export { InvalidOutboxEventError, OUTBOX_STATUSES, OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxClaimedEvent, OutboxErrorInfo, OutboxFailOptions, OutboxFailureContext, OutboxFailureDecision, OutboxFailurePolicy, OutboxOwnershipError, OutboxStatus, OutboxStore, OutboxWriteOptions };
|
|
362
|
+
export { InvalidOutboxEventError, OUTBOX_STATUSES, OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxClaimedEvent, OutboxConflictError, OutboxErrorInfo, OutboxFailOptions, OutboxFailureContext, OutboxFailureDecision, OutboxFailurePolicy, OutboxOwnershipError, OutboxStatus, OutboxStore, OutboxWriteOptions, outboxPayloadFingerprint };
|
package/dist/outbox.mjs
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
|
+
import { canonicalDigest } from "./canonical.mjs";
|
|
1
2
|
//#region src/events/outbox.ts
|
|
2
3
|
/**
|
|
4
|
+
* Transactional outbox CONTRACT — the canonical owner. `@classytic/arc`
|
|
5
|
+
* re-exports this; it keeps no copy of its own.
|
|
6
|
+
*
|
|
7
|
+
* Contract surface only: {@link OutboxStore}, its option types, the
|
|
8
|
+
* {@link OutboxFailurePolicy} retry/DLQ contract, and the error classes.
|
|
9
|
+
*
|
|
10
|
+
* The split exists because a domain package MUST NOT peer-dep arc. Owning the
|
|
11
|
+
* contract here lets a package write `implements OutboxStore` and hand the
|
|
12
|
+
* result to arc's `new EventOutbox({ store })`, and since arc re-exports these
|
|
13
|
+
* exact classes, `instanceof OutboxOwnershipError` works across the boundary.
|
|
14
|
+
*
|
|
15
|
+
* Runtimes live elsewhere: the DURABLE one (`EventOutbox` relay, Mongo store,
|
|
16
|
+
* backoff, Fastify wiring) in `@classytic/arc/events`; the dependency-free
|
|
17
|
+
* in-process reference (kernel bus + memory store, for tests/dev) in
|
|
18
|
+
* `@classytic/primitives/event-infra`.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
3
21
|
* Thrown by a store when `acknowledge` / `fail` is called by a consumer that
|
|
4
22
|
* does not own the event's current lease.
|
|
5
23
|
*
|
|
@@ -19,6 +37,30 @@ var OutboxOwnershipError = class extends Error {
|
|
|
19
37
|
this.currentOwner = currentOwner;
|
|
20
38
|
}
|
|
21
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Thrown by `save` when a row with this event id already exists with a DIFFERENT payload.
|
|
42
|
+
* A same-id, same-payload save is an idempotent success; a different payload is two
|
|
43
|
+
* occurrences minted onto one id, and dropping either one loses money silently.
|
|
44
|
+
*/
|
|
45
|
+
var OutboxConflictError = class extends Error {
|
|
46
|
+
name = "OutboxConflictError";
|
|
47
|
+
code = "outbox.conflict";
|
|
48
|
+
eventId;
|
|
49
|
+
eventType;
|
|
50
|
+
constructor(eventId, eventType) {
|
|
51
|
+
super(`Outbox already holds event "${eventId}" (${eventType}) with a different payload — two occurrences share one id. Derive the occurrence from the aggregate id plus the step.`);
|
|
52
|
+
this.eventId = eventId;
|
|
53
|
+
this.eventType = eventType;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Order-independent fingerprint of an event payload as it travels (JSON: `Date` becomes its
|
|
58
|
+
* ISO string, `undefined` drops). Stores compare it on a duplicate id; see {@link OutboxConflictError}.
|
|
59
|
+
*/
|
|
60
|
+
function outboxPayloadFingerprint(event) {
|
|
61
|
+
const wire = JSON.parse(JSON.stringify(event.payload ?? null));
|
|
62
|
+
return canonicalDigest(wire);
|
|
63
|
+
}
|
|
22
64
|
/** Thrown when an event missing `type` or `meta.id` is passed to the outbox. */
|
|
23
65
|
var InvalidOutboxEventError = class extends Error {
|
|
24
66
|
name = "InvalidOutboxEventError";
|
|
@@ -90,4 +132,4 @@ const OUTBOX_STATUSES = [
|
|
|
90
132
|
"dead_letter"
|
|
91
133
|
];
|
|
92
134
|
//#endregion
|
|
93
|
-
export { InvalidOutboxEventError, OUTBOX_STATUSES, OutboxOwnershipError };
|
|
135
|
+
export { InvalidOutboxEventError, OUTBOX_STATUSES, OutboxConflictError, OutboxOwnershipError, outboxPayloadFingerprint };
|
package/dist/price-basis.d.mts
CHANGED
|
@@ -8,7 +8,7 @@ type MeasureDimension = 'mass' | 'duration' | 'volume' | 'length' | 'area';
|
|
|
8
8
|
interface MeasuredPriceBasis {
|
|
9
9
|
readonly kind: 'measured';
|
|
10
10
|
readonly dimension: MeasureDimension;
|
|
11
|
-
/**
|
|
11
|
+
/** Unit carried by the line quantity, such as `gram` or `minute`; the quantity may be fractional. */
|
|
12
12
|
readonly quantityUnit: string;
|
|
13
13
|
/** Unit named by the quoted price, such as `kilogram` or `hour`. */
|
|
14
14
|
readonly priceUnit: string;
|
|
@@ -25,7 +25,10 @@ declare function fixedPriceBasis(): FixedPriceBasis;
|
|
|
25
25
|
declare function measuredPriceBasis(dimension: MeasureDimension, quantityUnit: string, priceUnit: string, quantityPerPriceUnit: number): MeasuredPriceBasis;
|
|
26
26
|
/** Structural guard for database and wire boundaries. */
|
|
27
27
|
declare function isPriceBasis(value: unknown): value is PriceBasis;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Extend a minor-unit price across a non-negative quantity, exactly, on the 6-dp quantity grid,
|
|
30
|
+
* rounding once (half-even). A quantity off the grid throws.
|
|
31
|
+
*/
|
|
29
32
|
declare function resolvePriceBasisTotal(unitPriceMinor: number, quantity: number, basis?: PriceBasis): number;
|
|
30
33
|
//#endregion
|
|
31
34
|
export { FixedPriceBasis, MeasureDimension, MeasuredPriceBasis, PriceBasis, PriceBasisError, PriceBasisErrorCode, fixedPriceBasis, isPriceBasis, measuredPriceBasis, resolvePriceBasisTotal };
|
package/dist/price-basis.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { t as roundedDiv } from "./rounded-div-BWJb98Ws.mjs";
|
|
2
|
+
import { QUANTITY_SCALE, isOnQuantityGrid, quantityToGrid } from "./quantity.mjs";
|
|
2
3
|
//#region src/money/price-basis.ts
|
|
3
4
|
var PriceBasisError = class extends Error {
|
|
4
5
|
name = "PriceBasisError";
|
|
@@ -36,12 +37,17 @@ function isPriceBasis(value) {
|
|
|
36
37
|
if (candidate.kind === "fixed") return true;
|
|
37
38
|
return candidate.kind === "measured" && MEASURE_DIMENSIONS.has(candidate.dimension) && typeof candidate.quantityUnit === "string" && candidate.quantityUnit.trim() !== "" && typeof candidate.priceUnit === "string" && candidate.priceUnit.trim() !== "" && typeof candidate.quantityPerPriceUnit === "number" && Number.isSafeInteger(candidate.quantityPerPriceUnit) && candidate.quantityPerPriceUnit > 0;
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Extend a minor-unit price across a non-negative quantity, exactly, on the 6-dp quantity grid,
|
|
42
|
+
* rounding once (half-even). A quantity off the grid throws.
|
|
43
|
+
*/
|
|
40
44
|
function resolvePriceBasisTotal(unitPriceMinor, quantity, basis = fixedPriceBasis()) {
|
|
41
45
|
if (!Number.isSafeInteger(unitPriceMinor) || unitPriceMinor < 0) throw new PriceBasisError("INVALID_PRICE", "unitPriceMinor must be a non-negative safe integer");
|
|
42
|
-
if (!Number.isSafeInteger(quantity) || quantity
|
|
43
|
-
const
|
|
44
|
-
const
|
|
46
|
+
if (!(quantity >= 0) || !(Number.isSafeInteger(quantity) || isOnQuantityGrid(quantity))) throw new PriceBasisError("INVALID_QUANTITY", `quantity ${quantity} must be a non-negative integer, or a fraction on the 1/${QUANTITY_SCALE} grid`);
|
|
47
|
+
const whole = Number.isSafeInteger(quantity);
|
|
48
|
+
const perPriceUnit = basis.kind === "measured" ? BigInt(basis.quantityPerPriceUnit) : 1n;
|
|
49
|
+
const numerator = BigInt(unitPriceMinor) * (whole ? BigInt(quantity) : quantityToGrid(quantity));
|
|
50
|
+
const denominator = whole ? perPriceUnit : perPriceUnit * BigInt(QUANTITY_SCALE);
|
|
45
51
|
const rounded = roundedDiv(numerator, denominator, "half-even");
|
|
46
52
|
const total = Number(rounded);
|
|
47
53
|
if (!Number.isSafeInteger(total)) throw new PriceBasisError("UNSAFE_TOTAL", "resolved total exceeds Number.MAX_SAFE_INTEGER");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { t as RoundingMode } from "./rounded-div-D7lyJXt8.mjs";
|
|
2
|
+
//#region src/inventory/quantity.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Grid units per stock unit. Equal to `unit-cost-rate`'s internal `QTY_SCALE`;
|
|
5
|
+
* `tests/unit/quantity.test.ts` pins the equality through `extendedAmount`.
|
|
6
|
+
*/
|
|
7
|
+
declare const QUANTITY_SCALE = 1000000;
|
|
8
|
+
type QuantityErrorCode = 'NOT_FINITE' | 'INVALID_STEP' | 'UNSAFE_QUANTITY';
|
|
9
|
+
declare class QuantityError extends Error {
|
|
10
|
+
readonly name = "QuantityError";
|
|
11
|
+
readonly code: QuantityErrorCode;
|
|
12
|
+
constructor(code: QuantityErrorCode, message: string);
|
|
13
|
+
}
|
|
14
|
+
/** A quantity as a signed integer count of grid units (sign-symmetric half-even). */
|
|
15
|
+
declare function quantityToGrid(quantity: number): bigint;
|
|
16
|
+
/** The step in grid units. Refuses a non-positive step, one finer than the grid, or one off it. */
|
|
17
|
+
declare function quantityStepToGrid(step: number): bigint;
|
|
18
|
+
/**
|
|
19
|
+
* Round `quantity` to the nearest multiple of `step` (default: the bare grid).
|
|
20
|
+
*
|
|
21
|
+
* @example snapQuantity(10 - 2.35 - 2.35, 0.001) // 5.3
|
|
22
|
+
* @example snapQuantity(2.3505, 0.001) // 2.35 (half-even)
|
|
23
|
+
*/
|
|
24
|
+
declare function snapQuantity(quantity: number, step?: number, mode?: RoundingMode): number;
|
|
25
|
+
/**
|
|
26
|
+
* Does `value` lie ON the grid (within float noise)? Strict, unlike {@link isOnStep}, which snaps
|
|
27
|
+
* first: `2.3500004` is off the grid here and on the 0.001 step there. Check this at an input
|
|
28
|
+
* boundary, where a 7th decimal is a caller error rather than float drift.
|
|
29
|
+
*/
|
|
30
|
+
declare function isOnQuantityGrid(value: number): boolean;
|
|
31
|
+
/** Is `quantity` a whole multiple of `step`, within half a grid unit? */
|
|
32
|
+
declare function isOnStep(quantity: number, step: number): boolean;
|
|
33
|
+
/** Compare two quantities on the grid: equal when they snap to the same grid unit. */
|
|
34
|
+
declare function compareQuantity(a: number, b: number): -1 | 0 | 1;
|
|
35
|
+
/** `compareQuantity(a, b) === 0`. */
|
|
36
|
+
declare function quantitiesEqual(a: number, b: number): boolean;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { QUANTITY_SCALE, QuantityError, QuantityErrorCode, type RoundingMode, compareQuantity, isOnQuantityGrid, isOnStep, quantitiesEqual, quantityStepToGrid, quantityToGrid, snapQuantity };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { t as roundedDiv } from "./rounded-div-BWJb98Ws.mjs";
|
|
2
|
+
//#region src/inventory/quantity.ts
|
|
3
|
+
/**
|
|
4
|
+
* A decimal stock quantity snapped to its unit's rounding step, on the fixed
|
|
5
|
+
* {@link QUANTITY_SCALE} grid (6 dp — the grid `unit-cost-rate` extends money on).
|
|
6
|
+
*
|
|
7
|
+
* Snap at every write boundary: `10 - 2.35 - 2.35` is `5.300000000000001` in
|
|
8
|
+
* IEEE doubles, and a stored drift never comes back out.
|
|
9
|
+
*
|
|
10
|
+
* Rounding is half-even, the package's money mode: a quantity snap is a
|
|
11
|
+
* per-event normalisation, and half-up biases a running stock balance upward
|
|
12
|
+
* across thousands of weighings.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Grid units per stock unit. Equal to `unit-cost-rate`'s internal `QTY_SCALE`;
|
|
16
|
+
* `tests/unit/quantity.test.ts` pins the equality through `extendedAmount`.
|
|
17
|
+
*/
|
|
18
|
+
const QUANTITY_SCALE = 1e6;
|
|
19
|
+
var QuantityError = class extends Error {
|
|
20
|
+
name = "QuantityError";
|
|
21
|
+
code;
|
|
22
|
+
constructor(code, message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** A quantity as a signed integer count of grid units (sign-symmetric half-even). */
|
|
28
|
+
function quantityToGrid(quantity) {
|
|
29
|
+
if (!Number.isFinite(quantity)) throw new QuantityError("NOT_FINITE", `quantity must be finite, got ${quantity}`);
|
|
30
|
+
const scaled = Math.abs(quantity) * QUANTITY_SCALE;
|
|
31
|
+
if (!Number.isSafeInteger(Math.ceil(scaled))) throw new QuantityError("UNSAFE_QUANTITY", `quantity ${quantity} exceeds the precise grid range`);
|
|
32
|
+
const floor = Math.floor(scaled);
|
|
33
|
+
const frac = scaled - floor;
|
|
34
|
+
const up = frac > .5 || frac === .5 && floor % 2 === 1;
|
|
35
|
+
const magnitude = BigInt(up ? floor + 1 : floor);
|
|
36
|
+
return quantity < 0 ? -magnitude : magnitude;
|
|
37
|
+
}
|
|
38
|
+
function gridToNumber(grid) {
|
|
39
|
+
const value = Number(grid) / QUANTITY_SCALE;
|
|
40
|
+
return value === 0 ? 0 : value;
|
|
41
|
+
}
|
|
42
|
+
/** The step in grid units. Refuses a non-positive step, one finer than the grid, or one off it. */
|
|
43
|
+
function quantityStepToGrid(step) {
|
|
44
|
+
if (!Number.isFinite(step) || step <= 0) throw new QuantityError("INVALID_STEP", `rounding step must be a positive number, got ${step}`);
|
|
45
|
+
const grid = quantityToGrid(step);
|
|
46
|
+
const scaled = step * QUANTITY_SCALE;
|
|
47
|
+
if (grid === 0n || Math.abs(Number(grid) - scaled) > 1e-9 * Math.max(1, scaled)) throw new QuantityError("INVALID_STEP", `rounding step ${step} is not a multiple of 1/${QUANTITY_SCALE}`);
|
|
48
|
+
return grid;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Round `quantity` to the nearest multiple of `step` (default: the bare grid).
|
|
52
|
+
*
|
|
53
|
+
* @example snapQuantity(10 - 2.35 - 2.35, 0.001) // 5.3
|
|
54
|
+
* @example snapQuantity(2.3505, 0.001) // 2.35 (half-even)
|
|
55
|
+
*/
|
|
56
|
+
function snapQuantity(quantity, step, mode = "half-even") {
|
|
57
|
+
const grid = quantityToGrid(quantity);
|
|
58
|
+
if (step === void 0) return gridToNumber(grid);
|
|
59
|
+
const stepGrid = quantityStepToGrid(step);
|
|
60
|
+
return gridToNumber(roundedDiv(grid, stepGrid, mode) * stepGrid);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Does `value` lie ON the grid (within float noise)? Strict, unlike {@link isOnStep}, which snaps
|
|
64
|
+
* first: `2.3500004` is off the grid here and on the 0.001 step there. Check this at an input
|
|
65
|
+
* boundary, where a 7th decimal is a caller error rather than float drift.
|
|
66
|
+
*/
|
|
67
|
+
function isOnQuantityGrid(value) {
|
|
68
|
+
if (!Number.isFinite(value)) return false;
|
|
69
|
+
const scaled = value * QUANTITY_SCALE;
|
|
70
|
+
const grid = Math.round(scaled);
|
|
71
|
+
return Number.isSafeInteger(grid) && Math.abs(grid - scaled) <= 1e-9 * Math.max(1, Math.abs(scaled));
|
|
72
|
+
}
|
|
73
|
+
/** Is `quantity` a whole multiple of `step`, within half a grid unit? */
|
|
74
|
+
function isOnStep(quantity, step) {
|
|
75
|
+
return quantityToGrid(quantity) % quantityStepToGrid(step) === 0n;
|
|
76
|
+
}
|
|
77
|
+
/** Compare two quantities on the grid: equal when they snap to the same grid unit. */
|
|
78
|
+
function compareQuantity(a, b) {
|
|
79
|
+
const ga = quantityToGrid(a);
|
|
80
|
+
const gb = quantityToGrid(b);
|
|
81
|
+
return ga === gb ? 0 : ga < gb ? -1 : 1;
|
|
82
|
+
}
|
|
83
|
+
/** `compareQuantity(a, b) === 0`. */
|
|
84
|
+
function quantitiesEqual(a, b) {
|
|
85
|
+
return compareQuantity(a, b) === 0;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { QUANTITY_SCALE, QuantityError, compareQuantity, isOnQuantityGrid, isOnStep, quantitiesEqual, quantityStepToGrid, quantityToGrid, snapQuantity };
|
|
@@ -60,7 +60,7 @@ interface SplitResult {
|
|
|
60
60
|
readonly method: SplitMethod;
|
|
61
61
|
readonly parts: readonly SplitPart[];
|
|
62
62
|
}
|
|
63
|
-
type SplitAllocationErrorCode = 'INVALID_TOTAL' | 'EMPTY_SUBJECTS' | 'INVALID_WEIGHT' | 'INVALID_PERCENT' | 'ZERO_WEIGHT_SUM' | 'DUPLICATE_SUBJECT_ID';
|
|
63
|
+
type SplitAllocationErrorCode = 'INVALID_TOTAL' | 'EMPTY_SUBJECTS' | 'INVALID_WEIGHT' | 'INVALID_PERCENT' | 'ZERO_WEIGHT_SUM' | 'DUPLICATE_SUBJECT_ID' | 'DISCOUNT_EXCEEDS_BASE';
|
|
64
64
|
declare class SplitAllocationError extends Error {
|
|
65
65
|
readonly name = "SplitAllocationError";
|
|
66
66
|
readonly code: SplitAllocationErrorCode;
|
|
@@ -80,5 +80,23 @@ declare class SplitAllocationError extends Error {
|
|
|
80
80
|
declare function allocate(total: number, subjects: readonly SplitSubject[], method: SplitMethod): SplitResult;
|
|
81
81
|
/** True if `sum(parts[i].amount) === result.total`. Useful as an assertion in host code. */
|
|
82
82
|
declare function isBalanced(result: SplitResult): boolean;
|
|
83
|
+
/** A line a document-level discount may be spread over. */
|
|
84
|
+
interface DiscountSubject {
|
|
85
|
+
readonly id: string;
|
|
86
|
+
/** The line's value before the discount, integer minor units (non-negative). */
|
|
87
|
+
readonly weight: number;
|
|
88
|
+
/** `false` for a line the discount was never computed on (a delivery charge, an auto-added fee). Absent ⇒ eligible. */
|
|
89
|
+
readonly discountable?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* THE discount allocation: spread a document-level discount over its eligible lines, by value,
|
|
93
|
+
* exact largest remainder. Every document that splits one discount — the order, its invoice,
|
|
94
|
+
* its credit notes, a return's value — calls this, so they agree to the minor unit.
|
|
95
|
+
*
|
|
96
|
+
* Returns a FULL map in input order: every subject id, `0` for an ineligible or zero-weight one.
|
|
97
|
+
* `0` discount allocates zeros; a discount equal to the eligible base allocates all of it; a
|
|
98
|
+
* discount above it throws `DISCOUNT_EXCEEDS_BASE` — the figures disagree.
|
|
99
|
+
*/
|
|
100
|
+
declare function allocateDiscount(discount: number, subjects: readonly DiscountSubject[]): ReadonlyMap<string, number>;
|
|
83
101
|
//#endregion
|
|
84
|
-
export { SplitAllocationError, SplitAllocationErrorCode, SplitMethod, SplitPart, SplitResult, SplitSubject, allocate, isBalanced };
|
|
102
|
+
export { DiscountSubject, SplitAllocationError, SplitAllocationErrorCode, SplitMethod, SplitPart, SplitResult, SplitSubject, allocate, allocateDiscount, isBalanced };
|
|
@@ -88,6 +88,7 @@ function resolveWeights(subjects, method) {
|
|
|
88
88
|
function distribute(total, weights) {
|
|
89
89
|
const weightSum = weights.reduce((a, b) => a + b, 0);
|
|
90
90
|
if (weightSum === 0) return weights.map(() => 0);
|
|
91
|
+
if (weights.every(Number.isSafeInteger) && Number.isSafeInteger(weightSum)) return distributeExact(total, weights, weightSum);
|
|
91
92
|
const raw = weights.map((w) => total * w / weightSum);
|
|
92
93
|
const floored = raw.map((r) => Math.floor(r));
|
|
93
94
|
const residue = total - floored.reduce((a, b) => a + b, 0);
|
|
@@ -107,9 +108,64 @@ function distribute(total, weights) {
|
|
|
107
108
|
}
|
|
108
109
|
return result;
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Largest remainder in exact integer arithmetic, for integer weights (minor-unit amounts). Shares
|
|
113
|
+
* and remainders are compared as BigInt quotients of one denominator, so neither the floor nor the
|
|
114
|
+
* remainder ranking can drift once `total × weight` passes 2^53.
|
|
115
|
+
*/
|
|
116
|
+
function distributeExact(total, weights, weightSum) {
|
|
117
|
+
const t = BigInt(total);
|
|
118
|
+
const d = BigInt(weightSum);
|
|
119
|
+
const shares = weights.map((w) => {
|
|
120
|
+
const n = t * BigInt(w);
|
|
121
|
+
return {
|
|
122
|
+
floor: n / d,
|
|
123
|
+
rem: n % d
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const result = shares.map((s) => Number(s.floor));
|
|
127
|
+
const residue = total - result.reduce((a, b) => a + b, 0);
|
|
128
|
+
const order = shares.map((s, i) => ({
|
|
129
|
+
i,
|
|
130
|
+
rem: s.rem
|
|
131
|
+
})).sort((a, b) => a.rem === b.rem ? a.i - b.i : a.rem > b.rem ? -1 : 1);
|
|
132
|
+
for (let k = 0; k < residue; k++) {
|
|
133
|
+
const entry = order[k];
|
|
134
|
+
if (entry === void 0) throw new Error("split-allocation invariant: residue exceeds the subject count");
|
|
135
|
+
result[entry.i] = (result[entry.i] ?? 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
110
139
|
/** True if `sum(parts[i].amount) === result.total`. Useful as an assertion in host code. */
|
|
111
140
|
function isBalanced(result) {
|
|
112
141
|
return result.parts.reduce((acc, p) => acc + p.amount, 0) === result.total;
|
|
113
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* THE discount allocation: spread a document-level discount over its eligible lines, by value,
|
|
145
|
+
* exact largest remainder. Every document that splits one discount — the order, its invoice,
|
|
146
|
+
* its credit notes, a return's value — calls this, so they agree to the minor unit.
|
|
147
|
+
*
|
|
148
|
+
* Returns a FULL map in input order: every subject id, `0` for an ineligible or zero-weight one.
|
|
149
|
+
* `0` discount allocates zeros; a discount equal to the eligible base allocates all of it; a
|
|
150
|
+
* discount above it throws `DISCOUNT_EXCEEDS_BASE` — the figures disagree.
|
|
151
|
+
*/
|
|
152
|
+
function allocateDiscount(discount, subjects) {
|
|
153
|
+
if (!Number.isInteger(discount) || discount < 0) throw new SplitAllocationError("INVALID_TOTAL", `discount must be a non-negative integer, got ${discount}`);
|
|
154
|
+
const out = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const s of subjects) {
|
|
156
|
+
if (out.has(s.id)) throw new SplitAllocationError("DUPLICATE_SUBJECT_ID", `duplicate subject id: ${s.id}`);
|
|
157
|
+
if (!Number.isFinite(s.weight) || s.weight < 0) throw new SplitAllocationError("INVALID_WEIGHT", `subject ${s.id}: weight must be a non-negative number`);
|
|
158
|
+
out.set(s.id, 0);
|
|
159
|
+
}
|
|
160
|
+
const eligible = subjects.filter((s) => s.discountable !== false && s.weight > 0);
|
|
161
|
+
const base = eligible.reduce((sum, s) => sum + s.weight, 0);
|
|
162
|
+
if (discount > base) throw new SplitAllocationError("DISCOUNT_EXCEEDS_BASE", `discount ${discount} exceeds the discountable base ${base}`);
|
|
163
|
+
if (discount === 0) return out;
|
|
164
|
+
for (const part of allocate(discount, eligible.map((s) => ({
|
|
165
|
+
id: s.id,
|
|
166
|
+
weight: s.weight
|
|
167
|
+
})), "by-value").parts) out.set(part.id, part.amount);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
114
170
|
//#endregion
|
|
115
|
-
export { SplitAllocationError, allocate, isBalanced };
|
|
171
|
+
export { SplitAllocationError, allocate, allocateDiscount, isBalanced };
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { O as CurrencyCode, t as MinorUnits } from "./money-C9iLWiWR.mjs";
|
|
2
|
-
|
|
3
|
-
type RoundingMode = 'half-even' | 'half-up';
|
|
4
|
-
//#endregion
|
|
2
|
+
import { t as RoundingMode } from "./rounded-div-D7lyJXt8.mjs";
|
|
5
3
|
//#region src/money/unit-cost-rate.d.ts
|
|
6
4
|
/** Fixed, ecosystem-wide rate scale: 6 decimal places below the minor unit. */
|
|
7
5
|
declare const RATE_SCALE = 1000000;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { n as Money } from "./money-C9iLWiWR.mjs";
|
|
2
|
+
import { UsageRating } from "./monetization.mjs";
|
|
3
|
+
//#region src/money/usage-pricing.d.ts
|
|
4
|
+
/** What ONE unit of a sale line measured: `{ value: 2.3, unit: 'kg' }`. `unit` must equal the rating's `unitLabel`. */
|
|
5
|
+
interface UsageMeasure {
|
|
6
|
+
readonly value: number;
|
|
7
|
+
readonly unit: string;
|
|
8
|
+
}
|
|
9
|
+
/** Structural guard for wire and database boundaries: a positive finite value and a non-blank unit. */
|
|
10
|
+
declare function isUsageMeasure(value: unknown): value is UsageMeasure;
|
|
11
|
+
type UsagePricingErrorCode = 'INVALID_MEASURE' | 'INVALID_INCLUDED_UNITS' | 'UNKNOWN_SCHEME' | 'MISSING_PER_UNIT' | 'MISSING_TIERS' | 'INVALID_TIER' | 'MISSING_PACKAGE' | 'INVALID_MONEY' | 'CURRENCY_MISMATCH' | 'EXCEEDS_RATE_TABLE' | 'UNSAFE_TOTAL';
|
|
12
|
+
declare class UsagePricingError extends Error {
|
|
13
|
+
readonly name = "UsagePricingError";
|
|
14
|
+
readonly code: UsagePricingErrorCode;
|
|
15
|
+
constructor(code: UsagePricingErrorCode, message: string);
|
|
16
|
+
}
|
|
17
|
+
/** One priced line of the breakdown. All money fields are integer minor units. */
|
|
18
|
+
interface UsageChargeLine {
|
|
19
|
+
/** Index into `rating.tiers` for `tiered` / `volume`; `null` for `per_unit` / `package`. */
|
|
20
|
+
readonly band: number | null;
|
|
21
|
+
/** Units priced on this line — PACKAGES under the `package` scheme. */
|
|
22
|
+
readonly quantity: number;
|
|
23
|
+
/** Rate applied per `quantity` (0 when the band is flat-only). */
|
|
24
|
+
readonly unitAmount: number;
|
|
25
|
+
/** Flat charge for landing in the band (0 when none). */
|
|
26
|
+
readonly flatAmount: number;
|
|
27
|
+
/** `quantity × unitAmount + flatAmount`. */
|
|
28
|
+
readonly amount: number;
|
|
29
|
+
}
|
|
30
|
+
/** The priced result and the facts that produced it — freeze this beside the price. */
|
|
31
|
+
interface UsageCharge {
|
|
32
|
+
readonly scheme: UsageRating['scheme'];
|
|
33
|
+
readonly measured: number;
|
|
34
|
+
readonly includedUnits: number;
|
|
35
|
+
/** Whole units charged after `includedUnits`, rounded up. */
|
|
36
|
+
readonly billableUnits: number;
|
|
37
|
+
/** Packages charged — present only under the `package` scheme. */
|
|
38
|
+
readonly packages?: number;
|
|
39
|
+
readonly lines: readonly UsageChargeLine[];
|
|
40
|
+
readonly amount: Money;
|
|
41
|
+
readonly unitLabel?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Price `measured` units against `rating`.
|
|
45
|
+
*
|
|
46
|
+
* `tiered` (graduated) charges each band for the units that fall inside it, plus the band's
|
|
47
|
+
* `flatPrice` when any unit lands there. `volume` charges ALL units at the band the total lands
|
|
48
|
+
* in. A bounded final band refuses a larger amount (`EXCEEDS_RATE_TABLE`) — a courier that does
|
|
49
|
+
* not carry 40 kg parcels says so, it does not quote them at the last rate.
|
|
50
|
+
*/
|
|
51
|
+
declare function priceUsage(rating: UsageRating, measured: number): UsageCharge;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { UsageCharge, UsageChargeLine, UsageMeasure, UsagePricingError, UsagePricingErrorCode, isUsageMeasure, priceUsage };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//#region src/money/usage-pricing.ts
|
|
2
|
+
/** Structural guard for wire and database boundaries: a positive finite value and a non-blank unit. */
|
|
3
|
+
function isUsageMeasure(value) {
|
|
4
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5
|
+
const candidate = value;
|
|
6
|
+
return typeof candidate.value === "number" && Number.isFinite(candidate.value) && candidate.value > 0 && typeof candidate.unit === "string" && candidate.unit.trim() !== "";
|
|
7
|
+
}
|
|
8
|
+
var UsagePricingError = class extends Error {
|
|
9
|
+
name = "UsagePricingError";
|
|
10
|
+
code;
|
|
11
|
+
constructor(code, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Price `measured` units against `rating`.
|
|
18
|
+
*
|
|
19
|
+
* `tiered` (graduated) charges each band for the units that fall inside it, plus the band's
|
|
20
|
+
* `flatPrice` when any unit lands there. `volume` charges ALL units at the band the total lands
|
|
21
|
+
* in. A bounded final band refuses a larger amount (`EXCEEDS_RATE_TABLE`) — a courier that does
|
|
22
|
+
* not carry 40 kg parcels says so, it does not quote them at the last rate.
|
|
23
|
+
*/
|
|
24
|
+
function priceUsage(rating, measured) {
|
|
25
|
+
if (typeof measured !== "number" || !Number.isFinite(measured) || measured < 0) throw new UsagePricingError("INVALID_MEASURE", `measured must be a finite number >= 0, got ${String(measured)}`);
|
|
26
|
+
const includedUnits = rating.includedUnits ?? 0;
|
|
27
|
+
if (typeof includedUnits !== "number" || !Number.isFinite(includedUnits) || includedUnits < 0) throw new UsagePricingError("INVALID_INCLUDED_UNITS", `includedUnits must be a finite number >= 0, got ${String(includedUnits)}`);
|
|
28
|
+
const over = Number((measured - includedUnits).toPrecision(12));
|
|
29
|
+
const billableUnits = over > 0 ? Math.ceil(over) : 0;
|
|
30
|
+
const base = {
|
|
31
|
+
scheme: rating.scheme,
|
|
32
|
+
measured,
|
|
33
|
+
includedUnits,
|
|
34
|
+
billableUnits,
|
|
35
|
+
...rating.unitLabel !== void 0 ? { unitLabel: rating.unitLabel } : {}
|
|
36
|
+
};
|
|
37
|
+
switch (rating.scheme) {
|
|
38
|
+
case "per_unit": {
|
|
39
|
+
const rate = requireMoney(rating.perUnit, "MISSING_PER_UNIT", "per_unit needs perUnit");
|
|
40
|
+
const lines = billableUnits > 0 ? [line(null, billableUnits, rate.amount, 0)] : [];
|
|
41
|
+
return {
|
|
42
|
+
...base,
|
|
43
|
+
lines,
|
|
44
|
+
amount: total(lines, rate.currency)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
case "package": {
|
|
48
|
+
if (!Number.isSafeInteger(rating.packageSize) || (rating.packageSize ?? 0) <= 0) throw new UsagePricingError("MISSING_PACKAGE", "package needs packageSize as a positive integer");
|
|
49
|
+
const price = requireMoney(rating.packagePrice, "MISSING_PACKAGE", "package needs packagePrice");
|
|
50
|
+
const packages = Math.ceil(billableUnits / rating.packageSize);
|
|
51
|
+
const lines = packages > 0 ? [line(null, packages, price.amount, 0)] : [];
|
|
52
|
+
return {
|
|
53
|
+
...base,
|
|
54
|
+
packages,
|
|
55
|
+
lines,
|
|
56
|
+
amount: total(lines, price.currency)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
case "tiered":
|
|
60
|
+
case "volume": {
|
|
61
|
+
const { tiers, currency } = validTiers(rating.tiers);
|
|
62
|
+
const lines = rating.scheme === "tiered" ? graduated(tiers, billableUnits) : volume(tiers, billableUnits);
|
|
63
|
+
return {
|
|
64
|
+
...base,
|
|
65
|
+
lines,
|
|
66
|
+
amount: total(lines, currency)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
default: {
|
|
70
|
+
const unknownScheme = rating.scheme;
|
|
71
|
+
throw new UsagePricingError("UNKNOWN_SCHEME", `unknown usage scheme: ${String(unknownScheme)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function requireMoney(value, code, message) {
|
|
76
|
+
if (!value) throw new UsagePricingError(code, message);
|
|
77
|
+
assertMoney(value);
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function assertMoney(value) {
|
|
81
|
+
if (!Number.isSafeInteger(value.amount) || value.amount < 0 || typeof value.currency !== "string") throw new UsagePricingError("INVALID_MONEY", `a rate must be a non-negative integer minor amount with a currency, got ${JSON.stringify(value)}`);
|
|
82
|
+
}
|
|
83
|
+
function validTiers(tiers) {
|
|
84
|
+
if (!Array.isArray(tiers) || tiers.length === 0) throw new UsagePricingError("MISSING_TIERS", "tiered and volume schemes need at least one tier");
|
|
85
|
+
let currency;
|
|
86
|
+
let previous = 0;
|
|
87
|
+
tiers.forEach((tier, i) => {
|
|
88
|
+
if (tier.perUnit === void 0 && tier.flatPrice === void 0) throw new UsagePricingError("INVALID_TIER", `tier ${i} has neither perUnit nor flatPrice`);
|
|
89
|
+
for (const money of [tier.perUnit, tier.flatPrice]) {
|
|
90
|
+
if (money === void 0) continue;
|
|
91
|
+
assertMoney(money);
|
|
92
|
+
if (currency === void 0) currency = money.currency;
|
|
93
|
+
else if (money.currency !== currency) throw new UsagePricingError("CURRENCY_MISMATCH", `tier ${i} is in ${money.currency}, the table in ${currency}`);
|
|
94
|
+
}
|
|
95
|
+
if (tier.upTo === void 0) {
|
|
96
|
+
if (i !== tiers.length - 1) throw new UsagePricingError("INVALID_TIER", `tier ${i} is open-ended but is not the last tier`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!Number.isSafeInteger(tier.upTo) || tier.upTo <= previous) throw new UsagePricingError("INVALID_TIER", `tier ${i} upTo must be an integer above the previous bound ${previous}, got ${String(tier.upTo)}`);
|
|
100
|
+
previous = tier.upTo;
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
tiers,
|
|
104
|
+
currency
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function graduated(tiers, units) {
|
|
108
|
+
const lines = [];
|
|
109
|
+
let floor = 0;
|
|
110
|
+
for (let i = 0; i < tiers.length && floor < units; i++) {
|
|
111
|
+
const tier = tiers[i];
|
|
112
|
+
const ceiling = tier.upTo ?? Number.POSITIVE_INFINITY;
|
|
113
|
+
const inBand = Math.min(units, ceiling) - floor;
|
|
114
|
+
lines.push(line(i, inBand, tier.perUnit?.amount ?? 0, tier.flatPrice?.amount ?? 0));
|
|
115
|
+
floor = ceiling;
|
|
116
|
+
}
|
|
117
|
+
if (floor < units) throw exceeds(units, floor);
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
function volume(tiers, units) {
|
|
121
|
+
if (units === 0) return [];
|
|
122
|
+
const band = tiers.findIndex((tier) => tier.upTo === void 0 || units <= tier.upTo);
|
|
123
|
+
if (band === -1) throw exceeds(units, tiers[tiers.length - 1]?.upTo ?? 0);
|
|
124
|
+
const tier = tiers[band];
|
|
125
|
+
return [line(band, units, tier.perUnit?.amount ?? 0, tier.flatPrice?.amount ?? 0)];
|
|
126
|
+
}
|
|
127
|
+
function exceeds(units, limit) {
|
|
128
|
+
return new UsagePricingError("EXCEEDS_RATE_TABLE", `${units} units exceed the rate table's last bound ${limit}`);
|
|
129
|
+
}
|
|
130
|
+
function line(band, quantity, unitAmount, flatAmount) {
|
|
131
|
+
const amount = quantity * unitAmount + flatAmount;
|
|
132
|
+
if (!Number.isSafeInteger(amount)) throw new UsagePricingError("UNSAFE_TOTAL", "usage charge exceeds Number.MAX_SAFE_INTEGER");
|
|
133
|
+
return {
|
|
134
|
+
band,
|
|
135
|
+
quantity,
|
|
136
|
+
unitAmount,
|
|
137
|
+
flatAmount,
|
|
138
|
+
amount
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function total(lines, currency) {
|
|
142
|
+
const amount = lines.reduce((sum, l) => sum + l.amount, 0);
|
|
143
|
+
if (!Number.isSafeInteger(amount)) throw new UsagePricingError("UNSAFE_TOTAL", "usage charge exceeds Number.MAX_SAFE_INTEGER");
|
|
144
|
+
return {
|
|
145
|
+
amount,
|
|
146
|
+
currency
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { UsagePricingError, isUsageMeasure, priceUsage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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",
|
|
@@ -96,6 +96,11 @@
|
|
|
96
96
|
"types": "./dist/memory-outbox.d.mts",
|
|
97
97
|
"default": "./dist/memory-outbox.mjs"
|
|
98
98
|
},
|
|
99
|
+
"./durable-events": {
|
|
100
|
+
"@classytic/source": "./src/events/durable.ts",
|
|
101
|
+
"types": "./dist/durable-events.d.mts",
|
|
102
|
+
"default": "./dist/durable-events.mjs"
|
|
103
|
+
},
|
|
99
104
|
"./event-infra": {
|
|
100
105
|
"@classytic/source": "./src/events/event-infra.ts",
|
|
101
106
|
"types": "./dist/event-infra.d.mts",
|
|
@@ -292,6 +297,16 @@
|
|
|
292
297
|
"types": "./dist/signed-token.d.mts",
|
|
293
298
|
"default": "./dist/signed-token.mjs"
|
|
294
299
|
},
|
|
300
|
+
"./quantity": {
|
|
301
|
+
"@classytic/source": "./src/inventory/quantity.ts",
|
|
302
|
+
"types": "./dist/quantity.d.mts",
|
|
303
|
+
"default": "./dist/quantity.mjs"
|
|
304
|
+
},
|
|
305
|
+
"./usage-pricing": {
|
|
306
|
+
"@classytic/source": "./src/money/usage-pricing.ts",
|
|
307
|
+
"types": "./dist/usage-pricing.d.mts",
|
|
308
|
+
"default": "./dist/usage-pricing.mjs"
|
|
309
|
+
},
|
|
295
310
|
"./item-stock": {
|
|
296
311
|
"@classytic/source": "./src/inventory/item-stock.ts",
|
|
297
312
|
"types": "./dist/item-stock.d.mts",
|