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", description: "aaaa",
unit_price: { unit_price: {
amount: "10000", amount: "10000",
precision: 4, scale: 4,
currency: "EUR", currency: "EUR",
}, },
discount: { discount: {

View File

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

View File

@ -12,7 +12,6 @@ import { CurrencyData, IUpdateQuote_Request_DTO, MoneyValue } from "@shared/cont
import { t } from "i18next"; import { t } from "i18next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
import useFormPersist from "react-hook-form-persist";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors"; import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors";
@ -57,42 +56,46 @@ export const QuoteEdit = () => {
validity: "", validity: "",
subtotal_price: { subtotal_price: {
amount: undefined, amount: undefined,
precision: 2, scale: 2,
currency_code: data?.currency_code, currency_code: data?.currency_code,
}, },
discount: { discount: {
amount: undefined, amount: undefined,
precision: 0, scale: 0,
}, },
total_price: { total_price: {
amount: undefined, amount: undefined,
precision: 2, scale: 2,
currency_code: data?.currency_code, currency_code: data?.currency_code,
}, },
items: [ /*items: [
{ {
quantity: {
amount: undefined,
scale: 2,
},
subtotal_price: { subtotal_price: {
amount: undefined, amount: undefined,
precision: 4, scale: 4,
currency_code: data?.currency_code, currency_code: data?.currency_code,
}, },
discount: { discount: {
amount: undefined, amount: undefined,
precision: 0, scale: 0,
}, },
total_price: { total_price: {
amount: undefined, amount: undefined,
precision: 4, scale: 4,
currency_code: data?.currency_code, currency_code: data?.currency_code,
}, },
}, },
], ],*/
}, },
}); });
const { watch, getValues, setValue, formState } = form; const { watch, getValues, setValue, formState } = form;
const { clear } = useFormPersist( /*const { clear } = useFormPersist(
"quote-edit", "quote-edit",
{ {
watch, watch,
@ -102,7 +105,7 @@ export const QuoteEdit = () => {
storage: window.localStorage, // default window.sessionStorage storage: window.localStorage, // default window.sessionStorage
//exclude: ['foo'] //exclude: ['foo']
} }
); );*/
const { isSubmitting } = formState; const { isSubmitting } = formState;
@ -116,7 +119,7 @@ export const QuoteEdit = () => {
//onSettled: () => {}, //onSettled: () => {},
onSuccess: () => { onSuccess: () => {
toast("Guardado!"); toast("Guardado!");
clear(); //clear();
}, },
}); });
}; };
@ -139,18 +142,21 @@ export const QuoteEdit = () => {
let quoteSubtotal = MoneyValue.create().object; let quoteSubtotal = MoneyValue.create().object;
// Recálculo líneas // Recálculo líneas
items.map((item, index) => { items &&
const itemTotals = calculateItemTotals(item); items.map((item, index) => {
const itemTotals = calculateItemTotals(item);
if (itemTotals === null) { console.log(itemTotals?.quantity.toObject());
return;
}
quoteSubtotal = quoteSubtotal.add(itemTotals.totalPrice); if (itemTotals === null) {
return;
}
setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject()); quoteSubtotal = quoteSubtotal.add(itemTotals.totalPrice);
setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject());
}); setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject());
setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject());
});
// Recálculo completo // Recálculo completo
setValue("subtotal_price", quoteSubtotal.toObject()); 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 { CurrencyData, MoneyValue } from "@shared/contexts";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react"; 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 { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
import { FormErrorMessage } from "./FormErrorMessage"; import { FormErrorMessage } from "./FormErrorMessage";
import { FormLabel, FormLabelProps } from "./FormLabel"; import { FormLabel, FormLabelProps } from "./FormLabel";
@ -38,7 +38,7 @@ export type FormCurrencyFieldProps<
UseControllerProps<TFieldValues, TName> & UseControllerProps<TFieldValues, TName> &
VariantProps<typeof formCurrencyFieldVariants> & { VariantProps<typeof formCurrencyFieldVariants> & {
currency: CurrencyData; currency: CurrencyData;
precision: number; scale: number;
}; };
export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrencyFieldProps>( export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrencyFieldProps>(
@ -54,7 +54,7 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
defaultValue, defaultValue,
rules, rules,
readOnly, readOnly,
precision, scale,
currency, currency,
variant, variant,
} = props; } = props;
@ -68,22 +68,17 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
} }
const moneyOrError = MoneyValue.create(value); const moneyOrError = MoneyValue.create(value);
if (moneyOrError.isFailure) { if (moneyOrError.isFailure) {
throw moneyOrError.error; throw moneyOrError.error;
} }
return moneyOrError.object return moneyOrError.object.toString();
.convertPrecision(precision ?? value.precision)
.toUnit()
.toString();
}, },
output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
const { value: amount } = values ?? { value: null };
output: (value: string | undefined) => { const moneyOrError = MoneyValue.createFromFormattedValue(amount, currency.code);
const moneyOrError = MoneyValue.create({
amount: value?.replace(",", "") ?? null,
precision,
currencyCode: currency.code,
});
if (moneyOrError.isFailure) { if (moneyOrError.isFailure) {
throw moneyOrError.error; throw moneyOrError.error;
} }
@ -118,11 +113,17 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
groupSeparator='.' groupSeparator='.'
decimalSeparator=',' decimalSeparator=','
placeholder={placeholder} placeholder={placeholder}
//fixedDecimalLength={precision} <- no activar para que sea más cómodo escribir las cantidades //allowDecimals={scale !== 0}
decimalsLimit={precision} decimalsLimit={scale}
decimalScale={precision} decimalScale={scale}
//fixedDecimalLength={scale} <- no activar para que sea más cómodo escribir las cantidades
step={1}
// { ...field }
value={transform.input(field.value)} 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> </FormControl>
{description && <FormDescription>{description}</FormDescription>} {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 { FormControl, FormDescription, FormField, FormItem, InputProps } from "@/ui";
import { Percentage } from "@shared/contexts"; import { Percentage } from "@shared/contexts";
import { cva, type VariantProps } from "class-variance-authority"; 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 { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
import { FormErrorMessage } from "./FormErrorMessage"; import { FormErrorMessage } from "./FormErrorMessage";
import { FormLabel, FormLabelProps } from "./FormLabel"; import { FormLabel, FormLabelProps } from "./FormLabel";
@ -39,7 +39,7 @@ export type FormPercentageFieldProps<
FormInputWithIconProps & FormInputWithIconProps &
UseControllerProps<TFieldValues, TName> & UseControllerProps<TFieldValues, TName> &
VariantProps<typeof formPercentageFieldVariants> & { VariantProps<typeof formPercentageFieldVariants> & {
precision: number; scale: number;
}; };
export const FormPercentageField = React.forwardRef< export const FormPercentageField = React.forwardRef<
@ -57,7 +57,7 @@ export const FormPercentageField = React.forwardRef<
defaultValue, defaultValue,
rules, rules,
readOnly, readOnly,
precision, scale,
variant, variant,
} = props; } = props;
@ -74,18 +74,14 @@ export const FormPercentageField = React.forwardRef<
throw percentageOrError.error; throw percentageOrError.error;
} }
return ( return percentageOrError.object.toString();
percentageOrError.object
.toNumber()
//.toPrecision(precision ?? value.precision)
.toString()
);
}, },
output: (value: string | undefined) => { output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
const percentageOrError = Percentage.create({ console.log(values);
amount: value?.replace(",", "") ?? null, const { value: amount } = values ?? { value: null };
precision,
}); const percentageOrError = Percentage.createFromFormattedValue(amount);
if (percentageOrError.isFailure) { if (percentageOrError.isFailure) {
throw percentageOrError.error; throw percentageOrError.error;
} }
@ -122,10 +118,16 @@ export const FormPercentageField = React.forwardRef<
groupSeparator='.' groupSeparator='.'
decimalSeparator=',' decimalSeparator=','
placeholder={placeholder} placeholder={placeholder}
decimalsLimit={precision} allowDecimals={scale !== 0}
decimalScale={precision} decimalsLimit={scale}
decimalScale={scale}
step={1}
//{...field}
value={transform.input(field.value)} 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> </FormControl>
{description && <FormDescription>{description}</FormDescription>} {description && <FormDescription>{description}</FormDescription>}

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@
"build": "tsc", "build": "tsc",
"lint": "eslint --ignore-path .gitignore . --ext .ts", "lint": "eslint --ignore-path .gitignore . --ext .ts",
"lint:fix": "npm run lint -- --fix", "lint:fix": "npm run lint -- --fix",
"test": "jest --verbose", "test": "jest --config=./jest.config.ts --rootDir=./src/ --verbose",
"clean": "rm -rf node_modules" "clean": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
@ -22,7 +22,7 @@
"@types/express-session": "^1.18.0", "@types/express-session": "^1.18.0",
"@types/glob": "^8.1.0", "@types/glob": "^8.1.0",
"@types/http-status": "^1.1.2", "@types/http-status": "^1.1.2",
"@types/jest": "^29.5.6", "@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6", "@types/jsonwebtoken": "^9.0.6",
"@types/luxon": "^3.3.1", "@types/luxon": "^3.3.1",
"@types/module-alias": "^2.0.1", "@types/module-alias": "^2.0.1",
@ -49,7 +49,7 @@
"module-alias": "^2.2.3", "module-alias": "^2.2.3",
"prettier": "3.0.1", "prettier": "3.0.1",
"supertest": "^6.2.2", "supertest": "^6.2.2",
"ts-jest": "^29.1.1", "ts-jest": "^29.2.2",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"typescript": "^5.2.2" "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> { async execute(request: IGetQuoteUseCaseRequest): Promise<GetQuoteResponseOrError> {
const { id } = request; const { id } = request;
// Validación de datos
// No hay en este caso
return await this.findQuote(id); return await this.findQuote(id);
} }

View File

@ -2,7 +2,8 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"types": ["jest"], "types": ["jest"],
"baseUrl": "./src", //"baseUrl": "./src",
"moduleResolution": "node",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"],
"@shared/*": ["../shared/lib/*"] "@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", () => { it("Should create a valid money value from string and format it", () => {
const validMoneyValueFromString = MoneyValue.create({ const validMoneyValueFromString = MoneyValue.create({
amount: "5075", amount: "5075",
scale: 2,
}); });
expect(validMoneyValueFromString.isSuccess).toBe(true); 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. // 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.getAmount()).toBe(100);
expect(moneyValue.getCurrency().code).toBe("EUR"); expect(moneyValue.getCurrency().code).toBe("EUR");
expect(moneyValue.getPrecision()).toBe(3); expect(moneyValue.getScale()).toBe(3);
}); });
it("should create MoneyValue from string and currency", () => { it("should create MoneyValue from string and currency", () => {
@ -67,7 +68,7 @@ describe("MoneyValue Value Object", () => {
expect(moneyValue.getAmount()).toBe(12345); expect(moneyValue.getAmount()).toBe(12345);
expect(moneyValue.getCurrency().code).toBe("USD"); 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", () => { it("should fail to create MoneyValue with invalid amount", () => {
@ -86,12 +87,7 @@ describe("MoneyValue Value Object", () => {
}).object; }).object;
const result = moneyValue.toString(); const result = moneyValue.toString();
expect(result).toBe("7525"); expect(result).toBe("75.25");
});
// Prueba la verificación de valor nulo.
it("Should check if value is null", () => {
expect(() => MoneyValue.create(null)).toThrowError();
}); });
// Prueba la verificación de valor cero. // Prueba la verificación de valor cero.

View File

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

View File

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

View File

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