@classytic/primitives 0.18.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 (46) hide show
  1. package/CHANGELOG.md +906 -715
  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 +195 -0
  22. package/dist/monetization.mjs +79 -0
  23. package/dist/money-DPo9fLBi.d.mts +347 -0
  24. package/dist/money.d.mts +1 -140
  25. package/dist/money.mjs +6 -15
  26. package/dist/outbox.d.mts +49 -1
  27. package/dist/outbox.mjs +64 -1
  28. package/dist/payment-events.d.mts +132 -4
  29. package/dist/payment-events.mjs +22 -2
  30. package/dist/payment-gateway.d.mts +210 -7
  31. package/dist/payment-gateway.mjs +18 -1
  32. package/dist/period.d.mts +138 -7
  33. package/dist/period.mjs +316 -5
  34. package/dist/person-parse.d.mts +82 -0
  35. package/dist/person-parse.mjs +109 -0
  36. package/dist/proration.d.mts +76 -0
  37. package/dist/proration.mjs +132 -0
  38. package/dist/sla-policy.d.mts +1 -1
  39. package/dist/state-machine.mjs +13 -0
  40. package/dist/suspension.d.mts +1 -1
  41. package/dist/timezone-tlG7ygwx.d.mts +114 -0
  42. package/dist/timezone.d.mts +2 -100
  43. package/dist/timezone.mjs +38 -3
  44. package/dist/unit-cost-rate.d.mts +1 -2
  45. package/package.json +244 -224
  46. package/dist/currency-BCJnJBlb.d.mts +0 -119
@@ -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 };
package/dist/money.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { minorUnitFactor } from "./currency.mjs";
1
+ import { CurrencyMismatchError, currencyCode, isCurrencyCode, minorUnitFactor } from "./currency.mjs";
2
2
  //#region src/money/money.ts
3
3
  /**
4
4
  * Cast a raw number to {@link MinorUnits}. Pass the currency as the second
@@ -12,26 +12,17 @@ import { minorUnitFactor } from "./currency.mjs";
12
12
  function MinorUnits(amount, _currency) {
13
13
  return amount;
14
14
  }
15
- var CurrencyMismatchError = class extends Error {
16
- name = "CurrencyMismatchError";
17
- left;
18
- right;
19
- constructor(left, right) {
20
- super(`Currency mismatch: ${left} vs ${right}`);
21
- this.left = left;
22
- this.right = right;
23
- }
24
- };
25
15
  /**
26
16
  * Construct {@link Money} from an integer minor-unit amount.
27
17
  *
28
18
  * @throws TypeError if `amount` is not a finite integer.
19
+ * @throws InvalidCurrencyCodeError if `currency` is not ISO 4217 format.
29
20
  */
30
21
  function money(amount, currency) {
31
22
  if (!Number.isFinite(amount) || !Number.isInteger(amount)) throw new TypeError(`Money.amount must be a finite integer in minor units, got ${amount}`);
32
23
  return {
33
24
  amount,
34
- currency
25
+ currency: currencyCode(currency)
35
26
  };
36
27
  }
37
28
  /**
@@ -60,7 +51,7 @@ function fromMajor(major, currency) {
60
51
  const cleaned = Number(raw.toPrecision(15));
61
52
  return {
62
53
  amount: Math.round(Math.abs(cleaned)) * (cleaned < 0 ? -1 : 1),
63
- currency
54
+ currency: currencyCode(currency)
64
55
  };
65
56
  }
66
57
  /** Convert {@link Money} back to its major-unit floating-point representation. */
@@ -100,7 +91,7 @@ function multiplyMoney(m, scalar) {
100
91
  function sumMoney(values, currency) {
101
92
  return values.reduce((acc, v) => addMoney(acc, v), {
102
93
  amount: 0,
103
- currency
94
+ currency: currencyCode(currency)
104
95
  });
105
96
  }
106
97
  /** Strict equality (amount and currency both match). */
@@ -180,7 +171,7 @@ function formatMinorUnits(minor, minorUnitDecimals = 2) {
180
171
  }
181
172
  /** Structural type guard. */
182
173
  function isMoney(value) {
183
- return typeof value === "object" && value !== null && typeof value.amount === "number" && Number.isInteger(value.amount) && typeof value.currency === "string";
174
+ return typeof value === "object" && value !== null && typeof value.amount === "number" && Number.isInteger(value.amount) && isCurrencyCode(value.currency);
184
175
  }
185
176
  //#endregion
186
177
  export { CurrencyMismatchError, MinorUnits, absMoney, addMoney, compareMoney, equalsMoney, formatMinorUnits, fromMajor, isMoney, isNegativeMoney, isPositiveMoney, isZeroMoney, majorToMinorUnits, minorUnitsToMajor, money, multiplyMoney, negateMoney, percentOfMinor, subtractMoney, sumMoney, toMajor };
package/dist/outbox.d.mts CHANGED
@@ -157,6 +157,19 @@ declare class InvalidOutboxEventError extends Error {
157
157
  * `claimPending`.** If the underlying storage has corrupt rows, the
158
158
  * store is responsible for quarantining them.
159
159
  */
160
+ /**
161
+ * The delivery states an outbox row can hold.
162
+ *
163
+ * Named HERE, in the contract, rather than in each store. It went unnamed while every
164
+ * method took an event id and none took a state — `countByStatus` is the first member
165
+ * that makes the vocabulary part of the interface, and two stores had already written
166
+ * the same three strings independently by then.
167
+ *
168
+ * `dead_letter` is terminal until an operator calls `requeue`; it is a durable statement
169
+ * that something is owed, not a deletion.
170
+ */
171
+ declare const OUTBOX_STATUSES: readonly ["pending", "delivered", "dead_letter"];
172
+ type OutboxStatus = (typeof OUTBOX_STATUSES)[number];
160
173
  interface OutboxStore {
161
174
  /**
162
175
  * Declares that `save` ENLISTS `options.session` in the caller's transaction,
@@ -242,6 +255,41 @@ interface OutboxStore {
242
255
  * @returns Number of purged events.
243
256
  */
244
257
  purge?(olderThanMs: number): Promise<number>;
258
+ /**
259
+ * Re-drive ONE dead-lettered event: back to `pending`, attempts reset, lease cleared,
260
+ * immediately visible.
261
+ *
262
+ * The other half of `fail({ deadLetter: true })`. Dead-lettering is only a durable
263
+ * statement that something is owed; a store that can dead-letter but not requeue leaves
264
+ * an operator who has fixed the root cause with no way to act on the worklist except
265
+ * hand-editing rows in the database.
266
+ *
267
+ * MUST be scoped to `dead_letter` — requeuing a `pending` row would reset the attempt
268
+ * count on an event that is merely backing off, defeating the retry policy, and
269
+ * requeuing a `delivered` row would republish an event every subscriber already saw.
270
+ *
271
+ * @returns `true` if a dead-lettered event was requeued, `false` if no such event
272
+ * exists — NOT an error. An operator re-driving a worklist races the relay, and an
273
+ * id that has already been dealt with is an ordinary outcome.
274
+ */
275
+ requeue?(eventId: string): Promise<boolean>;
276
+ /**
277
+ * How many events are in `status`. The health surface behind an operator dashboard or
278
+ * an alert (`dead_letter > 0` means money or access may be unrecorded).
279
+ */
280
+ countByStatus?(status: OutboxStatus): Promise<number>;
281
+ /**
282
+ * Age in ms of the OLDEST pending event, or `null` when nothing is pending.
283
+ *
284
+ * The single most useful outbox health signal, and the one a naive dashboard misses: a
285
+ * pending COUNT looks identical whether the queue is draining briskly or has been
286
+ * wedged for an hour behind one poison row. Age distinguishes them.
287
+ *
288
+ * Measured from the event's creation, not its last attempt — the question being asked
289
+ * is "how long has this event gone undelivered", and resetting the clock on each retry
290
+ * would make a permanently-failing row look permanently fresh.
291
+ */
292
+ oldestPendingAgeMs?(): Promise<number | null>;
245
293
  }
246
294
  //#endregion
247
- export { InvalidOutboxEventError, OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxErrorInfo, OutboxFailOptions, OutboxFailureContext, OutboxFailureDecision, OutboxFailurePolicy, OutboxOwnershipError, OutboxStore, OutboxWriteOptions };
295
+ export { InvalidOutboxEventError, OUTBOX_STATUSES, OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxErrorInfo, OutboxFailOptions, OutboxFailureContext, OutboxFailureDecision, OutboxFailurePolicy, OutboxOwnershipError, OutboxStatus, OutboxStore, OutboxWriteOptions };