This commit is contained in:
David Arranz 2024-07-17 20:10:07 +02:00
parent a845edb9c7
commit 47802ab932
31 changed files with 259 additions and 227 deletions

View File

@ -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",

View File

@ -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<TData extends RowData> {
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<unknown, unknown>[];
data: Record<"id", string>[];
defaultValues: Readonly<{ [x: string]: any }> | undefined;
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields">;
};
@ -94,34 +95,12 @@ const dropAnimationConfig: DropAnimation = {
},
};
/*const defaultColumn: Partial<ColumnDef<unknown>> = {
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 (
<input
value={value as string}
onChange={(e) => 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<RowSelectionState>({});
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
@ -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}
>
<SortableDataTableToolbar table={table} />
<QuoteItemsSortableDataTableToolbar table={table} />
<Table className='table-fixed'>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@ -346,13 +325,13 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
strategy={verticalListSortingStrategy}
>
{filterItems(table.getRowModel().rows).map((row) => (
<SortableTableRow key={row.id} id={row.id}>
<QuoteItemsSortableTableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className='px-2 py-1 align-top'>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</SortableTableRow>
</QuoteItemsSortableTableRow>
))}
</SortableContext>
</TableBody>

View File

@ -27,7 +27,7 @@ import {
Trash2Icon,
} from "lucide-react";
export const SortableDataTableToolbar = ({ table }: { table: Table<unknown> }) => {
export const QuoteItemsSortableDataTableToolbar = ({ table }: { table: Table<unknown> }) => {
const selectedRowsCount = table.getSelectedRowModel().rows.length;
if (selectedRowsCount) {

View File

@ -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<string, any>;
listeners: DraggableSyntheticListeners;
ref(node: HTMLElement | null): void;
}
export const SortableTableRowContext = createContext<Context>({
export const QuoteItemsSortableTableRowContext = createContext<Context>({
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<SortableProps>) {
export function QuoteItemsSortableTableRow({
id,
children,
}: PropsWithChildren<QuoteItemsSortableProps>) {
const {
attributes,
isDragging,
@ -54,7 +57,7 @@ export function SortableTableRow({ id, children }: PropsWithChildren<SortablePro
);
return (
<SortableTableRowContext.Provider value={context}>
<QuoteItemsSortableTableRowContext.Provider value={context}>
<TableRow
key={id}
id={String(id)}
@ -67,6 +70,6 @@ export function SortableTableRow({ id, children }: PropsWithChildren<SortablePro
>
{children}
</TableRow>
</SortableTableRowContext.Provider>
</QuoteItemsSortableTableRowContext.Provider>
);
}

View File

@ -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";

View File

@ -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 <SortableDataTable actions={fieldActions} columns={columns} data={fields} />;
return (
<QuoteItemsSortableDataTable
actions={fieldActions}
columns={columns}
data={fields}
defaultValues={defaultValues}
/>
);
return (
<ResizablePanelGroup
@ -226,7 +235,7 @@ export const QuoteDetailsCardEditor = ({
}}
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
>
<SortableDataTable actions={fieldActions} columns={columns} data={fields} />
<QuoteItemsSortableDataTable actions={fieldActions} columns={columns} data={fields} />
</ResizablePanel>
<ResizableHandle withHandle className='mx-3' />
<ResizablePanel defaultSize={defaultLayout[1]} minSize={10}>

View File

@ -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 (
<div className='grid gap-6 md:grid-cols-6'>
<FormGroup

View File

@ -48,7 +48,7 @@ export const QuoteCreate = () => {
setWarnWhen(false);
mutate(formData, {
onSuccess: (data) => {
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
navigate(`/quotes/edit/${data.id}`, { relative: "path", replace: true });
},
});
} finally {

View File

@ -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<IGetQuote_Response_DTO, "items"> & {
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<QuoteDataForm>({
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<QuoteDataForm>({
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 = () => {
<QuoteGeneralCardEditor />
</TabsContent>
<TabsContent value='items'>
<QuoteDetailsCardEditor currency={quoteCurrency} language={quoteLanguage} />
<QuoteDetailsCardEditor
currency={quoteCurrency}
language={quoteLanguage}
defaultValues={defaultValues}
/>
</TabsContent>
<TabsContent value='history'></TabsContent>

View File

@ -110,6 +110,9 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
)}
<FormControl>
<CurrencyInput
intlConfig={{
locale: language.code,
}}
name={field.name}
//ref={field.ref} <-- no activar que hace cosas raras
onBlur={field.onBlur}

View File

@ -48,7 +48,7 @@ import { SortableTableRow } from "./SortableTableRow";
declare module "@tanstack/react-table" {
interface TableMeta<TData extends RowData> {
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;

View File

@ -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";

View File

@ -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) {

View File

@ -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<number> {
protected static validate(
value: NullOr<number | string>,
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<NullOr<number>>(rules, value);
}
public static create(
value: NullOr<number | string>,
options: IArticleIdentifierOptions = {},
) {
public static create(value: NullOr<number | string>, options: IArticleIdentifierOptions = {}) {
const _options = {
label: "ArticleIdentifier",
...options,
@ -64,8 +60,8 @@ export class ArticleIdentifier extends NullableValueObject<number> {
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) {

View File

@ -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,

View File

@ -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<QuoteItem>(
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<QuoteItem>;
try {
items = new Collection<QuoteItem>(
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(
{

View File

@ -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<IQuoteItemProps> implements IQuoteItem {
return Result.ok(new QuoteItem(props, id));
}
get articleId(): string | null {
get articleId(): ArticleIdentifier {
return this.props.articleId;
}

View File

@ -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(),

View File

@ -30,7 +30,7 @@ export class QuoteItem_Model extends Model<
declare quote_id: string;
declare item_id: string;
declare id_article: CreationOptional<string | null>;
declare id_article: CreationOptional<number | null>;
declare position: number;
declare description: CreationOptional<string | null>;
declare quantity: CreationOptional<number | null>;

View File

@ -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;
}

View File

@ -1,5 +1,5 @@
export interface IPercentage_DTO {
amount: number;
amount: number | null;
scale: number;
}

View File

@ -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 {}

View File

@ -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<string>,
options: IStringValueObjectOptions
) {
protected static validate(value: UndefinedOr<string>, options: IStringValueObjectOptions) {
const ruleIsEmpty = Joi.string()
.optional()
.allow(null)
.allow("")
.default("")
.label(String(options.label));
return RuleValidator.validate<string>(ruleIsEmpty, value);
}
public static create(
value: UndefinedOr<string>,
options: IStringValueObjectOptions = {}
) {
public static create(value: UndefinedOr<string>, 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));

View File

@ -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<string>,
options: IStringValueObjectOptions,
) {
const ruleIsEmpty = RuleValidator.RULE_ALLOW_EMPTY.default("").label(
String(options.label),
);
protected static validate(value: UndefinedOr<string>, options: IStringValueObjectOptions) {
const ruleIsEmpty = RuleValidator.RULE_ALLOW_EMPTY.default("").label(String(options.label));
return RuleValidator.validate<string>(ruleIsEmpty, value);
}
public static create(
value: UndefinedOr<string>,
options: IStringValueObjectOptions = {},
) {
public static create(value: UndefinedOr<string>, 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));

View File

@ -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<Dinero> 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<number> = MoneyValue.sanitize(validationResult.object);

View File

@ -218,7 +218,7 @@ export class Percentage extends NullableValueObject<IPercentage> {
}
get scale(): number {
return this.isNull() ? 0 : Number(this.props?.scale);
return Number(this.props?.scale);
}
public getAmount(): NullOr<number> {

View File

@ -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<IQuantity> {
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<number> = Quantity._sanitize(amount);

View File

@ -22,14 +22,8 @@ export class FieldCriteria extends StringValueObject implements IFieldCriteria {
}
protected static validate(value: UndefinedOr<string>) {
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;

View File

@ -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<string>,
options: IStringValueObjectOptions
) {
protected static validate(value: UndefinedOr<string>, 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<string>,
options: IStringValueObjectOptions = {}
) {
public static create(value: UndefinedOr<string>, 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);

View File

@ -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;
}

View File

@ -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);