Uecko_ERP/packages/rdx-ddd/src/value-objects/money-value.ts

220 lines
6.2 KiB
TypeScript
Raw Normal View History

2025-05-09 10:45:32 +00:00
import { Result } from "@repo/rdx-utils";
2025-02-24 19:00:28 +00:00
import DineroFactory, { Currency, Dinero } from "dinero.js";
2025-02-25 15:25:30 +00:00
import { Percentage } from "./percentage";
import { Quantity } from "./quantity";
2025-02-24 19:00:28 +00:00
import { ValueObject } from "./value-object";
const DEFAULT_SCALE = 2;
2025-04-01 14:26:15 +00:00
const DEFAULT_CURRENCY_CODE = "EUR";
2025-02-24 19:00:28 +00:00
type CurrencyData = Currency;
2025-02-25 10:16:34 +00:00
export type RoundingMode =
| "HALF_ODD"
| "HALF_EVEN"
| "HALF_UP"
| "HALF_DOWN"
| "HALF_TOWARDS_ZERO"
| "HALF_AWAY_FROM_ZERO"
| "DOWN";
2025-09-01 14:07:59 +00:00
export interface MoneyValueProps {
2025-09-04 10:02:24 +00:00
value: number;
2025-04-01 14:26:15 +00:00
scale?: number;
currency_code?: string;
2025-02-24 19:00:28 +00:00
}
2025-05-26 10:38:45 +00:00
export interface IMoneyValue {
2025-09-04 10:02:24 +00:00
value: number;
2025-02-24 19:00:28 +00:00
scale: number;
currency: Dinero.Currency;
2025-09-04 10:02:24 +00:00
getProps(): MoneyValueProps;
2025-02-24 19:00:28 +00:00
convertScale(newScale: number): MoneyValue;
add(addend: MoneyValue): MoneyValue;
subtract(subtrahend: MoneyValue): MoneyValue;
2025-02-25 15:25:30 +00:00
multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue;
divide(divisor: number, roundingMode?: RoundingMode): MoneyValue;
percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue;
2025-02-24 19:00:28 +00:00
equalsTo(comparator: MoneyValue): boolean;
greaterThan(comparator: MoneyValue): boolean;
lessThan(comparator: MoneyValue): boolean;
isZero(): boolean;
isPositive(): boolean;
isNegative(): boolean;
2025-02-25 15:25:30 +00:00
hasSameCurrency(comparator: MoneyValue): boolean;
hasSameAmount(comparator: MoneyValue): boolean;
2025-02-24 19:00:28 +00:00
format(locale: string): string;
}
2025-09-01 14:07:59 +00:00
export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyValue {
2025-02-25 10:16:34 +00:00
private readonly dinero: Dinero;
2025-02-24 19:00:28 +00:00
2025-05-26 10:38:45 +00:00
static DEFAULT_SCALE = DEFAULT_SCALE;
static DEFAULT_CURRENCY_CODE = DEFAULT_CURRENCY_CODE;
2025-09-04 10:02:24 +00:00
static create({ value, currency_code, scale }: MoneyValueProps) {
2025-05-26 10:38:45 +00:00
const props = {
2025-09-04 10:02:24 +00:00
value: Number(value),
2025-05-26 10:38:45 +00:00
scale: scale ?? MoneyValue.DEFAULT_SCALE,
currency_code: currency_code ?? MoneyValue.DEFAULT_CURRENCY_CODE,
};
2025-02-24 19:00:28 +00:00
return Result.ok(new MoneyValue(props));
}
2025-09-01 14:07:59 +00:00
constructor(props: MoneyValueProps) {
2025-02-24 19:00:28 +00:00
super(props);
2025-09-04 10:02:24 +00:00
const { value: amount, scale, currency_code } = props;
2025-02-25 10:16:34 +00:00
this.dinero = Object.freeze(
2025-02-24 19:00:28 +00:00
DineroFactory({
amount,
2025-05-26 10:38:45 +00:00
precision: scale || MoneyValue.DEFAULT_SCALE,
currency: (currency_code as Currency) || MoneyValue.DEFAULT_CURRENCY_CODE,
2025-02-24 19:00:28 +00:00
})
); // 🔒 Garantiza inmutabilidad
}
2025-09-04 10:02:24 +00:00
get value(): number {
2025-05-04 20:06:57 +00:00
return this.dinero.getAmount() / 10 ** this.dinero.getPrecision(); // ** => Math.pow
2025-02-24 19:00:28 +00:00
}
get currency(): CurrencyData {
2025-02-25 10:16:34 +00:00
return this.dinero.getCurrency();
2025-02-24 19:00:28 +00:00
}
get scale(): number {
2025-02-25 10:16:34 +00:00
return this.dinero.getPrecision();
2025-02-24 19:00:28 +00:00
}
2025-09-04 10:02:24 +00:00
getProps(): MoneyValueProps {
2025-02-24 19:00:28 +00:00
return this.props;
}
2025-04-01 14:26:15 +00:00
/** Reconstruye el VO desde la cadena persistida */
static fromPersistence(value: string): MoneyValue {
const [currencyCode, amountStr, scaleStr] = value.split(":");
2025-05-04 20:06:57 +00:00
const amount = Number.parseInt(amountStr, 10);
const scale = Number.parseInt(scaleStr, 10);
2025-04-22 08:11:50 +00:00
const currency = currencyCode;
2025-09-04 10:02:24 +00:00
return new MoneyValue({ value: amount, scale, currency_code: currency });
2025-04-01 14:26:15 +00:00
}
toPrimitive() {
return {
2025-09-04 10:02:24 +00:00
value: this.value,
2025-04-01 14:26:15 +00:00
scale: this.scale,
currency_code: this.currency,
};
}
2025-02-25 10:16:34 +00:00
convertScale(newScale: number, roundingMode: RoundingMode = "HALF_UP"): MoneyValue {
const _newDinero = this.dinero.convertPrecision(newScale, roundingMode);
2025-02-24 19:00:28 +00:00
return new MoneyValue({
2025-09-04 10:02:24 +00:00
value: _newDinero.getAmount(),
2025-02-25 10:16:34 +00:00
scale: _newDinero.getPrecision(),
currency_code: _newDinero.getCurrency(),
2025-02-24 19:00:28 +00:00
});
}
add(addend: MoneyValue): MoneyValue {
return new MoneyValue({
2025-09-04 10:02:24 +00:00
value: this.dinero.add(addend.dinero).getAmount(),
2025-02-24 19:00:28 +00:00
scale: this.scale,
currency_code: this.currency,
});
}
subtract(subtrahend: MoneyValue): MoneyValue {
return new MoneyValue({
2025-09-04 10:02:24 +00:00
value: this.dinero.subtract(subtrahend.dinero).getAmount(),
2025-02-24 19:00:28 +00:00
scale: this.scale,
currency_code: this.currency,
});
}
2025-02-25 15:25:30 +00:00
multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue {
const _multiplier = typeof multiplier === "number" ? multiplier : multiplier.toNumber();
const _newDinero = this.dinero.multiply(_multiplier, roundingMode);
2025-02-24 19:00:28 +00:00
return new MoneyValue({
2025-09-04 10:02:24 +00:00
value: _newDinero.getAmount(),
2025-02-25 15:25:30 +00:00
scale: _newDinero.getPrecision(),
currency_code: _newDinero.getCurrency(),
2025-02-24 19:00:28 +00:00
});
}
2025-02-25 15:25:30 +00:00
divide(divisor: number | Quantity, roundingMode?: RoundingMode): MoneyValue {
const _divisor = typeof divisor === "number" ? divisor : divisor.toNumber();
const _newDinero = this.dinero.divide(_divisor, roundingMode);
2025-02-24 19:00:28 +00:00
return new MoneyValue({
2025-09-04 10:02:24 +00:00
value: _newDinero.getAmount(),
2025-02-25 15:25:30 +00:00
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({
2025-09-04 10:02:24 +00:00
value: _newDinero.getAmount(),
2025-02-25 15:25:30 +00:00
scale: _newDinero.getPrecision(),
currency_code: _newDinero.getCurrency(),
2025-02-24 19:00:28 +00:00
});
}
equalsTo(comparator: MoneyValue): boolean {
2025-02-25 10:16:34 +00:00
return this.dinero.equalsTo(comparator.dinero);
2025-02-24 19:00:28 +00:00
}
greaterThan(comparator: MoneyValue): boolean {
2025-02-25 10:16:34 +00:00
return this.dinero.greaterThan(comparator.dinero);
2025-02-24 19:00:28 +00:00
}
lessThan(comparator: MoneyValue): boolean {
2025-02-25 10:16:34 +00:00
return this.dinero.lessThan(comparator.dinero);
2025-02-24 19:00:28 +00:00
}
isZero(): boolean {
2025-02-25 10:16:34 +00:00
return this.dinero.isZero();
2025-02-24 19:00:28 +00:00
}
isPositive(): boolean {
2025-09-04 10:02:24 +00:00
return this.value > 0;
2025-02-24 19:00:28 +00:00
}
isNegative(): boolean {
2025-09-04 10:02:24 +00:00
return this.value < 0;
2025-02-24 19:00:28 +00:00
}
2025-02-25 15:25:30 +00:00
hasSameCurrency(comparator: MoneyValue): boolean {
return this.dinero.hasSameCurrency(comparator.dinero);
}
hasSameAmount(comparator: MoneyValue): boolean {
return this.dinero.hasSameAmount(comparator.dinero);
}
2025-04-01 14:26:15 +00:00
/**
* 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
*/
2025-02-24 19:00:28 +00:00
format(locale: string): string {
2025-09-04 10:02:24 +00:00
const value = this.value;
2025-02-24 19:00:28 +00:00
const currency = this.currency;
const scale = this.scale;
return new Intl.NumberFormat(locale, {
style: "currency",
currency: currency,
minimumFractionDigits: scale,
maximumFractionDigits: scale,
useGrouping: true,
2025-09-04 10:02:24 +00:00
}).format(value);
2025-02-24 19:00:28 +00:00
}
}