From 740f5fd2383804e2e55915c5c53ac44d9896aa7f Mon Sep 17 00:00:00 2001 From: David Arranz Date: Tue, 16 Jul 2024 19:17:52 +0200 Subject: [PATCH] . --- .../quotes/components/SortableDataTable.tsx | 2 +- .../editors/QuoteDetailsCardEditor.tsx | 46 +++--- client/src/app/quotes/edit.tsx | 48 +++--- .../components/Forms/FormCurrencyField.tsx | 35 +++-- .../src/components/Forms/FormMoneyField.tsx | 147 ------------------ .../components/Forms/FormPercentageField.tsx | 36 +++-- .../components/Forms/FormQuantityField.tsx | 36 ++--- client/src/components/Forms/index.ts | 1 - server/jest.config.js | 18 +-- server/package.json | 6 +- .../Quote/GetQuote.useCase.test.ts | 81 ++++++++++ .../application/Quote/GetQuote.useCase.ts | 3 - server/tsconfig.eslint.json | 3 +- .../common/domain/entities/MoneyValue.test.ts | 14 +- .../common/domain/entities/MoneyValue.ts | 85 +++++++++- .../common/domain/entities/Percentage.ts | 39 +++-- .../common/domain/entities/Quantity.ts | 24 +-- 17 files changed, 319 insertions(+), 305 deletions(-) delete mode 100644 client/src/components/Forms/FormMoneyField.tsx create mode 100644 server/src/contexts/sales/application/Quote/GetQuote.useCase.test.ts diff --git a/client/src/app/quotes/components/SortableDataTable.tsx b/client/src/app/quotes/components/SortableDataTable.tsx index 0f33ed9..bd02583 100644 --- a/client/src/app/quotes/components/SortableDataTable.tsx +++ b/client/src/app/quotes/components/SortableDataTable.tsx @@ -273,7 +273,7 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP description: "aaaa", unit_price: { amount: "10000", - precision: 4, + scale: 4, currency: "EUR", }, discount: { diff --git a/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx b/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx index 68f484f..1434d46 100644 --- a/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx +++ b/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx @@ -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: () => (
{t("quotes.form_fields.items.quantity.label")}
), - size: 5, + size: 8, cell: ({ row: { index } }) => { return ( ); @@ -73,14 +73,14 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData }) ); }, }, - { + /*{ id: "subtotal_price" as const, accessorKey: "subtotal_price", header: () => ( @@ -91,34 +91,33 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData }) ); }, - }, + },*/ { id: "discount" as const, accessorKey: "discount", - size: 5, + size: 8, header: () => (
{t("quotes.form_fields.items.discount.label")}
), - cell: ({ row: { index }, column: { id } }) => { + cell: ({ row: { index } }) => { return ( - <> - - + ); }, }, - { + /*{ id: "total_price" as const, accessorKey: "total_price", header: () => ( @@ -129,13 +128,14 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData }) ); }, - }, + },*/ ], { 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 }) - + {/**/} diff --git a/client/src/app/quotes/edit.tsx b/client/src/app/quotes/edit.tsx index f867c1b..759301b 100644 --- a/client/src/app/quotes/edit.tsx +++ b/client/src/app/quotes/edit.tsx @@ -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()); diff --git a/client/src/components/Forms/FormCurrencyField.tsx b/client/src/components/Forms/FormCurrencyField.tsx index 0908afc..826f237 100644 --- a/client/src/components/Forms/FormCurrencyField.tsx +++ b/client/src/components/Forms/FormCurrencyField.tsx @@ -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 & VariantProps & { currency: CurrencyData; - precision: number; + scale: number; }; export const FormCurrencyField = React.forwardRef( @@ -54,7 +54,7 @@ export const FormCurrencyField = React.forwardRef { + 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 field.onChange(transform.output(e))} + //onChange={() => {}} + onValueChange={(value, name, values) => + field.onChange(transform.output(value, name, values)) + } /> {description && {description}} diff --git a/client/src/components/Forms/FormMoneyField.tsx b/client/src/components/Forms/FormMoneyField.tsx deleted file mode 100644 index 30ac6cb..0000000 --- a/client/src/components/Forms/FormMoneyField.tsx +++ /dev/null @@ -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 & { - defaultValue?: any; -}; - -export const FormMoneyField = forwardRef< - HTMLDivElement, - React.HTMLAttributes & 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(MoneyValue.DEFAULT_PRECISION); - const [currencyCode, setCurrencyCode] = useState(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) => { - 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 ( - { - return ( - field.onChange(transform.output(e))} - value={transform.input(field.value)} - /> - ); - - return ( -
- {label && } -
-
- {leadIcon && ( -
- {createElement( - leadIcon, - { - className: "h-5 w-5 text-muted-foreground", - "aria-hidden": true, - }, - null - )} -
- )} - - - field.onChange(transform.output(e))} - value={transform.input(field.value)} - /> - - {trailIcon && ( -
- {createElement( - trailIcon, - { - className: "h-5 w-5 text-muted-foreground", - "aria-hidden": true, - }, - null - )} -
- )} -
- {button && <>{createElement(button)}} -
- {description && {description}} - -
- ); - }} - /> - ); -}); diff --git a/client/src/components/Forms/FormPercentageField.tsx b/client/src/components/Forms/FormPercentageField.tsx index d4ea2f6..e10e6f0 100644 --- a/client/src/components/Forms/FormPercentageField.tsx +++ b/client/src/components/Forms/FormPercentageField.tsx @@ -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 & VariantProps & { - 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)) + } /> {description && {description}} diff --git a/client/src/components/Forms/FormQuantityField.tsx b/client/src/components/Forms/FormQuantityField.tsx index f44e139..cfcad85 100644 --- a/client/src/components/Forms/FormQuantityField.tsx +++ b/client/src/components/Forms/FormQuantityField.tsx @@ -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 & VariantProps & { - precision: number; + scale: number; }; export const FormQuantityField = React.forwardRef( @@ -54,7 +54,7 @@ export const FormQuantityField = React.forwardRef { - 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 field.onChange(transform.output(e))} + //onChange={() => {}} + onValueChange={(value, name, values) => + field.onChange(transform.output(value, name, values)) + } /> {description && {description}} diff --git a/client/src/components/Forms/index.ts b/client/src/components/Forms/index.ts index 615f2cc..467f9b9 100644 --- a/client/src/components/Forms/index.ts +++ b/client/src/components/Forms/index.ts @@ -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"; diff --git a/server/jest.config.js b/server/jest.config.js index 6b7b9ef..940a000 100644 --- a/server/jest.config.js +++ b/server/jest.config.js @@ -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/(.*)$": "/../shared/lib/$1", + "^@/(.*)$": "/src/$1", + }, }; diff --git a/server/package.json b/server/package.json index 31950b6..93dcda5 100644 --- a/server/package.json +++ b/server/package.json @@ -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" }, diff --git a/server/src/contexts/sales/application/Quote/GetQuote.useCase.test.ts b/server/src/contexts/sales/application/Quote/GetQuote.useCase.test.ts new file mode 100644 index 0000000..f4fbb69 --- /dev/null +++ b/server/src/contexts/sales/application/Quote/GetQuote.useCase.test.ts @@ -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; + let mockRepositoryManager: jest.Mocked; + let mockQuoteRepository: jest.Mocked; + let mockTransaction: { complete: jest.Mock }; + + beforeEach(() => { + mockTransaction = { complete: jest.fn() }; + + mockAdapter = { + startTransaction: jest.fn().mockReturnValue(mockTransaction), + } as unknown as jest.Mocked; + + mockQuoteRepository = { + getById: jest.fn(), + } as unknown as jest.Mocked; + + mockRepositoryManager = { + getRepository: jest.fn().mockReturnValue(() => mockQuoteRepository), + } as unknown as jest.Mocked; + + 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"); + }); +}); diff --git a/server/src/contexts/sales/application/Quote/GetQuote.useCase.ts b/server/src/contexts/sales/application/Quote/GetQuote.useCase.ts index aa27e5c..fa6197a 100644 --- a/server/src/contexts/sales/application/Quote/GetQuote.useCase.ts +++ b/server/src/contexts/sales/application/Quote/GetQuote.useCase.ts @@ -37,9 +37,6 @@ export class GetQuoteUseCase async execute(request: IGetQuoteUseCaseRequest): Promise { const { id } = request; - // Validación de datos - // No hay en este caso - return await this.findQuote(id); } diff --git a/server/tsconfig.eslint.json b/server/tsconfig.eslint.json index bc6937c..7afb9a0 100644 --- a/server/tsconfig.eslint.json +++ b/server/tsconfig.eslint.json @@ -2,7 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["jest"], - "baseUrl": "./src", + //"baseUrl": "./src", + "moduleResolution": "node", "paths": { "@/*": ["./src/*"], "@shared/*": ["../shared/lib/*"] diff --git a/shared/lib/contexts/common/domain/entities/MoneyValue.test.ts b/shared/lib/contexts/common/domain/entities/MoneyValue.test.ts index 791f4a1..11f5929 100644 --- a/shared/lib/contexts/common/domain/entities/MoneyValue.test.ts +++ b/shared/lib/contexts/common/domain/entities/MoneyValue.test.ts @@ -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. diff --git a/shared/lib/contexts/common/domain/entities/MoneyValue.ts b/shared/lib/contexts/common/domain/entities/MoneyValue.ts index 4da095b..1a378be 100644 --- a/shared/lib/contexts/common/domain/entities/MoneyValue.ts +++ b/shared/lib/contexts/common/domain/entities/MoneyValue.ts @@ -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 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 implements IMoneyValue { return Result.ok(new this(prop, isNull(_amount), options)); } + public static createFromFormattedValue( + value: NullOr, + 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): NullOr { let _amount: NullOr = null; @@ -191,6 +258,16 @@ export class MoneyValue extends ValueObject implements IMoneyValue { .object; } + private static _toString(value: NullOr, 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 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 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), }; diff --git a/shared/lib/contexts/common/domain/entities/Percentage.ts b/shared/lib/contexts/common/domain/entities/Percentage.ts index cc88f50..80d4dd1 100644 --- a/shared/lib/contexts/common/domain/entities/Percentage.ts +++ b/shared/lib/contexts/common/domain/entities/Percentage.ts @@ -47,22 +47,33 @@ export class Percentage extends NullableValueObject { scale: NullOr, 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>( - Joi.alternatives(ruleNull, RuleValidator.RULE_IS_TYPE_NUMBER), + Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString), value ), RuleValidator.validate>( - 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 { return _value; } - private static _toNumber(value: NullOr, scale: number): number { - if (isNull(value)) { - return 0; + private static _toString(value: NullOr, 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, 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 { 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 { diff --git a/shared/lib/contexts/common/domain/entities/Quantity.ts b/shared/lib/contexts/common/domain/entities/Quantity.ts index 1ef3552..de6cdd5 100644 --- a/shared/lib/contexts/common/domain/entities/Quantity.ts +++ b/shared/lib/contexts/common/domain/entities/Quantity.ts @@ -47,27 +47,28 @@ export class Quantity extends NullableValueObject { 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>( - Joi.alternatives(ruleNull, ruleNumber, ruleString), + Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString), value ), RuleValidator.validate>( - 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 { 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(); }