/* eslint-disable no-use-before-define */ import DineroFactory, { Dinero } from "dinero.js"; import { Currency } from "./Currency"; import Joi from "joi"; import { isNull } from "lodash"; import { NullOr } from "../../../../utilities"; import { RuleValidator } from "../RuleValidator"; import { Result } from "./Result"; import { IValueObjectOptions, ValueObject } from "./ValueObject"; export interface IMoneyValueOptions extends IValueObjectOptions { locale: string; } export const defaultMoneyValueOptions: IMoneyValueOptions = { locale: "es-ES", }; export interface MoneyValueObject { amount: number; precision: number; currency: string; } type RoundingMode = | "HALF_ODD" | "HALF_EVEN" | "HALF_UP" | "HALF_DOWN" | "HALF_TOWARDS_ZERO" | "HALF_AWAY_FROM_ZERO" | "DOWN"; export { RoundingMode }; export interface IMoneyValueProps { amount: NullOr; currencyCode?: string; precision?: number; } const defaultMoneyValueProps = { amount: 0, currencyCode: Currency.DEFAULT_CURRENCY_CODE, precision: 2, }; interface IMoneyValue { toPrimitive(): number; toPrimitives(): MoneyValueObject; isEmpty(): boolean; toString(): string; toJSON(): MoneyValueObject | {}; isNull(): boolean; getAmount(): number; getCurrency(): Currency; getLocale(): string; getPrecision(): number; convertPrecision( newPrecision: number, roundingMode?: RoundingMode, ): MoneyValue; add(addend: MoneyValue): MoneyValue; subtract(subtrahend: MoneyValue): MoneyValue; multiply(multiplier: number, roundingMode?: RoundingMode): MoneyValue; divide(divisor: number, roundingMode?: RoundingMode): MoneyValue; percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue; allocate(ratios: ReadonlyArray): MoneyValue[]; equalsTo(comparator: MoneyValue): boolean; lessThan(comparator: MoneyValue): boolean; lessThanOrEqual(comparator: MoneyValue): boolean; greaterThan(comparator: MoneyValue): boolean; greaterThanOrEqual(comparator: MoneyValue): boolean; isZero(): boolean; isPositive(): boolean; isNegative(): boolean; hasSameCurrency(comparator: MoneyValue): boolean; hasSameAmount(comparator: MoneyValue): boolean; toFormat(format?: string, roundingMode?: RoundingMode): string; toUnit(): number; toRoundedUnit(digits: number, roundingMode?: RoundingMode): number; toObject(): MoneyValueObject; toNumber(): number; } export class MoneyValue extends ValueObject implements IMoneyValue { private static readonly MIN_VALUE = Number.MIN_VALUE; private static readonly MAX_VALUE = Number.MAX_VALUE; private readonly _isNull: boolean; private readonly _options: IMoneyValueOptions; protected static validate( amount: NullOr, options: IMoneyValueOptions, ) { const ruleNull = Joi.any() .optional() // <- undefined .valid(null); // <- null const ruleNumber = Joi.number() .optional() .default(0) .min(-1000) .max(this.MAX_VALUE) .label(options.label ? options.label : "amount"); const rules = Joi.alternatives(ruleNull, ruleNumber); return RuleValidator.validate>(rules, amount); } protected static getMonetaryValueInfo(amount: string): [string, number] { // Divide la cadena de entrada en dos partes: valor y precisión const [valuePart, precisionPart] = amount.split("."); // Calcula la precisión utilizada const precision = precisionPart ? precisionPart.length : 0; // Elimina cualquier carácter no numérico de la parte del valor y concaténalo const sanitizedValue = (valuePart + precisionPart).replace(/[^0-9]/g, ""); return [sanitizedValue, precision]; } public static create( props: IMoneyValueProps = defaultMoneyValueProps, options = defaultMoneyValueOptions, ) { if (props === null) { throw new Error(`InvalidParams: props params is missing`); } const { amount = defaultMoneyValueProps.amount, currencyCode = defaultMoneyValueProps.currencyCode, precision = defaultMoneyValueProps.precision, } = props; const validationResult = MoneyValue.validate(amount, options); if (validationResult.isFailure) { return Result.fail(validationResult.error); } const _amount: NullOr = MoneyValue.sanitize( validationResult.object, ); const prop = DineroFactory({ amount: !isNull(_amount) ? _amount : 0, currency: Currency.DEFAULT_CURRENCY_CODE, precision, }).setLocale(options.locale); return Result.ok(new this(prop, isNull(_amount), options)); } private static sanitize(amount: NullOr): NullOr { let _amount: NullOr = null; if (typeof amount === "string") { _amount = parseFloat(amount); } else { _amount = amount; } return _amount; } protected static createFromDinero(dinero: Dinero) { return Result.ok( new MoneyValue(dinero, false, defaultMoneyValueOptions), ); } public static normalizePrecision( objects: ReadonlyArray, ): MoneyValue[] { return DineroFactory.normalizePrecision( objects.map((object) => object.props), ).map((dinero) => MoneyValue.createFromDinero(dinero).object); } public static minimum(objects: ReadonlyArray): MoneyValue { return MoneyValue.createFromDinero( DineroFactory.minimum(objects.map((object) => object.props)), ).object; } public static maximum(objects: ReadonlyArray): MoneyValue { return MoneyValue.createFromDinero( DineroFactory.maximum(objects.map((object) => object.props)), ).object; } constructor(value: Dinero, isNull: boolean, options: IMoneyValueOptions) { super(value); this._isNull = Object.freeze(isNull); this._options = Object.freeze(options); } public isEmpty = (): boolean => { return this.isNull(); }; public toString(): string { return this._isNull ? "" : String(this.props?.getAmount()); } public toJSON() { return this._isNull ? {} : this.props?.toJSON(); } public toPrimitive(): number { return this.toUnit(); } public toPrimitives(): MoneyValueObject { return this.toObject(); } public isNull = (): boolean => { return this._isNull; }; public getAmount(): number { return this.props.getAmount(); } public getPrecision(): number { return this.props.getPrecision(); } public convertPrecision( newPrecision: number, roundingMode?: RoundingMode, ): MoneyValue { return MoneyValue.createFromDinero( this.props.convertPrecision(newPrecision, roundingMode), ).object; } public getCurrency(): Currency { return Currency.createFromCode(this.props.getCurrency()).object; } public getLocale(): string { return this.props.getLocale(); } public add(addend: MoneyValue): MoneyValue { return MoneyValue.createFromDinero(this.props.add(addend.props)).object; } public subtract(subtrahend: MoneyValue): MoneyValue { return MoneyValue.createFromDinero(this.props.subtract(subtrahend.props)) .object; } public multiply(multiplier: number, roundingMode?: RoundingMode): MoneyValue { return MoneyValue.createFromDinero( this.props.multiply(multiplier, roundingMode), ).object; } public divide(divisor: number, roundingMode?: RoundingMode): MoneyValue { return MoneyValue.createFromDinero(this.props.divide(divisor, roundingMode)) .object; } public percentage( percentage: number, roundingMode?: RoundingMode, ): MoneyValue { return MoneyValue.createFromDinero( this.props.percentage(percentage, roundingMode), ).object; } public allocate(ratios: ReadonlyArray): MoneyValue[] { return this.props .allocate(ratios) .map((dinero) => MoneyValue.createFromDinero(dinero).object); } public equalsTo(comparator: MoneyValue): boolean { return this.props.equalsTo(comparator.props); } public lessThan(comparator: MoneyValue): boolean { return this.props.lessThan(comparator.props); } public lessThanOrEqual(comparator: MoneyValue): boolean { return this.props.lessThanOrEqual(comparator.props); } public greaterThan(comparator: MoneyValue): boolean { return this.props.greaterThan(comparator.props); } public greaterThanOrEqual(comparator: MoneyValue): boolean { return this.props.greaterThanOrEqual(comparator.props); } public isZero(): boolean { return this.props.isZero(); } public isPositive(): boolean { return this.props.isPositive(); } public isNegative(): boolean { return this.props.isNegative(); } public hasSameCurrency(comparator: MoneyValue): boolean { return this.props.hasSameCurrency(comparator.props); } public hasSameAmount(comparator: MoneyValue): boolean { return this.props.hasSameAmount(comparator.props); } public toFormat(format?: string, roundingMode?: RoundingMode): string { return this.props.toFormat(format, roundingMode); } public toUnit(): number { return this.props.toUnit(); } public toRoundedUnit(digits: number, roundingMode?: RoundingMode): number { return this.props.toRoundedUnit(digits, roundingMode); } public toObject(): MoneyValueObject { const obj = this.props.toObject(); return { amount: obj.amount, precision: obj.precision, currency: String(obj.currency), }; } public toNumber(): number { return this.toUnit(); } }