Presupuestador_web/shared/lib/contexts/common/domain/entities/Quantity.ts
2024-07-24 18:01:31 +02:00

330 lines
8.0 KiB
TypeScript

import Joi from "joi";
import { NullOr } from "../../../../utilities";
import { DomainError, handleDomainError } from "../errors";
import { RuleValidator } from "../RuleValidator";
import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject";
import { Result } from "./Result";
import { ResultCollection } from "./ResultCollection";
const DEFAULT_SCALE = 2;
export interface IQuantityOptions extends INullableValueObjectOptions {}
export interface IQuantityProps {
amount: NullOr<number | string>;
scale?: number;
}
interface IQuantity {
amount: NullOr<number>;
scale: number;
}
export interface QuantityObject {
amount: number | null;
scale: number;
}
const defaultQuantityProps = {
amount: 0,
scale: DEFAULT_SCALE,
};
export class Quantity extends NullableValueObject<IQuantity> {
public static readonly DEFAULT_SCALE = DEFAULT_SCALE;
public static readonly MIN_SCALE = 0;
public static readonly MAX_SCALE = 2;
private readonly _isNull: boolean;
private readonly _options: IQuantityOptions;
protected static validate(
value: NullOr<number | string>,
scale: NullOr<number>,
options: IQuantityOptions = {}
) {
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED;
const ruleEmpty = RuleValidator.RULE_ALLOW_EMPTY;
const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
options.label ? options.label : "amount"
);
const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex(/^[-]?\d+$/).label(
options.label ? options.label : "amount"
);
const ruleScale = Joi.number()
.min(Quantity.MIN_SCALE)
.max(Quantity.MAX_SCALE)
.label(options.label ? options.label : "scale");
const validationResults = new ResultCollection([
RuleValidator.validate<NullOr<number>>(
Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString),
value
),
RuleValidator.validate<NullOr<number>>(
Joi.alternatives(
RuleValidator.RULE_IS_TYPE_NUMBER.label(options.label ? options.label : "scale"),
ruleScale
),
scale
),
]);
if (validationResults.hasSomeFaultyResult()) {
return validationResults.getFirstFaultyResult();
}
return Result.ok();
}
public static create(
props: IQuantityProps = defaultQuantityProps,
options: IQuantityOptions = {}
) {
if (props === null) {
throw new Error(`InvalidParams: props params is missing`);
}
const { amount = defaultQuantityProps.amount, scale = defaultQuantityProps.scale } = props;
const _options = {
label: "quantity",
...options,
};
const validationResult = Quantity.validate(amount, scale, _options);
if (validationResult.isFailure) {
return Result.fail(
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
);
}
let _amount: NullOr<number> = Quantity._sanitize(amount);
const _props = {
amount: _amount === null ? 0 : _amount,
scale,
};
return Result.ok<Quantity>(new Quantity(_props, _amount === null, options));
}
public static createFromFormattedValue(
value: NullOr<number | string>,
_options: IQuantityOptions = {}
) {
if (value === null || value === "") {
return Quantity.create({
amount: null,
scale: Quantity.DEFAULT_SCALE,
});
}
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;
}
}
}
return Quantity.create(
{
amount: _amount,
scale: _scale,
},
_options
);
}
private static _sanitize(value: NullOr<number | string>): NullOr<number> {
let _value: NullOr<number> = null;
if (typeof value === "string") {
_value = parseInt(value, 10);
} else {
_value = value;
}
return _value;
}
private static _toString(value: NullOr<number>, scale: number): string {
if (value === null) {
return "";
}
const factor = Math.pow(10, scale);
const amount = Number(value) / factor;
return amount.toFixed(scale);
}
constructor(quantity: IQuantity, isNull: boolean, options: IQuantityOptions) {
super(quantity);
this._isNull = Object.freeze(isNull);
this._options = Object.freeze(options);
}
get amount(): NullOr<number> {
return this.isNull() ? null : Number(this.props?.amount);
}
get scale(): number {
return Number(this.props?.scale);
}
public getAmount(): NullOr<number> {
return this.isNull() ? null : Number(this.props?.amount);
}
public getScale(): number {
return this.isNull() ? 0 : Number(this.props?.scale);
}
public isEmpty = (): boolean => {
return this.isNull();
};
public isNull = (): boolean => {
return this._isNull;
};
public toString(): string {
return Quantity._toString(this.amount, this.scale);
}
public toNumber(): number {
return this.isNull() ? 0 : Number(this.toString());
}
public toFormat(): string {
return this._isNull
? ""
: Intl.NumberFormat("es-ES", {
maximumFractionDigits: 2,
}).format(this.toNumber());
}
public toPrimitive(): NullOr<number> {
if (this.scale !== Quantity.DEFAULT_SCALE) {
return this.convertScale(Quantity.DEFAULT_SCALE).toPrimitive();
} else {
return this.amount;
}
}
public toPrimitives() {
return this.toObject();
}
public toObject(): QuantityObject {
return {
amount: this.amount,
scale: this.scale,
};
}
public convertScale(newScale: number): Quantity {
if (newScale < Quantity.MIN_SCALE || newScale > Quantity.MAX_SCALE) {
throw new Error(`Scale out of range: ${newScale}`);
}
if (this.isNull()) {
return new Quantity({ amount: null, scale: newScale }, true, this._options);
}
const oldFactor = Math.pow(10, this.scale);
const value = Number(this.amount) / oldFactor;
const newFactor = Math.pow(10, newScale);
const newValue = Math.round(value * newFactor);
return new Quantity({ amount: newValue, scale: newScale }, false, this._options);
}
public hasSameScale(quantity: Quantity) {
return this.scale === quantity.scale;
}
public increment(anotherQuantity?: Quantity) {
if (this.isNull()) {
return anotherQuantity ? Quantity.create(anotherQuantity.toObject()) : Quantity.create();
}
if (!anotherQuantity) {
return Quantity.create(
{
amount: Number(this.amount) + 1,
scale: this.scale,
},
this._options
);
}
if (!this.hasSameScale(anotherQuantity)) {
return Result.fail(Error("No se pueden sumar cantidades con diferentes escalas."));
}
return Quantity.create(
{
amount: Number(this.amount) + Number(anotherQuantity.amount),
scale: this.scale,
},
this._options
);
}
public decrement(anotherQuantity?: Quantity) {
if (this.isNull()) {
return anotherQuantity ? Quantity.create(anotherQuantity.toObject()) : Quantity.create();
}
if (!anotherQuantity) {
return Quantity.create(
{
amount: Number(this.amount) - 1,
scale: this.scale,
},
this._options
);
}
if (!this.hasSameScale(anotherQuantity)) {
return Result.fail(Error("No se pueden restar cantidades con diferentes escalas."));
}
return Quantity.create(
{
amount: Number(this.amount) - Number(anotherQuantity.amount),
scale: this.scale,
},
this._options
);
}
}