Presupuestador_web/shared/lib/contexts/common/domain/entities/MoneyValue.ts

423 lines
12 KiB
TypeScript
Raw Normal View History

2024-07-15 13:24:22 +00:00
import DineroFactory, { Currency, Dinero } from "dinero.js";
2024-04-23 15:29:38 +00:00
import Joi from "joi";
import { isNull } from "lodash";
2024-07-18 10:02:59 +00:00
import { NullOr } from "../../../../utilities";
2024-08-28 18:38:20 +00:00
import { adjustPrecision } from "../../../../utilities/helpers";
2024-07-17 18:10:07 +00:00
import { DomainError, handleDomainError } from "../errors";
2024-04-23 15:29:38 +00:00
import { RuleValidator } from "../RuleValidator";
2024-07-09 16:21:12 +00:00
import { CurrencyData } from "./CurrencyData";
2024-04-23 15:29:38 +00:00
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 {
2024-07-16 17:17:52 +00:00
amount: number | null;
2024-07-15 15:41:42 +00:00
scale: number;
2024-07-09 16:21:12 +00:00
currency_code: string;
2024-04-23 15:29:38 +00:00
}
export type RoundingMode =
2024-04-23 15:29:38 +00:00
| "HALF_ODD"
| "HALF_EVEN"
| "HALF_UP"
| "HALF_DOWN"
| "HALF_TOWARDS_ZERO"
| "HALF_AWAY_FROM_ZERO"
| "DOWN";
export interface IMoneyValueProps {
amount: NullOr<number | string>;
currencyCode?: string;
2024-07-15 15:41:42 +00:00
scale?: number;
2024-04-23 15:29:38 +00:00
}
const defaultMoneyValueProps = {
2024-07-16 17:17:52 +00:00
amount: null,
2024-07-09 16:21:12 +00:00
currencyCode: CurrencyData.DEFAULT_CURRENCY_CODE,
2024-07-15 15:41:42 +00:00
scale: 2,
2024-04-23 15:29:38 +00:00
};
interface IMoneyValue {
2024-07-18 10:02:59 +00:00
toPrimitive(): NullOr<number>;
2024-04-23 15:29:38 +00:00
toPrimitives(): MoneyValueObject;
isEmpty(): boolean;
toString(): string;
toJSON(): MoneyValueObject | {};
isNull(): boolean;
getAmount(): number;
2024-07-09 16:21:12 +00:00
getCurrency(): CurrencyData;
2024-04-23 15:29:38 +00:00
getLocale(): string;
2024-07-15 15:41:42 +00:00
getScale(): number;
convertScale(newScale: number, roundingMode?: RoundingMode): MoneyValue;
2024-04-23 15:29:38 +00:00
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 {
2024-07-15 15:41:42 +00:00
public static readonly DEFAULT_SCALE = defaultMoneyValueProps.scale;
2024-07-09 16:21:12 +00:00
public static readonly DEFAULT_CURRENCY_CODE = defaultMoneyValueProps.currencyCode;
2024-04-23 15:29:38 +00:00
private readonly _isNull: boolean;
2024-04-23 15:29:38 +00:00
private readonly _options: IMoneyValueOptions;
2024-07-09 16:21:12 +00:00
protected static validate(amount: NullOr<number | string>, options: IMoneyValueOptions) {
2024-04-23 15:29:38 +00:00
const ruleNull = Joi.any()
.optional() // <- undefined
.valid(null); // <- null
2024-07-18 10:02:59 +00:00
const ruleNumber = Joi.number().label(options.label ? options.label : "amount");
2024-04-23 15:29:38 +00:00
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
2024-07-15 15:41:42 +00:00
const [valuePart, scalePart] = amount.split(".");
2024-04-23 15:29:38 +00:00
// Calcula la precisión utilizada
2024-07-15 15:41:42 +00:00
const scale = scalePart ? scalePart.length : 0;
2024-04-23 15:29:38 +00:00
// Elimina cualquier carácter no numérico de la parte del valor y concaténalo
2024-07-15 15:41:42 +00:00
const sanitizedValue = (valuePart + scalePart).replace(/[^0-9]/g, "");
2024-04-23 15:29:38 +00:00
2024-07-15 15:41:42 +00:00
return [sanitizedValue, scale];
2024-04-23 15:29:38 +00:00
}
public static create(
props: IMoneyValueProps = defaultMoneyValueProps,
2024-07-09 16:21:12 +00:00
options = defaultMoneyValueOptions
2024-04-23 15:29:38 +00:00
) {
if (props === null) {
throw new Error(`InvalidParams: props params is missing`);
}
const {
amount = defaultMoneyValueProps.amount,
currencyCode = defaultMoneyValueProps.currencyCode,
2024-07-15 15:41:42 +00:00
scale = defaultMoneyValueProps.scale,
2024-07-09 16:21:12 +00:00
} = props || {};
2024-04-23 15:29:38 +00:00
const validationResult = MoneyValue.validate(amount, options);
if (validationResult.isFailure) {
2024-07-17 18:10:07 +00:00
return Result.fail(
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, options)
);
2024-04-23 15:29:38 +00:00
}
2024-07-09 16:21:12 +00:00
const _amount: NullOr<number> = MoneyValue.sanitize(validationResult.object);
2024-07-15 13:24:22 +00:00
const _currency = CurrencyData.createFromCode(currencyCode).object.code;
2024-04-23 15:29:38 +00:00
const prop = DineroFactory({
2024-07-18 10:02:59 +00:00
amount: Number(_amount),
2024-07-15 13:24:22 +00:00
currency: _currency as Currency,
2024-07-15 15:41:42 +00:00
precision: scale,
2024-04-23 15:29:38 +00:00
}).setLocale(options.locale);
return Result.ok<MoneyValue>(new this(prop, isNull(_amount), options));
}
2024-07-16 17:17:52 +00:00
public static createFromFormattedValue(
value: NullOr<number | string>,
currencyCode: string,
_options: IMoneyValueOptions = {
locale: defaultMoneyValueOptions.locale,
}
) {
if (value === null || value === "") {
return MoneyValue.create({
amount: null,
scale: MoneyValue.DEFAULT_SCALE,
currencyCode,
});
}
const valueStr = String(value);
const [integerPart, decimalPart] = valueStr.split(",");
let _amount = integerPart;
let _scale = 2;
if (decimalPart === undefined) {
// 99
_scale = 0;
} else {
if (decimalPart === "") {
// 99,
_amount = integerPart + decimalPart.padEnd(1, "0");
_scale = 1;
}
if (decimalPart.length === 1) {
// 99,1
_amount = integerPart + decimalPart.padEnd(1, "0");
_scale = 1;
} else {
if (decimalPart.length === 2) {
// 99,12
_amount = integerPart + decimalPart.padEnd(2, "0");
_scale = 2;
} else {
if (decimalPart.length === 3) {
// 99,123
_amount = integerPart + decimalPart.padEnd(3, "0");
_scale = 3;
} else {
if (decimalPart.length === 4) {
// 99,1235
_amount = integerPart + decimalPart.padEnd(4, "0");
_scale = 4;
}
}
}
}
}
return MoneyValue.create(
{
amount: _amount,
scale: _scale,
currencyCode,
},
_options
);
}
2024-04-23 15:29:38 +00:00
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) {
2024-07-09 16:21:12 +00:00
return Result.ok<MoneyValue>(new MoneyValue(dinero, false, defaultMoneyValueOptions));
2024-04-23 15:29:38 +00:00
}
2024-07-15 15:41:42 +00:00
public static normalizeScale(objects: ReadonlyArray<MoneyValue>): MoneyValue[] {
2024-07-09 16:21:12 +00:00
return DineroFactory.normalizePrecision(objects.map((object) => object.props)).map(
(dinero) => MoneyValue.createFromDinero(dinero).object
);
2024-04-23 15:29:38 +00:00
}
public static minimum(objects: ReadonlyArray<MoneyValue>): MoneyValue {
2024-07-09 16:21:12 +00:00
return MoneyValue.createFromDinero(DineroFactory.minimum(objects.map((object) => object.props)))
.object;
2024-04-23 15:29:38 +00:00
}
public static maximum(objects: ReadonlyArray<MoneyValue>): MoneyValue {
2024-07-09 16:21:12 +00:00
return MoneyValue.createFromDinero(DineroFactory.maximum(objects.map((object) => object.props)))
.object;
2024-04-23 15:29:38 +00:00
}
private static _toString(
value: NullOr<number>,
scale: number,
locales?: Intl.LocalesArgument
): string {
2024-07-16 17:17:52 +00:00
if (value === null) {
return "";
}
new Intl.NumberFormat(locales, {
2024-08-28 18:38:20 +00:00
/*minimumSignificantDigits: scale,
maximumSignificantDigits: scale,
minimumFractionDigits: scale,*/
useGrouping: true,
}).format(value === null ? 0 : adjustPrecision({ amount: value, scale }));
2024-07-16 17:17:52 +00:00
const factor = Math.pow(10, scale);
const amount = Number(value) / factor;
return amount.toFixed(scale);
}
2024-04-23 15:29:38 +00:00
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 MoneyValue._toString(
this.isNull() ? null : this.getAmount(),
this.getScale(),
this._options.locale
);
2024-04-23 15:29:38 +00:00
}
public toJSON() {
return this._isNull ? {} : this.props?.toJSON();
}
2024-07-18 10:02:59 +00:00
public toPrimitive(): NullOr<number> {
return this._isNull ? null : Number(this.props?.getAmount());
2024-04-23 15:29:38 +00:00
}
public toPrimitives(): MoneyValueObject {
return this.toObject();
}
public isNull = (): boolean => {
return this._isNull;
};
public getAmount(): number {
return this.props.getAmount();
}
2024-07-15 15:41:42 +00:00
public getScale(): number {
2024-04-23 15:29:38 +00:00
return this.props.getPrecision();
}
2024-07-15 16:53:33 +00:00
public convertScale(newScale: number, roundingMode: RoundingMode = "HALF_UP"): MoneyValue {
2024-07-18 10:02:59 +00:00
if (this._isNull) {
return MoneyValue.create({
amount: null,
scale: newScale,
currencyCode: this.getCurrency().code,
}).object;
} else {
return MoneyValue.createFromDinero(this.props.convertPrecision(newScale, roundingMode))
.object;
}
2024-04-23 15:29:38 +00:00
}
2024-07-09 16:21:12 +00:00
public getCurrency(): CurrencyData {
return CurrencyData.createFromCode(this.props.getCurrency()).object;
2024-04-23 15:29:38 +00:00
}
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 {
2024-07-09 16:21:12 +00:00
return MoneyValue.createFromDinero(this.props.subtract(subtrahend.props)).object;
2024-04-23 15:29:38 +00:00
}
public multiply(multiplier: number, roundingMode?: RoundingMode): MoneyValue {
2024-07-09 16:21:12 +00:00
return MoneyValue.createFromDinero(this.props.multiply(multiplier, roundingMode)).object;
2024-04-23 15:29:38 +00:00
}
public divide(divisor: number, roundingMode?: RoundingMode): MoneyValue {
2024-07-09 16:21:12 +00:00
return MoneyValue.createFromDinero(this.props.divide(divisor, roundingMode)).object;
2024-04-23 15:29:38 +00:00
}
2024-07-09 16:21:12 +00:00
public percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue {
return MoneyValue.createFromDinero(this.props.percentage(percentage, roundingMode)).object;
2024-04-23 15:29:38 +00:00
}
public allocate(ratios: ReadonlyArray<number>): MoneyValue[] {
2024-07-09 16:21:12 +00:00
return this.props.allocate(ratios).map((dinero) => MoneyValue.createFromDinero(dinero).object);
2024-04-23 15:29:38 +00:00
}
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 {
2024-07-24 16:01:31 +00:00
return this._isNull ? "" : this.props.toFormat(format, roundingMode);
2024-04-23 15:29:38 +00:00
}
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 {
2024-07-16 17:17:52 +00:00
amount: this._isNull ? null : obj.amount,
2024-07-15 15:41:42 +00:00
scale: obj.precision,
2024-07-09 16:21:12 +00:00
currency_code: String(obj.currency),
2024-04-23 15:29:38 +00:00
};
}
public toNumber(): number {
return this.toUnit();
}
}