358 lines
9.4 KiB
TypeScript
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();
|
|
}
|
|
}
|