@shirudo/ddd-kit 2.2.0 → 3.0.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,376 @@
1
+ import { o as DomainError } from "./chunks/errors.js";
2
+ import { Result } from "@shirudo/result";
3
+
4
+ //#region src/money/money.d.ts
5
+ /**
6
+ * Currency identifier for {@link Money}. The kit deliberately ships no
7
+ * currency table: which codes exist and which scale they use is the
8
+ * consumer's decision (see `createMoneyFactory` for wiring a resolver
9
+ * once). ISO 4217 alpha-3 codes are the recommended convention.
10
+ */
11
+ type CurrencyCode = string;
12
+ /**
13
+ * The canonical money representation for domain state, domain events,
14
+ * and snapshots: an exact integer amount in minor units plus the
15
+ * explicit scale that maps minor to major units.
16
+ *
17
+ * Plain data by design: values created by `moneyOfMinor` are frozen,
18
+ * carry no methods or library internals, and survive `structuredClone`,
19
+ * deep-freeze, and the kit's state diffing. The kit ships exact
20
+ * operations only (lossless `addMoney`/`subtractMoney`/`negateMoney`
21
+ * and lossless `rescaleMoney`); everything that carries a rounding or
22
+ * distribution policy (multiplication, ratios, fees, division, lossy
23
+ * rescaling, allocation, FX) is domain policy executed by your
24
+ * calculation library at the use-case boundary (see
25
+ * `moneyFromSnapshot` / `moneyToSnapshot`).
26
+ *
27
+ * Never `number`, never a decimal string, never `amountCents`: `10.99`
28
+ * EUR is `{ amountMinor: 1099n, currency: "EUR", scale: 2 }`, and JPY
29
+ * has scale 0, not an assumed 2.
30
+ */
31
+ interface Money {
32
+ /**
33
+ * Exact integer amount in minor units AT THIS VALUE'S SCALE. The
34
+ * scale may deliberately differ from the currency's default
35
+ * exponent (intermediate precision; the snapshot interop honors
36
+ * the same distinction), so the reference unit is always
37
+ * `this.scale`, never an assumed per-currency constant.
38
+ */
39
+ readonly amountMinor: bigint;
40
+ /** Required currency identifier; operations never mix currencies. */
41
+ readonly currency: CurrencyCode;
42
+ /** Number of minor-unit digits per major unit (EUR 2, JPY 0). */
43
+ readonly scale: number;
44
+ /**
45
+ * Type-level brand: a NON-EXPORTED unique symbol, never present at
46
+ * runtime. A string key ("__brand") could be spelled out by any
47
+ * caller; the module-private symbol cannot, so only the module's
48
+ * constructors mint the type. Convert foreign plain shapes with
49
+ * `moneyFromUnknown` (validates, copies, freezes) or `moneyFromDto`.
50
+ */
51
+ readonly [MONEY_BRAND]: true;
52
+ }
53
+ declare const MONEY_BRAND: unique symbol;
54
+ /**
55
+ * Wire shape for {@link Money}: `amountMinor` travels as a string
56
+ * because JSON numbers are floats (silent precision loss past 2^53) and
57
+ * `JSON.stringify` throws on bigint. Validate with `moneyFromDto`
58
+ * immediately after deserialization; emit with `moneyToDto` right
59
+ * before serialization.
60
+ *
61
+ * A type alias, not an interface, on purpose: only type aliases get the
62
+ * implicit index signature that makes the DTO assignable to `JsonValue`,
63
+ * which `PublishedCommand` payloads require.
64
+ */
65
+ type MoneyDto = {
66
+ /** Integer string matching `/^-?\d+$/`. */readonly amountMinor: string;
67
+ readonly currency: string;
68
+ readonly scale: number;
69
+ };
70
+ /**
71
+ * Validates an unknown plain shape and mints a FRESH, frozen
72
+ * {@link Money} from it: the result shares no reference with the
73
+ * input, so later mutation of the input cannot reach domain state.
74
+ * The door for re-hydrating foreign minor-units data (rows already
75
+ * mapped by an ORM, caches, deserialized snapshots); wire strings go
76
+ * through `moneyFromDto` instead.
77
+ */
78
+ declare function moneyFromUnknown(value: unknown): Money;
79
+ /**
80
+ * Constructs a frozen {@link Money} from an exact minor-unit amount.
81
+ * The only door into the shape: rejects `number` amounts (floats have
82
+ * no place in stored money), invalid scales, and empty or
83
+ * whitespace-carrying currency codes with `InvalidMoneyError`.
84
+ */
85
+ declare function moneyOfMinor(amountMinor: bigint, currency: CurrencyCode, scale: number): Money;
86
+ /**
87
+ * Validates a {@link MoneyDto} fresh off the wire and converts it to
88
+ * {@link Money}. Takes `unknown` on purpose: this IS the trust
89
+ * boundary, so callers never cast before validating. `amountMinor`
90
+ * must be a plain integer string (`/^-?\d+$/`); anything `Number()`
91
+ * would tolerate but bigint arithmetic cannot represent exactly
92
+ * ("1e5", "10.99", "0x10") is rejected with `InvalidMoneyError`.
93
+ */
94
+ declare function moneyFromDto(dto: unknown): Money;
95
+ /**
96
+ * Converts {@link Money} to its JSON-safe wire shape. Guards the input
97
+ * so untyped callers cannot leak a number-amount object onto the wire.
98
+ */
99
+ declare function moneyToDto(money: Money): MoneyDto;
100
+ /**
101
+ * Narrows an unknown value to the canonical {@link Money} shape. A
102
+ * CHECK, not a door: narrowing neither copies nor freezes, so an
103
+ * external alias can still mutate the underlying object after the
104
+ * check. For anything entering domain state, mint a fresh frozen
105
+ * value with {@link moneyFromUnknown} instead.
106
+ */
107
+ declare function isMoney(value: unknown): value is Money;
108
+ /**
109
+ * REPRESENTATION equality, deliberately: amount, currency, AND scale.
110
+ * `10.0` EUR at scale 1 and `10.00` EUR at scale 2 denote the same
111
+ * monetary value but are NOT equal here, because silently conflating
112
+ * scales is how precision bugs hide. For monetary-value comparison,
113
+ * align the scales explicitly first (lossless `rescaleMoney` upscales
114
+ * the coarser side) and then compare.
115
+ */
116
+ declare function moneyEquals(a: Money, b: Money): boolean;
117
+ /** True when the amount is exactly zero. */
118
+ declare function isZeroMoney(money: Money): boolean;
119
+ /** True when the amount is strictly greater than zero. */
120
+ declare function isPositiveMoney(money: Money): boolean;
121
+ /** True when the amount is strictly less than zero. */
122
+ declare function isNegativeMoney(money: Money): boolean;
123
+ /**
124
+ * Renders the exact decimal representation ("10.99", "-0.05", "10").
125
+ * The inverse of `parseMoneyInput` and the precision-safe input for
126
+ * display formatting; never feed the result back into arithmetic.
127
+ * Guards its input like the wire emitters do: a non-Money value fails
128
+ * loudly instead of rendering garbage.
129
+ */
130
+ declare function moneyToDecimalString(money: Money): string;
131
+ //#endregion
132
+ //#region src/money/arithmetic.d.ts
133
+ /**
134
+ * Exact addition of same-currency, same-scale amounts. Mismatches
135
+ * throw (`MONEY_CURRENCY_MISMATCH` / `MONEY_SCALE_MISMATCH`); there is
136
+ * no implicit conversion of either. A result past the amount bound
137
+ * fails as `INVALID_MONEY` instead of wrapping.
138
+ */
139
+ declare function addMoney(a: Money, b: Money): Money;
140
+ /** Exact subtraction under the same guards as {@link addMoney}. */
141
+ declare function subtractMoney(a: Money, b: Money): Money;
142
+ /** Exact sign flip; useful for ledger reversals and refunds. */
143
+ declare function negateMoney(money: Money): Money;
144
+ /**
145
+ * Converts to another scale, LOSSLESSLY or not at all: upscaling and
146
+ * exact downscaling succeed; a downscale that would drop non-zero
147
+ * digits throws `MONEY_PRECISION_LOSS`. There is deliberately no
148
+ * rounding parameter; lossy conversions carry a rounding policy and
149
+ * belong to your calculation library. The intended use is aligning
150
+ * mixed-scale amounts for `addMoney`/`subtractMoney` by upscaling the
151
+ * coarser one.
152
+ */
153
+ declare function rescaleMoney(money: Money, scale: number): Money;
154
+ //#endregion
155
+ //#region src/money/errors.d.ts
156
+ declare class InvalidMoneyError extends DomainError<"INVALID_MONEY"> {
157
+ constructor(message: string);
158
+ }
159
+ declare class MoneyCurrencyMismatchError extends DomainError<"MONEY_CURRENCY_MISMATCH"> {
160
+ constructor(left: string, right: string);
161
+ }
162
+ declare class MoneyScaleMismatchError extends DomainError<"MONEY_SCALE_MISMATCH"> {
163
+ constructor(left: number, right: number);
164
+ }
165
+ declare class MoneyPrecisionLossError extends DomainError<"MONEY_PRECISION_LOSS"> {
166
+ constructor(message: string);
167
+ }
168
+ declare class UnknownCurrencyError extends DomainError<"UNKNOWN_CURRENCY"> {
169
+ constructor(currency: string);
170
+ }
171
+ //#endregion
172
+ //#region src/money/factory.d.ts
173
+ /**
174
+ * Resolves a currency to its scale, or `undefined` for currencies it
175
+ * does not know. The kit ships no currency table; this is the seam
176
+ * where the consumer provides one, once, at the composition root. Any
177
+ * source works as a one-liner: a plain record, the runtime's own data
178
+ * via {@link currencyScaleFromIntl}, or the currency package of a
179
+ * calculation library (`(code) => currencies[code]?.exponent`).
180
+ */
181
+ type CurrencyScaleResolver = (currency: CurrencyCode) => number | undefined;
182
+ /**
183
+ * Currency-aware construction helpers bound to one
184
+ * {@link CurrencyScaleResolver}. Unknown currencies fail loudly with
185
+ * `UNKNOWN_CURRENCY` instead of guessing a scale.
186
+ */
187
+ interface MoneyFactory {
188
+ /** `moneyOfMinor` with the scale resolved from the currency. */
189
+ ofMinor(amountMinor: bigint, currency: CurrencyCode): Money;
190
+ /** `parseMoneyInput` (exact-only) with the scale resolved from the currency. */
191
+ parse(input: unknown, currency: CurrencyCode): Money;
192
+ /** Zero in the given currency at its resolved scale. */
193
+ zero(currency: CurrencyCode): Money;
194
+ /** The resolved scale; throws `UNKNOWN_CURRENCY` when unresolved. */
195
+ scaleOf(currency: CurrencyCode): number;
196
+ }
197
+ /** Options for {@link createMoneyFactory}. */
198
+ interface CreateMoneyFactoryOptions {
199
+ readonly scaleFor: CurrencyScaleResolver;
200
+ }
201
+ /**
202
+ * Binds a {@link CurrencyScaleResolver} once and returns construction
203
+ * helpers that no longer need an explicit scale per call.
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * const money = createMoneyFactory({
208
+ * scaleFor: currencyScaleFromRecord({ EUR: 2, JPY: 0 }),
209
+ * });
210
+ * money.parse("10.99", "EUR"); // { amountMinor: 1099n, currency: "EUR", scale: 2 }
211
+ * ```
212
+ */
213
+ declare function createMoneyFactory(options: CreateMoneyFactoryOptions): MoneyFactory;
214
+ /**
215
+ * Resolver over a plain currency-to-scale record
216
+ * (`{ EUR: 2, JPY: 0 }`). The record is copied into a `Map` at
217
+ * creation, so later mutation of the input and hostile own keys have
218
+ * no effect.
219
+ */
220
+ declare function currencyScaleFromRecord(record: Readonly<Record<string, number>>): CurrencyScaleResolver;
221
+ /**
222
+ * Resolver backed by the runtime's own currency data (ICU via
223
+ * `Intl.NumberFormat`), so no currency table ships with the kit or the
224
+ * consumer. Resolves ONLY canonical uppercase ISO 4217 codes: Intl
225
+ * itself would accept "eur", but a silent alias would let "eur"-Money
226
+ * and "EUR"-Money circulate side by side until an operation throws
227
+ * `MONEY_CURRENCY_MISMATCH`; here "eur" resolves to `undefined` and
228
+ * fails fast as `UNKNOWN_CURRENCY` at the factory.
229
+ *
230
+ * A CONVENIENCE, not an enterprise source of truth: ICU resolves
231
+ * well-formed but UNASSIGNED codes to its default of 2 rather than
232
+ * `undefined`, and the data shifts with the runtime's ICU version.
233
+ * Production money paths should pin a closed, versioned currency map
234
+ * (`currencyScaleFromRecord`) or a calculation library's versioned
235
+ * currency package; use this resolver for demos, prototypes, and
236
+ * internal tooling.
237
+ */
238
+ declare function currencyScaleFromIntl(): CurrencyScaleResolver;
239
+ //#endregion
240
+ //#region src/money/format.d.ts
241
+ /**
242
+ * Formats for display via `Intl.NumberFormat`, feeding the exact
243
+ * decimal string (never a float), with the money's own scale as the
244
+ * fraction-digit count. Presentation only: the output is
245
+ * locale-dependent text and must never flow back into parsing,
246
+ * storage, or arithmetic. Non-Money input fails loudly with
247
+ * `INVALID_MONEY` before Intl is touched.
248
+ *
249
+ * The currency must be well-formed for `Intl` (ISO alpha-3); for
250
+ * non-ISO codes, format `moneyToDecimalString(money)` yourself.
251
+ */
252
+ declare function formatMoney(money: Money, locale: string): string;
253
+ /**
254
+ * Binds the locale once and caches one `Intl.NumberFormat` per
255
+ * currency/scale pair; constructing formatters is expensive, so use
256
+ * this over `formatMoney` anywhere hot (lists, tables, exports).
257
+ */
258
+ declare function createMoneyFormatter(locale: string): (money: Money) => string;
259
+ //#endregion
260
+ //#region src/money/parse.d.ts
261
+ /** Options for {@link parseMoneyInput}. */
262
+ interface ParseMoneyInputOptions {
263
+ readonly currency: CurrencyCode;
264
+ /** Target scale; the kit ships no currency table, so it is explicit. */
265
+ readonly scale: number;
266
+ }
267
+ /**
268
+ * Parses a plain decimal string ("10.99") into exact minor units,
269
+ * without ever touching floating point. This is the safe replacement
270
+ * for the classic bugs `Number(input) * 100` and `parseFloat`: no
271
+ * exponents, no `Infinity`, no locale separators, and NO rounding; the
272
+ * kit never rounds.
273
+ *
274
+ * EXACT OR REJECTED: missing fraction digits pad losslessly ("10.5" at
275
+ * scale 2 is 1050n) and all-zero excess digits are accepted ("10.990"
276
+ * at scale 2 is 1099n), but input that cannot be represented exactly
277
+ * at the target scale throws `MONEY_PRECISION_LOSS`. Whether "10.999"
278
+ * should be rejected or become 11.00 is a BUSINESS decision, not a
279
+ * parsing feature: put it in a domain-named policy function (a
280
+ * `normalizeQuotedPrice`, a `calculateVat`) that rounds via your
281
+ * calculation library and returns `Money`.
282
+ *
283
+ * The grammar is deliberately strict (`/^-?\d+(\.\d+)?$/`). Locale
284
+ * input ("10,99", grouping, currency signs) is a UI concern; normalize
285
+ * it to this grammar before calling.
286
+ *
287
+ * Takes `unknown` on purpose: this is the trust boundary for raw
288
+ * request values, so callers pass `req.body.amount` directly instead
289
+ * of coercing (`String([...])` silently joins arrays) or casting.
290
+ */
291
+ declare function parseMoneyInput(input: unknown, options: ParseMoneyInputOptions): Money;
292
+ //#endregion
293
+ //#region src/money/snapshot.d.ts
294
+ /**
295
+ * The currency part of {@link MoneySnapshotLike}: either a bare code or
296
+ * a currency object as calculation libraries model it (code, numeric
297
+ * base, exponent). Structural on purpose; the kit depends on no
298
+ * calculation library.
299
+ */
300
+ type MoneySnapshotCurrencyLike = string | {
301
+ readonly code: string;
302
+ readonly base?: number | bigint | ReadonlyArray<number | bigint>;
303
+ readonly exponent?: number | bigint;
304
+ };
305
+ /**
306
+ * The `{ amount, currency, scale }` shape that calculation libraries
307
+ * expose when serializing their money objects (a `toJSON()` result,
308
+ * typically). `scale` falls back to the currency's `exponent` when
309
+ * absent; number and bigint calculators are both accepted.
310
+ */
311
+ interface MoneySnapshotLike {
312
+ readonly amount: number | bigint;
313
+ readonly currency: MoneySnapshotCurrencyLike;
314
+ readonly scale?: number | bigint;
315
+ }
316
+ /**
317
+ * The canonical snapshot {@link moneyToSnapshot} emits: number-based
318
+ * with an explicit base-10 currency object, which is exactly what the
319
+ * common calculation-library constructors accept.
320
+ */
321
+ interface MoneySnapshot {
322
+ readonly amount: number;
323
+ readonly currency: {
324
+ readonly code: CurrencyCode;
325
+ readonly base: 10;
326
+ readonly exponent: number;
327
+ };
328
+ readonly scale: number;
329
+ }
330
+ /**
331
+ * Converts a calculation-library snapshot into canonical {@link Money}.
332
+ * The anti-corruption checks live here so they run exactly once, at
333
+ * the boundary:
334
+ *
335
+ * - non-decimal currencies are rejected (`base` other than 10; some
336
+ * library currency packages model MGA/MRU with base 5, and pre-1971
337
+ * GBP with a base array): their minor units do not map onto a
338
+ * power-of-ten scale
339
+ * - number amounts must be safe integers; fractional or beyond-2^53
340
+ * amounts are rejected instead of silently corrupted
341
+ * - bigint amounts pass through exactly
342
+ *
343
+ * Takes `unknown` on purpose: this is the trust boundary for foreign
344
+ * library data, so callers never cast before validating (the
345
+ * {@link MoneySnapshotLike} type documents the expected shape).
346
+ */
347
+ declare function moneyFromSnapshot(snapshot: unknown): Money;
348
+ /**
349
+ * Converts {@link Money} into the snapshot shape the common
350
+ * calculation-library constructors accept. Number-based by design (the
351
+ * libraries' default calculators are), so amounts past
352
+ * `Number.MAX_SAFE_INTEGER` are rejected loudly; wire such amounts
353
+ * into a bigint calculator directly from `money.amountMinor` instead.
354
+ */
355
+ declare function moneyToSnapshot(money: Money): MoneySnapshot;
356
+ //#endregion
357
+ //#region src/money/try-parse.d.ts
358
+ /**
359
+ * {@link parseMoneyInput} as a `Result`: `Err` for the documented
360
+ * rejections (malformed input as `InvalidMoneyError`, over-precise
361
+ * input as `MoneyPrecisionLossError`), a throw for everything else.
362
+ */
363
+ declare function tryParseMoneyInput(input: unknown, options: ParseMoneyInputOptions): Result<Money, InvalidMoneyError | MoneyPrecisionLossError>;
364
+ /**
365
+ * {@link moneyFromDto} as a `Result`: `Err` for the documented
366
+ * rejection (`InvalidMoneyError`), a throw for everything else.
367
+ */
368
+ declare function tryMoneyFromDto(dto: unknown): Result<Money, InvalidMoneyError>;
369
+ /**
370
+ * {@link moneyFromSnapshot} as a `Result`: `Err` for the documented
371
+ * rejection (`InvalidMoneyError`), a throw for everything else.
372
+ */
373
+ declare function tryMoneyFromSnapshot(snapshot: unknown): Result<Money, InvalidMoneyError>;
374
+ //#endregion
375
+ export { type CreateMoneyFactoryOptions, type CurrencyCode, type CurrencyScaleResolver, InvalidMoneyError, type Money, MoneyCurrencyMismatchError, type MoneyDto, type MoneyFactory, MoneyPrecisionLossError, MoneyScaleMismatchError, type MoneySnapshot, type MoneySnapshotCurrencyLike, type MoneySnapshotLike, type ParseMoneyInputOptions, UnknownCurrencyError, addMoney, createMoneyFactory, createMoneyFormatter, currencyScaleFromIntl, currencyScaleFromRecord, formatMoney, isMoney, isNegativeMoney, isPositiveMoney, isZeroMoney, moneyEquals, moneyFromDto, moneyFromSnapshot, moneyFromUnknown, moneyOfMinor, moneyToDecimalString, moneyToDto, moneyToSnapshot, negateMoney, parseMoneyInput, rescaleMoney, subtractMoney, tryMoneyFromDto, tryMoneyFromSnapshot, tryParseMoneyInput };
376
+ //# sourceMappingURL=money.d.ts.map