From 47802ab93229b1471127efe31b8adb0443ae1772 Mon Sep 17 00:00:00 2001 From: David Arranz Date: Wed, 17 Jul 2024 20:10:07 +0200 Subject: [PATCH] . --- client/package.json | 2 +- ...le.tsx => QuoteItemsSortableDataTable.tsx} | 61 +++++-------- ...=> QuoteItemsSortableDataTableToolbar.tsx} | 2 +- ...Row.tsx => QuoteItemsSortableTableRow.tsx} | 15 ++-- .../app/quotes/components/QuotesDataTable.tsx | 3 +- .../editors/QuoteDetailsCardEditor.tsx | 19 ++-- .../editors/QuoteGeneralCardEditor.tsx | 6 ++ client/src/app/quotes/create.tsx | 2 +- client/src/app/quotes/edit.tsx | 62 ++++++++----- .../components/Forms/FormCurrencyField.tsx | 3 + .../SorteableDataTable/SortableDataTable.tsx | 2 +- client/src/components/index.ts | 2 +- client/src/lib/calc.ts | 17 +--- .../domain/entities/ArticleIdentifier.ts | 22 ++--- .../application/Quote/CreateQuote.useCase.ts | 5 +- .../application/Quote/UpdateQuote.useCase.ts | 90 ++++++++++++++----- .../sales/domain/entities/Quotes/QuoteItem.ts | 7 +- .../mappers/quoteItem.mapper.ts | 7 +- .../sequelize/quoteItem.model.ts | 2 +- .../common/application/dto/IMoney.dto.ts | 2 +- .../common/application/dto/IPercentage.dto.ts | 2 +- .../common/application/dto/IQuantity.dto.ts | 7 +- .../common/domain/entities/Description.ts | 22 ++--- .../common/domain/entities/Measure.ts | 24 ++--- .../common/domain/entities/MoneyValue.ts | 5 +- .../common/domain/entities/Percentage.ts | 2 +- .../common/domain/entities/Quantity.ts | 5 +- .../QueryCriteria/Field/FieldCriteria.ts | 10 +-- .../contexts/common/domain/entities/Slug.ts | 20 ++--- .../GetQuote.dto/IGetQuote_Response.dto.ts | 22 ++--- .../IUpdateQuote_Request.dto.ts | 36 ++++---- 31 files changed, 259 insertions(+), 227 deletions(-) rename client/src/app/quotes/components/{SortableDataTable.tsx => QuoteItemsSortableDataTable.tsx} (90%) rename client/src/app/quotes/components/{SortableDataTableToolbar.tsx => QuoteItemsSortableDataTableToolbar.tsx} (98%) rename client/src/app/quotes/components/{SortableTableRow.tsx => QuoteItemsSortableTableRow.tsx} (77%) diff --git a/client/package.json b/client/package.json index 90ab9c5..dd936e8 100644 --- a/client/package.json +++ b/client/package.json @@ -57,7 +57,7 @@ "react-currency-input-field": "^3.8.0", "react-day-picker": "^8.10.1", "react-dom": "^18.2.0", - "react-hook-form": "^7.51.5", + "react-hook-form": "^7.52.1", "react-hook-form-persist": "^2.1.0", "react-i18next": "^14.1.2", "react-resizable-panels": "^2.0.19", diff --git a/client/src/app/quotes/components/SortableDataTable.tsx b/client/src/app/quotes/components/QuoteItemsSortableDataTable.tsx similarity index 90% rename from client/src/app/quotes/components/SortableDataTable.tsx rename to client/src/app/quotes/components/QuoteItemsSortableDataTable.tsx index b7a15a6..25bead1 100644 --- a/client/src/app/quotes/components/SortableDataTable.tsx +++ b/client/src/app/quotes/components/QuoteItemsSortableDataTable.tsx @@ -42,26 +42,27 @@ import { useCallback, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { FieldValues, UseFieldArrayReturn } from "react-hook-form"; import { AddNewRowButton } from "./AddNewRowButton"; -import { SortableDataTableToolbar } from "./SortableDataTableToolbar"; -import { SortableTableRow } from "./SortableTableRow"; +import { QuoteItemsSortableDataTableToolbar } from "./QuoteItemsSortableDataTableToolbar"; +import { QuoteItemsSortableTableRow } from "./QuoteItemsSortableTableRow"; declare module "@tanstack/react-table" { interface TableMeta { - insertItem: (rowIndex: number, data: TData) => void; - appendItem: (data: TData) => void; + insertItem: (rowIndex: number, data?: unknown) => void; + appendItem: (data?: unknown) => void; duplicateItems: (rowIndex?: number) => void; deleteItems: (rowIndex?: number | number[]) => void; updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void; } } -export interface SortableProps { +export interface QuoteItemsSortableProps { id: UniqueIdentifier; } -export type SortableDataTableProps = { +export type QuoteItemsSortableDataTableProps = { columns: ColumnDef[]; data: Record<"id", string>[]; + defaultValues: Readonly<{ [x: string]: any }> | undefined; actions: Omit, "fields">; }; @@ -94,34 +95,12 @@ const dropAnimationConfig: DropAnimation = { }, }; -/*const defaultColumn: Partial> = { - cell: ({ table, row: { index, original }, column, getValue }) => { - const initialValue = getValue(); - - // We need to keep and update the state of the cell normally - // eslint-disable-next-line react-hooks/rules-of-hooks - const [value, setValue] = useState(initialValue); - - // If the initialValue is changed external, sync it up with our state - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - setValue(initialValue); - }, [initialValue]); - - return ( - setValue(e.target.value)} - onBlur={() => { - console.log(column.id, value); - table.options.meta?.updateItem(index, original, column.id, value); - }} - /> - ); - }, -};*/ - -export function SortableDataTable({ columns, data, actions }: SortableDataTableProps) { +export function QuoteItemsSortableDataTable({ + columns, + data, + defaultValues, + actions, +}: QuoteItemsSortableDataTableProps) { const [rowSelection, setRowSelection] = useState({}); const [activeId, setActiveId] = useState(); @@ -154,11 +133,11 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP maxSize: 96, //enforced during column resizing }, meta: { - insertItem: (rowIndex: number, data: object = {}) => { - actions.insert(rowIndex, data, { shouldFocus: true }); + insertItem: (rowIndex: number, data?: unknown) => { + actions.insert(rowIndex, data || defaultValues?.items[0], { shouldFocus: true }); }, - appendItem: (data: object = {}) => { - actions.append(data, { shouldFocus: true }); + appendItem: (data?: unknown) => { + actions.append(data || defaultValues?.items[0], { shouldFocus: true }); }, duplicateItems: (rowIndex?: number) => { if (rowIndex != undefined) { @@ -323,7 +302,7 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP onDragCancel={handleDragCancel} collisionDetection={closestCenter} > - + {table.getHeaderGroups().map((headerGroup) => ( @@ -346,13 +325,13 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP strategy={verticalListSortingStrategy} > {filterItems(table.getRowModel().rows).map((row) => ( - + {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} - + ))} diff --git a/client/src/app/quotes/components/SortableDataTableToolbar.tsx b/client/src/app/quotes/components/QuoteItemsSortableDataTableToolbar.tsx similarity index 98% rename from client/src/app/quotes/components/SortableDataTableToolbar.tsx rename to client/src/app/quotes/components/QuoteItemsSortableDataTableToolbar.tsx index 5ab72fe..6f41c66 100644 --- a/client/src/app/quotes/components/SortableDataTableToolbar.tsx +++ b/client/src/app/quotes/components/QuoteItemsSortableDataTableToolbar.tsx @@ -27,7 +27,7 @@ import { Trash2Icon, } from "lucide-react"; -export const SortableDataTableToolbar = ({ table }: { table: Table }) => { +export const QuoteItemsSortableDataTableToolbar = ({ table }: { table: Table }) => { const selectedRowsCount = table.getSelectedRowModel().rows.length; if (selectedRowsCount) { diff --git a/client/src/app/quotes/components/SortableTableRow.tsx b/client/src/app/quotes/components/QuoteItemsSortableTableRow.tsx similarity index 77% rename from client/src/app/quotes/components/SortableTableRow.tsx rename to client/src/app/quotes/components/QuoteItemsSortableTableRow.tsx index 4a95e1d..6ee0f43 100644 --- a/client/src/app/quotes/components/SortableTableRow.tsx +++ b/client/src/app/quotes/components/QuoteItemsSortableTableRow.tsx @@ -4,20 +4,20 @@ import { DraggableSyntheticListeners } from "@dnd-kit/core"; import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react"; -import { SortableProps } from "./SortableDataTable"; +import { QuoteItemsSortableProps } from "./QuoteItemsSortableDataTable"; interface Context { attributes: Record; listeners: DraggableSyntheticListeners; ref(node: HTMLElement | null): void; } -export const SortableTableRowContext = createContext({ +export const QuoteItemsSortableTableRowContext = createContext({ attributes: {}, listeners: undefined, ref() {}, }); -function animateLayoutChanges(args) { +function animateLayoutChanges(args: any) { if (args.isSorting || args.wasDragging) { return defaultAnimateLayoutChanges(args); } @@ -25,7 +25,10 @@ function animateLayoutChanges(args) { return true; } -export function SortableTableRow({ id, children }: PropsWithChildren) { +export function QuoteItemsSortableTableRow({ + id, + children, +}: PropsWithChildren) { const { attributes, isDragging, @@ -54,7 +57,7 @@ export function SortableTableRow({ id, children }: PropsWithChildren + {children} - + ); } diff --git a/client/src/app/quotes/components/QuotesDataTable.tsx b/client/src/app/quotes/components/QuotesDataTable.tsx index b6e955e..cbb665e 100644 --- a/client/src/app/quotes/components/QuotesDataTable.tsx +++ b/client/src/app/quotes/components/QuotesDataTable.tsx @@ -1,8 +1,7 @@ import { Badge, Button, Card, CardContent } from "@/ui"; -import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components"; +import { DataTable, DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components"; -import { DataTable } from "@/components"; import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar"; import { useDataTable, useDataTableContext } from "@/lib/hooks"; import { IListQuotes_Response_DTO, MoneyValue, UTCDateValue } from "@shared/contexts"; diff --git a/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx b/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx index 7e09237..ee74994 100644 --- a/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx +++ b/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx @@ -12,16 +12,18 @@ import { t } from "i18next"; import { useCallback, useState } from "react"; import { useFieldArray, useFormContext } from "react-hook-form"; import { useDetailColumns } from "../../hooks"; -import { SortableDataTable } from "../SortableDataTable"; +import { QuoteItemsSortableDataTable } from "../QuoteItemsSortableDataTable"; export const QuoteDetailsCardEditor = ({ currency, language, + defaultValues, }: { currency: CurrencyData; language: Language; + defaultValues: Readonly<{ [x: string]: any }> | undefined; }) => { - const { control, register, getValues } = useFormContext(); + const { control, register } = useFormContext(); const { fields, ...fieldActions } = useFieldArray({ control, @@ -186,7 +188,7 @@ export const QuoteDetailsCardEditor = ({ ); const handleInsertArticle = useCallback( - (newArticle) => { + (newArticle: any) => { fieldActions.append({ ...newArticle, quantity: { @@ -204,7 +206,14 @@ export const QuoteDetailsCardEditor = ({ const defaultLayout = [265, 440, 655]; const navCollapsedSize = 4; - return ; + return ( + + ); return ( - + diff --git a/client/src/app/quotes/components/editors/QuoteGeneralCardEditor.tsx b/client/src/app/quotes/components/editors/QuoteGeneralCardEditor.tsx index 67d0dd6..5f7534d 100644 --- a/client/src/app/quotes/components/editors/QuoteGeneralCardEditor.tsx +++ b/client/src/app/quotes/components/editors/QuoteGeneralCardEditor.tsx @@ -6,6 +6,12 @@ import { useFormContext } from "react-hook-form"; export const QuoteGeneralCardEditor = () => { const { register, formState } = useFormContext(); + console.log({ + ...register("customer_information", { + required: true, + }), + }); + return (
{ setWarnWhen(false); mutate(formData, { onSuccess: (data) => { - navigate(`/quotes/edit/${data.id}`, { relative: "path" }); + navigate(`/quotes/edit/${data.id}`, { relative: "path", replace: true }); }, }); } finally { diff --git a/client/src/app/quotes/edit.tsx b/client/src/app/quotes/edit.tsx index 029006b..707c413 100644 --- a/client/src/app/quotes/edit.tsx +++ b/client/src/app/quotes/edit.tsx @@ -8,16 +8,20 @@ import { import { calculateItemTotals } from "@/lib/calc"; import { useUrlId } from "@/lib/hooks/useUrlId"; import { Badge, Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui"; -import { CurrencyData, IUpdateQuote_Request_DTO, Language, MoneyValue } from "@shared/contexts"; +import { CurrencyData, IGetQuote_Response_DTO, Language, MoneyValue } from "@shared/contexts"; import { t } from "i18next"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { SubmitHandler, useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors"; import { useQuotes } from "./hooks"; -interface QuoteDataForm extends IUpdateQuote_Request_DTO {} +/*type QuoteDataForm = Omit & { + items: IGetQuote_QuoteItem_Response_DTO; +};*/ + +type QuoteDataForm = IGetQuote_Response_DTO; // eslint-disable-next-line @typescript-eslint/no-unused-vars export const QuoteEdit = () => { @@ -42,12 +46,8 @@ export const QuoteEdit = () => { const { data, status, error: queryError } = useOne(quoteId); - const { mutate } = useUpdate(String(quoteId)); - - const form = useForm({ - mode: "onBlur", - values: data, - defaultValues: { + const defaultValues = useMemo( + () => ({ date: "", reference: "", customer_information: "", @@ -59,7 +59,7 @@ export const QuoteEdit = () => { subtotal_price: { amount: undefined, scale: 2, - currency_code: data?.currency_code, + currency_code: data?.currency_code ?? quoteCurrency.code, }, discount: { amount: undefined, @@ -68,32 +68,46 @@ export const QuoteEdit = () => { total_price: { amount: undefined, scale: 2, - currency_code: data?.currency_code, + currency_code: data?.currency_code ?? quoteCurrency.code, }, items: [ { description: "", quantity: { - amount: undefined, + amount: 1, scale: 0, }, - subtotal_price: { - amount: undefined, + unit_price: { + amount: null, scale: 4, - currency_code: data?.currency_code, + currency_code: data?.currency_code ?? quoteCurrency.code, + }, + subtotal_price: { + amount: null, + scale: 4, + currency_code: data?.currency_code ?? quoteCurrency.code, }, discount: { - amount: undefined, + amount: null, scale: 2, }, total_price: { - amount: undefined, + amount: null, scale: 4, - currency_code: data?.currency_code, + currency_code: data?.currency_code ?? quoteCurrency.code, }, }, ], - }, + }), + [data, quoteCurrency] + ); + + const { mutate } = useUpdate(String(quoteId)); + + const form = useForm({ + mode: "onBlur", + values: data, + defaultValues, }); const { watch, getValues, setValue, formState } = form; @@ -117,6 +131,7 @@ export const QuoteEdit = () => { mutate(data, { onError: (error) => { console.debug(error); + toast.error(error.message); //alert(error.message); }, //onSettled: () => {}, @@ -132,9 +147,6 @@ export const QuoteEdit = () => { const { unsubscribe } = watch((_, { name, type }) => { const value = getValues(); - console.log("USEEFFECT !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); - console.log(name); - if (name) { if (name === "currency_code") { setQuoteCurrency( @@ -241,7 +253,11 @@ export const QuoteEdit = () => { - + diff --git a/client/src/components/Forms/FormCurrencyField.tsx b/client/src/components/Forms/FormCurrencyField.tsx index 06ccbec..d60c3ae 100644 --- a/client/src/components/Forms/FormCurrencyField.tsx +++ b/client/src/components/Forms/FormCurrencyField.tsx @@ -110,6 +110,9 @@ export const FormCurrencyField = React.forwardRef { insertItem: (rowIndex: number, data: TData) => void; - appendItem: (data: TData) => void; + appendItem: (data?: TData) => void; duplicateItems: (rowIndex?: number) => void; deleteItems: (rowIndex?: number | number[]) => void; updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void; diff --git a/client/src/components/index.ts b/client/src/components/index.ts index 9a2b6c7..1398e96 100644 --- a/client/src/components/index.ts +++ b/client/src/components/index.ts @@ -11,5 +11,5 @@ export * from "./Layout"; export * from "./LoadingIndicator"; export * from "./LoadingOverlay"; export * from "./ProtectedRoute"; -export * from "./SorteableDataTable"; +//export * from "./SorteableDataTable"; export * from "./TailwindIndicator"; diff --git a/client/src/lib/calc.ts b/client/src/lib/calc.ts index 0ce34df..5b3e6c0 100644 --- a/client/src/lib/calc.ts +++ b/client/src/lib/calc.ts @@ -1,24 +1,13 @@ import { MoneyValue, Percentage, Quantity } from "@shared/contexts"; -import { IMoney, IPercentage, IQuantity } from "./types"; -export const calculateItemTotals = (item: { - quantity?: IQuantity; - unit_price?: IMoney; - discount?: IPercentage; -}): { - quantity: Quantity; - unitPrice: MoneyValue; - subtotalPrice: MoneyValue; - discount: Percentage; - totalPrice: MoneyValue; -} | null => { +export const calculateItemTotals = (item: any) => { console.log(item); const { quantity: quantity_dto, unit_price: unit_price_dto, discount: discount_dto } = item; - if (quantity_dto === "" || unit_price_dto === "") { + /*if (quantity_dto.amount === null || unit_price_dto.amount === null) { return null; - } + }*/ const quantityOrError = Quantity.create(quantity_dto); if (quantityOrError.isFailure) { diff --git a/server/src/contexts/catalog/domain/entities/ArticleIdentifier.ts b/server/src/contexts/catalog/domain/entities/ArticleIdentifier.ts index bb65dbe..88fe07b 100644 --- a/server/src/contexts/catalog/domain/entities/ArticleIdentifier.ts +++ b/server/src/contexts/catalog/domain/entities/ArticleIdentifier.ts @@ -7,33 +7,29 @@ import { import { NullOr } from "@shared/utilities"; import Joi from "joi"; -export interface IArticleIdentifierOptions - extends INullableValueObjectOptions {} +export interface IArticleIdentifierOptions extends INullableValueObjectOptions {} export class ArticleIdentifier extends NullableValueObject { protected static validate( value: NullOr, - options: IArticleIdentifierOptions = {}, + options: IArticleIdentifierOptions = {} ) { const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(null); const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label( - options.label ? options.label : "ArticleIdentifier", + options.label ? options.label : "ArticleIdentifier" ); - const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex( - /^[-]?\d+$/, - ).label(options.label ? options.label : "ArticleIdentifier"); + const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex(/^[-]?\d+$/).label( + options.label ? options.label : "ArticleIdentifier" + ); const rules = Joi.alternatives(ruleNull, ruleNumber, ruleString); return RuleValidator.validate>(rules, value); } - public static create( - value: NullOr, - options: IArticleIdentifierOptions = {}, - ) { + public static create(value: NullOr, options: IArticleIdentifierOptions = {}) { const _options = { label: "ArticleIdentifier", ...options, @@ -64,8 +60,8 @@ export class ArticleIdentifier extends NullableValueObject { return this.isNull() ? "" : String(this.value); } - public toPrimitive(): number { - return this.toNumber(); + public toPrimitive(): number | null { + return this.isNull() ? null : this.toNumber(); } public increment(amount: number = 1) { diff --git a/server/src/contexts/sales/application/Quote/CreateQuote.useCase.ts b/server/src/contexts/sales/application/Quote/CreateQuote.useCase.ts index 779fe85..a070749 100644 --- a/server/src/contexts/sales/application/Quote/CreateQuote.useCase.ts +++ b/server/src/contexts/sales/application/Quote/CreateQuote.useCase.ts @@ -190,7 +190,10 @@ export class CreateQuoteUseCase QuoteItem.create({ articleId: item.article_id, description: Description.create(item.description).object, - quantity: Quantity.create(item.quantity).object, + quantity: Quantity.create({ + amount: item.quantity.amount, + scale: item.quantity.scale, + }).object, unitPrice: UnitPrice.create({ amount: item.unit_price?.amount, currencyCode: item.unit_price?.currency_code, diff --git a/server/src/contexts/sales/application/Quote/UpdateQuote.useCase.ts b/server/src/contexts/sales/application/Quote/UpdateQuote.useCase.ts index 271eeb1..76d956e 100644 --- a/server/src/contexts/sales/application/Quote/UpdateQuote.useCase.ts +++ b/server/src/contexts/sales/application/Quote/UpdateQuote.useCase.ts @@ -24,6 +24,7 @@ import { UnitPrice, } from "@shared/contexts"; +import { ArticleIdentifier } from "@/contexts/catalog/domain"; import { IUpdateQuote_Request_DTO } from "@shared/contexts"; import { Dealer, @@ -183,28 +184,73 @@ export class UpdateQuoteUseCase return Result.fail(discountOrError.error); } - const items = new Collection( - quoteDTO.items?.map( - (item) => - QuoteItem.create({ - articleId: item.article_id, - description: Description.create(item.description).object, - quantity: Quantity.create({ - amount: item.quantity.amount, - scale: item.quantity.scale, - }).object, - unitPrice: UnitPrice.create({ - amount: item.unit_price?.amount, - currencyCode: item.unit_price?.currency_code, - scale: item.unit_price?.scale, - }).object, - discount: Percentage.create({ - amount: item.discount?.amount, - scale: item.discount?.scale, - }).object, - }).object - ) - ); + let items: Collection; + + try { + items = new Collection( + quoteDTO.items?.map((item) => { + const articleIdOrError = ArticleIdentifier.create(item.article_id); + if (articleIdOrError.isFailure) { + throw articleIdOrError.error; + } + + const descriptionOrError = Description.create(item.description); + if (descriptionOrError.isFailure) { + throw descriptionOrError.error; + } + + const quantityOrError = Quantity.create({ + amount: item.quantity.amount, + scale: item.quantity.scale, + }); + if (quantityOrError.isFailure) { + throw quantityOrError.error; + } + + const unitPriceOrError = UnitPrice.create({ + amount: item.unit_price?.amount, + currencyCode: item.unit_price?.currency_code, + scale: item.unit_price?.scale, + }); + if (unitPriceOrError.isFailure) { + throw unitPriceOrError.error; + } + + const percentageOrError = Percentage.create({ + amount: item.discount?.amount, + scale: item.discount?.scale, + }); + if (percentageOrError.isFailure) { + throw percentageOrError.error; + } + + const quoteItemOrError = QuoteItem.create({ + articleId: articleIdOrError.object, + description: descriptionOrError.object, + quantity: quantityOrError.object, + unitPrice: unitPriceOrError.object, + discount: percentageOrError.object, + }); + + if (quoteItemOrError.isFailure) { + throw quoteItemOrError.error; + } + + return quoteItemOrError.object; + }) + ); + } catch (e: unknown) { + //let error = e as Error; + /*if (error.name === "ValidationError") { + error = e as ValidationError; + } + + if (error.name === "DomainError") { + error = e as DomainError; + }*/ + + return Result.fail(e as IDomainError); + } return Quote.create( { diff --git a/server/src/contexts/sales/domain/entities/Quotes/QuoteItem.ts b/server/src/contexts/sales/domain/entities/Quotes/QuoteItem.ts index 2d77826..1bd79b8 100644 --- a/server/src/contexts/sales/domain/entities/Quotes/QuoteItem.ts +++ b/server/src/contexts/sales/domain/entities/Quotes/QuoteItem.ts @@ -1,3 +1,4 @@ +import { ArticleIdentifier } from "@/contexts/catalog/domain"; import { Description, Entity, @@ -11,7 +12,7 @@ import { } from "@shared/contexts"; export interface IQuoteItemProps extends IEntityProps { - articleId: string | null; + articleId: ArticleIdentifier; description: Description; // Descripción del artículo o servicio quantity: Quantity; // Cantidad de unidades unitPrice: MoneyValue; // Precio unitario en la moneda de la factura @@ -21,7 +22,7 @@ export interface IQuoteItemProps extends IEntityProps { } export interface IQuoteItem { - articleId: string | null; + articleId: ArticleIdentifier; description: Description; quantity: Quantity; unitPrice: MoneyValue; @@ -35,7 +36,7 @@ export class QuoteItem extends Entity implements IQuoteItem { return Result.ok(new QuoteItem(props, id)); } - get articleId(): string | null { + get articleId(): ArticleIdentifier { return this.props.articleId; } diff --git a/server/src/contexts/sales/infrastructure/mappers/quoteItem.mapper.ts b/server/src/contexts/sales/infrastructure/mappers/quoteItem.mapper.ts index 0cf0e6a..c8feda8 100644 --- a/server/src/contexts/sales/infrastructure/mappers/quoteItem.mapper.ts +++ b/server/src/contexts/sales/infrastructure/mappers/quoteItem.mapper.ts @@ -1,3 +1,4 @@ +import { ArticleIdentifier } from "@/contexts/catalog/domain"; import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure"; import { Description, MoneyValue, Percentage, Quantity, UniqueID } from "@shared/contexts"; import { IQuoteItemProps, Quote, QuoteItem } from "../../domain"; @@ -22,8 +23,10 @@ class QuoteItemMapper const { sourceParent } = params; const id = this.mapsValue(source, "item_id", UniqueID.create); + const articleId = this.mapsValue(source, "id_article", ArticleIdentifier.create); + const props: IQuoteItemProps = { - articleId: source.id_article === "" ? null : source.id_article, + articleId, description: this.mapsValue(source, "description", Description.create), quantity: this.mapsValue(source, "quantity", (quantity) => Quantity.create({ @@ -83,7 +86,7 @@ class QuoteItemMapper item_id: source.id.toString(), quote_id: sourceParent.id.toPrimitive(), position: index, - id_article: source.articleId, + id_article: source.articleId.toPrimitive(), description: source.description.toPrimitive(), quantity: source.quantity.convertScale(2).toPrimitive(), unit_price: source.unitPrice.convertScale(4).toPrimitive(), diff --git a/server/src/contexts/sales/infrastructure/sequelize/quoteItem.model.ts b/server/src/contexts/sales/infrastructure/sequelize/quoteItem.model.ts index 757144a..7edc154 100644 --- a/server/src/contexts/sales/infrastructure/sequelize/quoteItem.model.ts +++ b/server/src/contexts/sales/infrastructure/sequelize/quoteItem.model.ts @@ -30,7 +30,7 @@ export class QuoteItem_Model extends Model< declare quote_id: string; declare item_id: string; - declare id_article: CreationOptional; + declare id_article: CreationOptional; declare position: number; declare description: CreationOptional; declare quantity: CreationOptional; diff --git a/shared/lib/contexts/common/application/dto/IMoney.dto.ts b/shared/lib/contexts/common/application/dto/IMoney.dto.ts index a54f0e0..5429d88 100644 --- a/shared/lib/contexts/common/application/dto/IMoney.dto.ts +++ b/shared/lib/contexts/common/application/dto/IMoney.dto.ts @@ -2,7 +2,7 @@ import Joi from "joi"; import { Result, RuleValidator } from "../../domain"; export interface IMoney_DTO { - amount: number; + amount: number | null; scale: number; currency_code: string; } diff --git a/shared/lib/contexts/common/application/dto/IPercentage.dto.ts b/shared/lib/contexts/common/application/dto/IPercentage.dto.ts index 79a17f5..a16376f 100644 --- a/shared/lib/contexts/common/application/dto/IPercentage.dto.ts +++ b/shared/lib/contexts/common/application/dto/IPercentage.dto.ts @@ -1,5 +1,5 @@ export interface IPercentage_DTO { - amount: number; + amount: number | null; scale: number; } diff --git a/shared/lib/contexts/common/application/dto/IQuantity.dto.ts b/shared/lib/contexts/common/application/dto/IQuantity.dto.ts index 9ea552e..8bdf1eb 100644 --- a/shared/lib/contexts/common/application/dto/IQuantity.dto.ts +++ b/shared/lib/contexts/common/application/dto/IQuantity.dto.ts @@ -1,8 +1,8 @@ import Joi from "joi"; import { Result, RuleValidator } from "../../domain"; -export interface IQuantity_Request_DTO { - amount: number; +export interface IQuantity_DTO { + amount: number | null; scale: number; } @@ -21,4 +21,5 @@ export function ensureQuantity_DTOIsValid(quantity: IQuantity_Request_DTO) { return Result.ok(true); } -export interface IQuantity_Response_DTO extends IQuantity_Request_DTO {} +export interface IQuantity_Request_DTO extends IQuantity_DTO {} +export interface IQuantity_Response_DTO extends IQuantity_DTO {} diff --git a/shared/lib/contexts/common/domain/entities/Description.ts b/shared/lib/contexts/common/domain/entities/Description.ts index 263cf2d..b5bf720 100644 --- a/shared/lib/contexts/common/domain/entities/Description.ts +++ b/shared/lib/contexts/common/domain/entities/Description.ts @@ -1,29 +1,23 @@ import Joi from "joi"; import { UndefinedOr } from "../../../../utilities"; +import { DomainError, handleDomainError } from "../errors"; import { RuleValidator } from "../RuleValidator"; import { Result } from "./Result"; -import { - IStringValueObjectOptions, - StringValueObject, -} from "./StringValueObject"; +import { IStringValueObjectOptions, StringValueObject } from "./StringValueObject"; export class Description extends StringValueObject { - protected static validate( - value: UndefinedOr, - options: IStringValueObjectOptions - ) { + protected static validate(value: UndefinedOr, options: IStringValueObjectOptions) { const ruleIsEmpty = Joi.string() .optional() + .allow(null) + .allow("") .default("") .label(String(options.label)); return RuleValidator.validate(ruleIsEmpty, value); } - public static create( - value: UndefinedOr, - options: IStringValueObjectOptions = {} - ) { + public static create(value: UndefinedOr, options: IStringValueObjectOptions = {}) { const _options = { label: "description", ...options, @@ -32,7 +26,9 @@ export class Description extends StringValueObject { const validationResult = Description.validate(value, _options); if (validationResult.isFailure) { - return Result.fail(validationResult.error); + return Result.fail( + handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options) + ); } return Result.ok(new Description(validationResult.object)); diff --git a/shared/lib/contexts/common/domain/entities/Measure.ts b/shared/lib/contexts/common/domain/entities/Measure.ts index df34d00..bdff1e5 100644 --- a/shared/lib/contexts/common/domain/entities/Measure.ts +++ b/shared/lib/contexts/common/domain/entities/Measure.ts @@ -1,27 +1,17 @@ import { UndefinedOr } from "../../../../utilities"; +import { DomainError, handleDomainError } from "../errors"; import { RuleValidator } from "../RuleValidator"; import { Result } from "./Result"; -import { - IStringValueObjectOptions, - StringValueObject, -} from "./StringValueObject"; +import { IStringValueObjectOptions, StringValueObject } from "./StringValueObject"; export class Measure extends StringValueObject { - protected static validate( - value: UndefinedOr, - options: IStringValueObjectOptions, - ) { - const ruleIsEmpty = RuleValidator.RULE_ALLOW_EMPTY.default("").label( - String(options.label), - ); + protected static validate(value: UndefinedOr, options: IStringValueObjectOptions) { + const ruleIsEmpty = RuleValidator.RULE_ALLOW_EMPTY.default("").label(String(options.label)); return RuleValidator.validate(ruleIsEmpty, value); } - public static create( - value: UndefinedOr, - options: IStringValueObjectOptions = {}, - ) { + public static create(value: UndefinedOr, options: IStringValueObjectOptions = {}) { const _options = { label: "description", ...options, @@ -30,7 +20,9 @@ export class Measure extends StringValueObject { const validationResult = Measure.validate(value, _options); if (validationResult.isFailure) { - return Result.fail(validationResult.error); + return Result.fail( + handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options) + ); } return Result.ok(new Measure(validationResult.object)); diff --git a/shared/lib/contexts/common/domain/entities/MoneyValue.ts b/shared/lib/contexts/common/domain/entities/MoneyValue.ts index 008842d..d032e2a 100644 --- a/shared/lib/contexts/common/domain/entities/MoneyValue.ts +++ b/shared/lib/contexts/common/domain/entities/MoneyValue.ts @@ -4,6 +4,7 @@ import DineroFactory, { Currency, Dinero } from "dinero.js"; import Joi from "joi"; import { isNull } from "lodash"; import { NullOr, UndefinedOr } from "../../../../utilities"; +import { DomainError, handleDomainError } from "../errors"; import { RuleValidator } from "../RuleValidator"; import { CurrencyData } from "./CurrencyData"; import { Result } from "./Result"; @@ -144,7 +145,9 @@ export class MoneyValue extends ValueObject implements IMoneyValue { const validationResult = MoneyValue.validate(amount, options); if (validationResult.isFailure) { - return Result.fail(validationResult.error); + return Result.fail( + handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, options) + ); } const _amount: NullOr = MoneyValue.sanitize(validationResult.object); diff --git a/shared/lib/contexts/common/domain/entities/Percentage.ts b/shared/lib/contexts/common/domain/entities/Percentage.ts index 80d4dd1..e5500bd 100644 --- a/shared/lib/contexts/common/domain/entities/Percentage.ts +++ b/shared/lib/contexts/common/domain/entities/Percentage.ts @@ -218,7 +218,7 @@ export class Percentage extends NullableValueObject { } get scale(): number { - return this.isNull() ? 0 : Number(this.props?.scale); + return Number(this.props?.scale); } public getAmount(): NullOr { diff --git a/shared/lib/contexts/common/domain/entities/Quantity.ts b/shared/lib/contexts/common/domain/entities/Quantity.ts index de6cdd5..ed52e64 100644 --- a/shared/lib/contexts/common/domain/entities/Quantity.ts +++ b/shared/lib/contexts/common/domain/entities/Quantity.ts @@ -1,5 +1,6 @@ import Joi from "joi"; import { NullOr } from "../../../../utilities"; +import { DomainError, handleDomainError } from "../errors"; import { RuleValidator } from "../RuleValidator"; import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject"; import { Result } from "./Result"; @@ -98,7 +99,9 @@ export class Quantity extends NullableValueObject { const validationResult = Quantity.validate(amount, scale, _options); if (validationResult.isFailure) { - return Result.fail(validationResult.error); + return Result.fail( + handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options) + ); } let _amount: NullOr = Quantity._sanitize(amount); diff --git a/shared/lib/contexts/common/domain/entities/QueryCriteria/Field/FieldCriteria.ts b/shared/lib/contexts/common/domain/entities/QueryCriteria/Field/FieldCriteria.ts index e032b17..adcd240 100644 --- a/shared/lib/contexts/common/domain/entities/QueryCriteria/Field/FieldCriteria.ts +++ b/shared/lib/contexts/common/domain/entities/QueryCriteria/Field/FieldCriteria.ts @@ -22,14 +22,8 @@ export class FieldCriteria extends StringValueObject implements IFieldCriteria { } protected static validate(value: UndefinedOr) { - if ( - RuleValidator.validate(RuleValidator.RULE_NOT_NULL_OR_UNDEFINED, value) - .isSuccess - ) { - const stringOrError = RuleValidator.validate( - RuleValidator.RULE_IS_TYPE_STRING, - value - ); + if (RuleValidator.validate(RuleValidator.RULE_NOT_NULL_OR_UNDEFINED, value).isSuccess) { + const stringOrError = RuleValidator.validate(RuleValidator.RULE_IS_TYPE_STRING, value); if (stringOrError.isFailure) { return stringOrError; diff --git a/shared/lib/contexts/common/domain/entities/Slug.ts b/shared/lib/contexts/common/domain/entities/Slug.ts index 7f0c96f..ad0525b 100644 --- a/shared/lib/contexts/common/domain/entities/Slug.ts +++ b/shared/lib/contexts/common/domain/entities/Slug.ts @@ -1,21 +1,16 @@ import Joi from "joi"; import { UndefinedOr } from "../../../../utilities"; +import { DomainError, handleDomainError } from "../errors"; import { RuleValidator } from "../RuleValidator"; import { Result } from "./Result"; -import { - IStringValueObjectOptions, - StringValueObject, -} from "./StringValueObject"; +import { IStringValueObjectOptions, StringValueObject } from "./StringValueObject"; export class Slug extends StringValueObject { protected static readonly MIN_LENGTH = 2; protected static readonly MAX_LENGTH = 100; - protected static validate( - value: UndefinedOr, - options: IStringValueObjectOptions - ) { + protected static validate(value: UndefinedOr, options: IStringValueObjectOptions) { const rule = Joi.string() .allow(null) .allow("") @@ -66,10 +61,7 @@ export class Slug extends StringValueObject { return slug ? slug.trim() : ""; } - public static create( - value: UndefinedOr, - options: IStringValueObjectOptions = {} - ) { + public static create(value: UndefinedOr, options: IStringValueObjectOptions = {}) { const _options = { label: "slug", ...options, @@ -78,7 +70,9 @@ export class Slug extends StringValueObject { const validationResult = Slug.validate(value, _options); if (validationResult.isFailure) { - return Result.fail(validationResult.error); + return Result.fail( + handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options) + ); } const slugValue = Slug.sanitize(validationResult.object); diff --git a/shared/lib/contexts/sales/application/dto/Quote/GetQuote.dto/IGetQuote_Response.dto.ts b/shared/lib/contexts/sales/application/dto/Quote/GetQuote.dto/IGetQuote_Response.dto.ts index 17e50de..963821e 100644 --- a/shared/lib/contexts/sales/application/dto/Quote/GetQuote.dto/IGetQuote_Response.dto.ts +++ b/shared/lib/contexts/sales/application/dto/Quote/GetQuote.dto/IGetQuote_Response.dto.ts @@ -1,8 +1,4 @@ -import { - IMoney_Response_DTO, - IPercentage_Response_DTO, - IQuantity_Response_DTO, -} from "../../../../../common"; +import { IMoney_DTO, IPercentage_DTO, IQuantity_DTO } from "../../../../../common"; export interface IGetQuote_Response_DTO { id: string; @@ -17,9 +13,9 @@ export interface IGetQuote_Response_DTO { notes: string; validity: string; - subtotal_price: IMoney_Response_DTO; - discount: IPercentage_Response_DTO; - total_price: IMoney_Response_DTO; + subtotal_price: IMoney_DTO; + discount: IPercentage_DTO; + total_price: IMoney_DTO; items: IGetQuote_QuoteItem_Response_DTO[]; @@ -28,10 +24,10 @@ export interface IGetQuote_Response_DTO { export interface IGetQuote_QuoteItem_Response_DTO { article_id: string; - quantity: IQuantity_Response_DTO; + quantity: IQuantity_DTO; description: string; - unit_price: IMoney_Response_DTO; - subtotal_price: IMoney_Response_DTO; - discount: IPercentage_Response_DTO; - total_price: IMoney_Response_DTO; + unit_price: IMoney_DTO; + subtotal_price: IMoney_DTO; + discount: IPercentage_DTO; + total_price: IMoney_DTO; } diff --git a/shared/lib/contexts/sales/application/dto/Quote/UpdateQuote.dto/IUpdateQuote_Request.dto.ts b/shared/lib/contexts/sales/application/dto/Quote/UpdateQuote.dto/IUpdateQuote_Request.dto.ts index b2e727e..c8928ac 100644 --- a/shared/lib/contexts/sales/application/dto/Quote/UpdateQuote.dto/IUpdateQuote_Request.dto.ts +++ b/shared/lib/contexts/sales/application/dto/Quote/UpdateQuote.dto/IUpdateQuote_Request.dto.ts @@ -50,49 +50,49 @@ export function ensureUpdateQuote_Request_DTOIsValid(quoteDTO: IUpdateQuote_Requ validity: Joi.string().optional().allow(null).allow("").default(""), subtotal_price: Joi.object({ - amount: Joi.number(), + amount: Joi.number().allow(null), scale: Joi.number(), currency_code: Joi.string(), - }).unknown(), + }).optional(), discount: Joi.object({ - amount: Joi.number(), + amount: Joi.number().allow(null), scale: Joi.number(), - }).unknown(), + }).optional(), total_price: Joi.object({ - amount: Joi.number(), + amount: Joi.number().allow(null), scale: Joi.number(), currency_code: Joi.string(), - }).unknown(), + }).optional(), items: Joi.array().items( Joi.object({ - article_id: Joi.string().optional().allow(null).allow("").default(""), + article_id: Joi.number().optional().allow(null).allow("").default(""), quantity: Joi.object({ - amount: Joi.number(), + amount: Joi.number().allow(null), scale: Joi.number(), - }).unknown(), - description: Joi.string(), + }).optional(), + description: Joi.string().optional().allow(null).allow("").default(""), unit_price: Joi.object({ - amount: Joi.number(), + amount: Joi.number().allow(null), scale: Joi.number(), currency_code: Joi.string(), - }).unknown(), + }).optional(), subtotal_price: Joi.object({ - amount: Joi.number(), + amount: Joi.number().allow(null), scale: Joi.number(), currency_code: Joi.string(), - }).unknown(), + }).optional(), discount: Joi.object({ - amount: Joi.number(), + amount: Joi.number().allow(null), scale: Joi.number(), - }).unknown(), + }).optional(), total_price: Joi.object({ - amount: Joi.number(), + amount: Joi.number().allow(null), scale: Joi.number(), currency_code: Joi.string(), - }).unknown(), + }).optional(), }).unknown(true) ), }).unknown(true);