import { Result } from "@repo/rdx-utils"; import DineroFactory, { Currency, Dinero } from "dinero.js"; import { Percentage } from "./percentage"; import { Quantity } from "./quantity"; import { ValueObject } from "./value-object"; const DEFAULT_SCALE = 2; const DEFAULT_CURRENCY_CODE = "EUR"; type CurrencyData = Currency; export type RoundingMode = | "HALF_ODD" | "HALF_EVEN" | "HALF_UP" | "HALF_DOWN" | "HALF_TOWARDS_ZERO" | "HALF_AWAY_FROM_ZERO" | "DOWN"; export interface MoneyValueProps { value: number; scale?: number; currency_code?: string; } export interface IMoneyValue { value: number; scale: number; currency: Dinero.Currency; getProps(): MoneyValueProps; convertScale(newScale: number): MoneyValue; add(addend: MoneyValue): MoneyValue; subtract(subtrahend: MoneyValue): MoneyValue; multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue; divide(divisor: number, roundingMode?: RoundingMode): MoneyValue; percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue; equalsTo(comparator: MoneyValue): boolean; greaterThan(comparator: MoneyValue): boolean; lessThan(comparator: MoneyValue): boolean; isZero(): boolean; isPositive(): boolean; isNegative(): boolean; hasSameCurrency(comparator: MoneyValue): boolean; hasSameAmount(comparator: MoneyValue): boolean; format(locale: string): string; } export class MoneyValue extends ValueObject implements IMoneyValue { private readonly dinero: Dinero; static DEFAULT_SCALE = DEFAULT_SCALE; static DEFAULT_CURRENCY_CODE = DEFAULT_CURRENCY_CODE; static create({ value, currency_code, scale }: MoneyValueProps) { const props = { value: Number(value), scale: scale ?? MoneyValue.DEFAULT_SCALE, currency_code: currency_code ?? MoneyValue.DEFAULT_CURRENCY_CODE, }; return Result.ok(new MoneyValue(props)); } constructor(props: MoneyValueProps) { super(props); const { value: amount, scale, currency_code } = props; this.dinero = Object.freeze( DineroFactory({ amount, precision: scale || MoneyValue.DEFAULT_SCALE, currency: (currency_code as Currency) || MoneyValue.DEFAULT_CURRENCY_CODE, }) ); // 🔒 Garantiza inmutabilidad } get value(): number { return this.dinero.getAmount() / 10 ** this.dinero.getPrecision(); // ** => Math.pow } get currency(): CurrencyData { return this.dinero.getCurrency(); } get scale(): number { return this.dinero.getPrecision(); } getProps(): MoneyValueProps { return this.props; } /** Reconstruye el VO desde la cadena persistida */ static fromPersistence(value: string): MoneyValue { const [currencyCode, amountStr, scaleStr] = value.split(":"); const amount = Number.parseInt(amountStr, 10); const scale = Number.parseInt(scaleStr, 10); const currency = currencyCode; return new MoneyValue({ value: amount, scale, currency_code: currency }); } toPrimitive() { return { value: this.value, scale: this.scale, currency_code: this.currency, }; } convertScale(newScale: number, roundingMode: RoundingMode = "HALF_UP"): MoneyValue { const _newDinero = this.dinero.convertPrecision(newScale, roundingMode); return new MoneyValue({ value: _newDinero.getAmount(), scale: _newDinero.getPrecision(), currency_code: _newDinero.getCurrency(), }); } add(addend: MoneyValue): MoneyValue { return new MoneyValue({ value: this.dinero.add(addend.dinero).getAmount(), scale: this.scale, currency_code: this.currency, }); } subtract(subtrahend: MoneyValue): MoneyValue { return new MoneyValue({ value: this.dinero.subtract(subtrahend.dinero).getAmount(), scale: this.scale, currency_code: this.currency, }); } multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue { const _multiplier = typeof multiplier === "number" ? multiplier : multiplier.toNumber(); const _newDinero = this.dinero.multiply(_multiplier, roundingMode); return new MoneyValue({ value: _newDinero.getAmount(), scale: _newDinero.getPrecision(), currency_code: _newDinero.getCurrency(), }); } divide(divisor: number | Quantity, roundingMode?: RoundingMode): MoneyValue { const _divisor = typeof divisor === "number" ? divisor : divisor.toNumber(); const _newDinero = this.dinero.divide(_divisor, roundingMode); return new MoneyValue({ value: _newDinero.getAmount(), scale: _newDinero.getPrecision(), currency_code: _newDinero.getCurrency(), }); } percentage(percentage: number | Percentage, roundingMode?: RoundingMode): MoneyValue { const _percentage = typeof percentage === "number" ? percentage : percentage.toNumber(); const _newDinero = this.dinero.percentage(_percentage, roundingMode); return new MoneyValue({ value: _newDinero.getAmount(), scale: _newDinero.getPrecision(), currency_code: _newDinero.getCurrency(), }); } equalsTo(comparator: MoneyValue): boolean { return this.dinero.equalsTo(comparator.dinero); } greaterThan(comparator: MoneyValue): boolean { return this.dinero.greaterThan(comparator.dinero); } lessThan(comparator: MoneyValue): boolean { return this.dinero.lessThan(comparator.dinero); } isZero(): boolean { return this.dinero.isZero(); } isPositive(): boolean { return this.value > 0; } isNegative(): boolean { return this.value < 0; } hasSameCurrency(comparator: MoneyValue): boolean { return this.dinero.hasSameCurrency(comparator.dinero); } hasSameAmount(comparator: MoneyValue): boolean { return this.dinero.hasSameAmount(comparator.dinero); } /** * Devuelve una cadena con el importe formateado. * Ejemplo: 123456 -> €1,234.56 * @param locale Código de idioma y país (ej. "es-ES") * @returns Importe formateado */ format(locale: string): string { const value = this.value; const currency = this.currency; const scale = this.scale; return new Intl.NumberFormat(locale, { style: "currency", currency: currency, minimumFractionDigits: scale, maximumFractionDigits: scale, useGrouping: true, }).format(value); } }