This commit is contained in:
David Arranz 2026-07-14 20:39:31 +02:00
parent 47dffacf03
commit 0129c5963c
59 changed files with 864 additions and 425 deletions

View File

@ -0,0 +1 @@
export * from "../../../../../modules/core/src/web/lib/data-source/errors";

View File

@ -26,6 +26,10 @@
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
"allowUnreachableCode": true "allowUnreachableCode": true
}, },
"include": ["src"], "include": [
"src",
"../../modules/core/src/web/lib/data-source/errors",
"../../modules/core/src/web/lib/data-source/axios/normalize-axios-error.ts"
],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@ -24,7 +24,6 @@ export class CreatePaymentMethodUseCase {
public execute(params: CreatePaymentMethodUseCaseInput) { public execute(params: CreatePaymentMethodUseCaseInput) {
const { dto, companyId } = params; const { dto, companyId } = params;
console.log("Executing CreatePaymentMethodUseCase with params:", params);
const mappedPropsResult = this.deps.dtoMapper.map(dto, { companyId }); const mappedPropsResult = this.deps.dtoMapper.map(dto, { companyId });
if (mappedPropsResult.isFailure) { if (mappedPropsResult.isFailure) {

View File

@ -10,7 +10,7 @@ import type {
export class TaxDefinitionPublicModelMapper { export class TaxDefinitionPublicModelMapper {
public toPublicModel(taxDefinition: TaxDefinition): TaxDefinitionPublicModel { public toPublicModel(taxDefinition: TaxDefinition): TaxDefinitionPublicModel {
console.log(taxDefinition.description)
return { return {
id: taxDefinition.id, id: taxDefinition.id,
companyId: Maybe.some(taxDefinition.companyId), companyId: Maybe.some(taxDefinition.companyId),

View File

@ -80,8 +80,6 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
const allowedSurchargeCodes = extractOrPushError( const allowedSurchargeCodes = extractOrPushError(
maybeFromNullableResult(raw.allowed_surcharge_codes, (v) => { maybeFromNullableResult(raw.allowed_surcharge_codes, (v) => {
console.log(v);
const values = v.split(";"); const values = v.split(";");
if (!Array.isArray(values)) if (!Array.isArray(values))
return Result.fail(new Error("Invalid allowed_surcharge_codes")); return Result.fail(new Error("Invalid allowed_surcharge_codes"));

View File

@ -59,6 +59,7 @@ export abstract class ExpressController {
await this.executeImpl(); await this.executeImpl();
} catch (error: unknown) { } catch (error: unknown) {
console.error("Error in ExpressController.execute:", error);
this.handleError(error as Error); this.handleError(error as Error);
} }
} }

View File

@ -0,0 +1,74 @@
// packages/rdx-ui/src/components/error-state.tsx
import { Button } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { AlertCircle, RefreshCw } from "lucide-react";
import type React from "react";
export interface ErrorStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
title: React.ReactNode;
description?: React.ReactNode;
reference?: string;
retryLabel?: string;
isRetrying?: boolean;
onRetry?: () => void;
}
export const ErrorState = ({
title,
description,
reference,
retryLabel = "Reintentar",
isRetrying = false,
onRetry,
className,
...props
}: ErrorStateProps) => {
return (
<div
aria-live="polite"
className={cn(
"flex min-h-64 flex-col items-center justify-center rounded-lg border border-destructive/30 bg-destructive/5 px-6 py-10 text-center",
className
)}
role="alert"
{...props}
>
<div
aria-hidden="true"
className="mb-4 flex size-11 items-center justify-center rounded-full bg-destructive/10 text-destructive"
>
<AlertCircle className="size-5" />
</div>
<h2 className="text-base font-semibold text-foreground">{title}</h2>
{description ? (
<div className="mt-2 max-w-xl text-sm text-muted-foreground">{description}</div>
) : null}
{reference ? (
<p className="mt-3 text-xs text-muted-foreground">
Referencia:{" "}
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.75rem] text-foreground">
{reference}
</code>
</p>
) : null}
{onRetry ? (
<Button
className="mt-5"
disabled={isRetrying}
onClick={onRetry}
type="button"
variant="outline"
>
<RefreshCw aria-hidden="true" className={cn("size-4", isRetrying && "animate-spin")} />
<span aria-live="polite">{isRetrying ? "Reintentando…" : retryLabel}</span>
</Button>
) : null}
</div>
);
};

View File

@ -1,4 +1,5 @@
export * from "./error-alert"; export * from "./error-alert";
export * from "./error-state";
export * from "./form"; export * from "./form";
export * from "./not-found-card"; export * from "./not-found-card";
export * from "./page-header"; export * from "./page-header";

View File

@ -3,13 +3,23 @@ import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import type { ICustomParams, IDataSource } from "../datasource.interface"; import type { ICustomParams, IDataSource } from "../datasource.interface";
import { defaultAxiosRequestConfig } from "./create-axios-instance"; import { defaultAxiosRequestConfig } from "./create-axios-instance";
import { normalizeAxiosError } from "./normalize-axios-error";
const executeRequest = async <R>(request: () => Promise<AxiosResponse<R>>): Promise<R> => {
try {
const response = await request();
return response.data;
} catch (error) {
throw normalizeAxiosError(error);
}
};
export const createAxiosDataSource = (client: AxiosInstance): IDataSource => { export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
if (!client) { if (!client) {
throw new Error("[Axios] Se esperaba una instancia de Axios."); throw new Error("[Axios] Se esperaba una instancia de Axios.");
} }
if (!client.getUri || typeof client.getUri !== "function") { if (typeof client.getUri !== "function") {
throw new Error("[Axios] La instancia proporcionada no es una instancia de Axios válida."); throw new Error("[Axios] La instancia proporcionada no es una instancia de Axios válida.");
} }
@ -21,117 +31,83 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
getBaseUrl: () => client.getUri(), getBaseUrl: () => client.getUri(),
getList: async <R>(resource: string, params?: Record<string, unknown>): Promise<R> => { getList: async <R>(resource: string, params?: Record<string, unknown>): Promise<R> => {
const { signal, ...rest } = (params ?? {}) as Record<string, unknown> & { const { signal, ...queryParams } = (params ?? {}) as Record<string, unknown> & {
signal?: AbortSignal; signal?: AbortSignal;
}; };
const res = await client.get<R>(resource, { return executeRequest(() =>
signal, client.get<R>(resource, {
params: rest, signal,
}); params: queryParams,
})
return res.data; );
}, },
getOne: async <R>( getOne: async <R>(
resource: string, resource: string,
id: string | number, id: string | number,
params?: Record<string, unknown> params?: Record<string, unknown>
): Promise<R> => { ): Promise<R> =>
const res = await client.get<R>(`${resource}/${id}`, params); executeRequest(() => client.get<R>(`${resource}/${id}`, params as AxiosRequestConfig)),
return res.data;
},
getMany: async <R>(resource: string, ids: Array<string | number>): Promise<R> => { getMany: async <R>(resource: string, ids: Array<string | number>): Promise<R> =>
const res = await client.get<R>(resource, { executeRequest(() =>
params: { ids }, client.get<R>(resource, {
}); params: { ids },
})
return res.data; ),
},
createOne: async <TData, R>( createOne: async <TData, R>(
resource: string, resource: string,
data: TData, data: TData,
params?: Record<string, unknown> params?: Record<string, unknown>
): Promise<R> => { ): Promise<R> =>
const res = await client.post<R, AxiosResponse<R>, TData>( executeRequest(() =>
resource, client.post<R, AxiosResponse<R>, TData>(resource, data, params as AxiosRequestConfig<TData>)
data, ),
params as AxiosRequestConfig<TData>
);
return res.data;
},
updateOne: async <TData, R>( updateOne: async <TData, R>(
resource: string, resource: string,
id: string | number, id: string | number,
data: TData, data: TData,
params?: Record<string, unknown> params?: Record<string, unknown>
): Promise<R> => { ): Promise<R> =>
const url = `${resource}/${id}`; executeRequest(() =>
client.put<R, AxiosResponse<R>, TData>(
const res = await client.put<R, AxiosResponse<R>, TData>( `${resource}/${id}`,
url, data,
data, params as AxiosRequestConfig<TData>
params as AxiosRequestConfig<TData> )
); ),
return res.data;
},
deleteOne: async <R = void>( deleteOne: async <R = void>(
resource: string, resource: string,
id: string | number, id: string | number,
params?: Record<string, unknown> params?: Record<string, unknown>
): Promise<R> => { ): Promise<R> =>
const res = await client.delete<R>(`${resource}/${id}`, params); executeRequest(() => client.delete<R>(`${resource}/${id}`, params as AxiosRequestConfig)),
return res.data;
},
custom: async <TData, R>(customParams: ICustomParams<TData>): Promise<R> => { custom: async <TData, R>(customParams: ICustomParams<TData>): Promise<R> => {
const { url, path, method, responseType, headers, signal, data, ...payload } = customParams; const { url, path, method, responseType, headers, signal, data, ...payload } = customParams;
const requestUrl = path ? `${client.getUri()}/${path}` : url; const requestUrl = path ? `${client.getUri()}/${path}` : url;
if (!requestUrl) throw new Error('"url" or "path" param is missing');
if (!requestUrl) {
throw new Error('"url" or "path" param is missing');
}
const config: AxiosRequestConfig<TData> = { const config: AxiosRequestConfig<TData> = {
url: requestUrl, url: requestUrl,
method, method,
responseType, responseType,
signal, signal,
...payload,
...defaultAxiosRequestConfig, ...defaultAxiosRequestConfig,
...payload,
headers, headers,
data,
}; };
let customResponse: AxiosResponse<R, TData>; return executeRequest(() => client.request<R, AxiosResponse<R>, TData>(config));
switch (method) {
case "put":
case "post":
case "patch":
customResponse = await client.request<R, AxiosResponse<R>, TData>({
...config,
data,
});
break;
case "delete":
customResponse = await client.delete<R, AxiosResponse<R>, TData>(requestUrl, {
...config,
});
break;
default:
customResponse = await client.get<R, AxiosResponse<R>, TData>(requestUrl, {
...config,
});
break;
}
return customResponse.data;
}, },
}; };
}; };

View File

@ -0,0 +1,88 @@
// modules/core/src/web/lib/data-source/axios/normalize-axios-error.ts
import axios, { AxiosError } from "axios";
import {
ApiClientError,
type ApiClientErrorKind,
isApiProblemDetails,
} from "../../../../../../../apps/web/src/lib/data-source";
const getErrorKind = (status: number): ApiClientErrorKind => {
switch (status) {
case 401:
return "unauthorized";
case 403:
return "forbidden";
case 404:
return "not-found";
case 409:
return "conflict";
case 422:
return "validation";
case 503:
return "unavailable";
default:
return status >= 500 ? "server" : "unknown";
}
};
const normalizeProblemError = (error: AxiosError, data: unknown): ApiClientError => {
if (!isApiProblemDetails(data)) {
return new ApiClientError({
kind: error.response?.status && error.response.status >= 500 ? "server" : "unknown",
status: error.response?.status,
message: "The server returned an unexpected error response.",
cause: error,
});
}
return new ApiClientError({
kind: getErrorKind(data.status),
status: data.status,
title: data.title,
detail: data.detail,
code: data.code,
source: data.source,
instance: data.instance,
correlationId: data.correlationId,
issues: data.errors,
problem: data,
message: data.detail || data.title,
cause: error,
});
};
export const normalizeAxiosError = (error: unknown): Error => {
if (!axios.isAxiosError(error)) {
return error instanceof Error
? error
: new ApiClientError({
kind: "unknown",
message: "An unexpected error occurred.",
cause: error,
});
}
if (error.code === AxiosError.ERR_CANCELED) {
return error;
}
if (error.code === AxiosError.ECONNABORTED) {
return new ApiClientError({
kind: "timeout",
message: "The request timed out.",
cause: error,
});
}
if (!error.response) {
return new ApiClientError({
kind: "network",
message: "The server could not be reached.",
cause: error,
});
}
return normalizeProblemError(error, error.response.data);
};

View File

@ -0,0 +1,65 @@
// modules/core/src/web/lib/data-source/errors/api-client-error.ts
import type { ApiProblemDetails, ApiProblemIssue, ApiProblemSource } from "./api-problem-details";
export type ApiClientErrorKind =
| "unauthorized"
| "forbidden"
| "not-found"
| "conflict"
| "validation"
| "unavailable"
| "server"
| "network"
| "timeout"
| "unknown";
export interface ApiClientErrorOptions {
kind: ApiClientErrorKind;
message: string;
status?: number;
title?: string;
detail?: string;
code?: string;
source?: ApiProblemSource;
instance?: string;
correlationId?: string;
issues?: ApiProblemIssue[];
problem?: ApiProblemDetails;
cause?: unknown;
}
export class ApiClientError extends Error {
readonly kind: ApiClientErrorKind;
readonly status?: number;
readonly title?: string;
readonly detail?: string;
readonly code?: string;
readonly source?: ApiProblemSource;
readonly instance?: string;
readonly correlationId?: string;
readonly issues: readonly ApiProblemIssue[];
readonly problem?: ApiProblemDetails;
constructor(options: ApiClientErrorOptions) {
super(options.message, { cause: options.cause });
this.name = "ApiClientError";
this.kind = options.kind;
this.status = options.status;
this.title = options.title;
this.detail = options.detail;
this.code = options.code;
this.source = options.source;
this.instance = options.instance;
this.correlationId = options.correlationId;
this.issues = options.issues ?? [];
this.problem = options.problem;
Object.setPrototypeOf(this, ApiClientError.prototype);
}
}
export const isApiClientError = (error: unknown): error is ApiClientError =>
error instanceof ApiClientError;

View File

@ -0,0 +1,26 @@
export type ApiProblemPathSegment = string | number;
export interface ApiProblemIssue {
path?: ApiProblemPathSegment[];
code?: string;
message: string;
origin?: string;
format?: string;
}
export type ApiProblemSource = "request" | "response" | "domain" | "infrastructure";
export interface ApiProblemDetails {
type: string;
title: string;
status: number;
detail: string;
instance?: string;
method?: string;
correlationId?: string;
code?: string;
source?: ApiProblemSource;
errors?: ApiProblemIssue[];
}

View File

@ -0,0 +1,92 @@
import { isApiClientError } from "./api-client-error";
export interface UserErrorMessage {
title: string;
description: string;
reference?: string;
}
export const getUserErrorMessage = (error: unknown): UserErrorMessage => {
if (!isApiClientError(error)) {
return {
title: "No se ha podido completar la operación",
description: "Se ha producido un error inesperado.",
};
}
const reference = error.correlationId;
switch (error.kind) {
case "unauthorized":
return {
title: "La sesión no es válida",
description: "Vuelve a iniciar sesión para continuar.",
reference,
};
case "forbidden":
return {
title: "Acceso no permitido",
description: "No tienes permisos para realizar esta operación.",
reference,
};
case "not-found":
return {
title: "Recurso no encontrado",
description: "El recurso solicitado ya no existe o no está disponible.",
reference,
};
case "conflict":
return {
title: "No se puede completar la operación",
description: error.detail || "El estado actual del recurso impide realizarla.",
reference,
};
case "validation":
if (error.source === "request") {
return {
title: "Revisa los datos introducidos",
description: "Algunos datos enviados no son válidos.",
reference,
};
}
return {
title: "No se han podido procesar los datos",
description:
"El servidor ha devuelto información inconsistente. Inténtalo de nuevo o comunica la incidencia.",
reference,
};
case "network":
return {
title: "Sin conexión con el servidor",
description: "Comprueba la conexión de red e inténtalo de nuevo.",
};
case "timeout":
return {
title: "La operación está tardando demasiado",
description: "El servidor no ha respondido dentro del tiempo esperado.",
};
case "unavailable":
return {
title: "Servicio temporalmente no disponible",
description: "Inténtalo de nuevo dentro de unos instantes.",
reference,
};
case "server":
case "unknown":
default:
return {
title: "No se ha podido completar la operación",
description: "Se ha producido un error interno. Inténtalo de nuevo.",
reference,
};
}
};

View File

@ -0,0 +1,4 @@
export * from "./api-client-error";
export * from "./api-problem-details";
export * from "./get-user-error-message";
export * from "./is-api-problem-details";

View File

@ -0,0 +1,47 @@
import type {
ApiProblemDetails,
ApiProblemIssue,
ApiProblemPathSegment,
} from "./api-problem-details";
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;
const isPathSegment = (value: unknown): value is ApiProblemPathSegment =>
typeof value === "string" || typeof value === "number";
const isApiProblemIssue = (value: unknown): value is ApiProblemIssue => {
if (!isRecord(value) || typeof value.message !== "string") {
return false;
}
if (value.path !== undefined && !(Array.isArray(value.path) && value.path.every(isPathSegment))) {
return false;
}
return true;
};
export const isApiProblemDetails = (value: unknown): value is ApiProblemDetails => {
if (!isRecord(value)) {
return false;
}
if (
typeof value.type !== "string" ||
typeof value.title !== "string" ||
typeof value.status !== "number" ||
typeof value.detail !== "string"
) {
return false;
}
if (
value.errors !== undefined &&
!(Array.isArray(value.errors) && value.errors.every(isApiProblemIssue))
) {
return false;
}
return true;
};

View File

@ -1,3 +1,4 @@
export * from "./axios"; export * from "./axios";
export * from "./build-text-filters"; export * from "./build-text-filters";
export * from "./datasource.interface"; export * from "./datasource.interface";
export * from "./errors";

View File

@ -4,9 +4,9 @@ import type { Maybe } from "@repo/rdx-utils";
import type { import type {
InvoiceAmount, InvoiceAmount,
InvoiceNumber, InvoiceNumber,
InvoiceRecipient,
InvoiceSerie, InvoiceSerie,
InvoiceStatus, InvoiceStatus,
IssuedInvoiceRecipient,
VerifactuRecord, VerifactuRecord,
} from "../../../domain"; } from "../../../domain";
@ -14,7 +14,6 @@ export type IssuedInvoiceSummary = {
id: UniqueID; id: UniqueID;
companyId: UniqueID; companyId: UniqueID;
isProforma: boolean;
invoiceNumber: InvoiceNumber; invoiceNumber: InvoiceNumber;
status: InvoiceStatus; status: InvoiceStatus;
series: Maybe<InvoiceSerie>; series: Maybe<InvoiceSerie>;
@ -26,7 +25,7 @@ export type IssuedInvoiceSummary = {
description: Maybe<string>; description: Maybe<string>;
customerId: UniqueID; customerId: UniqueID;
recipient: InvoiceRecipient; recipient: IssuedInvoiceRecipient;
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;

View File

@ -31,16 +31,15 @@ export class IssuedInvoiceSummarySnapshotBuilder implements IIssuedInvoiceSummar
return { return {
id: invoice.id.toString(), id: invoice.id.toString(),
company_id: invoice.companyId.toString(), company_id: invoice.companyId.toString(),
is_proforma: invoice.isProforma,
invoice_number: invoice.invoiceNumber.toString(), invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(), status: invoice.status.toPrimitive(),
series: maybeToEmptyString(invoice.series, (v) => v.toString()), series: maybeToEmptyString(invoice.series, (v) => v.toString()),
invoice_date: invoice.invoiceDate.toDateString(), invoice_date: invoice.invoiceDate.toDateString(),
operation_date: maybeToEmptyString(invoice.operationDate, (v) => v.toDateString()), operation_date: maybeToNullable(invoice.operationDate, (v) => v.toDateString()),
reference: maybeToEmptyString(invoice.reference, (v) => v.toString()), reference: maybeToNullable(invoice.reference, (v) => v.toString()),
description: maybeToEmptyString(invoice.description, (v) => v.toString()), description: maybeToNullable(invoice.description, (v) => v.toString()),
customer_id: invoice.customerId.toString(), customer_id: invoice.customerId.toString(),

View File

@ -4,20 +4,19 @@
export interface IIssuedInvoiceSummarySnapshot { export interface IIssuedInvoiceSummarySnapshot {
id: string; id: string;
company_id: string; company_id: string;
is_proforma: boolean;
invoice_number: string; invoice_number: string;
status: string; status: string;
series: string; series: string;
invoice_date: string; invoice_date: string;
operation_date: string; operation_date: string | null;
language_code: string; language_code: string;
currency_code: string; currency_code: string;
reference: string | null; reference: string | null;
description: string; description: string | null;
customer_id: string; customer_id: string;
recipient: { recipient: {

View File

@ -15,12 +15,12 @@ import { Maybe, NumberHelper, Result } from "@repo/rdx-utils";
import type { CreateProformaRequestDTO } from "../../../../common"; import type { CreateProformaRequestDTO } from "../../../../common";
import { import {
type InvoiceRecipient,
InvoiceSerie, InvoiceSerie,
InvoiceStatus, InvoiceStatus,
ItemAmount, ItemAmount,
ItemDescription, ItemDescription,
ItemQuantity, ItemQuantity,
type ProformaRecipient,
} from "../../../domain"; } from "../../../domain";
import type { import type {
ProformaCreateInputProps, ProformaCreateInputProps,
@ -64,8 +64,8 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
errors errors
); );
const invoiceDate = extractOrPushError( const proformaDate = extractOrPushError(
UtcDate.createFromISO(dto.invoice_date), UtcDate.createFromISO(dto.proforma_date),
"invoice_date", "invoice_date",
errors errors
); );
@ -142,11 +142,11 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
//invoiceNumber: invoiceNumber!, //invoiceNumber: invoiceNumber!,
series: series!, series: series!,
proformaDate: invoiceDate!, proformaDate: proformaDate!,
operationDate: operationDate!, operationDate: operationDate!,
customerId: customerId!, customerId: customerId!,
recipient: Maybe.none<InvoiceRecipient>(), recipient: Maybe.none<ProformaRecipient>(),
reference: reference!, reference: reference!,
description: description!, description: description!,
@ -169,6 +169,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
props, props,
}); });
} catch (err: unknown) { } catch (err: unknown) {
console.error(err);
return Result.fail(new DomainError("Proforma props mapping failed", { cause: err })); return Result.fail(new DomainError("Proforma props mapping failed", { cause: err }));
} }
} }

View File

@ -12,7 +12,7 @@ import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.
* - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio. * - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
*/ */
export type ProformaCreateInputProps = Omit<IProformaCreateProps, "items" | "invoiceNumber"> & { export type ProformaCreateInputProps = Omit<IProformaCreateProps, "items" | "proformaReference"> & {
items: ProformaItemCreateInputProps[]; items: ProformaItemCreateInputProps[];
}; };

View File

@ -77,8 +77,6 @@ export class ProformaUpdater implements IProformaUpdater {
}): Promise<Result<ProformaPatchProps, Error>> { }): Promise<Result<ProformaPatchProps, Error>> {
const { patch, companyId, currentInvoiceDate } = params; const { patch, companyId, currentInvoiceDate } = params;
console.log(patch);
if (patch.taxRegimeCode !== undefined) { if (patch.taxRegimeCode !== undefined) {
const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({ const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
companyId, companyId,

View File

@ -10,7 +10,6 @@ export interface IProformaSummarySnapshotBuilder
export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotBuilder { export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotBuilder {
toOutput(proforma: ProformaSummary): ProformaSummaryDTO { toOutput(proforma: ProformaSummary): ProformaSummaryDTO {
const recipient = proforma.recipient.toObjectString(); const recipient = proforma.recipient.toObjectString();
console.log("Proforma => ", proforma);
return { return {
id: proforma.id.toString(), id: proforma.id.toString(),

View File

@ -19,48 +19,85 @@ export class IssuedInvoiceItemModel extends Model<
InferAttributes<IssuedInvoiceItemModel>, InferAttributes<IssuedInvoiceItemModel>,
InferCreationAttributes<IssuedInvoiceItemModel, { omit: "issued_invoice" }> InferCreationAttributes<IssuedInvoiceItemModel, { omit: "issued_invoice" }>
> { > {
declare id: string; declare item_id: string;
declare issued_invoice_id: string; declare issued_invoice_id: string;
declare position: number; declare position: number;
declare description: CreationOptional<string | null>; declare description: CreationOptional<string | null>;
declare quantity_value: CreationOptional<number | null>; declare quantity_value: CreationOptional<number | null>;
declare quantity_scale: number; declare quantity_scale: number;
declare unit_amount_value: CreationOptional<number | null>; declare unit_amount_value: CreationOptional<number | null>;
declare unit_amount_scale: number; declare unit_amount_scale: number;
// Subtotal (cantidad * importe unitario)
declare subtotal_amount_value: number; declare subtotal_amount_value: number;
declare subtotal_amount_scale: number; declare subtotal_amount_scale: number;
// Discount percentage
declare item_discount_percentage_value: CreationOptional<number | null>; declare item_discount_percentage_value: CreationOptional<number | null>;
declare item_discount_percentage_scale: number; declare item_discount_percentage_scale: number;
// Discount amount
declare item_discount_amount_value: number; declare item_discount_amount_value: number;
declare item_discount_amount_scale: number; declare item_discount_amount_scale: number;
// Porcentaje de descuento global proporcional a esta línea.
declare global_discount_percentage_value: number; declare global_discount_percentage_value: number;
declare global_discount_percentage_scale: number; declare global_discount_percentage_scale: number;
// Importe del descuento global para esta línea
declare global_discount_amount_value: number; declare global_discount_amount_value: number;
declare global_discount_amount_scale: number; declare global_discount_amount_scale: number;
// Suma de los dos descuentos: el de la linea + el global
declare total_discount_amount_value: number; declare total_discount_amount_value: number;
declare total_discount_amount_scale: number; declare total_discount_amount_scale: number;
// Taxable amount (base imponible tras los dos descuentos)
declare taxable_amount_value: number; declare taxable_amount_value: number;
declare taxable_amount_scale: number; declare taxable_amount_scale: number;
// IVA percentage
declare iva_code: CreationOptional<string | null>; declare iva_code: CreationOptional<string | null>;
declare iva_percentage_value: CreationOptional<number | null>; declare iva_percentage_value: CreationOptional<number | null>;
declare iva_percentage_scale: number; declare iva_percentage_scale: number;
// IVA amount
declare iva_amount_value: number; declare iva_amount_value: number;
declare iva_amount_scale: number; declare iva_amount_scale: number;
// Recargo de equivalencia percentage
declare rec_code: CreationOptional<string | null>; declare rec_code: CreationOptional<string | null>;
declare rec_percentage_value: CreationOptional<number | null>; declare rec_percentage_value: CreationOptional<number | null>;
declare rec_percentage_scale: number; declare rec_percentage_scale: number;
// Recargo de equivalencia amount
declare rec_amount_value: number; declare rec_amount_value: number;
declare rec_amount_scale: number; declare rec_amount_scale: number;
// Retention percentage
declare retention_code: CreationOptional<string | null>; declare retention_code: CreationOptional<string | null>;
declare retention_percentage_value: CreationOptional<number | null>; declare retention_percentage_value: CreationOptional<number | null>;
declare retention_percentage_scale: number; declare retention_percentage_scale: number;
declare retention_amount_value: number; declare retention_amount_value: number;
declare retention_amount_scale: number; declare retention_amount_scale: number;
// Total taxes amount / taxes total
declare taxes_amount_value: number; declare taxes_amount_value: number;
declare taxes_amount_scale: number; declare taxes_amount_scale: number;
// Total
declare total_amount_value: number; declare total_amount_value: number;
declare total_amount_scale: number; declare total_amount_scale: number;
// Relaciones
declare issued_invoice: NonAttribute<IssuedInvoiceModel>; declare issued_invoice: NonAttribute<IssuedInvoiceModel>;
static associate(database: Sequelize) { static associate(database: Sequelize) {
@ -88,10 +125,9 @@ export class IssuedInvoiceItemModel extends Model<
export default (database: Sequelize) => { export default (database: Sequelize) => {
IssuedInvoiceItemModel.init( IssuedInvoiceItemModel.init(
{ {
id: { item_id: {
type: DataTypes.UUID, type: DataTypes.UUID,
primaryKey: true, primaryKey: true,
field: "item_id",
charset: "utf8mb4", charset: "utf8mb4",
collate: "utf8mb4_bin", collate: "utf8mb4_bin",
} as any, } as any,
@ -112,96 +148,117 @@ export default (database: Sequelize) => {
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
quantity_value: { quantity_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
quantity_scale: { quantity_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 2, defaultValue: 2,
}, },
unit_amount_value: { unit_amount_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
unit_amount_scale: { unit_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,
}, },
subtotal_amount_value: { subtotal_amount_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
}, },
subtotal_amount_scale: { subtotal_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,
}, },
item_discount_percentage_value: { item_discount_percentage_value: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
item_discount_percentage_scale: { item_discount_percentage_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 2, defaultValue: 2,
}, },
item_discount_amount_value: { item_discount_amount_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
}, },
item_discount_amount_scale: { item_discount_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,
}, },
global_discount_percentage_value: { global_discount_percentage_value: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
}, },
global_discount_percentage_scale: { global_discount_percentage_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 2, defaultValue: 2,
}, },
global_discount_amount_value: { global_discount_amount_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
}, },
global_discount_amount_scale: { global_discount_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,
}, },
total_discount_amount_value: { total_discount_amount_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
}, },
total_discount_amount_scale: { total_discount_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,
}, },
taxable_amount_value: { taxable_amount_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
}, },
taxable_amount_scale: { taxable_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,
}, },
// IVA %
iva_code: { iva_code: {
type: DataTypes.STRING(40), type: DataTypes.STRING(40),
allowNull: true, allowNull: true,
@ -217,6 +274,7 @@ export default (database: Sequelize) => {
allowNull: false, allowNull: false,
defaultValue: 2, defaultValue: 2,
}, },
iva_amount_value: { iva_amount_value: {
type: DataTypes.BIGINT, type: DataTypes.BIGINT,
allowNull: false, allowNull: false,
@ -227,11 +285,14 @@ export default (database: Sequelize) => {
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,
}, },
// REC %
rec_code: { rec_code: {
type: DataTypes.STRING(40), type: DataTypes.STRING(40),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
rec_percentage_value: { rec_percentage_value: {
type: DataTypes.SMALLINT, type: DataTypes.SMALLINT,
allowNull: true, allowNull: true,
@ -242,6 +303,7 @@ export default (database: Sequelize) => {
allowNull: false, allowNull: false,
defaultValue: 2, defaultValue: 2,
}, },
rec_amount_value: { rec_amount_value: {
type: DataTypes.BIGINT, type: DataTypes.BIGINT,
allowNull: false, allowNull: false,
@ -252,11 +314,14 @@ export default (database: Sequelize) => {
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,
}, },
// Retención %
retention_code: { retention_code: {
type: DataTypes.STRING(40), type: DataTypes.STRING(40),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
retention_percentage_value: { retention_percentage_value: {
type: DataTypes.SMALLINT, type: DataTypes.SMALLINT,
allowNull: true, allowNull: true,
@ -267,6 +332,7 @@ export default (database: Sequelize) => {
allowNull: false, allowNull: false,
defaultValue: 2, defaultValue: 2,
}, },
retention_amount_value: { retention_amount_value: {
type: DataTypes.BIGINT, type: DataTypes.BIGINT,
allowNull: false, allowNull: false,
@ -277,21 +343,25 @@ export default (database: Sequelize) => {
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,
}, },
taxes_amount_value: { taxes_amount_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
}, },
taxes_amount_scale: { taxes_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,
}, },
total_amount_value: { total_amount_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
}, },
total_amount_scale: { total_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
@ -302,11 +372,15 @@ export default (database: Sequelize) => {
sequelize: database, sequelize: database,
modelName: "IssuedInvoiceItemModel", modelName: "IssuedInvoiceItemModel",
tableName: "issued_invoice_items", tableName: "issued_invoice_items",
charset: "utf8mb4", charset: "utf8mb4",
collate: "utf8mb4_unicode_ci", collate: "utf8mb4_unicode_ci",
underscored: true, underscored: true,
indexes: [{ fields: ["issued_invoice_id"] }, { fields: ["issued_invoice_id", "position"] }], indexes: [{ fields: ["issued_invoice_id"] }, { fields: ["issued_invoice_id", "position"] }],
whereMergeStrategy: "and",
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
defaultScope: { defaultScope: {
order: [["position", "ASC"]], order: [["position", "ASC"]],
}, },

View File

@ -51,43 +51,68 @@ export class IssuedInvoiceModel extends Model<
declare reference: CreationOptional<string | null>; declare reference: CreationOptional<string | null>;
declare description: string; declare description: string;
declare notes: CreationOptional<string | null>; declare notes: CreationOptional<string | null>;
declare language_code: CreationOptional<string>; declare language_code: CreationOptional<string>;
declare currency_code: CreationOptional<string>; declare currency_code: CreationOptional<string>;
// Método de pago
declare payment_method_id: CreationOptional<string | null>; declare payment_method_id: CreationOptional<string | null>;
declare payment_method_name: CreationOptional<string | null>; declare payment_method_name: CreationOptional<string | null>;
declare payment_method_description: CreationOptional<string | null>; declare payment_method_description: CreationOptional<string | null>;
// Plazo de pago
declare payment_term_id: CreationOptional<string | null>; declare payment_term_id: CreationOptional<string | null>;
declare payment_term_name: CreationOptional<string | null>; declare payment_term_name: CreationOptional<string | null>;
declare payment_term_description: CreationOptional<string | null>; declare payment_term_description: CreationOptional<string | null>;
declare payment_term_snapshot_json: CreationOptional<string | null>; declare payment_term_snapshot_json: CreationOptional<string | null>;
// Régimen fiscal
declare tax_regime_code: string; declare tax_regime_code: string;
declare tax_regime_description: string; declare tax_regime_description: string;
// Subtotal
declare subtotal_amount_value: number; declare subtotal_amount_value: number;
declare subtotal_amount_scale: number; declare subtotal_amount_scale: number;
// Items discount amount (suma de descuentos individuales por ítem)
declare items_discount_amount_value: number; declare items_discount_amount_value: number;
declare items_discount_amount_scale: number; declare items_discount_amount_scale: number;
// Global/header discount percentage
declare global_discount_percentage_value: number; declare global_discount_percentage_value: number;
declare global_discount_percentage_scale: number; declare global_discount_percentage_scale: number;
// Global/header discount amount
declare global_discount_amount_value: number; declare global_discount_amount_value: number;
declare global_discount_amount_scale: number; declare global_discount_amount_scale: number;
// Total discount amount (subtotal - descuentos)
declare total_discount_amount_value: number; declare total_discount_amount_value: number;
declare total_discount_amount_scale: number; declare total_discount_amount_scale: number;
// Taxable amount (base imponible)
declare taxable_amount_value: number; declare taxable_amount_value: number;
declare taxable_amount_scale: number; declare taxable_amount_scale: number;
declare iva_amount_value: CreationOptional<number | null>;
// IVA amount
declare iva_amount_value: number;
declare iva_amount_scale: number; declare iva_amount_scale: number;
declare rec_amount_value: CreationOptional<number | null>;
// Recargo de equivalencia amount
declare rec_amount_value: number;
declare rec_amount_scale: number; declare rec_amount_scale: number;
declare retention_amount_value: CreationOptional<number | null>;
// Retention amount
declare retention_amount_value: number;
declare retention_amount_scale: number; declare retention_amount_scale: number;
// Total taxes amount / taxes total
declare taxes_amount_value: number; declare taxes_amount_value: number;
declare taxes_amount_scale: number; declare taxes_amount_scale: number;
// Total
declare total_amount_value: number; declare total_amount_value: number;
declare total_amount_scale: number; declare total_amount_scale: number;

View File

@ -76,18 +76,18 @@ export class ProformaModel extends Model<
declare total_amount_scale: number; declare total_amount_scale: number;
declare customer_id: string; declare customer_id: string;
declare customer_tin: CreationOptional<string | null>; /*declare customer_tin: CreationOptional<string | null>;
declare customer_name: CreationOptional<string | null>; declare customer_name: CreationOptional<string | null>;
declare customer_street: CreationOptional<string | null>; declare customer_street: CreationOptional<string | null>;
declare customer_street2: CreationOptional<string | null>; declare customer_street2: CreationOptional<string | null>;
declare customer_city: CreationOptional<string | null>; declare customer_city: CreationOptional<string | null>;
declare customer_province: CreationOptional<string | null>; declare customer_province: CreationOptional<string | null>;
declare customer_postal_code: CreationOptional<string | null>; declare customer_postal_code: CreationOptional<string | null>;
declare customer_country: CreationOptional<string | null>; declare customer_country: CreationOptional<string | null>;*/
declare items: NonAttribute<ProformaItemModel[]>; declare items: NonAttribute<ProformaItemModel[]>;
declare taxes: NonAttribute<ProformaTaxModel[]>; declare taxes: NonAttribute<ProformaTaxModel[]>;
declare current_customer?: NonAttribute<CustomerModel | null>; declare current_customer: NonAttribute<CustomerModel>;
declare linked_invoice?: NonAttribute<IssuedInvoiceModel | null>; declare linked_invoice?: NonAttribute<IssuedInvoiceModel | null>;
static associate(database: Sequelize) { static associate(database: Sequelize) {
@ -352,7 +352,7 @@ export default (database: Sequelize) => {
collate: "utf8mb4_bin", collate: "utf8mb4_bin",
} as any, } as any,
customer_tin: { /*customer_tin: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
@ -391,7 +391,7 @@ export default (database: Sequelize) => {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },*/
}, },
{ {
sequelize: database, sequelize: database,

View File

@ -52,7 +52,7 @@ export const issuedInvoicesRouter = (params: StartParams) => {
); );
router.get( router.get(
"/:invoice_id/report", "/:invoice_id/report/pdf",
//checkTabContext, //checkTabContext,
validateRequest(ReportIssueInvoiceByIdQueryRequestSchema, "query"), validateRequest(ReportIssueInvoiceByIdQueryRequestSchema, "query"),
validateRequest(ReportIssueInvoiceByIdParamsRequestSchema, "params"), validateRequest(ReportIssueInvoiceByIdParamsRequestSchema, "params"),

View File

@ -1,6 +1,6 @@
export * from "./sequelize-issued-invoice-domain.mapper";
export * from "./sequelize-issued-invoice-v2-domain.mapper"; export * from "./sequelize-issued-invoice-v2-domain.mapper";
export * from "./sequelize-issued-invoice-v2-item-domain.mapper"; /*export * from "./sequelize-issued-invoice-v2-item-domain.mapper";
export * from "./sequelize-issued-invoice-v2-recipient-domain.mapper"; export * from "./sequelize-issued-invoice-v2-recipient-domain.mapper";
export * from "./sequelize-issued-invoice-v2-taxes-domain.mapper"; export * from "./sequelize-issued-invoice-v2-taxes-domain.mapper";
export * from "./sequelize-issued-invoice-v2-verifactu-domain.mapper"; export * from "./sequelize-issued-invoice-v2-verifactu-domain.mapper";
*/

View File

@ -98,7 +98,11 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
"reference", "reference",
errors errors
); );
const description = extractOrPushError(Result.ok(String(raw.description ?? "")), "description", errors); const description = extractOrPushError(
Result.ok(String(raw.description ?? "")),
"description",
errors
);
const notes = extractOrPushError( const notes = extractOrPushError(
maybeFromNullableResult(raw.notes, (value) => TextValue.create(value)), maybeFromNullableResult(raw.notes, (value) => TextValue.create(value)),
"notes", "notes",
@ -124,18 +128,18 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
} }
const taxRegime = extractOrPushError( const taxRegime = extractOrPushError(
!isNullishOrEmpty(raw.tax_regime_code) isNullishOrEmpty(raw.tax_regime_code)
? InvoiceTaxRegime.create({ ? Result.fail(
code: String(raw.tax_regime_code),
description: String(raw.tax_regime_description ?? raw.tax_regime_code),
})
: Result.fail(
new DomainValidationError( new DomainValidationError(
"MISSING_TAX_REGIME", "MISSING_TAX_REGIME",
"tax_regime", "tax_regime",
"Issued invoice requires persisted tax regime" "Issued invoice requires persisted tax regime"
) )
), )
: InvoiceTaxRegime.create({
code: String(raw.tax_regime_code),
description: String(raw.tax_regime_description ?? raw.tax_regime_code),
}),
"tax_regime", "tax_regime",
errors errors
); );
@ -167,7 +171,10 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
errors errors
); );
const totalDiscountAmount = extractOrPushError( const totalDiscountAmount = extractOrPushError(
InvoiceAmount.create({ value: raw.total_discount_amount_value, currency_code: currencyCode?.code }), InvoiceAmount.create({
value: raw.total_discount_amount_value,
currency_code: currencyCode?.code,
}),
"total_discount_amount_value", "total_discount_amount_value",
errors errors
); );
@ -187,7 +194,10 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
errors errors
); );
const retentionAmount = extractOrPushError( const retentionAmount = extractOrPushError(
InvoiceAmount.create({ value: raw.retention_amount_value, currency_code: currencyCode?.code }), InvoiceAmount.create({
value: raw.retention_amount_value,
currency_code: currencyCode?.code,
}),
"retention_amount_value", "retention_amount_value",
errors errors
); );
@ -233,8 +243,13 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
}; };
} }
public mapToDomain(raw: IssuedInvoiceModel, params?: MapperParamsType): Result<IssuedInvoice, Error> { public mapToDomain(
raw: IssuedInvoiceModel,
params?: MapperParamsType
): Result<IssuedInvoice, Error> {
try { try {
console.debug(raw);
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const attributes = this._mapAttributesToDomain(raw, { errors, ...params }); const attributes = this._mapAttributesToDomain(raw, { errors, ...params });
const recipientResult = this._recipientMapper.mapToDomain(raw, { const recipientResult = this._recipientMapper.mapToDomain(raw, {
@ -319,23 +334,27 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
params?: MapperParamsType params?: MapperParamsType
): Result<IssuedInvoiceCreationAttributes, Error> { ): Result<IssuedInvoiceCreationAttributes, Error> {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, { const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, {
errors, errors,
parent: source, parent: source,
...params, ...params,
}); });
if (itemsResult.isFailure) errors.push({ path: "items", message: itemsResult.error.message }); if (itemsResult.isFailure) errors.push({ path: "items", message: itemsResult.error.message });
const taxesResult = this._taxesMapper.mapToPersistenceArray(source.taxes, { const taxesResult = this._taxesMapper.mapToPersistenceArray(source.taxes, {
errors, errors,
parent: source, parent: source,
...params, ...params,
}); });
if (taxesResult.isFailure) errors.push({ path: "taxes", message: taxesResult.error.message }); if (taxesResult.isFailure) errors.push({ path: "taxes", message: taxesResult.error.message });
const recipient = this._recipientMapper.mapToPersistence(source.recipient, { const recipient = this._recipientMapper.mapToPersistence(source.recipient, {
errors, errors,
parent: source, parent: source,
...params, ...params,
}); });
const verifactuResult = this._verifactuMapper.mapToPersistence(source.verifactu, { const verifactuResult = this._verifactuMapper.mapToPersistence(source.verifactu, {
errors, errors,
parent: source, parent: source,

View File

@ -13,13 +13,14 @@ import {
maybeFromNullableOrEmptyString, maybeFromNullableOrEmptyString,
maybeFromNullableResult, maybeFromNullableResult,
maybeToNullable, maybeToNullable,
maybeToNullableString,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import {
type IIssuedInvoiceCreateProps, type IIssuedInvoiceCreateProps,
type IssuedInvoice,
type IIssuedInvoiceItemCreateProps, type IIssuedInvoiceItemCreateProps,
type IssuedInvoice,
IssuedInvoiceItem, IssuedInvoiceItem,
ItemAmount, ItemAmount,
ItemDescription, ItemDescription,
@ -56,7 +57,11 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap
attributes: Partial<IIssuedInvoiceCreateProps>; attributes: Partial<IIssuedInvoiceCreateProps>;
}; };
const itemId = extractOrPushError(UniqueID.create(raw.id), `items[${index}].id`, errors); const itemId = extractOrPushError(
UniqueID.create(raw.item_id),
`items[${index}].item_id`,
errors
);
const description = extractOrPushError( const description = extractOrPushError(
maybeFromNullableResult(raw.description, (v) => ItemDescription.create(v)), maybeFromNullableResult(raw.description, (v) => ItemDescription.create(v)),
`items[${index}].description`, `items[${index}].description`,
@ -75,7 +80,10 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap
errors errors
); );
const subtotalAmount = extractOrPushError( const subtotalAmount = extractOrPushError(
ItemAmount.create({ value: raw.subtotal_amount_value, currency_code: attributes.currencyCode?.code }), ItemAmount.create({
value: raw.subtotal_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].subtotal_amount_value`, `items[${index}].subtotal_amount_value`,
errors errors
); );
@ -130,7 +138,10 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap
errors errors
); );
const ivaAmount = extractOrPushError( const ivaAmount = extractOrPushError(
ItemAmount.create({ value: raw.iva_amount_value, currency_code: attributes.currencyCode?.code }), ItemAmount.create({
value: raw.iva_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].iva_amount_value`, `items[${index}].iva_amount_value`,
errors errors
); );
@ -141,13 +152,18 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap
errors errors
); );
const recAmount = extractOrPushError( const recAmount = extractOrPushError(
ItemAmount.create({ value: raw.rec_amount_value, currency_code: attributes.currencyCode?.code }), ItemAmount.create({
value: raw.rec_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].rec_amount_value`, `items[${index}].rec_amount_value`,
errors errors
); );
const retentionCode = maybeFromNullableOrEmptyString(raw.retention_code); const retentionCode = maybeFromNullableOrEmptyString(raw.retention_code);
const retentionPercentage = extractOrPushError( const retentionPercentage = extractOrPushError(
maybeFromNullableResult(raw.retention_percentage_value, (value) => TaxPercentage.create({ value })), maybeFromNullableResult(raw.retention_percentage_value, (value) =>
TaxPercentage.create({ value })
),
`items[${index}].retention_percentage_value`, `items[${index}].retention_percentage_value`,
errors errors
); );
@ -160,12 +176,18 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap
errors errors
); );
const taxesAmount = extractOrPushError( const taxesAmount = extractOrPushError(
ItemAmount.create({ value: raw.taxes_amount_value, currency_code: attributes.currencyCode?.code }), ItemAmount.create({
value: raw.taxes_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].taxes_amount_value`, `items[${index}].taxes_amount_value`,
errors errors
); );
const totalAmount = extractOrPushError( const totalAmount = extractOrPushError(
ItemAmount.create({ value: raw.total_amount_value, currency_code: attributes.currencyCode?.code }), ItemAmount.create({
value: raw.total_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].total_amount_value`, `items[${index}].total_amount_value`,
errors errors
); );
@ -258,18 +280,25 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap
}; };
return Result.ok({ return Result.ok({
id: source.id.toPrimitive(), item_id: source.id.toPrimitive(),
issued_invoice_id: parent.id.toPrimitive(), issued_invoice_id: parent.id.toPrimitive(),
position: index, position: index,
description: maybeToNullable(source.description, (v) => v.toPrimitive()), description: maybeToNullable(source.description, (v) => v.toPrimitive()),
quantity_value: maybeToNullable(source.quantity, (v) => v.toPrimitive().value), quantity_value: maybeToNullable(source.quantity, (v) => v.toPrimitive().value),
quantity_scale: quantity_scale:
maybeToNullable(source.quantity, (v) => v.toPrimitive().scale) ?? ItemQuantity.DEFAULT_SCALE, maybeToNullable(source.quantity, (v) => v.toPrimitive().scale) ??
ItemQuantity.DEFAULT_SCALE,
unit_amount_value: maybeToNullable(source.unitAmount, (v) => v.toPrimitive().value), unit_amount_value: maybeToNullable(source.unitAmount, (v) => v.toPrimitive().value),
unit_amount_scale: unit_amount_scale:
maybeToNullable(source.unitAmount, (v) => v.toPrimitive().scale) ?? ItemAmount.DEFAULT_SCALE, maybeToNullable(source.unitAmount, (v) => v.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE,
subtotal_amount_value: source.subtotalAmount.toPrimitive().value, subtotal_amount_value: source.subtotalAmount.toPrimitive().value,
subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale, subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale,
item_discount_percentage_value: maybeToNullable( item_discount_percentage_value: maybeToNullable(
source.itemDiscountPercentage, source.itemDiscountPercentage,
(v) => v.toPrimitive().value (v) => v.toPrimitive().value
@ -277,36 +306,62 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap
item_discount_percentage_scale: item_discount_percentage_scale:
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
DiscountPercentage.DEFAULT_SCALE, DiscountPercentage.DEFAULT_SCALE,
item_discount_amount_value: source.itemDiscountAmount.toPrimitive().value, item_discount_amount_value: source.itemDiscountAmount.toPrimitive().value,
item_discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale, item_discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale,
global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale, global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale,
global_discount_amount_value: source.globalDiscountAmount.toPrimitive().value, global_discount_amount_value: source.globalDiscountAmount.toPrimitive().value,
global_discount_amount_scale: source.globalDiscountAmount.toPrimitive().scale, global_discount_amount_scale: source.globalDiscountAmount.toPrimitive().scale,
total_discount_amount_value: source.totalDiscountAmount.toPrimitive().value, total_discount_amount_value: source.totalDiscountAmount.toPrimitive().value,
total_discount_amount_scale: source.totalDiscountAmount.toPrimitive().scale, total_discount_amount_scale: source.totalDiscountAmount.toPrimitive().scale,
taxable_amount_value: source.taxableAmount.toPrimitive().value, taxable_amount_value: source.taxableAmount.toPrimitive().value,
taxable_amount_scale: source.taxableAmount.toPrimitive().scale, taxable_amount_scale: source.taxableAmount.toPrimitive().scale,
iva_code: source.ivaCode ?? null,
iva_percentage_value: source.ivaPercentage?.toPrimitive().value ?? null, // IVA
iva_percentage_scale: source.ivaPercentage?.toPrimitive().scale ?? TaxPercentage.DEFAULT_SCALE, iva_code: maybeToNullableString(source.ivaCode),
iva_amount_value: source.ivaAmount.toPrimitive().value,
iva_amount_scale: source.ivaAmount.toPrimitive().scale, iva_percentage_value: maybeToNullable(source.ivaPercentage, (v) => v.toPrimitive().value),
rec_code: source.recCode ?? null, iva_percentage_scale:
rec_percentage_value: source.recPercentage?.toPrimitive().value ?? null, maybeToNullable(source.ivaPercentage, (v) => v.toPrimitive().scale) ?? 2,
rec_percentage_scale: source.recPercentage?.toPrimitive().scale ?? TaxPercentage.DEFAULT_SCALE,
rec_amount_value: source.recAmount.toPrimitive().value, iva_amount_value: source.ivaAmount.value,
rec_amount_scale: source.recAmount.toPrimitive().scale, iva_amount_scale: source.ivaAmount.scale,
retention_code: source.retentionCode ?? null,
retention_percentage_value: source.retentionPercentage?.toPrimitive().value ?? null, // REC
rec_code: maybeToNullableString(source.recCode),
rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value),
rec_percentage_scale:
maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ?? 2,
rec_amount_value: source.recAmount.value,
rec_amount_scale: source.recAmount.scale,
// RET
retention_code: maybeToNullableString(source.retentionCode),
retention_percentage_value: maybeToNullable(
source.retentionPercentage,
(v) => v.toPrimitive().value
),
retention_percentage_scale: retention_percentage_scale:
source.retentionPercentage?.toPrimitive().scale ?? TaxPercentage.DEFAULT_SCALE, maybeToNullable(source.retentionPercentage, (v) => v.toPrimitive().scale) ?? 2,
retention_amount_value: source.retentionAmount.toPrimitive().value,
retention_amount_scale: source.retentionAmount.toPrimitive().scale, retention_amount_value: source.retentionAmount.value,
retention_amount_scale: source.retentionAmount.scale,
//
taxes_amount_value: source.taxesAmount.toPrimitive().value, taxes_amount_value: source.taxesAmount.toPrimitive().value,
taxes_amount_scale: source.taxesAmount.toPrimitive().scale, taxes_amount_scale: source.taxesAmount.toPrimitive().scale,
//
total_amount_value: source.totalAmount.toPrimitive().value, total_amount_value: source.totalAmount.toPrimitive().value,
total_amount_scale: source.totalAmount.toPrimitive().scale, total_amount_scale: source.totalAmount.toPrimitive().scale,
}); } satisfies IssuedInvoiceItemCreationAttributes);
} }
} }

View File

@ -28,15 +28,31 @@ export class SequelizeIssuedInvoiceV2RecipientDomainMapper {
attributes: Partial<IIssuedInvoiceCreateProps>; attributes: Partial<IIssuedInvoiceCreateProps>;
}; };
const customerName = extractOrPushError(Name.create(source.customer_name!), "customer_name", errors); const customerName = extractOrPushError(
const customerTin = extractOrPushError(TINNumber.create(source.customer_tin!), "customer_tin", errors); Name.create(source.customer_name!),
const customerStreet = extractOrPushError(Street.create(source.customer_street!), "customer_street", errors); "customer_name",
errors
);
const customerTin = extractOrPushError(
TINNumber.create(source.customer_tin!),
"customer_tin",
errors
);
const customerStreet = extractOrPushError(
Street.create(source.customer_street!),
"customer_street",
errors
);
const customerStreet2 = extractOrPushError( const customerStreet2 = extractOrPushError(
maybeFromNullableResult(source.customer_street2, (value) => Street.create(value)), maybeFromNullableResult(source.customer_street2, (value) => Street.create(value)),
"customer_street2", "customer_street2",
errors errors
); );
const customerCity = extractOrPushError(City.create(source.customer_city!), "customer_city", errors); const customerCity = extractOrPushError(
City.create(source.customer_city!),
"customer_city",
errors
);
const customerProvince = extractOrPushError( const customerProvince = extractOrPushError(
maybeFromNullableResult(source.customer_province, (value) => Province.create(value)), maybeFromNullableResult(source.customer_province, (value) => Province.create(value)),
"customer_province", "customer_province",
@ -75,7 +91,7 @@ export class SequelizeIssuedInvoiceV2RecipientDomainMapper {
return Result.ok(createResult.data); return Result.ok(createResult.data);
} }
mapToPersistence(source: IssuedInvoiceRecipient) { mapToPersistence(source: IssuedInvoiceRecipient, params?: MapperParamsType) {
return { return {
customer_tin: source.tin.toString(), customer_tin: source.tin.toString(),
customer_name: source.name.toPrimitive(), customer_name: source.name.toPrimitive(),

View File

@ -1,3 +1,2 @@
export * from "./sequelize-issued-invoice-summary.mapper";
export * from "./sequelize-issued-invoice-v2-summary.mapper"; export * from "./sequelize-issued-invoice-v2-summary.mapper";
export * from "./sequelize-issued-invoice-v2-recipient-summary.mapper"; //export * from "./sequelize-issued-invoice-v2-recipient-summary.mapper";

View File

@ -21,8 +21,8 @@ import {
} from "../../../../../../domain"; } from "../../../../../../domain";
import type { IssuedInvoiceModel } from "../../../../../common"; import type { IssuedInvoiceModel } from "../../../../../common";
import { SequelizeVerifactuRecordSummaryMapper } from "./sequelize-verifactu-record-summary.mapper";
import { SequelizeIssuedInvoiceV2RecipientSummaryMapper } from "./sequelize-issued-invoice-v2-recipient-summary.mapper"; import { SequelizeIssuedInvoiceV2RecipientSummaryMapper } from "./sequelize-issued-invoice-v2-recipient-summary.mapper";
import { SequelizeVerifactuRecordSummaryMapper } from "./sequelize-verifactu-record-summary.mapper";
export class SequelizeIssuedInvoiceV2SummaryMapper extends SequelizeQueryMapper< export class SequelizeIssuedInvoiceV2SummaryMapper extends SequelizeQueryMapper<
IssuedInvoiceModel, IssuedInvoiceModel,
@ -113,12 +113,17 @@ export class SequelizeIssuedInvoiceV2SummaryMapper extends SequelizeQueryMapper<
"invoice_number", "invoice_number",
errors errors
); );
const invoiceDate = extractOrPushError(UtcDate.createFromISO(raw.invoice_date), "invoice_date", errors); const invoiceDate = extractOrPushError(
UtcDate.createFromISO(raw.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError( const operationDate = extractOrPushError(
maybeFromNullableResult(raw.operation_date, (value) => UtcDate.createFromISO(value)), maybeFromNullableResult(raw.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date", "operation_date",
errors errors
); );
const reference = extractOrPushError( const reference = extractOrPushError(
maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))), maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))),
"reference", "reference",

View File

@ -5,10 +5,10 @@ import {
requireCompanyContextGuard, requireCompanyContextGuard,
} from "@erp/core/api"; } from "@erp/core/api";
import type { GetProformaByIdUseCase } from "../../../../application"; import type { GetProformaByIdUseCase } from "../../../../application/index.ts";
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class GetProformaController extends ExpressController { export class GetProformaByIdController extends ExpressController {
public constructor(private readonly useCase: GetProformaByIdUseCase) { public constructor(private readonly useCase: GetProformaByIdUseCase) {
super(); super();
this.errorMapper = proformasApiErrorMapper; this.errorMapper = proformasApiErrorMapper;

View File

@ -1,7 +1,7 @@
//export * from "./change-status-proforma.controller"; //export * from "./change-status-proforma.controller";
//export * from "./create-proforma.controller"; //export * from "./create-proforma.controller";
//export * from "./delete-proforma.controller"; //export * from "./delete-proforma.controller";
export * from "./get-proforma.controller"; export * from "./get-proforma-by-id.controller";
export * from "./issue-proforma.controller"; export * from "./issue-proforma.controller";
export * from "./list-proformas.controller"; export * from "./list-proformas.controller";
export * from "./preview-proforma-report.controller"; export * from "./preview-proforma-report.controller";

View File

@ -3,7 +3,7 @@ import { requireIdentityTenant } from "@erp/identity/api";
import { type NextFunction, type Request, type Response, Router } from "express"; import { type NextFunction, type Request, type Response, Router } from "express";
import { import {
GetProformaController, GetProformaByIdController,
IssueProformaController, IssueProformaController,
ListProformasController, ListProformasController,
PreviewProformaReportController, PreviewProformaReportController,
@ -15,9 +15,9 @@ import {
ChangeStatusProformaByIdParamsRequestSchema, ChangeStatusProformaByIdParamsRequestSchema,
ChangeStatusProformaByIdRequestSchema, ChangeStatusProformaByIdRequestSchema,
CreateProformaRequestSchema, CreateProformaRequestSchema,
LegacyIssueProformaByIdParamsRequestSchema,
GetProformaByIdRequestSchema, GetProformaByIdRequestSchema,
IssueProformaByIdParamsRequestSchema, IssueProformaByIdParamsRequestSchema,
LegacyIssueProformaByIdParamsRequestSchema,
ListProformasRequestSchema, ListProformasRequestSchema,
PreviewProformaReportByIdParamsRequestSchema, PreviewProformaReportByIdParamsRequestSchema,
ReportProformaPdfByIdParamsRequestSchema, ReportProformaPdfByIdParamsRequestSchema,
@ -28,8 +28,8 @@ import type { IIssuedInvoicePublicServices } from "../../../application";
import { ChangeStatusProformaController } from "./controllers/change-status-proforma.controller"; import { ChangeStatusProformaController } from "./controllers/change-status-proforma.controller";
import { CreateProformaController } from "./controllers/create-proforma.controller"; import { CreateProformaController } from "./controllers/create-proforma.controller";
import { IssueProformaByIdRequestMapper } from "./mappers";
import { UpdateProformaController } from "./controllers/update-proforma.controller"; import { UpdateProformaController } from "./controllers/update-proforma.controller";
import { IssueProformaByIdRequestMapper } from "./mappers";
export const proformasRouter = (params: StartParams) => { export const proformasRouter = (params: StartParams) => {
const { app, config, getService, getInternal } = params; const { app, config, getService, getInternal } = params;
@ -67,7 +67,7 @@ export const proformasRouter = (params: StartParams) => {
validateRequest(GetProformaByIdRequestSchema, "params"), validateRequest(GetProformaByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.getProformaById(); const useCase = deps.useCases.getProformaById();
const controller = new GetProformaController(useCase); const controller = new GetProformaByIdController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );

View File

@ -218,11 +218,7 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
errors.push({ path: "taxes", message: taxesResult.error.message }); errors.push({ path: "taxes", message: taxesResult.error.message });
} }
const recipient = this._recipientMapper.mapToPersistence(source.recipient, { const recipient = this._recipientMapper.mapToPersistence(source.recipient);
errors,
parent: source,
...params,
});
if (errors.length > 0) { if (errors.length > 0) {
return Result.fail( return Result.fail(
@ -233,8 +229,11 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
const allAmounts = source.totals(); const allAmounts = source.totals();
return Result.ok({ return Result.ok({
// Identificación
id: source.id.toPrimitive(), id: source.id.toPrimitive(),
company_id: source.companyId.toPrimitive(), company_id: source.companyId.toPrimitive(),
// Flags / estado / serie / número
status: source.status.toPrimitive(), status: source.status.toPrimitive(),
proforma_reference: source.proformaReference.toPrimitive(), proforma_reference: source.proformaReference.toPrimitive(),
proforma_date: source.invoiceDate.toPrimitive(), proforma_date: source.invoiceDate.toPrimitive(),
@ -242,36 +241,49 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
target_invoice_series_code: maybeToNullable(source.series, (v) => v.toPrimitive()), target_invoice_series_code: maybeToNullable(source.series, (v) => v.toPrimitive()),
language_code: source.languageCode.toPrimitive(), language_code: source.languageCode.toPrimitive(),
currency_code: source.currencyCode.toPrimitive(), currency_code: source.currencyCode.toPrimitive(),
reference: maybeToNullable(source.reference, (reference) => reference), reference: maybeToNullable(source.reference, (reference) => reference),
description: maybeToNullable(source.description, (description) => description), description: maybeToNullable(source.description, (description) => description),
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()), notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()), payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()),
payment_term_id: null, payment_term_id: null,
tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value), tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value),
subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_value: allAmounts.subtotalAmount.value,
subtotal_amount_scale: allAmounts.subtotalAmount.scale, subtotal_amount_scale: allAmounts.subtotalAmount.scale,
items_discount_amount_value: allAmounts.itemsDiscountAmount.value, items_discount_amount_value: allAmounts.itemsDiscountAmount.value,
items_discount_amount_scale: allAmounts.itemsDiscountAmount.scale, items_discount_amount_scale: allAmounts.itemsDiscountAmount.scale,
global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale, global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale,
global_discount_amount_value: allAmounts.globalDiscountAmount.value, global_discount_amount_value: allAmounts.globalDiscountAmount.value,
global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, global_discount_amount_scale: allAmounts.globalDiscountAmount.scale,
total_discount_amount_value: allAmounts.totalDiscountAmount.value, total_discount_amount_value: allAmounts.totalDiscountAmount.value,
total_discount_amount_scale: allAmounts.totalDiscountAmount.scale, total_discount_amount_scale: allAmounts.totalDiscountAmount.scale,
taxable_amount_value: allAmounts.taxableAmount.value, taxable_amount_value: allAmounts.taxableAmount.value,
taxable_amount_scale: allAmounts.taxableAmount.scale, taxable_amount_scale: allAmounts.taxableAmount.scale,
iva_amount_value: allAmounts.ivaAmount.value, iva_amount_value: allAmounts.ivaAmount.value,
iva_amount_scale: allAmounts.ivaAmount.scale, iva_amount_scale: allAmounts.ivaAmount.scale,
rec_amount_value: allAmounts.recAmount.value, rec_amount_value: allAmounts.recAmount.value,
rec_amount_scale: allAmounts.recAmount.scale, rec_amount_scale: allAmounts.recAmount.scale,
retention_amount_value: allAmounts.retentionAmount.value, retention_amount_value: allAmounts.retentionAmount.value,
retention_amount_scale: allAmounts.retentionAmount.scale, retention_amount_scale: allAmounts.retentionAmount.scale,
taxes_amount_value: allAmounts.taxesAmount.value, taxes_amount_value: allAmounts.taxesAmount.value,
taxes_amount_scale: allAmounts.taxesAmount.scale, taxes_amount_scale: allAmounts.taxesAmount.scale,
total_amount_value: allAmounts.totalAmount.value, total_amount_value: allAmounts.totalAmount.value,
total_amount_scale: allAmounts.totalAmount.scale, total_amount_scale: allAmounts.totalAmount.scale,
customer_id: source.customerId.toPrimitive(), customer_id: source.customerId.toPrimitive(),
...recipient,
taxes: taxesResult.data, taxes: taxesResult.data,
items: itemsResult.data, items: itemsResult.data,
} satisfies ProformaCreationAttributes); } satisfies ProformaCreationAttributes);

View File

@ -11,7 +11,6 @@ import {
type ValidationErrorDetail, type ValidationErrorDetail,
extractOrPushError, extractOrPushError,
maybeFromNullableResult, maybeFromNullableResult,
maybeToNullable,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
@ -28,44 +27,49 @@ export class SequelizeProformaV2RecipientDomainMapper {
parent: Partial<IProformaCreateProps>; parent: Partial<IProformaCreateProps>;
}; };
if (!source.customer_name) { const _name = source.current_customer.name;
return Result.ok(Maybe.none()); const _tin = source.current_customer.tin;
} const _street = source.current_customer.street;
const _street2 = source.current_customer.street2;
const _city = source.current_customer.city;
const _postal_code = source.current_customer.postal_code;
const _province = source.current_customer.province;
const _country = source.current_customer.country;
const customerName = extractOrPushError(Name.create(source.customer_name), "customer_name", errors); const customerName = extractOrPushError(Name.create(_name), "current_customer.name", errors);
const customerTin = extractOrPushError( const customerTin = extractOrPushError(
maybeFromNullableResult(source.customer_tin, (value) => TINNumber.create(value)), maybeFromNullableResult(_tin, (value) => TINNumber.create(value)),
"customer_tin", "current_customer.tin",
errors errors
); );
const customerStreet = extractOrPushError( const customerStreet = extractOrPushError(
maybeFromNullableResult(source.customer_street, (value) => Street.create(value)), maybeFromNullableResult(_street, (value) => Street.create(value)),
"customer_street", "current_customer.street",
errors errors
); );
const customerStreet2 = extractOrPushError( const customerStreet2 = extractOrPushError(
maybeFromNullableResult(source.customer_street2, (value) => Street.create(value)), maybeFromNullableResult(_street2, (value) => Street.create(value)),
"customer_street2", "current_customer.street2",
errors errors
); );
const customerCity = extractOrPushError( const customerCity = extractOrPushError(
maybeFromNullableResult(source.customer_city, (value) => City.create(value)), maybeFromNullableResult(_city, (value) => City.create(value)),
"customer_city", "current_customer.city",
errors errors
); );
const customerProvince = extractOrPushError( const customerProvince = extractOrPushError(
maybeFromNullableResult(source.customer_province, (value) => Province.create(value)), maybeFromNullableResult(_province, (value) => Province.create(value)),
"customer_province", "current_customer.province",
errors errors
); );
const customerPostalCode = extractOrPushError( const customerPostalCode = extractOrPushError(
maybeFromNullableResult(source.customer_postal_code, (value) => PostalCode.create(value)), maybeFromNullableResult(_postal_code, (value) => PostalCode.create(value)),
"customer_postal_code", "current_customer.postal_code",
errors errors
); );
const customerCountry = extractOrPushError( const customerCountry = extractOrPushError(
maybeFromNullableResult(source.customer_country, (value) => Country.create(value)), maybeFromNullableResult(_country, (value) => Country.create(value)),
"customer_country", "current_customer.country",
errors errors
); );
@ -92,7 +96,8 @@ export class SequelizeProformaV2RecipientDomainMapper {
} }
mapToPersistence(source: Maybe<ProformaRecipient>) { mapToPersistence(source: Maybe<ProformaRecipient>) {
if (source.isNone()) { return Result.ok();
/*if (source.isNone()) {
return { return {
customer_tin: null, customer_tin: null,
customer_name: null, customer_name: null,
@ -117,5 +122,6 @@ export class SequelizeProformaV2RecipientDomainMapper {
customer_postal_code: maybeToNullable(recipient.postalCode, (value) => value.toPrimitive()), customer_postal_code: maybeToNullable(recipient.postalCode, (value) => value.toPrimitive()),
customer_country: maybeToNullable(recipient.country, (value) => value.toPrimitive()), customer_country: maybeToNullable(recipient.country, (value) => value.toPrimitive()),
}; };
*/
} }
} }

View File

@ -203,6 +203,8 @@ export class SequelizeProformaRepositoryV2
transaction: Transaction, transaction: Transaction,
options: FindOptions<InferAttributes<ProformaModel>> = {} options: FindOptions<InferAttributes<ProformaModel>> = {}
): Promise<Result<Proforma, Error>> { ): Promise<Result<Proforma, Error>> {
const { CustomerModel } = this.database.models;
try { try {
const normalizedOrder = Array.isArray(options.order) const normalizedOrder = Array.isArray(options.order)
? options.order ? options.order
@ -225,6 +227,7 @@ export class SequelizeProformaRepositoryV2
order: [...normalizedOrder, [{ model: ProformaItemModel, as: "items" }, "position", "ASC"]], order: [...normalizedOrder, [{ model: ProformaItemModel, as: "items" }, "position", "ASC"]],
include: [ include: [
...normalizedInclude, ...normalizedInclude,
{ model: CustomerModel, as: "current_customer", required: false },
{ model: ProformaItemModel, as: "items", required: false }, { model: ProformaItemModel, as: "items", required: false },
{ model: ProformaTaxModel, as: "taxes", required: false }, { model: ProformaTaxModel, as: "taxes", required: false },
{ model: IssuedInvoiceModel, as: "linked_invoice", required: false, attributes: ["id"] }, { model: IssuedInvoiceModel, as: "linked_invoice", required: false, attributes: ["id"] },

View File

@ -21,7 +21,6 @@ import {
export const GetIssuedInvoiceByIdResponseSchema = z.object({ export const GetIssuedInvoiceByIdResponseSchema = z.object({
id: z.uuid(), id: z.uuid(),
company_id: z.uuid(), company_id: z.uuid(),
is_proforma: z.boolean(),
invoice_number: z.string(), invoice_number: z.string(),
status: IssuedInvoiceStatusSchema, status: IssuedInvoiceStatusSchema,

View File

@ -1,50 +1,9 @@
import { import { createPaginatedListSchema } from "@erp/core";
CurrencyCodeSchema, import type { z } from "zod/v4";
IsoDateSchema,
LanguageCodeSchema,
MoneySchema,
createPaginatedListSchema,
} from "@erp/core";
import { z } from "zod/v4";
import { import { IssuedInvoiceSummarySchema } from "../../shared";
IssuedInvoiceRecipientSummarySchema,
IssuedInvoiceStatusSchema,
VerifactuRecordSchema,
} from "../../shared";
export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema( export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema(
z.object({ IssuedInvoiceSummarySchema
id: z.uuid(),
company_id: z.uuid(),
is_proforma: z.boolean(),
invoice_number: z.string(),
status: IssuedInvoiceStatusSchema,
series: z.string(),
invoice_date: IsoDateSchema,
operation_date: IsoDateSchema.nullable(),
language_code: LanguageCodeSchema,
currency_code: CurrencyCodeSchema,
reference: z.string().nullable(),
description: z.string(),
customer_id: z.uuid(),
recipient: IssuedInvoiceRecipientSummarySchema,
subtotal_amount: MoneySchema,
total_discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
verifactu: VerifactuRecordSchema,
linked_proforma_id: z.uuid().nullable(),
})
); );
export type ListIssuedInvoicesResponseDTO = z.infer<typeof ListIssuedInvoicesResponseSchema>; export type ListIssuedInvoicesResponseDTO = z.infer<typeof ListIssuedInvoicesResponseSchema>;

View File

@ -1,4 +1,5 @@
export * from "./issued-invoice-item-detail.dto"; export * from "./issued-invoice-item-detail.dto";
export * from "./issued-invoice-recipient-summary.dto"; export * from "./issued-invoice-recipient-summary.dto";
export * from "./issued-invoice-status.dto"; export * from "./issued-invoice-status.dto";
export * from "./issued-invoice-summary.dto";
export * from "./verifactu-record.dto"; export * from "./verifactu-record.dto";

View File

@ -0,0 +1,39 @@
import { CurrencyCodeSchema, IsoDateSchema, LanguageCodeSchema, MoneySchema } from "@erp/core";
import { z } from "zod/v4";
import { IssuedInvoiceRecipientSummarySchema } from "./issued-invoice-recipient-summary.dto";
import { IssuedInvoiceStatusSchema } from "./issued-invoice-status.dto";
import { VerifactuRecordSchema } from "./verifactu-record.dto";
export const IssuedInvoiceSummarySchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
invoice_number: z.string(),
status: IssuedInvoiceStatusSchema,
series: z.string(),
invoice_date: IsoDateSchema,
operation_date: IsoDateSchema.nullable(),
language_code: LanguageCodeSchema,
currency_code: CurrencyCodeSchema,
reference: z.string().nullable(),
description: z.string().nullable(),
customer_id: z.uuid(),
recipient: IssuedInvoiceRecipientSummarySchema,
subtotal_amount: MoneySchema,
total_discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
verifactu: VerifactuRecordSchema,
linked_proforma_id: z.uuid().nullable(),
});
export type IssuedInvoiceSummaryDTO = z.infer<typeof IssuedInvoiceSummarySchema>;

View File

@ -1,3 +1,4 @@
import { getUserErrorMessage } from "@erp/core/client";
import { useDebounce } from "@erp/core/hooks"; import { useDebounce } from "@erp/core/hooks";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
@ -37,13 +38,15 @@ export const useIssuedInvoiceListController = () => {
filters: filters:
verifactuStatusFilter === "all" verifactuStatusFilter === "all"
? [] ? []
: [{ field: "verifactu.status", operator: "eq", value: verifactuStatusFilter }], : [{ field: "verifactu.status", operator: "EQUALS", value: verifactuStatusFilter }],
}), }),
[debouncedSearch, pageIndex, pageSize, verifactuStatusFilter] [debouncedSearch, pageIndex, pageSize, verifactuStatusFilter]
); );
const query = useIssuedInvoiceListQuery({ criteria }); const query = useIssuedInvoiceListQuery({ criteria });
const errorMessage = query.error ? getUserErrorMessage(query.error) : null;
const setStatusFilterValue = (value: string) => { const setStatusFilterValue = (value: string) => {
const nextValue = (value || "all") as VerifactuRecordListStatusFilter; const nextValue = (value || "all") as VerifactuRecordListStatusFilter;
@ -88,8 +91,8 @@ export const useIssuedInvoiceListController = () => {
isError: query.isError, isError: query.isError,
error: query.error, error: query.error,
errorMessage,
refetch: query.refetch, retry: query.refetch,
pageIndex, pageIndex,
pageSize, pageSize,

View File

@ -1,5 +1,5 @@
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components"; import { ErrorState, PageHeader, SimpleSearchInput } from "@erp/core/components";
import { AppContent, BackHistoryButton, LogoVerifactu } from "@repo/rdx-ui/components"; import { LogoVerifactu } from "@repo/rdx-ui/components";
import { import {
Alert, Alert,
AlertDescription, AlertDescription,
@ -44,8 +44,16 @@ export const ListIssuedInvoicesPage = () => {
widthClass: "w-[500px]", widthClass: "w-[500px]",
});*/ });*/
if (listCtrl.isError) { if (listCtrl.isError && listCtrl.errorMessage) {
return ( return (
<ErrorState
description={listCtrl.errorMessage.description}
onRetry={listCtrl.retry}
reference={listCtrl.errorMessage.reference}
title={listCtrl.errorMessage.title}
/>
);
/*return (
<AppContent> <AppContent>
<ErrorAlert <ErrorAlert
message={(listCtrl.error as Error)?.message || "Error al cargar el listado"} message={(listCtrl.error as Error)?.message || "Error al cargar el listado"}
@ -53,7 +61,7 @@ export const ListIssuedInvoicesPage = () => {
/> />
<BackHistoryButton /> <BackHistoryButton />
</AppContent> </AppContent>
); );*/
} }
return ( return (

View File

@ -1 +0,0 @@
export * from "./list";

View File

@ -1,3 +0,0 @@
export * from "../../../list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns";
export * from "./use-issued-invoices-list";

View File

@ -1,52 +0,0 @@
// src/modules/issued-invoices/hooks/use-proformas-list.ts
import type { CriteriaDTO } from "@erp/core";
import { useDebounce } from "@erp/core/hooks";
import { useMemo, useState } from "react";
import { useIssuedInvoicesQuery } from "../../../hooks";
export const useIssuedInvoicesList = () => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [search, setSearch] = useState("");
const [status, setStatus] = useState("all");
const debouncedQ = useDebounce(search, 300);
const criteria = useMemo<CriteriaDTO>(() => {
const baseFilters =
status === "all" ? [] : [{ field: "status", operator: "CONTAINS", value: status }];
return {
q: debouncedQ || "",
pageSize,
pageNumber: pageIndex,
order: "desc",
orderBy: "invoice_date",
filters: baseFilters,
};
}, [pageSize, pageIndex, debouncedQ, status]);
const query = useIssuedInvoicesQuery({ criteria });
const data = useMemo(
() => (query.data ? IssuedInvoiceSummaryDtoAdapter.fromDto(query.data) : undefined),
[query.data]
);
const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
const setStatusFilter = (newStatus: string) => setStatus(newStatus);
return {
...query,
data,
pageIndex,
pageSize,
search,
setPageIndex,
setPageSize,
setSearchValue,
setStatusFilter,
};
};

View File

@ -1 +0,0 @@
export * from "./issued-invoice-list-page";

View File

@ -1 +0,0 @@
export * from "./issued-invoices-grid";

View File

@ -1,108 +0,0 @@
import { SimpleSearchInput } from "@erp/core/components";
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/shadcn-ui/components";
import { FilterIcon } from "lucide-react";
import { useTranslation } from "../../../../i18n";
import type { IssuedInvoiceSummaryPageData } from "../../../schema/issued-invoice-summary.web.schema";
import { useIssuedInvoicesGridColumns } from "../hooks";
interface IssuedInvoicesGridProps {
data: IssuedInvoiceSummaryPageData;
loading?: boolean;
pageIndex: number;
pageSize: number;
searchValue: string;
onSearchChange: (v: string) => void;
onPageChange: (p: number) => void;
onPageSizeChange: (s: number) => void;
onRowClick?: (id: string) => void;
onExportClick?: () => void;
}
export const IssuedInvoicesGrid = ({
data,
loading,
pageIndex,
pageSize,
searchValue,
onSearchChange,
onPageChange,
onPageSizeChange,
onRowClick,
onExportClick,
}: IssuedInvoicesGridProps) => {
const { t } = useTranslation();
const { items, total_items } = data;
const columns = useIssuedInvoicesGridColumns({
onDownloadPdf: (invoice) => null, //downloadInvoicePdf(inv.id),
onSendEmail: (invoice) => null, //sendInvoiceEmail(inv.id),
});
if (loading)
return (
<SkeletonDataTable
columns={columns.length}
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
rows={Math.max(6, pageSize)}
showFooter
/>
);
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row gap-4">
<SimpleSearchInput loading={loading} onSearchChange={onSearchChange} />
<Select defaultValue="all">
<SelectTrigger className="w-full sm:w-48">
<FilterIcon aria-hidden className="mr-2 size-4" />
<SelectValue placeholder={t("filters.status")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t("catalog.issued_invoices.status.all")}</SelectItem>
<SelectItem value="pendiente">
{t("catalog.issued_invoices.status.pendiente")}
</SelectItem>
<SelectItem value="aceptado_con_error">
{t("catalog.issued_invoices.status.aceptado_con_error")}
</SelectItem>
<SelectItem value="incorrecto">
{t("catalog.issued_invoices.status.incorrecto")}
</SelectItem>
<SelectItem value="duplicado">
{t("catalog.issued_invoices.status.duplicado")}
</SelectItem>
<SelectItem value="anulado">{t("catalog.issued_invoices.status.anulado")}</SelectItem>
<SelectItem value="factura_inexistente">
{t("catalog.issued_invoices.status.factura_inexistente")}
</SelectItem>
<SelectItem value="rechazado">
{t("catalog.issued_invoices.status.rechazado")}
</SelectItem>
<SelectItem value="error">{t("catalog.issued_invoices.status.error")}</SelectItem>
</SelectContent>
</Select>
</div>
<DataTable
columns={columns}
data={items}
enablePagination
manualPagination
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
onRowClick={(row, _index) => onRowClick?.(row.id)}
pageIndex={pageIndex}
pageSize={pageSize}
totalItems={total_items}
/>
</div>
);
};

View File

@ -1,4 +1,5 @@
import type { CriteriaDTO } from "@erp/core"; import type { CriteriaDTO } from "@erp/core";
import { isApiClientError } from "@erp/core/client";
import { useDataSource } from "@erp/core/hooks"; import { useDataSource } from "@erp/core/hooks";
import { type DefaultError, useQuery } from "@tanstack/react-query"; import { type DefaultError, useQuery } from "@tanstack/react-query";
@ -27,5 +28,21 @@ export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesListQueryOptio
}, },
enabled, enabled,
placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
retry: (failureCount, error) => {
if (!isApiClientError(error)) {
return failureCount < 1;
}
if (
error.kind === "validation" ||
error.kind === "forbidden" ||
error.kind === "not-found" ||
error.kind === "conflict"
) {
return false;
}
return failureCount < 2;
},
}); });
}; };

View File

@ -38,8 +38,6 @@ export const buildUpdateProformaByIdParams = (
throw new Error("proformaId is required"); throw new Error("proformaId is required");
} }
console.log(patch);
const data: UpdateProformaByIdParams["data"] = {}; const data: UpdateProformaByIdParams["data"] = {};
if (ObjectHelper.hasOwn(patch, "series")) { if (ObjectHelper.hasOwn(patch, "series")) {

View File

@ -132,7 +132,6 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
}); });
toPatchField(dto.tax_regime_code).ifSetOrNull((taxRegimeCode) => { toPatchField(dto.tax_regime_code).ifSetOrNull((taxRegimeCode) => {
console.log("Hay taxRegimeCode", taxRegimeCode);
customerPatchProps.taxRegimeCode = extractOrPushError( customerPatchProps.taxRegimeCode = extractOrPushError(
maybeFromNullableResult(taxRegimeCode, (value) => Result.ok(String(value))), maybeFromNullableResult(taxRegimeCode, (value) => Result.ok(String(value))),
"tax_regime_code", "tax_regime_code",
@ -162,9 +161,6 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
this.throwIfValidationErrors(errors); this.throwIfValidationErrors(errors);
console.log("dto => ", dto);
console.log("customerPatchProps => ", customerPatchProps);
return Result.ok(customerPatchProps); return Result.ok(customerPatchProps);
} catch (err: unknown) { } catch (err: unknown) {
console.error(err); console.error(err);

View File

@ -68,8 +68,6 @@ export class CustomerRepository
const { id, ...updatePayload } = dtoResult.data; const { id, ...updatePayload } = dtoResult.data;
console.log(dtoResult.data);
const [affected] = await CustomerModel.update(updatePayload, { const [affected] = await CustomerModel.update(updatePayload, {
where: { id /*, version */ }, where: { id /*, version */ },
transaction, transaction,

View File

@ -99,9 +99,7 @@ export const useCreateCustomerController = (options?: UseCustomerCreateControlle
} }
options?.onCreated?.(created); options?.onCreated?.(created);
} catch (error: unknown) { } catch (error: unknown) {
console.log(error);
const normalizedError = normalizeSubmitError(error); const normalizedError = normalizeSubmitError(error);
console.debug(normalizedError);
// No revierto el form para que no se pierdan los cambios que // No revierto el form para que no se pierdan los cambios que
// ha hecho el usuario y no han sido guardados. // ha hecho el usuario y no han sido guardados.

View File

@ -617,6 +617,9 @@ importers:
'@erp/catalogs': '@erp/catalogs':
specifier: workspace:* specifier: workspace:*
version: link:../catalogs version: link:../catalogs
'@erp/companies':
specifier: workspace:*
version: link:../companies
'@erp/core': '@erp/core':
specifier: workspace:* specifier: workspace:*
version: link:../core version: link:../core