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