172 lines
5.6 KiB
Markdown
172 lines
5.6 KiB
Markdown
# OpenSpec Change: enterprise-money
|
|
|
|
- **Change ID**: enterprise-money
|
|
- **Package**: `@suaveplan/money`
|
|
- **Category**: finance
|
|
- **Target path**: `finance/currency/money`
|
|
- **Status**: PROPOSED
|
|
- **Date**: 2026-04-24
|
|
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/money.md`
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Deliver `@suaveplan/money` on the genesis branch from scratch at enterprise quality. The package provides precise currency / money arithmetic using BigInt minor-unit representation. It ships the immutable `Money` class, a `CurrencyRegistry` (constructor-injectable, with a default global instance), ISO 4217 metadata, virtual-currency registration, `Intl`-backed formatting, allocation helpers, aggregation helpers, `MoneyRange`, locale-aware parse, BigInt-safe JSON serialization, Zod schemas, property-based tests, and a typed error hierarchy. Zero runtime dependencies beyond `tslib` and `zod`.
|
|
|
|
---
|
|
|
|
## Current-state audit (genesis)
|
|
|
|
Does not exist on genesis — fresh build.
|
|
|
|
---
|
|
|
|
## Available workspace deps on genesis
|
|
|
|
| Package | Role |
|
|
|---|---|
|
|
| `@suaveplan/error` (`workspace:^`) | Base for `MoneyError` hierarchy |
|
|
| `@suaveplan/types` (`workspace:^`) | `LoggerLike` (not used at runtime; types only) |
|
|
| `zod` (`catalog:forms`) | `MoneyLike` / `CurrencyCode` schemas |
|
|
| `tslib` (`catalog:build`) | Runtime helpers |
|
|
|
|
Dev dep: `fast-check catalog:testing`.
|
|
|
|
---
|
|
|
|
## Public API surface (from main branch, fully preserved + genesis additions)
|
|
|
|
```ts
|
|
// Core
|
|
class Money {
|
|
static fromMajor(value: number | string, currency: string, registry?: CurrencyRegistry): Money
|
|
static fromMinor(minorUnits: bigint, currency: string, registry?: CurrencyRegistry): Money
|
|
static zero(currency: string, registry?: CurrencyRegistry): Money
|
|
static withRegistry(registry: CurrencyRegistry): typeof Money
|
|
get amount(): bigint // minor units
|
|
get currency(): string
|
|
add(other: Money): Money
|
|
subtract(other: Money): Money
|
|
multiply(factor: number): Money
|
|
divide(divisor: number, mode?: RoundingMode): Money
|
|
compare(other: Money): -1 | 0 | 1
|
|
equals(other: Money): boolean
|
|
abs(): Money
|
|
negate(): Money
|
|
isZero(): boolean
|
|
isPositive(): boolean
|
|
isNegative(): boolean
|
|
toMajor(): number
|
|
toDecimalString(): string
|
|
toJSON(): { amount: string; currency: string }
|
|
}
|
|
|
|
// Registry
|
|
class CurrencyRegistry {
|
|
static global(): CurrencyRegistry
|
|
getCurrency(code: string): CurrencyInfo | undefined
|
|
getCurrencyOrThrow(code: string): CurrencyInfo
|
|
getAllCurrencyCodes(): string[]
|
|
registerCurrency(info: CurrencyInfo): void
|
|
getVirtualCurrencies(): CurrencyInfo[]
|
|
isVirtualCurrency(code: string): boolean
|
|
minorUnitMultiplier(code: string): bigint
|
|
}
|
|
function getCurrency(code: string): CurrencyInfo | undefined // delegates to global
|
|
function getCurrencyOrThrow(code: string): CurrencyInfo
|
|
function getAllCurrencyCodes(): string[]
|
|
function registerCurrency(info: CurrencyInfo): void
|
|
function getVirtualCurrencies(): CurrencyInfo[]
|
|
function isVirtualCurrency(code: string): boolean
|
|
function minorUnitMultiplier(code: string): bigint
|
|
|
|
// Formatting
|
|
function formatMoney(money: Money, options?: FormatOptions): string
|
|
function formatMoneyCompact(money: Money, options?: FormatOptions): string
|
|
function parseMoney(input: string, currency: string, locale?: string): Money
|
|
|
|
// Allocation
|
|
function allocateEvenly(money: Money, parts: number): Money[]
|
|
function allocateByRatios(money: Money, ratios: number[]): Money[]
|
|
function allocateByPercentages(money: Money, percentages: number[]): Money[]
|
|
function sumMoney(moneys: Money[]): Money
|
|
function minMoney(...moneys: Money[]): Money
|
|
function maxMoney(...moneys: Money[]): Money
|
|
function applyRounding(money: Money, mode: RoundingMode): Money
|
|
|
|
// MoneyRange (genesis addition)
|
|
class MoneyRange {
|
|
constructor(min: Money, max: Money)
|
|
contains(money: Money): boolean
|
|
intersect(other: MoneyRange): MoneyRange | null
|
|
union(other: MoneyRange): MoneyRange
|
|
toArray(step: Money): Money[]
|
|
}
|
|
|
|
// Serialization
|
|
function moneyToJSON(money: Money): { amount: string; currency: string }
|
|
function moneyFromJSON(json: { amount: string; currency: string }): Money
|
|
|
|
// Zod schemas
|
|
const MoneyLikeSchema: z.ZodType<MoneyLike>
|
|
const CurrencyCodeSchema: z.ZodType<CurrencyCode>
|
|
const FormatOptionsSchema: z.ZodType<FormatOptions>
|
|
|
|
// Error types
|
|
class MoneyError extends SuaveplanError { }
|
|
class CurrencyMismatchError extends MoneyError { }
|
|
class UnknownCurrencyError extends MoneyError { }
|
|
class AllocationError extends MoneyError { }
|
|
class ParseError extends MoneyError { }
|
|
```
|
|
|
|
---
|
|
|
|
## File layout
|
|
|
|
```text
|
|
finance/currency/money/
|
|
package.json
|
|
tsconfig.json
|
|
vitest.config.ts
|
|
src/
|
|
index.ts
|
|
error-codes.ts
|
|
constants/index.ts
|
|
types/
|
|
money.types.ts
|
|
money.types.md
|
|
core/
|
|
money/
|
|
money.ts / money.md / money.types.ts / money.test.ts / index.ts
|
|
currency-registry/
|
|
currency-registry.ts / .md / .types.ts / .test.ts / index.ts
|
|
currencies/
|
|
currencies.ts / currencies.md / currencies.test.ts / index.ts
|
|
money-range/
|
|
money-range.ts / .md / .test.ts / index.ts
|
|
formatting/
|
|
formatter/
|
|
formatter.ts / formatter.md / formatter.test.ts / index.ts
|
|
allocation/
|
|
allocator/
|
|
allocator.ts / allocator.md / allocator.test.ts / index.ts
|
|
schemas/
|
|
money-schemas/
|
|
money-schemas.ts / money-schemas.md / money-schemas.test.ts / index.ts
|
|
serialization/
|
|
json/
|
|
json.ts / json.md / json.test.ts / index.ts
|
|
tests/
|
|
property.test.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Impact
|
|
|
|
Downstream: `@suaveplan/currency` (uses `MoneyLike`), `@suaveplan/currency-providers`, payment and billing packages.
|
|
|
|
**Prerequisites on Verdaccio**: `@suaveplan/error`, `zod`.
|