Presupuestador_web/shared/lib/contexts/common/domain/entities/MoneyValue.ts
2024-04-23 17:29:38 +02:00

358 lines
9.4 KiB
TypeScript

/* 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<number | string>;
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<number>): 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<Dinero> 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<number | string>,
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<NullOr<number | string>>(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<number> = MoneyValue.sanitize(
validationResult.object,
);
const prop = DineroFactory({
amount: !isNull(_amount) ? _amount : 0,
currency: Currency.DEFAULT_CURRENCY_CODE,
precision,
}).setLocale(options.locale);
return Result.ok<MoneyValue>(new this(prop, isNull(_amount), options));
}
private static sanitize(amount: NullOr<number | string>): NullOr<number> {
let _amount: NullOr<number> = null;
if (typeof amount === "string") {
_amount = parseFloat(amount);
} else {
_amount = amount;
}
return _amount;
}
protected static createFromDinero(dinero: Dinero) {
return Result.ok<MoneyValue>(
new MoneyValue(dinero, false, defaultMoneyValueOptions),
);
}
public static normalizePrecision(
objects: ReadonlyArray<MoneyValue>,
): MoneyValue[] {
return DineroFactory.normalizePrecision(
objects.map((object) => object.props),
).map((dinero) => MoneyValue.createFromDinero(dinero).object);
}
public static minimum(objects: ReadonlyArray<MoneyValue>): MoneyValue {
return MoneyValue.createFromDinero(
DineroFactory.minimum(objects.map((object) => object.props)),
).object;
}
public static maximum(objects: ReadonlyArray<MoneyValue>): 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<number>): 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();
}
}