Initial Commit
This commit is contained in:
126
openspec/archive/2026-05-17-add-money/proposal.md
Normal file
126
openspec/archive/2026-05-17-add-money/proposal.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Add @suaveplan/money — precise minor-unit currency arithmetic
|
||||
|
||||
**Package**: `@suaveplan/money`
|
||||
**Status**: PROPOSED
|
||||
**Type**: migration (upstream `../suaveplan/packages/finance/money`)
|
||||
|
||||
- **Design**: ./design.md
|
||||
- **Spec delta**: ./specs/money/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
`STATUS.md` line 131 lists `@suaveplan/money` as Pending. The upstream package is a small, universal-tier, runtime-pure library (5 source files, ~520 LOC, 5 test files, ~99% upstream coverage) that owns a problem genesis has no other answer for: representing monetary amounts as integer minor units (`bigint`) so consumer apps never hit floating-point drift, and surrounding that representation with the operators (arithmetic, comparison, allocation, formatting, JSON round-trip) that real-world commerce needs. No genesis package covers this surface today, and a string of pending downstream packages (`@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/payment`, `@suaveplan/payment-providers`, the `api-plugin-bank` / `api-plugin-payment` / `api-plugin-banking` plugins, plus `@suaveplan/booking`) all consume `MoneyLike` and the currency registry as their value-shape contract. Migrating `money` first unblocks every commerce-tier package downstream.
|
||||
|
||||
The migration is straightforward: preserve the entire public surface verbatim (same class names, same function signatures, same option fields, same return shapes), upgrade plain `throw new Error(...)` to `SuaveplanError` with a `MONEY.*` code registry (rule §63 of CLAUDE.md), drop the `core/` and `formatting/` redundant ancestors so files live under `<feature>/<feature>.ts` per the no-prefix and feature-folder rules, expose every feature module as a subpath export, and ship at the genesis quality bar (100/100/100/100 coverage, sibling `.md` per implementation file, `{ timeout: 5000 }` on every test, `@suaveplan/testing/runner` for runner primitives, no `export *` in feature barrels). The enterprise audit (`../suaveplan/enterprise/money.md`) lists ambitious additions (typed error hierarchy, property-based tests, locale-aware parser, `MoneyRange`, scoped `CurrencyRegistry`, Zod schemas, tax helpers, cryptocurrency interop) — the typed error hierarchy is in scope here; everything else is explicitly deferred to follow-up changes that build on this core.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `Money` class — immutable `bigint`-backed value type with `amount: bigint` + `currency: CurrencyCode`; static constructors `fromMajor(value: number | string, currency)`, `fromMinor(amount: bigint | number, currency)`, `zero(currency)`, `fromJSON(json)`; arithmetic `add(other)`, `subtract(other)`, `multiply(factor, rounding?)`, `divide(divisor, rounding?)`, `abs()`, `negate()`; comparison `compareTo(other)`, `equals(other)`, `greaterThan(other)`, `lessThan(other)`, `greaterThanOrEqual(other)`, `lessThanOrEqual(other)`, `isZero()`, `isPositive()`, `isNegative()`; conversion `toMajor()`, `toDecimalString()`, `toJSON()`, `toString()`.
|
||||
- ADDED: `applyRounding(value: number, mode: RoundingMode): bigint` — explicit helper exporting all five rounding modes (`half-up`, `half-down`, `half-even`, `floor`, `ceil`).
|
||||
- ADDED: Currency registry — `getCurrency(code)` (returns `CurrencyInfo | undefined`), `getCurrencyOrThrow(code)` (raises `MoneyError({ code: MONEY.UNKNOWN_CURRENCY })`), `registerCurrency(info)`, `getAllCurrencyCodes()`, `getVirtualCurrencies()`, `isVirtualCurrency(code)`, `minorUnitMultiplier(exponent)`. 35 built-in entries: 30 ISO 4217 fiats (USD, EUR, GBP, JPY, CHF, CAD, AUD, CNY, INR, BRL, KRW, MXN, SGD, HKD, NOK, SEK, DKK, NZD, ZAR, PLN, THB, TRY, TWD, AED, SAR, ILS, RUB, CZK, CLP, PHP) + 5 cryptocurrencies (BTC, ETH, SOL, USDT, USDC).
|
||||
- ADDED: Formatters — `formatMoney(money, options?)` with locale-aware decimal/thousands separators, `showSymbol`/`showCode`/`trailingZeros`/`decimalSeparator`/`thousandsSeparator` overrides; `formatMoneyCompact(money)` rendering K/M/B suffixes.
|
||||
- ADDED: Allocators — `allocateEvenly(money, parts)`, `allocateByRatios(money, ratios)`, `allocateByPercentages(money, percentages)`. Every allocator preserves the input total (remainder distributed round-robin) and returns `AllocationResult { shares, remainder: 0n }`.
|
||||
- ADDED: Aggregators — `minMoney(...values)`, `maxMoney(...values)`, `sumMoney(values)`.
|
||||
- ADDED: `MONEY_CODES` registry via `defineCodes("MONEY", [...])` — 6 codes: `CURRENCY_MISMATCH`, `UNKNOWN_CURRENCY`, `DIVISION_BY_ZERO`, `INVALID_ALLOCATION`, `EMPTY_INPUT`, `PARSE_ERROR`.
|
||||
- ADDED: `MoneyError extends SuaveplanError<MoneyErrorCode>` carrying optional `currency?` / `otherCurrency?` discriminants.
|
||||
- ADDED: Types — `CurrencyCode`, `RoundingMode`, `MoneyOptions`, `CurrencyInfo`, `FormatOptions`, `AllocationResult`, `MoneyLike` (re-exported from root and from `./types/common`).
|
||||
- ADDED: Subpath exports per public module — `./money/money`, `./currencies/currencies`, `./formatter/formatter`, `./allocator/allocator`, `./error-codes/error-codes`, `./types/common`. Tree-shakeable.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Locale-aware `parseMoney(input, currency, locale)` and the inverse round-trip identity `Money.fromMajor(m.toDecimalString())`-via-locale.** Listed in the enterprise audit as "feature-rich enhancement". The decimal-string round-trip is in scope (it is part of the upstream public surface); the locale-aware parser is a new feature whose acceptance criteria (Indian grouping, comma decimals, currency-symbol stripping) require a fully-considered locale-token API. Reason: scope is "match upstream + typed errors"; new parser belongs in a follow-up `add-money-parse` change.
|
||||
- **`MoneyRange` (interval arithmetic with `intersect`/`union`/`contains`).** Listed in enterprise audit as a feature-rich enhancement. Reason: the upstream library does not expose it; it is additive and orthogonal to the migration baseline. Will land as a follow-up.
|
||||
- **Scoped `CurrencyRegistry` class with constructor injection.** Listed in enterprise audit as multi-tenant requirement. The current module-level map is preserved bit-for-bit (`registerCurrency` still mutates module state) to match the upstream public contract; a scoped registry would change semantics for every caller and belongs in a separate change. Reason: behaviour-preserving migration; the scoped registry is the design pivot that requires its own proposal.
|
||||
- **Zod schemas for `MoneyLike` / `CurrencyCode` / `FormatOptions`.** Listed in enterprise audit. Reason: applicable to API boundaries; not needed by the in-process developer-facing API. Will land in `@suaveplan/api-schemas` when the first network-tier consumer surfaces.
|
||||
- **Tax helpers (VAT split, jurisdiction-aware rounding).** Listed in enterprise audit. Reason: requires a tax-rule registry of its own; belongs in a future `@suaveplan/tax` package that consumes `@suaveplan/money`.
|
||||
- **Cryptocurrency unit interop with `@suaveplan/cryptocurrency` (WEI / SATOSHI / LAMPORT converters).** Reason: `cryptocurrency` is not yet migrated; the BTC/ETH/SOL/USDT/USDC entries in the built-in registry use the standard exponents (8, 18, 9, 6, 6) and arithmetic works correctly today. Higher-level converters land when `@suaveplan/cryptocurrency` is migrated.
|
||||
- **Historical-currency `validFrom`/`validTo`.** Reason: not in the upstream `CurrencyInfo` shape; additive new feature.
|
||||
- **Property-based tests via fast-check.** Reason: while listed in the enterprise audit, the upstream test suite covers every arithmetic law, allocation invariant, and round-trip equality with example-based tests at ~99% coverage; reaching 100% on the migrated surface is the gate, and fast-check will be folded in once the deterministic suite is green. The new package depends on `fast-check` for future use but does not require it for the migration's acceptance criteria.
|
||||
- **`@suaveplan/error` consumer migration documentation.** Consumers do not yet exist on genesis (every potential downstream — `currency`, `payment`, etc. — is pending). When the first dependent package migrates it will catch the typed-error change at compile time.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `../suaveplan/enterprise/money.md` and the upstream source at `../suaveplan/packages/finance/money/src/`.
|
||||
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Universal-tier purity | enterprise/money.md L52-L60 (cross-cutting) | Genesis-wide rule; reinforced here |
|
||||
| `Money` immutable `bigint` value type with arithmetic / comparison / conversion | enterprise/money.md L12, L20-L25, L64-L65; upstream `src/core/money.ts` L1-L218 | Verbatim — every method preserved |
|
||||
| `applyRounding` with five rounding modes | enterprise/money.md L17, L194-L218; upstream `src/core/money.ts` L194-L218 | Verbatim |
|
||||
| Currency registry (`getCurrency`/`getCurrencyOrThrow`/`registerCurrency`/`getAllCurrencyCodes`/`getVirtualCurrencies`/`isVirtualCurrency`/`minorUnitMultiplier`) | enterprise/money.md L12, L17, L22; upstream `src/core/currencies.ts` L1-L106 | Verbatim, plus typed-error replacement |
|
||||
| Built-in 30 fiat + 5 crypto entries | upstream `src/core/currencies.ts` L4-L56 | Verbatim |
|
||||
| `formatMoney` + `formatMoneyCompact` with `Intl`-derived separators | enterprise/money.md L12, L17, L23; upstream `src/formatting/formatter.ts` L1-L128 | Verbatim |
|
||||
| Allocators (`allocateEvenly`, `allocateByRatios`, `allocateByPercentages`) preserving total | enterprise/money.md L12, L17, L24; upstream `src/allocation/allocator.ts` L1-L99 | Verbatim |
|
||||
| Aggregators (`sumMoney`, `minMoney`, `maxMoney`) | enterprise/money.md L12, L17; upstream `src/allocation/allocator.ts` L101-L151 | Verbatim |
|
||||
| `MoneyLike` / `CurrencyCode` / `CurrencyInfo` / `FormatOptions` / `AllocationResult` / `RoundingMode` / `MoneyOptions` types | enterprise/money.md L17, L25; upstream `src/types/money.types.ts` L1-L65 | Verbatim |
|
||||
| Lossless JSON round-trip via `toJSON` / `fromJSON` (string-encoded `bigint`) | enterprise/money.md L72; upstream `src/core/money.ts` L180-L187 | Verbatim |
|
||||
| Subpath exports per public module | enterprise/money.md L17 (tree-shakeable) | Reorganised: drop `core/`/`formatting/`/`allocation/` ancestors |
|
||||
| Typed error hierarchy `MoneyError` extending `SuaveplanError` with stable `MONEY.*` codes | enterprise/money.md L43-L44 | Migration upgrade: replaces `throw new Error(...)` |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| 100% test coverage (istanbul, all four axes) | Genesis-wide policy (CLAUDE.md §7) — upstream was at ~99% |
|
||||
| Sibling `.md` per implementation file (≥200 words, basic + advanced examples) | Genesis-wide policy (CLAUDE.md §4) — upstream had partial docs |
|
||||
| Test files import runner primitives from `@suaveplan/testing/runner` | Genesis-wide policy (AGENTS.md §7) — upstream imported `vitest` directly |
|
||||
| Explicit named re-exports in every feature-module barrel (no `export *`) | Genesis-wide policy (AGENTS.md §6a) |
|
||||
| Package version starts at `0.1.0` | Genesis-wide policy (CLAUDE.md "Editing") — upstream was `1.0.6` |
|
||||
|
||||
### Parity Checklist
|
||||
|
||||
| Capability | Upstream | Migrated | Notes |
|
||||
|---|---|---|---|
|
||||
| `Money` class — full constructor + arithmetic + comparison + conversion surface | ✅ | ✅ | Every method preserved bit-for-bit |
|
||||
| `applyRounding` standalone helper | ✅ | ✅ | Same five modes |
|
||||
| `getCurrency` / `getCurrencyOrThrow` | ✅ | ⬆️ Enhanced | `getCurrencyOrThrow` now raises `MoneyError({ code: MONEY.UNKNOWN_CURRENCY })` |
|
||||
| `registerCurrency` (module-level mutation) | ✅ | ✅ | Behaviour preserved — same semantics, same global map |
|
||||
| `getAllCurrencyCodes` / `getVirtualCurrencies` / `isVirtualCurrency` / `minorUnitMultiplier` | ✅ | ✅ | Verbatim |
|
||||
| 30 ISO 4217 fiats + 5 crypto built-ins | ✅ | ✅ | Verbatim entries |
|
||||
| `formatMoney` + `formatMoneyCompact` | ✅ | ✅ | Verbatim |
|
||||
| Allocators preserving input total | ✅ | ⬆️ Enhanced | `throw new Error(...)` → `MoneyError({ code: MONEY.INVALID_ALLOCATION })` |
|
||||
| Aggregators (`sumMoney`/`minMoney`/`maxMoney`) | ✅ | ⬆️ Enhanced | Currency mismatch raises `MoneyError({ code: MONEY.CURRENCY_MISMATCH })` |
|
||||
| `Money.toJSON` / `Money.fromJSON` BigInt-safe round-trip | ✅ | ✅ | Verbatim |
|
||||
| All public types | ✅ | ✅ | Re-exported from root + subpath |
|
||||
| Subpath exports per public module | ✅ | ⬆️ Enhanced | 6 subpaths (vs upstream's 3) — every feature is tree-shakeable |
|
||||
| Plain `throw new Error(...)` from boundaries | ✅ | ⬆️ Replaced | All raised errors extend `SuaveplanError` with stable `MONEY.*` codes |
|
||||
| `MoneyError` discriminants (`currency`/`otherCurrency`) | 🚫 upstream | ⬆️ Added | Genesis convention for typed errors |
|
||||
| Locale-aware `parseMoney` | 🚫 upstream (audit asks) | 🚫 deferred | Follow-up change; scope is migration baseline |
|
||||
| `MoneyRange` | 🚫 upstream (audit asks) | 🚫 deferred | Additive new feature; follow-up change |
|
||||
| Scoped `CurrencyRegistry` instance | 🚫 upstream (audit asks) | 🚫 deferred | Changes semantics for every caller; follow-up change |
|
||||
| Property-based tests (fast-check) | 🚫 upstream (audit asks) | 🚫 deferred | Deterministic suite hits 100% first; fast-check added in follow-up |
|
||||
| Tax helpers | 🚫 upstream (audit asks) | 🚫 deferred | Belongs to future `@suaveplan/tax` |
|
||||
| Zod schemas | 🚫 upstream (audit asks) | 🚫 deferred | Will land in `@suaveplan/api-schemas` when needed |
|
||||
| Cryptocurrency unit interop | 🚫 upstream (audit asks) | 🚫 deferred | `@suaveplan/cryptocurrency` not yet migrated |
|
||||
| Historical-currency `validFrom`/`validTo` | 🚫 upstream (audit asks) | 🚫 deferred | Additive new feature |
|
||||
|
||||
### Reference docs consulted
|
||||
|
||||
- ✅ `../suaveplan/packages/finance/money/src/` — all 5 implementation files + 5 test files read
|
||||
- ✅ `../suaveplan/packages/finance/money/README.md` — public API documentation
|
||||
- ✅ `../suaveplan/packages/finance/money/package.json` — dep graph + subpath exports
|
||||
- ✅ `../suaveplan/enterprise/money.md` — enterprise audit, in full
|
||||
- 🚫 `../suaveplan/openspec/specs/money/spec.md` — does not exist (upstream had no formal spec for this package; the parity contract is the source itself)
|
||||
- 🚫 `../suaveplan/openspec/changes/*/proposal.md` touching money — none found beyond `enterprise-money` itself
|
||||
|
||||
## Verification approach
|
||||
|
||||
Every requirement in the spec delta has a `**Verified by:**` line naming the test files that pin its scenarios. The test layout mirrors the source layout (one `<feature>.test.ts` per `<feature>.ts`):
|
||||
|
||||
- `src/money/money.test.ts` — `Money` class arithmetic, comparison, conversion, JSON round-trip, `applyRounding` modes
|
||||
- `src/currencies/currencies.test.ts` — registry lookup, registration, virtual-currency helpers
|
||||
- `src/formatter/formatter.test.ts` — `formatMoney` locale/options matrix, `formatMoneyCompact` K/M/B
|
||||
- `src/allocator/allocator.test.ts` — allocator total-preservation invariant, aggregator currency-mismatch raises
|
||||
- `src/error-codes/error-codes.test.ts` — every `MONEY.*` code reachable from a real call site; `SuaveplanError` contract preserved
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/money` to enumerate.
|
||||
|
||||
- No dependents — this is a new universal-tier leaf package being added to genesis. Every potential downstream (`@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/payment`, `@suaveplan/payment-providers`, `@suaveplan/booking`, the `api-plugin-bank`/`api-plugin-payment`/`api-plugin-banking` plugins) is still in the `Pending` list and will adopt this package when migrated.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. No existing genesis package imports `@suaveplan/money`.
|
||||
Reference in New Issue
Block a user