Files
2026-05-27 23:29:53 +01:00

5.6 KiB

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)

// 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

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.