@classytic/primitives 0.1.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.
@@ -0,0 +1,240 @@
1
+ //#region src/events.d.ts
2
+ /**
3
+ * Domain event transport primitives.
4
+ *
5
+ * These shapes MIRROR `@classytic/arc`'s event types verbatim. Arc owns the
6
+ * contract — primitives tracks. Keep this file bit-identical to
7
+ * `packages/reference-only/arc/src/events/EventTransport.ts` for `EventMeta`,
8
+ * `DomainEvent`, `EventTransport`, `DeadLetteredEvent`, and
9
+ * `PublishManyResult`. Helpers (`createEvent`, `createChildEvent`) are
10
+ * allowed to be more defensive (e.g. fall back when `crypto.randomUUID` is
11
+ * unavailable) but their signatures must match arc.
12
+ *
13
+ * Packages declare these from primitives (instead of importing arc) so they
14
+ * remain runtime-independent of arc while still plugging into any arc
15
+ * `EventTransport` without adapters. See PACKAGE_RULES.md §11.
16
+ */
17
+ /**
18
+ * Event metadata.
19
+ *
20
+ * Mirrored from arc v2.9. Any consumer using `@classytic/arc`'s `EventMeta`
21
+ * type gets a structural match with primitives' `EventMeta` and vice-versa.
22
+ */
23
+ interface EventMeta {
24
+ /** Unique event identifier — UUID v4 recommended. */
25
+ id: string;
26
+ /** Emit timestamp. */
27
+ timestamp: Date;
28
+ /**
29
+ * Schema version for this event type. Default: `1`.
30
+ *
31
+ * Use when the payload shape evolves so handlers can branch on version
32
+ * during migration windows (`if (event.meta.schemaVersion === 2) ...`).
33
+ * Bump ONLY when the payload contract changes in a breaking way.
34
+ */
35
+ schemaVersion?: number;
36
+ /**
37
+ * Correlation ID — stays stable across an entire causal chain so a single
38
+ * user action can be traced through every downstream event. Spans service
39
+ * boundaries. Generated at the edge (HTTP request, CLI invocation) and
40
+ * inherited by every child event.
41
+ *
42
+ * Distinct from {@link causationId}: **correlation groups, causation chains.**
43
+ */
44
+ correlationId?: string;
45
+ /**
46
+ * Causation ID — the `meta.id` of the direct parent event that caused
47
+ * this one. Forms a linked-list of cause-and-effect within a correlation.
48
+ *
49
+ * Use {@link createChildEvent} to populate this automatically so handlers
50
+ * can't forget to propagate causality.
51
+ */
52
+ causationId?: string;
53
+ /**
54
+ * Partition key hint for ordered transports (Kafka, Kinesis, Redis Streams
55
+ * consumer groups). Events with the same partitionKey are guaranteed to be
56
+ * delivered in publish order by transports that honour it.
57
+ *
58
+ * Defaults to `resourceId` if unset. Transports that don't support ordering
59
+ * (in-memory, simple pub/sub) ignore this field.
60
+ */
61
+ partitionKey?: string;
62
+ /** Source resource name — e.g. 'order', 'transaction'. */
63
+ resource?: string;
64
+ /** Resource identifier — typically the document's public id. */
65
+ resourceId?: string;
66
+ /** User who triggered the event. */
67
+ userId?: string;
68
+ /** Organization / tenant scope. */
69
+ organizationId?: string;
70
+ /**
71
+ * Originating service or package (e.g. `'commerce'`, `'billing'`, `'arc-core'`).
72
+ *
73
+ * In a multi-service deployment, consumers route / log / alert by `source`
74
+ * without parsing `type` prefixes. Arc itself never populates this — hosts
75
+ * set it once per emitter. Inherited by {@link createChildEvent} so
76
+ * downstream events carry the same source unless overridden.
77
+ */
78
+ source?: string;
79
+ /**
80
+ * Idempotency key — stable hint that this event represents a specific
81
+ * operation exactly once. Consumers dedupe with
82
+ * `if (processed.has(meta.idempotencyKey)) return`.
83
+ *
84
+ * Survives every transport (Memory / Pub-Sub / Streams / Kafka) because
85
+ * it's part of the event, not a transport-side option. Distinct from
86
+ * `meta.id` (which is fresh per emit — a retry would produce a new id).
87
+ *
88
+ * Typical sources: HTTP `Idempotency-Key` header, outbox `dedupeKey`, or
89
+ * `{aggregate.type}:{aggregate.id}:{action}`. Inherited by child events.
90
+ */
91
+ idempotencyKey?: string;
92
+ /**
93
+ * DDD aggregate marker — the aggregate that owns this event's invariant.
94
+ *
95
+ * Use when routing events by aggregate, doing event-sourcing replay, or
96
+ * enforcing consistency boundaries. Distinct from `resource` / `resourceId`
97
+ * (HTTP-origin entity) because an event emitted *by* one REST resource can
98
+ * *belong to* a different aggregate (e.g. `POST /orders/:id/ship` emits
99
+ * `shipment.dispatched` owned by a shipment aggregate).
100
+ *
101
+ * Downstream packages narrow `aggregate.type` to their own string union via
102
+ * interface extension:
103
+ *
104
+ * ```ts
105
+ * type CartAggregateType = 'cart' | 'cart-item';
106
+ * interface CartEventMeta extends EventMeta {
107
+ * aggregate?: { type: CartAggregateType; id: string };
108
+ * }
109
+ * ```
110
+ *
111
+ * Not inherited by {@link createChildEvent} — child events typically belong
112
+ * to a different aggregate than their parent.
113
+ */
114
+ aggregate?: {
115
+ type: string;
116
+ id: string;
117
+ };
118
+ }
119
+ interface DomainEvent<T = unknown> {
120
+ /** Dotted / colon-separated event name — e.g. 'order:placed'. */
121
+ type: string;
122
+ payload: T;
123
+ meta: EventMeta;
124
+ }
125
+ type EventHandler<T = unknown> = (event: DomainEvent<T>) => void | Promise<void>;
126
+ /**
127
+ * A permanently-failed event routed to a dead-letter sink after retries
128
+ * have been exhausted. Mirrors the shape a caller would log, alert on, or
129
+ * replay from once the upstream issue is fixed.
130
+ */
131
+ interface DeadLetteredEvent<T = unknown> {
132
+ /** The original event. */
133
+ event: DomainEvent<T>;
134
+ /** Serialised failure reason (message + optional machine code + stack). */
135
+ error: {
136
+ message: string;
137
+ code?: string;
138
+ stack?: string;
139
+ };
140
+ /** How many delivery attempts were made before giving up. */
141
+ attempts: number;
142
+ /** First failure timestamp. */
143
+ firstFailedAt: Date;
144
+ /** Last failure timestamp (immediately before dead-lettering). */
145
+ lastFailedAt: Date;
146
+ /** Optional handler / subscriber name that last failed (for debug). */
147
+ handlerName?: string;
148
+ }
149
+ /**
150
+ * Minimal logger interface for event transports. Compatible with `console`,
151
+ * `pino`, `fastify.log`, and any custom logger. Mirrors arc's `EventLogger`.
152
+ */
153
+ interface EventLogger {
154
+ warn(message: string, ...args: unknown[]): void;
155
+ error(message: string, ...args: unknown[]): void;
156
+ }
157
+ /**
158
+ * Per-event publish outcome keyed by `meta.id`. `null` entry = success,
159
+ * `Error` = failure for that specific event. Matches arc's `publishMany`
160
+ * return contract.
161
+ */
162
+ type PublishManyResult = ReadonlyMap<string, Error | null>;
163
+ /**
164
+ * Transport contract. Any object matching this shape — arc's
165
+ * `MemoryEventTransport`, a Redis-backed transport, a Kafka publisher, or an
166
+ * in-process bus — can be passed into a Classytic package as
167
+ * `config.eventTransport`.
168
+ */
169
+ interface EventTransport {
170
+ readonly name: string;
171
+ publish(event: DomainEvent): Promise<void>;
172
+ /**
173
+ * Publish a batch of events in one round-trip (optional). Arc v2.8.1+
174
+ * transports implement this; outbox relays auto-detect it for higher
175
+ * throughput. Consumers must fall back to per-event `publish()` when
176
+ * absent.
177
+ */
178
+ publishMany?(events: readonly DomainEvent[]): Promise<PublishManyResult>;
179
+ /**
180
+ * Subscribe to a glob pattern — exact match, `*`, or `resource.*`. Returns
181
+ * an unsubscribe function.
182
+ */
183
+ subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
184
+ /**
185
+ * Route a permanently-failed event to the transport's dead-letter sink
186
+ * (Kafka DLQ topic, SQS DLQ, Redis Stream `PEL` timeout handler, etc.).
187
+ *
188
+ * Called by outbox relays after exhausting retries. Transports that don't
189
+ * have a native DLQ can omit this — callers treat an absent `deadLetter`
190
+ * as "log and drop".
191
+ */
192
+ deadLetter?(dlq: DeadLetteredEvent): Promise<void>;
193
+ close?(): Promise<void>;
194
+ }
195
+ /**
196
+ * Build a {@link DomainEvent} with auto-filled `id` + `timestamp`. All other
197
+ * `EventMeta` fields are caller-controlled and override the defaults if
198
+ * supplied (same semantics as arc's `createEvent`).
199
+ */
200
+ declare function createEvent<T>(type: string, payload: T, meta?: Partial<EventMeta>): DomainEvent<T>;
201
+ /**
202
+ * Create a child event that chains causation from a parent event.
203
+ *
204
+ * Rules (mirrored from arc v2.9):
205
+ * - `causationId` is set to the parent's `id` (direct cause)
206
+ * - `correlationId` is inherited from the parent if set, else falls back
207
+ * to the parent's `id` (root correlation)
208
+ * - `userId` / `organizationId` / `source` / `idempotencyKey` are inherited
209
+ * when set on the parent so the whole chain stays scoped to the
210
+ * originating principal, tenant, emitter, and logical operation
211
+ * - `aggregate` is NOT inherited — child events typically belong to a
212
+ * different aggregate than their parent
213
+ *
214
+ * Caller-supplied `meta` wins over inherited fields — pass `{ userId: newActor }`
215
+ * to override when a subsystem acts on behalf of a different principal.
216
+ *
217
+ * @example
218
+ * const orderPlaced = createEvent('order.placed', { orderId: 'o1' }, {
219
+ * correlationId: req.id, userId: user.id,
220
+ * });
221
+ *
222
+ * const reserved = createChildEvent(orderPlaced, 'inventory.reserved', {
223
+ * orderId: 'o1', skus: ['sku-1', 'sku-2'],
224
+ * });
225
+ * // reserved.meta.causationId === orderPlaced.meta.id
226
+ * // reserved.meta.correlationId === orderPlaced.meta.correlationId
227
+ * // reserved.meta.userId === user.id (inherited)
228
+ */
229
+ declare function createChildEvent<T>(parent: DomainEvent, type: string, payload: T, meta?: Partial<EventMeta>): DomainEvent<T>;
230
+ /**
231
+ * Match an event name against a glob pattern. Implements the same rules as
232
+ * arc's `MemoryEventTransport`:
233
+ * - `*` matches anything
234
+ * - `prefix.*` matches anything starting with `prefix.`
235
+ * - `prefix:*` matches anything starting with `prefix:` (Classytic convention)
236
+ * - exact match otherwise
237
+ */
238
+ declare function matchEventPattern(pattern: string, type: string): boolean;
239
+ //#endregion
240
+ export { DeadLetteredEvent, DomainEvent, EventHandler, EventLogger, EventMeta, EventTransport, PublishManyResult, createChildEvent, createEvent, matchEventPattern };
@@ -0,0 +1,90 @@
1
+ //#region src/events.ts
2
+ /**
3
+ * Build a {@link DomainEvent} with auto-filled `id` + `timestamp`. All other
4
+ * `EventMeta` fields are caller-controlled and override the defaults if
5
+ * supplied (same semantics as arc's `createEvent`).
6
+ */
7
+ function createEvent(type, payload, meta) {
8
+ return {
9
+ type,
10
+ payload,
11
+ meta: {
12
+ id: randomEventId(),
13
+ timestamp: /* @__PURE__ */ new Date(),
14
+ ...meta
15
+ }
16
+ };
17
+ }
18
+ /**
19
+ * Create a child event that chains causation from a parent event.
20
+ *
21
+ * Rules (mirrored from arc v2.9):
22
+ * - `causationId` is set to the parent's `id` (direct cause)
23
+ * - `correlationId` is inherited from the parent if set, else falls back
24
+ * to the parent's `id` (root correlation)
25
+ * - `userId` / `organizationId` / `source` / `idempotencyKey` are inherited
26
+ * when set on the parent so the whole chain stays scoped to the
27
+ * originating principal, tenant, emitter, and logical operation
28
+ * - `aggregate` is NOT inherited — child events typically belong to a
29
+ * different aggregate than their parent
30
+ *
31
+ * Caller-supplied `meta` wins over inherited fields — pass `{ userId: newActor }`
32
+ * to override when a subsystem acts on behalf of a different principal.
33
+ *
34
+ * @example
35
+ * const orderPlaced = createEvent('order.placed', { orderId: 'o1' }, {
36
+ * correlationId: req.id, userId: user.id,
37
+ * });
38
+ *
39
+ * const reserved = createChildEvent(orderPlaced, 'inventory.reserved', {
40
+ * orderId: 'o1', skus: ['sku-1', 'sku-2'],
41
+ * });
42
+ * // reserved.meta.causationId === orderPlaced.meta.id
43
+ * // reserved.meta.correlationId === orderPlaced.meta.correlationId
44
+ * // reserved.meta.userId === user.id (inherited)
45
+ */
46
+ function createChildEvent(parent, type, payload, meta) {
47
+ const inherited = {
48
+ correlationId: parent.meta.correlationId ?? parent.meta.id,
49
+ causationId: parent.meta.id
50
+ };
51
+ if (parent.meta.userId !== void 0) inherited.userId = parent.meta.userId;
52
+ if (parent.meta.organizationId !== void 0) inherited.organizationId = parent.meta.organizationId;
53
+ if (parent.meta.source !== void 0) inherited.source = parent.meta.source;
54
+ if (parent.meta.idempotencyKey !== void 0) inherited.idempotencyKey = parent.meta.idempotencyKey;
55
+ return {
56
+ type,
57
+ payload,
58
+ meta: {
59
+ id: randomEventId(),
60
+ timestamp: /* @__PURE__ */ new Date(),
61
+ ...inherited,
62
+ ...meta
63
+ }
64
+ };
65
+ }
66
+ /**
67
+ * Match an event name against a glob pattern. Implements the same rules as
68
+ * arc's `MemoryEventTransport`:
69
+ * - `*` matches anything
70
+ * - `prefix.*` matches anything starting with `prefix.`
71
+ * - `prefix:*` matches anything starting with `prefix:` (Classytic convention)
72
+ * - exact match otherwise
73
+ */
74
+ function matchEventPattern(pattern, type) {
75
+ if (pattern === "*") return true;
76
+ if (pattern.endsWith(".*")) return type.startsWith(pattern.slice(0, -1));
77
+ if (pattern.endsWith(":*")) return type.startsWith(pattern.slice(0, -1));
78
+ return pattern === type;
79
+ }
80
+ /** Minimal UUID-v4-style identifier. Uses `crypto.randomUUID` when available. */
81
+ function randomEventId() {
82
+ const g = globalThis;
83
+ if (g.crypto?.randomUUID) return g.crypto.randomUUID();
84
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
85
+ const r = Math.random() * 16 | 0;
86
+ return (c === "x" ? r : r & 3 | 8).toString(16);
87
+ });
88
+ }
89
+ //#endregion
90
+ export { createChildEvent, createEvent, matchEventPattern };
@@ -0,0 +1,89 @@
1
+ //#region src/hold.d.ts
2
+ /**
3
+ * Hold reason value object + collection helpers.
4
+ *
5
+ * A `HoldReason` records that a domain document is blocked from progressing
6
+ * until the hold is resolved. The primitive is *pure data + pure helpers* —
7
+ * no FSM, no persistence, no events. Hosts embed `HoldReason[]` on their
8
+ * documents and use the helpers to add / resolve / query.
9
+ *
10
+ * Consumers: `@classytic/order` (fraud, manual, payment, credit, inventory
11
+ * holds), `@classytic/flow` (quality hold, customs hold), `@classytic/revenue`
12
+ * (chargeback hold).
13
+ *
14
+ * @example
15
+ * import { addHold, resolveHold, activeHolds, isOnHold } from '@classytic/primitives/hold';
16
+ *
17
+ * let holds = addHold([], {
18
+ * code: 'credit_check_failed',
19
+ * note: 'customer exceeds $50k limit',
20
+ * actor: { id: 'system', kind: 'system' },
21
+ * });
22
+ * isOnHold(holds); // true
23
+ * holds = resolveHold(holds, holds[0].id, { actor: { id: 'user_42', kind: 'user' }, note: 'override approved' });
24
+ * isOnHold(holds); // false
25
+ */
26
+ type HoldActorKind = 'user' | 'system' | 'service';
27
+ interface HoldActor {
28
+ readonly id: string;
29
+ readonly kind: HoldActorKind;
30
+ readonly name?: string;
31
+ }
32
+ interface HoldReason {
33
+ /**
34
+ * Unique identifier within the collection — used to resolve a specific hold
35
+ * when multiple are active. Generated by {@link addHold} if caller omits it.
36
+ */
37
+ readonly id: string;
38
+ /** Domain-defined stable code, e.g. `fraud_review`, `customs_detention`. */
39
+ readonly code: string;
40
+ readonly note?: string;
41
+ readonly actor: HoldActor;
42
+ readonly createdAt: Date;
43
+ readonly resolvedAt?: Date;
44
+ readonly resolvedBy?: HoldActor;
45
+ readonly resolutionNote?: string;
46
+ readonly metadata?: Readonly<Record<string, unknown>>;
47
+ }
48
+ type HoldErrorCode = 'UNKNOWN_HOLD_ID' | 'ALREADY_RESOLVED' | 'INVALID_CODE' | 'DUPLICATE_HOLD_ID';
49
+ declare class HoldError extends Error {
50
+ readonly name = "HoldError";
51
+ readonly code: HoldErrorCode;
52
+ constructor(code: HoldErrorCode, message: string);
53
+ }
54
+ interface AddHoldInput {
55
+ readonly id?: string;
56
+ readonly code: string;
57
+ readonly note?: string;
58
+ readonly actor: HoldActor;
59
+ readonly createdAt?: Date;
60
+ readonly metadata?: Readonly<Record<string, unknown>>;
61
+ }
62
+ interface ResolveHoldInput {
63
+ readonly actor: HoldActor;
64
+ readonly note?: string;
65
+ readonly resolvedAt?: Date;
66
+ }
67
+ /**
68
+ * Add a new hold to the collection. Returns a new array — original is not
69
+ * mutated. `id` is generated if omitted (stable format: `hold_{epochMs}_{rand}`).
70
+ */
71
+ declare function addHold(holds: readonly HoldReason[], input: AddHoldInput): readonly HoldReason[];
72
+ /**
73
+ * Resolve a specific hold by id. Throws if the id is unknown or the hold is
74
+ * already resolved.
75
+ */
76
+ declare function resolveHold(holds: readonly HoldReason[], id: string, input: ResolveHoldInput): readonly HoldReason[];
77
+ /** All holds that have not yet been resolved. */
78
+ declare function activeHolds(holds: readonly HoldReason[]): readonly HoldReason[];
79
+ /** True if at least one active (unresolved) hold exists. */
80
+ declare function isOnHold(holds: readonly HoldReason[]): boolean;
81
+ /**
82
+ * True if at least one active hold matches the given code. Useful for checking
83
+ * for a specific class of hold (e.g., "is this order on a fraud hold?").
84
+ */
85
+ declare function hasActiveHoldOfCode(holds: readonly HoldReason[], code: string): boolean;
86
+ /** All resolved holds — useful for audit timelines. */
87
+ declare function resolvedHolds(holds: readonly HoldReason[]): readonly HoldReason[];
88
+ //#endregion
89
+ export { AddHoldInput, HoldActor, HoldActorKind, HoldError, HoldErrorCode, HoldReason, ResolveHoldInput, activeHolds, addHold, hasActiveHoldOfCode, isOnHold, resolveHold, resolvedHolds };
package/dist/hold.mjs ADDED
@@ -0,0 +1,69 @@
1
+ //#region src/hold.ts
2
+ var HoldError = class extends Error {
3
+ name = "HoldError";
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.code = code;
8
+ }
9
+ };
10
+ /**
11
+ * Add a new hold to the collection. Returns a new array — original is not
12
+ * mutated. `id` is generated if omitted (stable format: `hold_{epochMs}_{rand}`).
13
+ */
14
+ function addHold(holds, input) {
15
+ if (!input.code || input.code.trim() === "") throw new HoldError("INVALID_CODE", "hold code must be a non-empty string");
16
+ const id = input.id ?? generateHoldId();
17
+ if (holds.some((h) => h.id === id)) throw new HoldError("DUPLICATE_HOLD_ID", `hold id already present: ${id}`);
18
+ const hold = {
19
+ id,
20
+ code: input.code,
21
+ ...input.note !== void 0 ? { note: input.note } : {},
22
+ actor: input.actor,
23
+ createdAt: input.createdAt ?? /* @__PURE__ */ new Date(),
24
+ ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
25
+ };
26
+ return [...holds, hold];
27
+ }
28
+ /**
29
+ * Resolve a specific hold by id. Throws if the id is unknown or the hold is
30
+ * already resolved.
31
+ */
32
+ function resolveHold(holds, id, input) {
33
+ const index = holds.findIndex((h) => h.id === id);
34
+ if (index === -1) throw new HoldError("UNKNOWN_HOLD_ID", `no hold with id ${id}`);
35
+ const hold = holds[index];
36
+ if (!hold) throw new HoldError("UNKNOWN_HOLD_ID", `no hold with id ${id}`);
37
+ if (hold.resolvedAt !== void 0) throw new HoldError("ALREADY_RESOLVED", `hold ${id} is already resolved`);
38
+ const resolved = {
39
+ ...hold,
40
+ resolvedAt: input.resolvedAt ?? /* @__PURE__ */ new Date(),
41
+ resolvedBy: input.actor,
42
+ ...input.note !== void 0 ? { resolutionNote: input.note } : {}
43
+ };
44
+ return holds.map((h, i) => i === index ? resolved : h);
45
+ }
46
+ /** All holds that have not yet been resolved. */
47
+ function activeHolds(holds) {
48
+ return holds.filter((h) => h.resolvedAt === void 0);
49
+ }
50
+ /** True if at least one active (unresolved) hold exists. */
51
+ function isOnHold(holds) {
52
+ return holds.some((h) => h.resolvedAt === void 0);
53
+ }
54
+ /**
55
+ * True if at least one active hold matches the given code. Useful for checking
56
+ * for a specific class of hold (e.g., "is this order on a fraud hold?").
57
+ */
58
+ function hasActiveHoldOfCode(holds, code) {
59
+ return holds.some((h) => h.resolvedAt === void 0 && h.code === code);
60
+ }
61
+ /** All resolved holds — useful for audit timelines. */
62
+ function resolvedHolds(holds) {
63
+ return holds.filter((h) => h.resolvedAt !== void 0);
64
+ }
65
+ function generateHoldId() {
66
+ return `hold_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
67
+ }
68
+ //#endregion
69
+ export { HoldError, activeHolds, addHold, hasActiveHoldOfCode, isOnHold, resolveHold, resolvedHolds };
@@ -0,0 +1,78 @@
1
+ import { CurrencyCode } from "./currency.mjs";
2
+
3
+ //#region src/money.d.ts
4
+ /**
5
+ * A monetary amount stored as an integer in the currency's minor unit.
6
+ *
7
+ * Examples:
8
+ * - `{ amount: 19_99, currency: 'USD' }` — 19.99 USD (2 decimals)
9
+ * - `{ amount: 1_500, currency: 'JPY' }` — 1500 JPY (0 decimals)
10
+ * - `{ amount: 500_000, currency: 'KWD' }` — 500.000 KWD (3 decimals)
11
+ *
12
+ * Storing as integers avoids IEEE 754 rounding across arithmetic. Never store
13
+ * `Money.amount` as a float.
14
+ */
15
+ interface Money {
16
+ readonly amount: number;
17
+ readonly currency: CurrencyCode | string;
18
+ }
19
+ declare class CurrencyMismatchError extends Error {
20
+ readonly name = "CurrencyMismatchError";
21
+ readonly left: string;
22
+ readonly right: string;
23
+ constructor(left: string, right: string);
24
+ }
25
+ /**
26
+ * Construct {@link Money} from an integer minor-unit amount.
27
+ *
28
+ * @throws TypeError if `amount` is not a finite integer.
29
+ */
30
+ declare function money(amount: number, currency: string): Money;
31
+ /**
32
+ * Construct {@link Money} from a major-unit number (e.g. dollars, taka) by
33
+ * scaling to the currency's minor unit and rounding half-away-from-zero.
34
+ *
35
+ * Uses `toPrecision(15)` to strip the trailing-bit noise that appears in
36
+ * multiply-then-round (e.g. `0.1 + 0.2 = 0.30000000000000004`). This is
37
+ * enough for the vast majority of decimals encountered in real-world pricing.
38
+ *
39
+ * **Precision caveat.** A handful of decimals cannot be represented exactly
40
+ * in IEEE 754 and will round to the IEEE-closest integer rather than the
41
+ * mathematically-expected one — e.g. `1.005` is stored as `1.00499999…` and
42
+ * rounds to `100` minor units, not `101`. If you need decimal-exact rounding,
43
+ * construct money from a string or use integer minor units directly via
44
+ * {@link money}.
45
+ *
46
+ * @example
47
+ * fromMajor(19.99, 'USD') // { amount: 1999, currency: 'USD' }
48
+ * fromMajor(0.1 + 0.2, 'USD') // { amount: 30, currency: 'USD' }
49
+ * fromMajor(1500, 'JPY') // { amount: 1500, currency: 'JPY' }
50
+ */
51
+ declare function fromMajor(major: number, currency: string): Money;
52
+ /** Convert {@link Money} back to its major-unit floating-point representation. */
53
+ declare function toMajor(m: Money): number;
54
+ declare function addMoney(a: Money, b: Money): Money;
55
+ declare function subtractMoney(a: Money, b: Money): Money;
56
+ /**
57
+ * Multiply money by a scalar. The result is rounded to an integer number of
58
+ * minor units using half-away-from-zero rounding.
59
+ */
60
+ declare function multiplyMoney(m: Money, scalar: number): Money;
61
+ /** Sum a list of {@link Money}. Returns zero in the given currency for empty input. */
62
+ declare function sumMoney(values: readonly Money[], currency: string): Money;
63
+ /** Strict equality (amount and currency both match). */
64
+ declare function equalsMoney(a: Money, b: Money): boolean;
65
+ /**
66
+ * Ordering predicate. Returns -1, 0, or 1. Throws on currency mismatch — use
67
+ * {@link equalsMoney} for a total equality check across currencies.
68
+ */
69
+ declare function compareMoney(a: Money, b: Money): -1 | 0 | 1;
70
+ declare function isZeroMoney(m: Money): boolean;
71
+ declare function isPositiveMoney(m: Money): boolean;
72
+ declare function isNegativeMoney(m: Money): boolean;
73
+ declare function negateMoney(m: Money): Money;
74
+ declare function absMoney(m: Money): Money;
75
+ /** Structural type guard. */
76
+ declare function isMoney(value: unknown): value is Money;
77
+ //#endregion
78
+ export { CurrencyMismatchError, Money, absMoney, addMoney, compareMoney, equalsMoney, fromMajor, isMoney, isNegativeMoney, isPositiveMoney, isZeroMoney, money, multiplyMoney, negateMoney, subtractMoney, sumMoney, toMajor };