.
This commit is contained in:
parent
cdda482253
commit
c86684812c
@ -1,5 +1,5 @@
|
|||||||
import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize";
|
import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize";
|
||||||
import { UniqueID } from "@shared/contexts";
|
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
|
||||||
import { ModelDefined, Transaction } from "sequelize";
|
import { ModelDefined, Transaction } from "sequelize";
|
||||||
|
|
||||||
import { IQuoteRepository } from "../domain";
|
import { IQuoteRepository } from "../domain";
|
||||||
|
|||||||
@ -114,7 +114,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
discount: {
|
discount: {
|
||||||
type: new DataTypes.BIGINT(),
|
type: new DataTypes.SMALLINT(),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
discount: {
|
discount: {
|
||||||
type: DataTypes.BIGINT(),
|
type: new DataTypes.SMALLINT(),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
total_price: {
|
total_price: {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable no-use-before-define */
|
/* eslint-disable no-use-before-define */
|
||||||
import DineroFactory, { Dinero } from "dinero.js";
|
import DineroFactory, { Currency, Dinero } from "dinero.js";
|
||||||
|
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import { isNull } from "lodash";
|
import { isNull } from "lodash";
|
||||||
import { NullOr } from "../../../../utilities";
|
import { NullOr, UndefinedOr } from "../../../../utilities";
|
||||||
import { RuleValidator } from "../RuleValidator";
|
import { RuleValidator } from "../RuleValidator";
|
||||||
import { CurrencyData } from "./CurrencyData";
|
import { CurrencyData } from "./CurrencyData";
|
||||||
import { Result } from "./Result";
|
import { Result } from "./Result";
|
||||||
@ -49,7 +49,7 @@ const defaultMoneyValueProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface IMoneyValue {
|
interface IMoneyValue {
|
||||||
toPrimitive(): number;
|
toPrimitive(): UndefinedOr<number>;
|
||||||
toPrimitives(): MoneyValueObject;
|
toPrimitives(): MoneyValueObject;
|
||||||
isEmpty(): boolean;
|
isEmpty(): boolean;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
@ -148,10 +148,11 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const _amount: NullOr<number> = MoneyValue.sanitize(validationResult.object);
|
const _amount: NullOr<number> = MoneyValue.sanitize(validationResult.object);
|
||||||
|
const _currency = CurrencyData.createFromCode(currencyCode).object.code;
|
||||||
|
|
||||||
const prop = DineroFactory({
|
const prop = DineroFactory({
|
||||||
amount: !isNull(_amount) ? _amount : options.defaultValue,
|
amount: !isNull(_amount) ? Number(_amount) : options.defaultValue,
|
||||||
currency: CurrencyData.createFromCode(currencyCode).object.code,
|
currency: _currency as Currency,
|
||||||
precision,
|
precision,
|
||||||
}).setLocale(options.locale);
|
}).setLocale(options.locale);
|
||||||
|
|
||||||
@ -208,8 +209,8 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
return this._isNull ? {} : this.props?.toJSON();
|
return this._isNull ? {} : this.props?.toJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
public toPrimitive(): number {
|
public toPrimitive(): UndefinedOr<number> {
|
||||||
return this.toUnit();
|
return this._isNull ? undefined : Number(this.props?.getAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
public toPrimitives(): MoneyValueObject {
|
public toPrimitives(): MoneyValueObject {
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { Percentage } from "./Percentage";
|
||||||
|
|
||||||
|
describe("Percentage", () => {
|
||||||
|
test("should create an instance with default values", () => {
|
||||||
|
const percentage = Percentage.create();
|
||||||
|
expect(percentage.isSuccess).toBe(true);
|
||||||
|
expect(percentage.object.amount).toBe(0);
|
||||||
|
expect(percentage.object.scale).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should create an instance with specific values", () => {
|
||||||
|
const percentage = Percentage.create({ amount: 50, scale: 1 });
|
||||||
|
expect(percentage.isSuccess).toBe(true);
|
||||||
|
expect(percentage.object.amount).toBe(50);
|
||||||
|
expect(percentage.object.scale).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should fail to create an instance with invalid values", () => {
|
||||||
|
const percentage = Percentage.create({ amount: 15000, scale: 2 });
|
||||||
|
expect(percentage.isFailure).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should convert scale and check range", () => {
|
||||||
|
const percentage = Percentage.create({ amount: 9999, scale: 2 }).object;
|
||||||
|
|
||||||
|
const converted = percentage.convertScale(0);
|
||||||
|
expect(converted.amount).toBe(100);
|
||||||
|
expect(converted.scale).toBe(0);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
percentage.convertScale(3);
|
||||||
|
}).toThrow("Scale out of range");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return primitive value", () => {
|
||||||
|
const percentage = Percentage.create({ amount: 9234, scale: 2 }).object;
|
||||||
|
expect(percentage.toPrimitive()).toBe(9234);
|
||||||
|
|
||||||
|
const convertedHalfUp = percentage.convertScale(1);
|
||||||
|
expect(convertedHalfUp.toPrimitive()).toBe(9230);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -5,51 +5,87 @@ import { DomainError, handleDomainError } from "../errors";
|
|||||||
import { RuleValidator } from "../RuleValidator";
|
import { RuleValidator } from "../RuleValidator";
|
||||||
import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject";
|
import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject";
|
||||||
import { Result } from "./Result";
|
import { Result } from "./Result";
|
||||||
|
import { ResultCollection } from "./ResultCollection";
|
||||||
|
|
||||||
|
const DEFAULT_SCALE = 2;
|
||||||
|
|
||||||
export interface IPercentageOptions extends INullableValueObjectOptions {}
|
export interface IPercentageOptions extends INullableValueObjectOptions {}
|
||||||
|
|
||||||
export interface IPercentageProps {
|
export interface IPercentageProps {
|
||||||
amount: NullOr<number | string>;
|
amount: NullOr<number | string>;
|
||||||
precision?: number;
|
scale?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPercentage {
|
interface IPercentage {
|
||||||
amount: NullOr<number>;
|
amount: NullOr<number>;
|
||||||
precision: number;
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PercentageObject {
|
export interface PercentageObject {
|
||||||
amount: number;
|
amount: number;
|
||||||
precision: number;
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPercentageProps = {
|
const defaultPercentageProps = {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
precision: 0,
|
scale: DEFAULT_SCALE,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Percentage extends NullableValueObject<IPercentage> {
|
export class Percentage extends NullableValueObject<IPercentage> {
|
||||||
public static readonly DEFAULT_PRECISION = 2;
|
public static readonly DEFAULT_SCALE = DEFAULT_SCALE;
|
||||||
public static readonly MIN_VALUE = 0;
|
public static readonly MIN_VALUE = 0;
|
||||||
public static readonly MAX_VALUE = 100;
|
public static readonly MAX_VALUE = 100;
|
||||||
|
|
||||||
|
public static readonly MIN_SCALE = 0;
|
||||||
|
public static readonly MAX_SCALE = 2;
|
||||||
|
|
||||||
private readonly _isNull: boolean;
|
private readonly _isNull: boolean;
|
||||||
private readonly _options: IPercentageOptions;
|
private readonly _options: IPercentageOptions;
|
||||||
|
|
||||||
protected static validate(value: NullOr<number | string>, options: IPercentageOptions) {
|
protected static validate(
|
||||||
|
value: NullOr<number | string>,
|
||||||
|
scale: NullOr<number>,
|
||||||
|
options: IPercentageOptions
|
||||||
|
) {
|
||||||
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(
|
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(
|
||||||
defaultPercentageProps.amount
|
defaultPercentageProps.amount
|
||||||
);
|
);
|
||||||
|
|
||||||
const rule = Joi.number()
|
const ruleScale = Joi.number()
|
||||||
.min(Percentage.MIN_VALUE)
|
.min(Percentage.MIN_SCALE)
|
||||||
.max(Percentage.MAX_VALUE)
|
.max(Percentage.MAX_SCALE)
|
||||||
|
|
||||||
.label(options.label ? options.label : "percentage");
|
.label(options.label ? options.label : "percentage");
|
||||||
|
|
||||||
const rules = Joi.alternatives(ruleNull, rule);
|
const validationResults = new ResultCollection([
|
||||||
|
RuleValidator.validate<NullOr<number>>(
|
||||||
|
Joi.alternatives(ruleNull, RuleValidator.RULE_IS_TYPE_NUMBER),
|
||||||
|
value
|
||||||
|
),
|
||||||
|
RuleValidator.validate<NullOr<number>>(
|
||||||
|
Joi.alternatives(ruleNull, RuleValidator.RULE_IS_TYPE_NUMBER, ruleScale),
|
||||||
|
scale
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
return RuleValidator.validate<NullOr<number>>(rules, value);
|
if (validationResults.hasSomeFaultyResult()) {
|
||||||
|
return validationResults.getFirstFaultyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the value to a number if it's a string
|
||||||
|
let numericValue = typeof value === "string" ? parseInt(value, 10) : Number(value);
|
||||||
|
|
||||||
|
// Check if scale is null, and set to default if so
|
||||||
|
let numericScale = isNull(scale) ? Percentage.DEFAULT_SCALE : Number(scale);
|
||||||
|
|
||||||
|
// Calculate the adjusted value
|
||||||
|
const adjustedValue = numericValue / Math.pow(10, numericScale);
|
||||||
|
|
||||||
|
// Check if the adjusted value is within the specified range
|
||||||
|
if (adjustedValue < Percentage.MIN_VALUE || adjustedValue > Percentage.MAX_VALUE) {
|
||||||
|
return Result.fail(new Error(`Value with scale is out of range: ${adjustedValue}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static create(
|
public static create(
|
||||||
@ -60,15 +96,14 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
|||||||
throw new Error(`InvalidParams: props params is missing`);
|
throw new Error(`InvalidParams: props params is missing`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { amount = defaultPercentageProps.amount, precision = defaultPercentageProps.precision } =
|
const { amount = defaultPercentageProps.amount, scale = defaultPercentageProps.scale } = props;
|
||||||
props;
|
|
||||||
|
|
||||||
const _options = {
|
const _options = {
|
||||||
label: "percentage",
|
label: "percentage",
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationResult = Percentage.validate(amount, _options);
|
const validationResult = Percentage.validate(amount, scale, _options);
|
||||||
|
|
||||||
if (validationResult.isFailure) {
|
if (validationResult.isFailure) {
|
||||||
return Result.fail(
|
return Result.fail(
|
||||||
@ -76,17 +111,17 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let _amount: NullOr<number> = Percentage.sanitize(validationResult.object);
|
let _amount: NullOr<number> = Percentage._sanitize(amount);
|
||||||
|
|
||||||
const _props = {
|
const _props = {
|
||||||
amount: isNull(_amount) ? 0 : _amount,
|
amount: isNull(_amount) ? 0 : _amount,
|
||||||
precision,
|
scale,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Result.ok<Percentage>(new this(_props, isNull(_amount), options));
|
return Result.ok<Percentage>(new this(_props, isNull(_amount), options));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static sanitize(value: NullOr<number | string>): NullOr<number> {
|
private static _sanitize(value: NullOr<number | string>): NullOr<number> {
|
||||||
let _value: NullOr<number> = null;
|
let _value: NullOr<number> = null;
|
||||||
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
@ -98,6 +133,21 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
|||||||
return _value;
|
return _value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static _toNumber(value: NullOr<number>, scale: number): number {
|
||||||
|
if (isNull(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const factor = Math.pow(10, scale);
|
||||||
|
const amount = Number(value) / factor;
|
||||||
|
return Number(amount.toFixed(scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _isWithinRange(value: NullOr<number>, scale: number): boolean {
|
||||||
|
const _value = Percentage._toNumber(value, scale);
|
||||||
|
return _value >= Percentage.MIN_VALUE && _value <= Percentage.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(percentage: IPercentage, isNull: boolean, options: IPercentageOptions) {
|
constructor(percentage: IPercentage, isNull: boolean, options: IPercentageOptions) {
|
||||||
super(percentage);
|
super(percentage);
|
||||||
this._isNull = Object.freeze(isNull);
|
this._isNull = Object.freeze(isNull);
|
||||||
@ -108,16 +158,16 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
|||||||
return this.isNull() ? null : Number(this.props?.amount);
|
return this.isNull() ? null : Number(this.props?.amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
get precision(): number {
|
get scale(): number {
|
||||||
return this.isNull() ? 0 : Number(this.props?.precision);
|
return this.isNull() ? 0 : Number(this.props?.scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAmount(): NullOr<number> {
|
public getAmount(): NullOr<number> {
|
||||||
return this.isNull() ? null : Number(this.props?.amount);
|
return this.isNull() ? null : Number(this.props?.amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPrecision(): number {
|
public getScale(): number {
|
||||||
return this.isNull() ? 0 : Number(this.props?.precision);
|
return this.isNull() ? 0 : Number(this.props?.scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEmpty = (): boolean => {
|
public isEmpty = (): boolean => {
|
||||||
@ -129,21 +179,19 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public toNumber(): number {
|
public toNumber(): number {
|
||||||
if (this.isNull()) {
|
return Percentage._toNumber(this.scale, this.scale);
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const factor = Math.pow(10, this.precision);
|
|
||||||
const amount = Number(this.amount) / factor;
|
|
||||||
return Number(amount.toFixed(this.precision));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString(): string {
|
public toString(): string {
|
||||||
return this.isNull() ? "" : String(this.toNumber());
|
return this.isNull() ? "" : String(this.toNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
public toPrimitive(): number {
|
public toPrimitive(): NullOr<number> {
|
||||||
return this.toNumber();
|
if (this.scale !== Percentage.DEFAULT_SCALE) {
|
||||||
|
return this.convertScale(Percentage.DEFAULT_SCALE).toPrimitive();
|
||||||
|
} else {
|
||||||
|
return this.amount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public toPrimitives() {
|
public toPrimitives() {
|
||||||
@ -152,12 +200,38 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
|||||||
|
|
||||||
public toObject(): PercentageObject {
|
public toObject(): PercentageObject {
|
||||||
return {
|
return {
|
||||||
amount: this.amount ? this.amount : 0,
|
amount: this.isNull() ? 0 : Number(this.amount),
|
||||||
precision: this.precision,
|
scale: this.scale,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasSamePrecision(quantity: Percentage) {
|
public convertScale(newScale: number): Percentage {
|
||||||
return this.precision === quantity.precision;
|
if (newScale < Percentage.MIN_SCALE || newScale > Percentage.MAX_SCALE) {
|
||||||
|
throw new Error(`Scale out of range: ${newScale}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNull()) {
|
||||||
|
return new Percentage({ 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);
|
||||||
|
|
||||||
|
if (!Percentage._isWithinRange(newValue, newScale)) {
|
||||||
|
throw new Error(`Value out of range after conversion: ${newValue} ${newScale}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Percentage({ amount: newValue, scale: newScale }, false, this._options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasSameScale(quantity: Percentage) {
|
||||||
|
return this.scale === quantity.scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isWithinRange(): boolean {
|
||||||
|
return Percentage._isWithinRange(this.amount, this.scale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { shallowEqual } from "shallow-equal-object";
|
import { shallowEqual } from "shallow-equal-object";
|
||||||
|
|
||||||
export type Primitive = string | boolean | number;
|
export type Primitive = string | boolean | number | null | undefined;
|
||||||
|
|
||||||
export interface IValueObjectOptions {
|
export interface IValueObjectOptions {
|
||||||
label?: string;
|
label?: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user