/** * Money Value Object * * Encapsulates currency and amount with proper validation * * Business Rules: * - Amount must be non-negative * - Currency must be valid ISO 4217 code * - Money is immutable * - Arithmetic operations return new Money instances */ export class Money { private readonly amount: number; private readonly currency: string; private static readonly SUPPORTED_CURRENCIES = ['USD', 'EUR', 'GBP', 'CNY', 'JPY']; private constructor(amount: number, currency: string) { this.amount = amount; this.currency = currency; } static create(amount: number, currency: string): Money { if (amount < 0) { throw new Error('Amount cannot be negative.'); } const normalizedCurrency = currency.trim().toUpperCase(); if (!Money.isValidCurrency(normalizedCurrency)) { throw new Error( `Invalid currency code: ${currency}. Supported currencies: ${Money.SUPPORTED_CURRENCIES.join( ', ' )}` ); } // Round to 2 decimal places to avoid floating point issues const roundedAmount = Math.round(amount * 100) / 100; return new Money(roundedAmount, normalizedCurrency); } static zero(currency: string): Money { return Money.create(0, currency); } private static isValidCurrency(currency: string): boolean { return Money.SUPPORTED_CURRENCIES.includes(currency); } getAmount(): number { return this.amount; } getCurrency(): string { return this.currency; } add(other: Money): Money { this.ensureSameCurrency(other); return Money.create(this.amount + other.amount, this.currency); } subtract(other: Money): Money { this.ensureSameCurrency(other); const result = this.amount - other.amount; if (result < 0) { throw new Error('Subtraction would result in negative amount.'); } return Money.create(result, this.currency); } multiply(multiplier: number): Money { if (multiplier < 0) { throw new Error('Multiplier cannot be negative.'); } return Money.create(this.amount * multiplier, this.currency); } divide(divisor: number): Money { if (divisor <= 0) { throw new Error('Divisor must be positive.'); } return Money.create(this.amount / divisor, this.currency); } isGreaterThan(other: Money): boolean { this.ensureSameCurrency(other); return this.amount > other.amount; } isLessThan(other: Money): boolean { this.ensureSameCurrency(other); return this.amount < other.amount; } isEqualTo(other: Money): boolean { return this.currency === other.currency && this.amount === other.amount; } isZero(): boolean { return this.amount === 0; } private ensureSameCurrency(other: Money): void { if (this.currency !== other.currency) { throw new Error(`Currency mismatch: ${this.currency} vs ${other.currency}`); } } /** * Format as string with currency symbol */ format(): string { const symbols: { [key: string]: string } = { USD: '$', EUR: '€', GBP: '£', CNY: '¥', JPY: '¥', }; const symbol = symbols[this.currency] || this.currency; return `${symbol}${this.amount.toFixed(2)}`; } toString(): string { return this.format(); } toObject(): { amount: number; currency: string } { return { amount: this.amount, currency: this.currency, }; } }