This commit is contained in:
David Arranz 2024-07-16 19:17:52 +02:00
parent d4665a6de6
commit 740f5fd238
17 changed files with 319 additions and 305 deletions

View File

@ -273,7 +273,7 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
description: "aaaa",
unit_price: {
amount: "10000",
precision: 4,
scale: 4,
currency: "EUR",
},
discount: {

View File

@ -12,7 +12,6 @@ import { t } from "i18next";
import { useCallback, useState } from "react";
import { useFieldArray, useFormContext } from "react-hook-form";
import { useDetailColumns } from "../../hooks";
import { CatalogPickerDataTable } from "../CatalogPickerDataTable";
import { SortableDataTable } from "../SortableDataTable";
export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData }) => {
@ -42,12 +41,13 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
header: () => (
<div className='text-right'>{t("quotes.form_fields.items.quantity.label")}</div>
),
size: 5,
size: 8,
cell: ({ row: { index } }) => {
return (
<FormQuantityField
variant='outline'
precision={Quantity.DEFAULT_PRECISION}
scale={0}
className='text-right'
{...register(`items.${index}.quantity`)}
/>
);
@ -73,14 +73,14 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
<FormCurrencyField
variant='outline'
currency={currency}
precision={4}
scale={4}
className='text-right'
{...register(`items.${index}.unit_price`)}
/>
);
},
},
{
/*{
id: "subtotal_price" as const,
accessorKey: "subtotal_price",
header: () => (
@ -91,34 +91,33 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
<FormCurrencyField
variant='outline'
currency={currency}
precision={4}
scale={4}
disabled
className='text-right'
{...register(`items.${index}.subtotal_price`)}
/>
);
},
},
},*/
{
id: "discount" as const,
accessorKey: "discount",
size: 5,
size: 8,
header: () => (
<div className='text-right'>{t("quotes.form_fields.items.discount.label")}</div>
),
cell: ({ row: { index }, column: { id } }) => {
cell: ({ row: { index } }) => {
return (
<>
<FormPercentageField
variant='outline'
precision={0}
className='text-right'
{...register(`items.${index}.discount`)}
/>
</>
<FormPercentageField
variant='outline'
scale={2}
className='text-right'
{...register(`items.${index}.discount`)}
/>
);
},
},
{
/*{
id: "total_price" as const,
accessorKey: "total_price",
header: () => (
@ -129,13 +128,14 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
<FormCurrencyField
variant='outline'
currency={currency}
precision={4}
scale={4}
disabled
className='text-right'
{...register(`items.${index}.total_price`)}
/>
);
},
},
},*/
],
{
enableDragHandleColumn: true,
@ -177,8 +177,8 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
fieldActions.append({
...newArticle,
quantity: {
amount: 12,
precision: Quantity.DEFAULT_PRECISION,
amount: 100,
scale: Quantity.DEFAULT_SCALE,
},
unit_price: newArticle.retail_price,
});
@ -216,7 +216,7 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
<ResizableHandle withHandle className='mx-3' />
<ResizablePanel defaultSize={defaultLayout[1]} minSize={10}>
<DataTableProvider syncWithLocation={false}>
<CatalogPickerDataTable onClick={handleInsertArticle} />
{/*<CatalogPickerDataTable onClick={handleInsertArticle} />*/}
</DataTableProvider>
</ResizablePanel>
</ResizablePanelGroup>

View File

@ -12,7 +12,6 @@ import { CurrencyData, IUpdateQuote_Request_DTO, MoneyValue } from "@shared/cont
import { t } from "i18next";
import { useEffect, useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import useFormPersist from "react-hook-form-persist";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors";
@ -57,42 +56,46 @@ export const QuoteEdit = () => {
validity: "",
subtotal_price: {
amount: undefined,
precision: 2,
scale: 2,
currency_code: data?.currency_code,
},
discount: {
amount: undefined,
precision: 0,
scale: 0,
},
total_price: {
amount: undefined,
precision: 2,
scale: 2,
currency_code: data?.currency_code,
},
items: [
/*items: [
{
quantity: {
amount: undefined,
scale: 2,
},
subtotal_price: {
amount: undefined,
precision: 4,
scale: 4,
currency_code: data?.currency_code,
},
discount: {
amount: undefined,
precision: 0,
scale: 0,
},
total_price: {
amount: undefined,
precision: 4,
scale: 4,
currency_code: data?.currency_code,
},
},
],
],*/
},
});
const { watch, getValues, setValue, formState } = form;
const { clear } = useFormPersist(
/*const { clear } = useFormPersist(
"quote-edit",
{
watch,
@ -102,7 +105,7 @@ export const QuoteEdit = () => {
storage: window.localStorage, // default window.sessionStorage
//exclude: ['foo']
}
);
);*/
const { isSubmitting } = formState;
@ -116,7 +119,7 @@ export const QuoteEdit = () => {
//onSettled: () => {},
onSuccess: () => {
toast("Guardado!");
clear();
//clear();
},
});
};
@ -139,18 +142,21 @@ export const QuoteEdit = () => {
let quoteSubtotal = MoneyValue.create().object;
// Recálculo líneas
items.map((item, index) => {
const itemTotals = calculateItemTotals(item);
items &&
items.map((item, index) => {
const itemTotals = calculateItemTotals(item);
if (itemTotals === null) {
return;
}
console.log(itemTotals?.quantity.toObject());
quoteSubtotal = quoteSubtotal.add(itemTotals.totalPrice);
if (itemTotals === null) {
return;
}
setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject());
setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject());
});
quoteSubtotal = quoteSubtotal.add(itemTotals.totalPrice);
setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject());
setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject());
});
// Recálculo completo
setValue("subtotal_price", quoteSubtotal.toObject());

View File

@ -3,7 +3,7 @@ import { FormControl, FormDescription, FormField, FormItem, InputProps } from "@
import { CurrencyData, MoneyValue } from "@shared/contexts";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import CurrencyInput from "react-currency-input-field";
import CurrencyInput, { CurrencyInputOnChangeValues } from "react-currency-input-field";
import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
import { FormErrorMessage } from "./FormErrorMessage";
import { FormLabel, FormLabelProps } from "./FormLabel";
@ -38,7 +38,7 @@ export type FormCurrencyFieldProps<
UseControllerProps<TFieldValues, TName> &
VariantProps<typeof formCurrencyFieldVariants> & {
currency: CurrencyData;
precision: number;
scale: number;
};
export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrencyFieldProps>(
@ -54,7 +54,7 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
defaultValue,
rules,
readOnly,
precision,
scale,
currency,
variant,
} = props;
@ -68,22 +68,17 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
}
const moneyOrError = MoneyValue.create(value);
if (moneyOrError.isFailure) {
throw moneyOrError.error;
}
return moneyOrError.object
.convertPrecision(precision ?? value.precision)
.toUnit()
.toString();
return moneyOrError.object.toString();
},
output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
const { value: amount } = values ?? { value: null };
output: (value: string | undefined) => {
const moneyOrError = MoneyValue.create({
amount: value?.replace(",", "") ?? null,
precision,
currencyCode: currency.code,
});
const moneyOrError = MoneyValue.createFromFormattedValue(amount, currency.code);
if (moneyOrError.isFailure) {
throw moneyOrError.error;
}
@ -118,11 +113,17 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
groupSeparator='.'
decimalSeparator=','
placeholder={placeholder}
//fixedDecimalLength={precision} <- no activar para que sea más cómodo escribir las cantidades
decimalsLimit={precision}
decimalScale={precision}
//allowDecimals={scale !== 0}
decimalsLimit={scale}
decimalScale={scale}
//fixedDecimalLength={scale} <- no activar para que sea más cómodo escribir las cantidades
step={1}
// { ...field }
value={transform.input(field.value)}
onValueChange={(e) => field.onChange(transform.output(e))}
//onChange={() => {}}
onValueChange={(value, name, values) =>
field.onChange(transform.output(value, name, values))
}
/>
</FormControl>
{description && <FormDescription>{description}</FormDescription>}

View File

@ -1,147 +0,0 @@
import { cn } from "@/lib/utils";
import { FormControl, FormDescription, FormMessage, Input } from "@/ui";
import { IMoney } from "@/lib/types";
import { MoneyValue } from "@shared/contexts";
import { createElement, forwardRef, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { FormLabel } from "./FormLabel";
import { FormTextFieldProps } from "./FormTextField";
type FormMoneyFieldProps = Omit<FormTextFieldProps, "type"> & {
defaultValue?: any;
};
export const FormMoneyField = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & FormMoneyFieldProps
>((props, ref) => {
const {
label,
placeholder,
hint,
description,
required,
className,
leadIcon,
trailIcon,
button,
disabled,
name,
defaultValue,
} = props;
//const error = Boolean(errors && errors[name]);
const { control } = useFormContext();
const [precision, setPrecision] = useState<number>(MoneyValue.DEFAULT_PRECISION);
const [currencyCode, setCurrencyCode] = useState<string>(MoneyValue.DEFAULT_CURRENCY_CODE);
const transform = {
input: (value: IMoney) => {
const moneyOrError = MoneyValue.create(value);
if (moneyOrError.isFailure) {
throw moneyOrError.error;
}
const moneyValue = moneyOrError.object;
setPrecision(moneyValue.getPrecision());
setCurrencyCode(moneyValue.getCurrency().code);
return moneyValue.toFormat();
},
output: (event: React.ChangeEvent<HTMLInputElement>) => {
const output = parseFloat(event.target.value);
const moneyOrError = MoneyValue.create({
amount: output * Math.pow(10, precision),
precision,
currencyCode,
});
if (moneyOrError.isFailure) {
throw moneyOrError.error;
}
return moneyOrError.object.toObject();
},
};
return (
<Controller
defaultValue={defaultValue}
control={control}
name={name}
rules={{ required }}
disabled={disabled}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field, fieldState, formState }) => {
return (
<input
{...field}
placeholder='number'
onChange={(e) => field.onChange(transform.output(e))}
value={transform.input(field.value)}
/>
);
return (
<div className={cn(className, "space-y-3")}>
{label && <FormLabel label={label} hint={hint} />}
<div className={cn(button ? "flex" : null)}>
<div
className={cn(
leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : ""
)}
>
{leadIcon && (
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
{createElement(
leadIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
<FormControl
className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
>
<Input
placeholder={placeholder}
className={cn(
fieldState.error ? "border-destructive focus-visible:ring-destructive" : ""
)}
{...field}
onChange={(e) => field.onChange(transform.output(e))}
value={transform.input(field.value)}
/>
</FormControl>
{trailIcon && (
<div className='absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none'>
{createElement(
trailIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
</div>
{button && <>{createElement(button)}</>}
</div>
{description && <FormDescription>{description}</FormDescription>}
<FormMessage />
</div>
);
}}
/>
);
});

View File

@ -4,7 +4,7 @@ import { cn } from "@/lib/utils";
import { FormControl, FormDescription, FormField, FormItem, InputProps } from "@/ui";
import { Percentage } from "@shared/contexts";
import { cva, type VariantProps } from "class-variance-authority";
import CurrencyInput from "react-currency-input-field";
import CurrencyInput, { CurrencyInputOnChangeValues } from "react-currency-input-field";
import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
import { FormErrorMessage } from "./FormErrorMessage";
import { FormLabel, FormLabelProps } from "./FormLabel";
@ -39,7 +39,7 @@ export type FormPercentageFieldProps<
FormInputWithIconProps &
UseControllerProps<TFieldValues, TName> &
VariantProps<typeof formPercentageFieldVariants> & {
precision: number;
scale: number;
};
export const FormPercentageField = React.forwardRef<
@ -57,7 +57,7 @@ export const FormPercentageField = React.forwardRef<
defaultValue,
rules,
readOnly,
precision,
scale,
variant,
} = props;
@ -74,18 +74,14 @@ export const FormPercentageField = React.forwardRef<
throw percentageOrError.error;
}
return (
percentageOrError.object
.toNumber()
//.toPrecision(precision ?? value.precision)
.toString()
);
return percentageOrError.object.toString();
},
output: (value: string | undefined) => {
const percentageOrError = Percentage.create({
amount: value?.replace(",", "") ?? null,
precision,
});
output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
console.log(values);
const { value: amount } = values ?? { value: null };
const percentageOrError = Percentage.createFromFormattedValue(amount);
if (percentageOrError.isFailure) {
throw percentageOrError.error;
}
@ -122,10 +118,16 @@ export const FormPercentageField = React.forwardRef<
groupSeparator='.'
decimalSeparator=','
placeholder={placeholder}
decimalsLimit={precision}
decimalScale={precision}
allowDecimals={scale !== 0}
decimalsLimit={scale}
decimalScale={scale}
step={1}
//{...field}
value={transform.input(field.value)}
onValueChange={(e) => field.onChange(transform.output(e))}
//onChange={() => {}}
onValueChange={(value, name, values) =>
field.onChange(transform.output(value, name, values))
}
/>
</FormControl>
{description && <FormDescription>{description}</FormDescription>}

View File

@ -4,7 +4,7 @@ import { cn } from "@/lib/utils";
import { FormControl, FormDescription, FormField, FormItem, InputProps } from "@/ui";
import { Quantity } from "@shared/contexts";
import { cva, type VariantProps } from "class-variance-authority";
import CurrencyInput from "react-currency-input-field";
import CurrencyInput, { CurrencyInputOnChangeValues } from "react-currency-input-field";
import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
import { FormErrorMessage } from "./FormErrorMessage";
import { FormLabel, FormLabelProps } from "./FormLabel";
@ -38,7 +38,7 @@ export type FormQuantityFieldProps<
FormInputWithIconProps &
UseControllerProps<TFieldValues, TName> &
VariantProps<typeof formQuantityFieldVariants> & {
precision: number;
scale: number;
};
export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantityFieldProps>(
@ -54,7 +54,7 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
defaultValue,
rules,
readOnly,
precision,
scale,
variant,
} = props;
@ -71,18 +71,12 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
throw quantityOrError.error;
}
return (
quantityOrError.object
.toNumber()
//.toPrecision(precision ?? value.precision)
.toString()
);
return quantityOrError.object.toString();
},
output: (value: string | undefined) => {
const quantityOrError = Quantity.create({
amount: value?.replace(",", "") ?? null,
precision,
});
output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
const { value: amount } = values ?? { value: null };
const quantityOrError = Quantity.createFromFormattedValue(amount);
if (quantityOrError.isFailure) {
throw quantityOrError.error;
}
@ -107,7 +101,7 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
<FormControl>
<CurrencyInput
name={field.name}
//ref={field.ref} <-- no activar que hace cosas raras
//ref={field.ref} //<-- no activar que hace cosas raras
onBlur={field.onBlur}
disabled={field.disabled}
readOnly={readOnly}
@ -115,10 +109,16 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
groupSeparator='.'
decimalSeparator=','
placeholder={placeholder}
decimalsLimit={precision}
decimalScale={precision}
allowDecimals={scale !== 0}
decimalsLimit={scale}
decimalScale={scale}
step={1}
//{...field}
value={transform.input(field.value)}
onValueChange={(e) => field.onChange(transform.output(e))}
//onChange={() => {}}
onValueChange={(value, name, values) =>
field.onChange(transform.output(value, name, values))
}
/>
</FormControl>
{description && <FormDescription>{description}</FormDescription>}

View File

@ -3,7 +3,6 @@ export * from "./FormDatePickerField";
export * from "./FormErrorMessage";
export * from "./FormGroup";
export * from "./FormLabel";
export * from "./FormMoneyField";
export * from "./FormPercentageField";
export * from "./FormQuantityField";
export * from "./FormTextAreaField";

View File

@ -1,13 +1,11 @@
// jest.config.js
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
globals: {
"ts-jest": {
tsconfig: "tsconfig.json",
},
},
moduleFileExtensions: ["ts", "js"],
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
testMatch: ["**/*.test.(ts|js)"],
preset: "ts-jest",
testEnvironment: "node",
moduleNameMapper: {
"^@shared/(.*)$": "<rootDir>/../shared/lib/$1",
"^@/(.*)$": "<rootDir>/src/$1",
},
};

View File

@ -11,7 +11,7 @@
"build": "tsc",
"lint": "eslint --ignore-path .gitignore . --ext .ts",
"lint:fix": "npm run lint -- --fix",
"test": "jest --verbose",
"test": "jest --config=./jest.config.ts --rootDir=./src/ --verbose",
"clean": "rm -rf node_modules"
},
"devDependencies": {
@ -22,7 +22,7 @@
"@types/express-session": "^1.18.0",
"@types/glob": "^8.1.0",
"@types/http-status": "^1.1.2",
"@types/jest": "^29.5.6",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@types/luxon": "^3.3.1",
"@types/module-alias": "^2.0.1",
@ -49,7 +49,7 @@
"module-alias": "^2.2.3",
"prettier": "3.0.1",
"supertest": "^6.2.2",
"ts-jest": "^29.1.1",
"ts-jest": "^29.2.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.2.2"
},

View File

@ -0,0 +1,81 @@
import { IRepositoryManager } from "@/contexts/common/domain";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { UniqueID } from "@shared/contexts";
import { IQuoteRepository } from "../../domain";
import { Quote } from "../../domain/entities/Quotes/Quote";
import { ISalesContext } from "../../infrastructure";
import { GetQuoteUseCase } from "./GetQuote.useCase";
describe("GetQuoteUseCase", () => {
let getQuoteUseCase: GetQuoteUseCase;
let mockAdapter: jest.Mocked<ISequelizeAdapter>;
let mockRepositoryManager: jest.Mocked<IRepositoryManager>;
let mockQuoteRepository: jest.Mocked<IQuoteRepository>;
let mockTransaction: { complete: jest.Mock };
beforeEach(() => {
mockTransaction = { complete: jest.fn() };
mockAdapter = {
startTransaction: jest.fn().mockReturnValue(mockTransaction),
} as unknown as jest.Mocked<ISequelizeAdapter>;
mockQuoteRepository = {
getById: jest.fn(),
} as unknown as jest.Mocked<IQuoteRepository>;
mockRepositoryManager = {
getRepository: jest.fn().mockReturnValue(() => mockQuoteRepository),
} as unknown as jest.Mocked<IRepositoryManager>;
const context: ISalesContext = {
adapter: mockAdapter,
repositoryManager: mockRepositoryManager,
dealer: undefined,
};
getQuoteUseCase = new GetQuoteUseCase(context);
});
it("should return a quote when found", async () => {
const id = new UniqueID();
const quote = new Quote({ id, content: "Test Quote" });
mockQuoteRepository.getById.mockResolvedValueOnce(quote);
mockTransaction.complete.mockImplementationOnce(async (fn: any) => await fn({}));
const request = { id };
const response = await getQuoteUseCase.execute(request);
expect(response.isSuccess).toBe(true);
expect(response.getValue()).toEqual(quote);
});
it("should return a NOT_FOUND_ERROR when quote is not found", async () => {
const id = new UniqueID();
mockQuoteRepository.getById.mockResolvedValueOnce(null);
mockTransaction.complete.mockImplementationOnce(async (fn: any) => await fn({}));
const request = { id };
const response = await getQuoteUseCase.execute(request);
expect(response.isFailure).toBe(true);
expect(response.errorValue().message).toBe("Quote not found");
});
it("should return a REPOSITORY_ERROR on database error", async () => {
const id = new UniqueID();
const error = new Error("Database error");
mockQuoteRepository.getById.mockRejectedValueOnce(error);
mockTransaction.complete.mockImplementationOnce(async (fn: any) => await fn({}));
const request = { id };
const response = await getQuoteUseCase.execute(request);
expect(response.isFailure).toBe(true);
expect(response.errorValue().message).toBe("Query error");
});
});

View File

@ -37,9 +37,6 @@ export class GetQuoteUseCase
async execute(request: IGetQuoteUseCaseRequest): Promise<GetQuoteResponseOrError> {
const { id } = request;
// Validación de datos
// No hay en este caso
return await this.findQuote(id);
}

View File

@ -2,7 +2,8 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest"],
"baseUrl": "./src",
//"baseUrl": "./src",
"moduleResolution": "node",
"paths": {
"@/*": ["./src/*"],
"@shared/*": ["../shared/lib/*"]

View File

@ -25,10 +25,11 @@ describe("MoneyValue Value Object", () => {
it("Should create a valid money value from string and format it", () => {
const validMoneyValueFromString = MoneyValue.create({
amount: "5075",
scale: 2,
});
expect(validMoneyValueFromString.isSuccess).toBe(true);
expect(validMoneyValueFromString.object.toString()).toEqual("50,75 €");
expect(validMoneyValueFromString.object.toString()).toEqual("50.75");
});
// Prueba la creación de un valor monetario con una cadena no válida.
@ -52,7 +53,7 @@ describe("MoneyValue Value Object", () => {
expect(moneyValue.getAmount()).toBe(100);
expect(moneyValue.getCurrency().code).toBe("EUR");
expect(moneyValue.getPrecision()).toBe(3);
expect(moneyValue.getScale()).toBe(3);
});
it("should create MoneyValue from string and currency", () => {
@ -67,7 +68,7 @@ describe("MoneyValue Value Object", () => {
expect(moneyValue.getAmount()).toBe(12345);
expect(moneyValue.getCurrency().code).toBe("USD");
expect(moneyValue.getPrecision()).toBe(2);
expect(moneyValue.getScale()).toBe(2);
});
it("should fail to create MoneyValue with invalid amount", () => {
@ -86,12 +87,7 @@ describe("MoneyValue Value Object", () => {
}).object;
const result = moneyValue.toString();
expect(result).toBe("7525");
});
// Prueba la verificación de valor nulo.
it("Should check if value is null", () => {
expect(() => MoneyValue.create(null)).toThrowError();
expect(result).toBe("75.25");
});
// Prueba la verificación de valor cero.

View File

@ -20,7 +20,7 @@ export const defaultMoneyValueOptions: IMoneyValueOptions = {
};
export interface MoneyValueObject {
amount: number;
amount: number | null;
scale: number;
currency_code: string;
}
@ -43,7 +43,7 @@ export interface IMoneyValueProps {
}
const defaultMoneyValueProps = {
amount: 0,
amount: null,
currencyCode: CurrencyData.DEFAULT_CURRENCY_CODE,
scale: 2,
};
@ -141,6 +141,8 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
scale = defaultMoneyValueProps.scale,
} = props || {};
console.log(props, { amount, currencyCode, scale });
const validationResult = MoneyValue.validate(amount, options);
if (validationResult.isFailure) {
@ -159,6 +161,71 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
return Result.ok<MoneyValue>(new this(prop, isNull(_amount), options));
}
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
);
}
private static sanitize(amount: NullOr<number | string>): NullOr<number> {
let _amount: NullOr<number> = null;
@ -191,6 +258,16 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
.object;
}
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(value: Dinero, isNull: boolean, options: IMoneyValueOptions) {
super(value);
this._isNull = Object.freeze(isNull);
@ -202,7 +279,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
};
public toString(): string {
return this._isNull ? "" : String(this.props?.getAmount());
return MoneyValue._toString(this.isNull() ? null : this.getAmount(), this.getScale());
}
public toJSON() {
@ -320,7 +397,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
public toObject(): MoneyValueObject {
const obj = this.props.toObject();
return {
amount: obj.amount,
amount: this._isNull ? null : obj.amount,
scale: obj.precision,
currency_code: String(obj.currency),
};

View File

@ -47,22 +47,33 @@ export class Percentage extends NullableValueObject<IPercentage> {
scale: NullOr<number>,
options: IPercentageOptions
) {
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(
defaultPercentageProps.amount
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(Percentage.MIN_SCALE)
.max(Percentage.MAX_SCALE)
.label(options.label ? options.label : "percentage");
.label(options.label ? options.label : "scale");
const validationResults = new ResultCollection([
RuleValidator.validate<NullOr<number>>(
Joi.alternatives(ruleNull, RuleValidator.RULE_IS_TYPE_NUMBER),
Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString),
value
),
RuleValidator.validate<NullOr<number>>(
Joi.alternatives(ruleNull, RuleValidator.RULE_IS_TYPE_NUMBER, ruleScale),
Joi.alternatives(
RuleValidator.RULE_IS_TYPE_NUMBER.label(options.label ? options.label : "scale"),
ruleScale
),
scale
),
]);
@ -181,18 +192,18 @@ export class Percentage extends NullableValueObject<IPercentage> {
return _value;
}
private static _toNumber(value: NullOr<number>, scale: number): number {
if (isNull(value)) {
return 0;
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 Number(amount.toFixed(scale));
return amount.toFixed(scale);
}
private static _isWithinRange(value: NullOr<number>, scale: number): boolean {
const _value = Percentage._toNumber(value, scale);
const _value = Number(Percentage._toString(value, scale));
return _value >= Percentage.MIN_VALUE && _value <= Percentage.MAX_VALUE;
}
@ -226,12 +237,12 @@ export class Percentage extends NullableValueObject<IPercentage> {
return this._isNull;
};
public toNumber(): number {
return Percentage._toNumber(this.amount, this.scale);
public toString(): string {
return Percentage._toString(this.amount, this.scale);
}
public toString(): string {
return this.isNull() ? "" : String(this.toNumber());
public toNumber(): number {
return this.isNull() ? 0 : Number(this.toString());
}
public toPrimitive(): NullOr<number> {

View File

@ -47,27 +47,28 @@ export class Quantity extends NullableValueObject<IQuantity> {
const ruleEmpty = RuleValidator.RULE_ALLOW_EMPTY;
const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
options.label ? options.label : "quantity"
options.label ? options.label : "amount"
);
const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex(/^[-]?\d+$/).label(
options.label ? options.label : "quantity"
options.label ? options.label : "amount"
);
const ruleScale = Joi.number()
.min(Quantity.MIN_SCALE)
.max(Quantity.MAX_SCALE)
.label(options.label ? options.label : "quantity");
const rules = Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString);
.label(options.label ? options.label : "scale");
const validationResults = new ResultCollection([
RuleValidator.validate<NullOr<number>>(
Joi.alternatives(ruleNull, ruleNumber, ruleString),
Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString),
value
),
RuleValidator.validate<NullOr<number>>(
Joi.alternatives(ruleNull, RuleValidator.RULE_IS_TYPE_NUMBER, ruleScale),
Joi.alternatives(
RuleValidator.RULE_IS_TYPE_NUMBER.label(options.label ? options.label : "scale"),
ruleScale
),
scale
),
]);
@ -76,15 +77,6 @@ export class Quantity extends NullableValueObject<IQuantity> {
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 = scale === null ? Quantity.DEFAULT_SCALE : Number(scale);
// Calculate the adjusted value
const adjustedValue = numericValue / Math.pow(10, numericScale);
return Result.ok();
}