@classytic/primitives 0.19.0 → 0.20.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +906 -751
  2. package/README.md +160 -159
  3. package/dist/approval.d.mts +49 -10
  4. package/dist/approval.mjs +44 -10
  5. package/dist/bank-transaction.d.mts +1 -1
  6. package/dist/cadence-BFQVC1Qp.d.mts +107 -0
  7. package/dist/cadence.d.mts +1 -92
  8. package/dist/cadence.mjs +133 -4
  9. package/dist/calendar-B6fm-FbR.d.mts +46 -0
  10. package/dist/calendar.d.mts +1 -45
  11. package/dist/canonical.mjs +17 -0
  12. package/dist/context.d.mts +28 -1
  13. package/dist/currency.d.mts +2 -2
  14. package/dist/currency.mjs +134 -19
  15. package/dist/event-infra.d.mts +1 -34
  16. package/dist/event-infra.mjs +16 -46
  17. package/dist/idempotency.d.mts +253 -0
  18. package/dist/idempotency.mjs +226 -0
  19. package/dist/memory-outbox.d.mts +64 -0
  20. package/dist/memory-outbox.mjs +172 -0
  21. package/dist/monetization.d.mts +1 -1
  22. package/dist/money-DPo9fLBi.d.mts +347 -0
  23. package/dist/money.d.mts +1 -140
  24. package/dist/money.mjs +6 -15
  25. package/dist/outbox.d.mts +49 -1
  26. package/dist/outbox.mjs +64 -1
  27. package/dist/payment-events.d.mts +132 -4
  28. package/dist/payment-events.mjs +22 -2
  29. package/dist/payment-gateway.d.mts +210 -7
  30. package/dist/payment-gateway.mjs +18 -1
  31. package/dist/period.d.mts +138 -7
  32. package/dist/period.mjs +316 -5
  33. package/dist/person-parse.d.mts +82 -0
  34. package/dist/person-parse.mjs +109 -0
  35. package/dist/proration.d.mts +2 -2
  36. package/dist/proration.mjs +18 -6
  37. package/dist/sla-policy.d.mts +1 -1
  38. package/dist/state-machine.mjs +13 -0
  39. package/dist/suspension.d.mts +1 -1
  40. package/dist/timezone-tlG7ygwx.d.mts +114 -0
  41. package/dist/timezone.d.mts +2 -100
  42. package/dist/timezone.mjs +38 -3
  43. package/dist/unit-cost-rate.d.mts +1 -2
  44. package/package.json +244 -232
  45. package/dist/currency-BCJnJBlb.d.mts +0 -119
@@ -0,0 +1,172 @@
1
+ import { InvalidOutboxEventError, OutboxOwnershipError } from "./outbox.mjs";
2
+ //#region src/events/memory-outbox.ts
3
+ /**
4
+ * In-memory outbox store — reference implementation for dev / testing.
5
+ *
6
+ * Supports the full `OutboxStore` capability set (claim/lease, fail/retry,
7
+ * dedupe, visibleAt). Production deployments should pass a `repository` to
8
+ * `EventOutbox` instead of using this; the memory store exists for unit
9
+ * tests and single-process dev where durable persistence is noise.
10
+ */
11
+ const DEFAULT_LEASE_MS = 3e4;
12
+ var MemoryOutboxStore = class {
13
+ entries = [];
14
+ seenDedupeKeys = /* @__PURE__ */ new Set();
15
+ /**
16
+ * Drop every entry (including delivered / failed / dead-lettered rows) and
17
+ * the dedupe-key memory.
18
+ *
19
+ * For tests only — it is the in-memory analogue of truncating the table, and
20
+ * exists so a suite can isolate cases against ONE store instance (the
21
+ * `reset` hook of `runOutboxStoreContract`). Not part of `OutboxStore`; a
22
+ * production store must never expose this.
23
+ */
24
+ clear() {
25
+ this.entries.length = 0;
26
+ this.seenDedupeKeys.clear();
27
+ }
28
+ /**
29
+ * For tests only — every stored event regardless of status, in insertion
30
+ * order. The in-memory analogue of `SELECT * FROM outbox`, used by the
31
+ * `/event-infra` contract test to assert what a `purge` left behind. Not part
32
+ * of `OutboxStore`; a production store must never expose this.
33
+ */
34
+ all() {
35
+ return this.entries.map((e) => e.event);
36
+ }
37
+ async save(event, options) {
38
+ if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
39
+ if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
40
+ if (options?.dedupeKey) {
41
+ if (this.seenDedupeKeys.has(options.dedupeKey)) return;
42
+ this.seenDedupeKeys.add(options.dedupeKey);
43
+ }
44
+ this.entries.push({
45
+ createdAt: Date.now(),
46
+ event,
47
+ status: "pending",
48
+ attempts: 0,
49
+ visibleAt: options?.visibleAt?.getTime() ?? 0,
50
+ leaseOwner: null,
51
+ leaseExpiresAt: 0,
52
+ deliveredAt: null,
53
+ firstFailedAt: null,
54
+ lastFailedAt: null,
55
+ lastError: null,
56
+ dedupeKey: options?.dedupeKey
57
+ });
58
+ }
59
+ async getPending(limit) {
60
+ const now = Date.now();
61
+ return this.entries.filter((e) => e.status === "pending" && e.visibleAt <= now && (e.leaseOwner === null || e.leaseExpiresAt <= now)).slice(0, limit).map((e) => e.event);
62
+ }
63
+ async claimPending(options) {
64
+ const now = Date.now();
65
+ const limit = options?.limit ?? 100;
66
+ const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS;
67
+ const consumerId = options?.consumerId ?? "anonymous";
68
+ const typeFilter = options?.types ? new Set(options.types) : null;
69
+ const claimed = [];
70
+ for (const entry of this.entries) {
71
+ if (claimed.length >= limit) break;
72
+ if (entry.status !== "pending") continue;
73
+ if (entry.visibleAt > now) continue;
74
+ if (entry.leaseOwner !== null && entry.leaseExpiresAt > now) continue;
75
+ if (typeFilter && !typeFilter.has(entry.event.type)) continue;
76
+ entry.leaseOwner = consumerId;
77
+ entry.leaseExpiresAt = now + leaseMs;
78
+ entry.attempts++;
79
+ claimed.push(entry.event);
80
+ }
81
+ return claimed;
82
+ }
83
+ async acknowledge(eventId, options) {
84
+ const entry = this.entries.find((e) => e.event.meta.id === eventId);
85
+ if (!entry) return;
86
+ if (entry.status === "delivered") return;
87
+ if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
88
+ entry.status = "delivered";
89
+ entry.deliveredAt = Date.now();
90
+ entry.leaseOwner = null;
91
+ }
92
+ async fail(eventId, error, options) {
93
+ const entry = this.entries.find((e) => e.event.meta.id === eventId);
94
+ if (!entry) return;
95
+ if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
96
+ const now = Date.now();
97
+ entry.lastError = error;
98
+ entry.leaseOwner = null;
99
+ entry.leaseExpiresAt = 0;
100
+ if (entry.firstFailedAt === null) entry.firstFailedAt = now;
101
+ entry.lastFailedAt = now;
102
+ if (options?.deadLetter) {
103
+ entry.status = "dead_letter";
104
+ return;
105
+ }
106
+ entry.status = "pending";
107
+ entry.visibleAt = options?.retryAt ? options.retryAt.getTime() : 0;
108
+ }
109
+ async getDeadLettered(limit) {
110
+ const out = [];
111
+ for (const entry of this.entries) {
112
+ if (out.length >= limit) break;
113
+ if (entry.status !== "dead_letter") continue;
114
+ out.push({
115
+ event: entry.event,
116
+ error: {
117
+ message: entry.lastError?.message ?? "unknown",
118
+ ...entry.lastError?.code !== void 0 ? { code: entry.lastError.code } : {}
119
+ },
120
+ attempts: entry.attempts,
121
+ firstFailedAt: new Date(entry.firstFailedAt ?? entry.lastFailedAt ?? Date.now()),
122
+ lastFailedAt: new Date(entry.lastFailedAt ?? entry.firstFailedAt ?? Date.now())
123
+ });
124
+ }
125
+ return out;
126
+ }
127
+ async purge(olderThanMs) {
128
+ const cutoff = Date.now() - olderThanMs;
129
+ let purged = 0;
130
+ for (let i = this.entries.length - 1; i >= 0; i--) {
131
+ const entry = this.entries[i];
132
+ if (!entry) continue;
133
+ if (entry.status === "delivered" && entry.deliveredAt !== null && entry.deliveredAt < cutoff) {
134
+ if (entry.dedupeKey) this.seenDedupeKeys.delete(entry.dedupeKey);
135
+ this.entries.splice(i, 1);
136
+ purged++;
137
+ }
138
+ }
139
+ return purged;
140
+ }
141
+ /**
142
+ * Re-drive one dead-lettered row. Scoped to `dead_letter`: a row that is merely
143
+ * backing off must keep its attempt count, and the id alone cannot tell the two apart.
144
+ */
145
+ async requeue(eventId) {
146
+ const entry = this.entries.find((e) => e.event.meta.id === eventId && e.status === "dead_letter");
147
+ if (!entry) return false;
148
+ entry.status = "pending";
149
+ entry.attempts = 0;
150
+ entry.leaseOwner = null;
151
+ entry.leaseExpiresAt = 0;
152
+ entry.visibleAt = 0;
153
+ entry.lastError = null;
154
+ return true;
155
+ }
156
+ async countByStatus(status) {
157
+ return this.entries.filter((e) => e.status === status).length;
158
+ }
159
+ /** Age of the oldest PENDING row — see `MemoryEntry.createdAt` for why not `visibleAt`. */
160
+ async oldestPendingAgeMs() {
161
+ const pending = this.entries.filter((e) => e.status === "pending");
162
+ if (pending.length === 0) return null;
163
+ const oldest = Math.min(...pending.map((e) => e.createdAt));
164
+ return Date.now() - oldest;
165
+ }
166
+ /** Test helper: inspect entry by id */
167
+ _getEntry(eventId) {
168
+ return this.entries.find((e) => e.event.meta.id === eventId);
169
+ }
170
+ };
171
+ //#endregion
172
+ export { MemoryOutboxStore };
@@ -1,4 +1,4 @@
1
- import { Money } from "./money.mjs";
1
+ import { n as Money } from "./money-DPo9fLBi.mjs";
2
2
 
3
3
  //#region src/money/monetization.d.ts
4
4
  /**
@@ -0,0 +1,347 @@
1
+ import { t as Brand } from "./brand-LLXqoh5J.mjs";
2
+
3
+ //#region src/money/currency.d.ts
4
+ /**
5
+ * ISO 4217 currency code (3 uppercase letters).
6
+ *
7
+ * Branded `string` — any 3-letter string is structurally valid, but the brand
8
+ * prevents accidental mixing with raw strings. Use {@link toCurrencyCode} to
9
+ * construct one safely.
10
+ */
11
+ type CurrencyCode = Brand<string, 'CurrencyCode'>;
12
+ /**
13
+ * Common ISO 4217 codes. Non-exhaustive — add more at the consumer level if
14
+ * needed. Listed here because shared-types ships with these by default.
15
+ */
16
+ declare const CURRENCIES: {
17
+ readonly USD: "USD";
18
+ readonly EUR: "EUR";
19
+ readonly GBP: "GBP";
20
+ readonly JPY: "JPY";
21
+ readonly CNY: "CNY";
22
+ readonly INR: "INR";
23
+ readonly BDT: "BDT";
24
+ readonly AED: "AED";
25
+ readonly SAR: "SAR";
26
+ readonly PKR: "PKR";
27
+ readonly NPR: "NPR";
28
+ readonly LKR: "LKR";
29
+ readonly MYR: "MYR";
30
+ readonly SGD: "SGD";
31
+ readonly AUD: "AUD";
32
+ readonly CAD: "CAD";
33
+ readonly CHF: "CHF";
34
+ readonly HKD: "HKD";
35
+ readonly THB: "THB";
36
+ readonly IDR: "IDR";
37
+ };
38
+ /**
39
+ * Number of minor units per major unit, by ISO 4217 code.
40
+ *
41
+ * **This table must list EVERY currency whose exponent is not 2.** ISO 4217
42
+ * defaults to 2 decimals, and {@link minorUnitFactor} falls back to 100 for a
43
+ * code it does not know — which is correct for the ~150 two-decimal currencies
44
+ * and catastrophically wrong for the others. A missing zero-decimal entry does
45
+ * not throw: it makes `fromMajor(1500, 'GNF')` store 150 000 minor units, a
46
+ * 100× error that reads as a plausible number all the way to the gateway.
47
+ *
48
+ * So the exceptions are enumerated EXHAUSTIVELY (all 0-, 3- and 4-decimal
49
+ * codes), leaving the `?? 100` fallback reachable only by a code that is not a
50
+ * real ISO 4217 currency — a typo, a lowercased code, or a placeholder. The
51
+ * two-decimal entries below are redundant with the default and kept only as
52
+ * documentation of the common set.
53
+ */
54
+ declare const MINOR_UNIT_FACTOR: Readonly<Record<string, number>>;
55
+ /**
56
+ * ISO 4217 format — 3 uppercase letters. Exported so consumers can embed
57
+ * the SAME pattern in JSON-Schema-representable validators (zod `.regex`,
58
+ * Mongoose `match`, OpenAPI `pattern`) instead of hand-rolling a copy:
59
+ * `.refine(isCurrencyCode)` validates at runtime but is dropped by
60
+ * `z.toJSONSchema`, losing the constraint from generated API docs.
61
+ * `isCurrencyCode` / `toCurrencyCode` remain the runtime API.
62
+ */
63
+ declare const CURRENCY_PATTERN: RegExp;
64
+ /**
65
+ * Narrow a raw string to a {@link CurrencyCode}. Returns `null` if the string
66
+ * is not a valid ISO 4217 format (3 uppercase letters). This does not verify
67
+ * the code is registered with ISO — use your own whitelist for that.
68
+ */
69
+ declare function toCurrencyCode(value: string): CurrencyCode | null;
70
+ /**
71
+ * Validate + brand, or throw. The constructor form — same relationship to
72
+ * {@link toCurrencyCode} that `civilDate` has to `isCivilDate`.
73
+ *
74
+ * `Money` carries a {@link CurrencyCode}, not a bare string, and this is where
75
+ * an untrusted value earns the brand. Rejecting `'jpy'` here matters: lowercase
76
+ * misses {@link MINOR_UNIT_FACTOR}, so it silently becomes a 2-decimal currency
77
+ * and every JPY amount is off by 100×.
78
+ */
79
+ declare function currencyCode(value: string): CurrencyCode;
80
+ declare class InvalidCurrencyCodeError extends Error {
81
+ readonly name = "InvalidCurrencyCodeError";
82
+ readonly value: string;
83
+ constructor(value: string);
84
+ }
85
+ /**
86
+ * Raised when an operation combines two currencies.
87
+ *
88
+ * Declared HERE rather than in `/money` so the FX seam below can throw it
89
+ * without a runtime import back into `/money` (which imports
90
+ * {@link minorUnitFactor} from this file). `/money` re-exports it, so
91
+ * `import { CurrencyMismatchError } from '@classytic/primitives/money'` still
92
+ * resolves.
93
+ */
94
+ declare class CurrencyMismatchError extends Error {
95
+ readonly name = "CurrencyMismatchError";
96
+ readonly left: string;
97
+ readonly right: string;
98
+ constructor(left: string, right: string);
99
+ }
100
+ /** Type predicate for `unknown` input. */
101
+ declare function isCurrencyCode(value: unknown): value is CurrencyCode;
102
+ /**
103
+ * Minor units per major unit (100 for USD, 1 for JPY, 1000 for KWD, 10 000 for CLF).
104
+ *
105
+ * Falls back to 100 for an unlisted code — the ISO 4217 default exponent. Because
106
+ * {@link MINOR_UNIT_FACTOR} enumerates every non-2-decimal currency, that fallback
107
+ * is now only reached by codes that are not ISO 4217 currencies at all; use
108
+ * {@link isKnownCurrency} at a config/boot boundary if you need to reject those
109
+ * rather than assume two decimals for them.
110
+ */
111
+ declare function minorUnitFactor(currency: string): number;
112
+ /**
113
+ * Does {@link MINOR_UNIT_FACTOR} carry an explicit exponent for this code?
114
+ *
115
+ * `false` means {@link minorUnitFactor} would ASSUME two decimals. That
116
+ * assumption is right for an ordinary ISO 4217 code and wrong by 100× for a
117
+ * mistyped or lowercased one (`'jpy'`), so a boot-time config validator that
118
+ * accepts an operator-supplied currency should gate on this instead of letting
119
+ * the default answer for it.
120
+ */
121
+ declare function isKnownCurrency(currency: string): boolean;
122
+ /**
123
+ * Frozen FX conversion record — the cost of record for cross-currency
124
+ * transactions. Stored alongside a base-currency amount at the seam where
125
+ * currency crossed (procurement receipt, landed-cost entry, foreign refund).
126
+ *
127
+ * Invariant: once written, never mutated. Revaluation is a new snapshot on
128
+ * a new document — rewriting an existing snapshot corrupts audit history.
129
+ *
130
+ * `rate` is expressed as: 1 sourceCurrency = rate * baseCurrency
131
+ * (e.g. USD→BDT at 110 means rate = 110, so sourceAmount 10 USD → baseAmount 1100 BDT).
132
+ * This matches how banks quote — the source is the "from" currency.
133
+ */
134
+ interface FxSnapshot {
135
+ /** Currency the transaction was invoiced / quoted in. ISO 4217. */
136
+ sourceCurrency: string;
137
+ /** Organisation's functional currency the base amount is expressed in. */
138
+ baseCurrency: string;
139
+ /** 1 unit of sourceCurrency equals this many units of baseCurrency. */
140
+ rate: number;
141
+ /** When the rate was captured — NOT when the document was last touched. */
142
+ snapshotAt: Date;
143
+ /**
144
+ * Optional provenance tag — e.g. 'manual', 'bangladesh-bank',
145
+ * 'openexchangerates.org', 'supplier-invoice'. Free-form; hosts use it for
146
+ * audit filtering and re-pricing policy.
147
+ */
148
+ source?: string;
149
+ }
150
+ /**
151
+ * How to land a converted amount on an integer number of minor units.
152
+ * Required to be deterministic and named, never implicit — the old signature
153
+ * returned an unrounded float and left the choice to whoever persisted it, so
154
+ * two call sites rounding differently produced two answers for one conversion.
155
+ */
156
+ type FxRounding = 'half-away-from-zero' | 'floor' | 'ceil';
157
+ /**
158
+ * Apply an {@link FxSnapshot} to a source-currency amount.
159
+ *
160
+ * ## Why this takes `Money` and not a number
161
+ *
162
+ * It used to take a bare `number`, which meant nothing could check the amount
163
+ * was denominated in `fx.sourceCurrency`. Applying a USD→BDT snapshot to a EUR
164
+ * amount type-checked, ran, and returned a plausible wrong number — the
165
+ * arithmetic is identical, only the meaning is wrong, so no test that asserts a
166
+ * total would notice. Taking `Money` lets the seam enforce its own
167
+ * precondition: a mismatch throws {@link CurrencyMismatchError}.
168
+ *
169
+ * ## Minor-unit exponents are handled
170
+ *
171
+ * The rate is quoted in MAJOR units (1 source = `rate` base), while `Money`
172
+ * holds minor units, and the two currencies need not share an exponent
173
+ * (USD has 2, JPY has 0). The conversion therefore goes
174
+ * `minor → major → ×rate → base minor`, not a naive `amount × rate`, which is
175
+ * off by 100× for any pair whose exponents differ.
176
+ */
177
+ declare function convertWithSnapshot(source: Money, fx: FxSnapshot, options?: {
178
+ rounding?: FxRounding;
179
+ }): Money;
180
+ /**
181
+ * Reverse an {@link FxSnapshot} — reconstruct the source-currency amount
182
+ * from a stored base amount. Useful for reporting "cost in original
183
+ * currency". Not round-trip safe under rounding; prefer storing the
184
+ * original source amount alongside the base amount when audit fidelity
185
+ * matters.
186
+ *
187
+ * Symmetrically guarded: a base amount that is not in `fx.baseCurrency` throws
188
+ * rather than dividing by a rate that does not describe it.
189
+ */
190
+ declare function reverseWithSnapshot(base: Money, fx: FxSnapshot, options?: {
191
+ rounding?: FxRounding;
192
+ }): Money;
193
+ /**
194
+ * Structural validation — checks shape only. Does NOT verify the rate is
195
+ * economically sensible (8.5 USD→BDT would pass this check). Host-level
196
+ * guards / anomaly detection handle that.
197
+ */
198
+ declare function isFxSnapshot(value: unknown): value is FxSnapshot;
199
+ //#endregion
200
+ //#region src/money/money.d.ts
201
+ /**
202
+ * Wire-format monetary amount: integer minor units, branded with a phantom
203
+ * currency type so calls like `formatBdt(usdValue)` fail at compile time.
204
+ *
205
+ * Use this for HTTP/JSON fields where transmitting a `Money` struct would be
206
+ * overkill: `interface Order { grandTotal: MinorUnits<'BDT'>; ... }`. Pair
207
+ * with the structured {@link Money} for in-process arithmetic.
208
+ *
209
+ * Erases at runtime — branded types have zero cost.
210
+ *
211
+ * @example
212
+ * type Paisa = MinorUnits<'BDT'>;
213
+ * type UsdCents = MinorUnits<'USD'>;
214
+ * const p: Paisa = MinorUnits(489900, 'BDT');
215
+ * const c: UsdCents = p; // ❌ type error: BDT vs USD
216
+ */
217
+ declare const __unitBrand: unique symbol;
218
+ declare const __currencyBrand: unique symbol;
219
+ type MinorUnits<C extends string = string> = number & {
220
+ readonly [__unitBrand]: 'minor';
221
+ readonly [__currencyBrand]: C;
222
+ };
223
+ /**
224
+ * Cast a raw number to {@link MinorUnits}. Pass the currency as the second
225
+ * argument to fix the phantom type. With no second arg, `MinorUnits<string>`
226
+ * — useful for generic SDK boundaries that don't know the currency yet.
227
+ *
228
+ * @example
229
+ * MinorUnits(489900, 'BDT') // typed MinorUnits<'BDT'>
230
+ * MinorUnits(199, 'USD') // typed MinorUnits<'USD'>
231
+ */
232
+ declare function MinorUnits<C extends string = string>(amount: number, _currency?: C): MinorUnits<C>;
233
+ /**
234
+ * A monetary amount stored as an integer in the currency's minor unit.
235
+ *
236
+ * Examples:
237
+ * - `{ amount: 19_99, currency: 'USD' }` — 19.99 USD (2 decimals)
238
+ * - `{ amount: 1_500, currency: 'JPY' }` — 1500 JPY (0 decimals)
239
+ * - `{ amount: 500_000, currency: 'KWD' }` — 500.000 KWD (3 decimals)
240
+ *
241
+ * Storing as integers avoids IEEE 754 rounding across arithmetic. Never store
242
+ * `Money.amount` as a float.
243
+ *
244
+ * ## `currency` is a branded {@link CurrencyCode}, not a string
245
+ *
246
+ * It was declared `CurrencyCode | string`, and a union with `string` collapses
247
+ * TO `string` — so the brand was decoration on the one type in the package
248
+ * where it matters, and `money()` validated nothing. Any string reached the
249
+ * field: `'jpy'`, `''`, `'US Dollar'`, a mistyped `'BTD'`. None of those are in
250
+ * {@link MINOR_UNIT_FACTOR}, so `minorUnitFactor` assumes two decimals for them
251
+ * and every JPY-style amount is 100× wrong while looking perfectly ordinary.
252
+ *
253
+ * Construct through {@link money} / {@link fromMajor} (which validate and
254
+ * brand) or through `currencyCode()` at your own boundary. A raw object literal
255
+ * `{ amount, currency: someString }` is now a compile error — deliberately, so
256
+ * the validation cannot be bypassed by the shape that skipped it before.
257
+ */
258
+ interface Money {
259
+ readonly amount: number;
260
+ readonly currency: CurrencyCode;
261
+ }
262
+ /**
263
+ * Construct {@link Money} from an integer minor-unit amount.
264
+ *
265
+ * @throws TypeError if `amount` is not a finite integer.
266
+ * @throws InvalidCurrencyCodeError if `currency` is not ISO 4217 format.
267
+ */
268
+ declare function money(amount: number, currency: string): Money;
269
+ /**
270
+ * Construct {@link Money} from a major-unit number (e.g. dollars, taka) by
271
+ * scaling to the currency's minor unit and rounding half-away-from-zero.
272
+ *
273
+ * Uses `toPrecision(15)` to strip the trailing-bit noise that appears in
274
+ * multiply-then-round (e.g. `0.1 + 0.2 = 0.30000000000000004`). This is
275
+ * enough for the vast majority of decimals encountered in real-world pricing.
276
+ *
277
+ * **Precision caveat.** A handful of decimals cannot be represented exactly
278
+ * in IEEE 754 and will round to the IEEE-closest integer rather than the
279
+ * mathematically-expected one — e.g. `1.005` is stored as `1.00499999…` and
280
+ * rounds to `100` minor units, not `101`. If you need decimal-exact rounding,
281
+ * construct money from a string or use integer minor units directly via
282
+ * {@link money}.
283
+ *
284
+ * @example
285
+ * fromMajor(19.99, 'USD') // { amount: 1999, currency: 'USD' }
286
+ * fromMajor(0.1 + 0.2, 'USD') // { amount: 30, currency: 'USD' }
287
+ * fromMajor(1500, 'JPY') // { amount: 1500, currency: 'JPY' }
288
+ */
289
+ declare function fromMajor(major: number, currency: string): Money;
290
+ /** Convert {@link Money} back to its major-unit floating-point representation. */
291
+ declare function toMajor(m: Money): number;
292
+ declare function addMoney(a: Money, b: Money): Money;
293
+ declare function subtractMoney(a: Money, b: Money): Money;
294
+ /**
295
+ * Multiply money by a scalar. The result is rounded to an integer number of
296
+ * minor units using half-away-from-zero rounding.
297
+ */
298
+ declare function multiplyMoney(m: Money, scalar: number): Money;
299
+ /** Sum a list of {@link Money}. Returns zero in the given currency for empty input. */
300
+ declare function sumMoney(values: readonly Money[], currency: string): Money;
301
+ /** Strict equality (amount and currency both match). */
302
+ declare function equalsMoney(a: Money, b: Money): boolean;
303
+ /**
304
+ * Ordering predicate. Returns -1, 0, or 1. Throws on currency mismatch — use
305
+ * {@link equalsMoney} for a total equality check across currencies.
306
+ */
307
+ declare function compareMoney(a: Money, b: Money): -1 | 0 | 1;
308
+ declare function isZeroMoney(m: Money): boolean;
309
+ declare function isPositiveMoney(m: Money): boolean;
310
+ declare function isNegativeMoney(m: Money): boolean;
311
+ declare function negateMoney(m: Money): Money;
312
+ declare function absMoney(m: Money): Money;
313
+ /**
314
+ * Decimal major-unit number → integer minor units by decimal exponent.
315
+ * Rounding: raw IEEE `Math.round` — half-up toward +∞ (so `-10.5 → -10`),
316
+ * documented above. Throws when the result leaves the safe-integer range
317
+ * (also catches `NaN`/`Infinity` inputs).
318
+ *
319
+ * @example
320
+ * majorToMinorUnits(10.5) // 1050
321
+ * majorToMinorUnits(1000, 0) // 1000 (JPY-style)
322
+ * majorToMinorUnits(1.005) // 100 — IEEE, see note above
323
+ */
324
+ declare function majorToMinorUnits(major: number, minorUnitDecimals?: number): number;
325
+ /** Integer minor units → decimal major-unit number by decimal exponent: `1050 → 10.5`. */
326
+ declare function minorUnitsToMajor(minor: number, minorUnitDecimals?: number): number;
327
+ /**
328
+ * Percentage of an integer minor-unit amount → integer minor units.
329
+ * EXACT multiply-then-round: `Math.round((minor × ratePercent) / 100)` —
330
+ * one rounding point, half-up toward +∞. Handles genuinely fractional rates
331
+ * (QST 9.975 %) without precision loss: `percentOfMinor(20000, 9.975) === 1995`.
332
+ *
333
+ * Hosts whose domain guarantees at-most-2-decimal rates MAY snap the rate to
334
+ * integer basis points BEFORE calling (be-prod's `#shared/money` seam does) —
335
+ * that is host policy layered on top, not part of this primitive.
336
+ */
337
+ declare function percentOfMinor(minor: number, ratePercent: number): number;
338
+ /**
339
+ * Integer minor units → plain decimal string (no currency symbol, no locale):
340
+ * `formatMinorUnits(10550) → "105.50"`, `formatMinorUnits(1000, 0) → "1000"`.
341
+ * For localized currency strings use `Intl.NumberFormat` at the display layer.
342
+ */
343
+ declare function formatMinorUnits(minor: number, minorUnitDecimals?: number): string;
344
+ /** Structural type guard. */
345
+ declare function isMoney(value: unknown): value is Money;
346
+ //#endregion
347
+ export { convertWithSnapshot as A, CURRENCY_PATTERN as C, FxSnapshot as D, FxRounding as E, minorUnitFactor as F, reverseWithSnapshot as I, toCurrencyCode as L, isCurrencyCode as M, isFxSnapshot as N, InvalidCurrencyCodeError as O, isKnownCurrency as P, CURRENCIES as S, CurrencyMismatchError as T, negateMoney as _, compareMoney as a, sumMoney as b, fromMajor as c, isPositiveMoney as d, isZeroMoney as f, multiplyMoney as g, money as h, addMoney as i, currencyCode as j, MINOR_UNIT_FACTOR as k, isMoney as l, minorUnitsToMajor as m, Money as n, equalsMoney as o, majorToMinorUnits as p, absMoney as r, formatMinorUnits as s, MinorUnits as t, isNegativeMoney as u, percentOfMinor as v, CurrencyCode as w, toMajor as x, subtractMoney as y };
package/dist/money.d.mts CHANGED
@@ -1,141 +1,2 @@
1
- import { r as CurrencyCode } from "./currency-BCJnJBlb.mjs";
2
-
3
- //#region src/money/money.d.ts
4
- /**
5
- * Wire-format monetary amount: integer minor units, branded with a phantom
6
- * currency type so calls like `formatBdt(usdValue)` fail at compile time.
7
- *
8
- * Use this for HTTP/JSON fields where transmitting a `Money` struct would be
9
- * overkill: `interface Order { grandTotal: MinorUnits<'BDT'>; ... }`. Pair
10
- * with the structured {@link Money} for in-process arithmetic.
11
- *
12
- * Erases at runtime — branded types have zero cost.
13
- *
14
- * @example
15
- * type Paisa = MinorUnits<'BDT'>;
16
- * type UsdCents = MinorUnits<'USD'>;
17
- * const p: Paisa = MinorUnits(489900, 'BDT');
18
- * const c: UsdCents = p; // ❌ type error: BDT vs USD
19
- */
20
- declare const __unitBrand: unique symbol;
21
- declare const __currencyBrand: unique symbol;
22
- type MinorUnits<C extends string = string> = number & {
23
- readonly [__unitBrand]: 'minor';
24
- readonly [__currencyBrand]: C;
25
- };
26
- /**
27
- * Cast a raw number to {@link MinorUnits}. Pass the currency as the second
28
- * argument to fix the phantom type. With no second arg, `MinorUnits<string>`
29
- * — useful for generic SDK boundaries that don't know the currency yet.
30
- *
31
- * @example
32
- * MinorUnits(489900, 'BDT') // typed MinorUnits<'BDT'>
33
- * MinorUnits(199, 'USD') // typed MinorUnits<'USD'>
34
- */
35
- declare function MinorUnits<C extends string = string>(amount: number, _currency?: C): MinorUnits<C>;
36
- /**
37
- * A monetary amount stored as an integer in the currency's minor unit.
38
- *
39
- * Examples:
40
- * - `{ amount: 19_99, currency: 'USD' }` — 19.99 USD (2 decimals)
41
- * - `{ amount: 1_500, currency: 'JPY' }` — 1500 JPY (0 decimals)
42
- * - `{ amount: 500_000, currency: 'KWD' }` — 500.000 KWD (3 decimals)
43
- *
44
- * Storing as integers avoids IEEE 754 rounding across arithmetic. Never store
45
- * `Money.amount` as a float.
46
- */
47
- interface Money {
48
- readonly amount: number;
49
- readonly currency: CurrencyCode | string;
50
- }
51
- declare class CurrencyMismatchError extends Error {
52
- readonly name = "CurrencyMismatchError";
53
- readonly left: string;
54
- readonly right: string;
55
- constructor(left: string, right: string);
56
- }
57
- /**
58
- * Construct {@link Money} from an integer minor-unit amount.
59
- *
60
- * @throws TypeError if `amount` is not a finite integer.
61
- */
62
- declare function money(amount: number, currency: string): Money;
63
- /**
64
- * Construct {@link Money} from a major-unit number (e.g. dollars, taka) by
65
- * scaling to the currency's minor unit and rounding half-away-from-zero.
66
- *
67
- * Uses `toPrecision(15)` to strip the trailing-bit noise that appears in
68
- * multiply-then-round (e.g. `0.1 + 0.2 = 0.30000000000000004`). This is
69
- * enough for the vast majority of decimals encountered in real-world pricing.
70
- *
71
- * **Precision caveat.** A handful of decimals cannot be represented exactly
72
- * in IEEE 754 and will round to the IEEE-closest integer rather than the
73
- * mathematically-expected one — e.g. `1.005` is stored as `1.00499999…` and
74
- * rounds to `100` minor units, not `101`. If you need decimal-exact rounding,
75
- * construct money from a string or use integer minor units directly via
76
- * {@link money}.
77
- *
78
- * @example
79
- * fromMajor(19.99, 'USD') // { amount: 1999, currency: 'USD' }
80
- * fromMajor(0.1 + 0.2, 'USD') // { amount: 30, currency: 'USD' }
81
- * fromMajor(1500, 'JPY') // { amount: 1500, currency: 'JPY' }
82
- */
83
- declare function fromMajor(major: number, currency: string): Money;
84
- /** Convert {@link Money} back to its major-unit floating-point representation. */
85
- declare function toMajor(m: Money): number;
86
- declare function addMoney(a: Money, b: Money): Money;
87
- declare function subtractMoney(a: Money, b: Money): Money;
88
- /**
89
- * Multiply money by a scalar. The result is rounded to an integer number of
90
- * minor units using half-away-from-zero rounding.
91
- */
92
- declare function multiplyMoney(m: Money, scalar: number): Money;
93
- /** Sum a list of {@link Money}. Returns zero in the given currency for empty input. */
94
- declare function sumMoney(values: readonly Money[], currency: string): Money;
95
- /** Strict equality (amount and currency both match). */
96
- declare function equalsMoney(a: Money, b: Money): boolean;
97
- /**
98
- * Ordering predicate. Returns -1, 0, or 1. Throws on currency mismatch — use
99
- * {@link equalsMoney} for a total equality check across currencies.
100
- */
101
- declare function compareMoney(a: Money, b: Money): -1 | 0 | 1;
102
- declare function isZeroMoney(m: Money): boolean;
103
- declare function isPositiveMoney(m: Money): boolean;
104
- declare function isNegativeMoney(m: Money): boolean;
105
- declare function negateMoney(m: Money): Money;
106
- declare function absMoney(m: Money): Money;
107
- /**
108
- * Decimal major-unit number → integer minor units by decimal exponent.
109
- * Rounding: raw IEEE `Math.round` — half-up toward +∞ (so `-10.5 → -10`),
110
- * documented above. Throws when the result leaves the safe-integer range
111
- * (also catches `NaN`/`Infinity` inputs).
112
- *
113
- * @example
114
- * majorToMinorUnits(10.5) // 1050
115
- * majorToMinorUnits(1000, 0) // 1000 (JPY-style)
116
- * majorToMinorUnits(1.005) // 100 — IEEE, see note above
117
- */
118
- declare function majorToMinorUnits(major: number, minorUnitDecimals?: number): number;
119
- /** Integer minor units → decimal major-unit number by decimal exponent: `1050 → 10.5`. */
120
- declare function minorUnitsToMajor(minor: number, minorUnitDecimals?: number): number;
121
- /**
122
- * Percentage of an integer minor-unit amount → integer minor units.
123
- * EXACT multiply-then-round: `Math.round((minor × ratePercent) / 100)` —
124
- * one rounding point, half-up toward +∞. Handles genuinely fractional rates
125
- * (QST 9.975 %) without precision loss: `percentOfMinor(20000, 9.975) === 1995`.
126
- *
127
- * Hosts whose domain guarantees at-most-2-decimal rates MAY snap the rate to
128
- * integer basis points BEFORE calling (be-prod's `#shared/money` seam does) —
129
- * that is host policy layered on top, not part of this primitive.
130
- */
131
- declare function percentOfMinor(minor: number, ratePercent: number): number;
132
- /**
133
- * Integer minor units → plain decimal string (no currency symbol, no locale):
134
- * `formatMinorUnits(10550) → "105.50"`, `formatMinorUnits(1000, 0) → "1000"`.
135
- * For localized currency strings use `Intl.NumberFormat` at the display layer.
136
- */
137
- declare function formatMinorUnits(minor: number, minorUnitDecimals?: number): string;
138
- /** Structural type guard. */
139
- declare function isMoney(value: unknown): value is Money;
140
- //#endregion
1
+ import { T as CurrencyMismatchError, _ as negateMoney, a as compareMoney, b as sumMoney, c as fromMajor, d as isPositiveMoney, f as isZeroMoney, g as multiplyMoney, h as money, i as addMoney, l as isMoney, m as minorUnitsToMajor, n as Money, o as equalsMoney, p as majorToMinorUnits, r as absMoney, s as formatMinorUnits, t as MinorUnits, u as isNegativeMoney, v as percentOfMinor, x as toMajor, y as subtractMoney } from "./money-DPo9fLBi.mjs";
141
2
  export { CurrencyMismatchError, MinorUnits, Money, absMoney, addMoney, compareMoney, equalsMoney, formatMinorUnits, fromMajor, isMoney, isNegativeMoney, isPositiveMoney, isZeroMoney, majorToMinorUnits, minorUnitsToMajor, money, multiplyMoney, negateMoney, percentOfMinor, subtractMoney, sumMoney, toMajor };