220 lines
6.2 KiB
TypeScript
220 lines
6.2 KiB
TypeScript
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<MoneyValueProps> 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);
|
|
}
|
|
}
|