xpeditis2.0/apps/backend/src/domain/value-objects/money.vo.ts
2025-10-20 12:30:08 +02:00

138 lines
3.5 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,
};
}
}