138 lines
3.3 KiB
TypeScript
138 lines
3.3 KiB
TypeScript
/**
|
|
* 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,
|
|
};
|
|
}
|
|
}
|