diff --git a/apps/web/src/lib/data-source/index.ts b/apps/web/src/lib/data-source/index.ts new file mode 100644 index 00000000..442dacb9 --- /dev/null +++ b/apps/web/src/lib/data-source/index.ts @@ -0,0 +1 @@ +export * from "../../../../../modules/core/src/web/lib/data-source/errors"; diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json index 565b8632..79c91ce8 100644 --- a/apps/web/tsconfig.app.json +++ b/apps/web/tsconfig.app.json @@ -26,6 +26,10 @@ "noUncheckedSideEffectImports": 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"] } diff --git a/modules/catalogs/src/api/application/payment-methods/use-cases/create-payment-method.use-case.ts b/modules/catalogs/src/api/application/payment-methods/use-cases/create-payment-method.use-case.ts index 5a7b2762..2d0734b8 100644 --- a/modules/catalogs/src/api/application/payment-methods/use-cases/create-payment-method.use-case.ts +++ b/modules/catalogs/src/api/application/payment-methods/use-cases/create-payment-method.use-case.ts @@ -24,7 +24,6 @@ export class CreatePaymentMethodUseCase { public execute(params: CreatePaymentMethodUseCaseInput) { const { dto, companyId } = params; - console.log("Executing CreatePaymentMethodUseCase with params:", params); const mappedPropsResult = this.deps.dtoMapper.map(dto, { companyId }); if (mappedPropsResult.isFailure) { diff --git a/modules/catalogs/src/api/application/tax-definitions/public/mappers/tax-definition-public-model.mapper.ts b/modules/catalogs/src/api/application/tax-definitions/public/mappers/tax-definition-public-model.mapper.ts index 4a7a0ffc..738d7678 100644 --- a/modules/catalogs/src/api/application/tax-definitions/public/mappers/tax-definition-public-model.mapper.ts +++ b/modules/catalogs/src/api/application/tax-definitions/public/mappers/tax-definition-public-model.mapper.ts @@ -10,7 +10,7 @@ import type { export class TaxDefinitionPublicModelMapper { public toPublicModel(taxDefinition: TaxDefinition): TaxDefinitionPublicModel { - console.log(taxDefinition.description) + return { id: taxDefinition.id, companyId: Maybe.some(taxDefinition.companyId), diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts index 7455b4cc..344bb124 100644 --- a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts @@ -80,8 +80,6 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper< const allowedSurchargeCodes = extractOrPushError( maybeFromNullableResult(raw.allowed_surcharge_codes, (v) => { - console.log(v); - const values = v.split(";"); if (!Array.isArray(values)) return Result.fail(new Error("Invalid allowed_surcharge_codes")); diff --git a/modules/core/src/api/infrastructure/express/express-controller.ts b/modules/core/src/api/infrastructure/express/express-controller.ts index bcae93c1..cc004444 100644 --- a/modules/core/src/api/infrastructure/express/express-controller.ts +++ b/modules/core/src/api/infrastructure/express/express-controller.ts @@ -59,6 +59,7 @@ export abstract class ExpressController { await this.executeImpl(); } catch (error: unknown) { + console.error("Error in ExpressController.execute:", error); this.handleError(error as Error); } } diff --git a/modules/core/src/web/components/error-state.tsx b/modules/core/src/web/components/error-state.tsx new file mode 100644 index 00000000..6a5a51b5 --- /dev/null +++ b/modules/core/src/web/components/error-state.tsx @@ -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, "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 ( +
+ + +

{title}

+ + {description ? ( +
{description}
+ ) : null} + + {reference ? ( +

+ Referencia:{" "} + + {reference} + +

+ ) : null} + + {onRetry ? ( + + ) : null} +
+ ); +}; diff --git a/modules/core/src/web/components/index.ts b/modules/core/src/web/components/index.ts index 77056ba5..77533758 100644 --- a/modules/core/src/web/components/index.ts +++ b/modules/core/src/web/components/index.ts @@ -1,4 +1,5 @@ export * from "./error-alert"; +export * from "./error-state"; export * from "./form"; export * from "./not-found-card"; export * from "./page-header"; diff --git a/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts b/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts index 9d76d934..69eba4df 100644 --- a/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts +++ b/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts @@ -3,13 +3,23 @@ import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import type { ICustomParams, IDataSource } from "../datasource.interface"; import { defaultAxiosRequestConfig } from "./create-axios-instance"; +import { normalizeAxiosError } from "./normalize-axios-error"; + +const executeRequest = async (request: () => Promise>): Promise => { + try { + const response = await request(); + return response.data; + } catch (error) { + throw normalizeAxiosError(error); + } +}; export const createAxiosDataSource = (client: AxiosInstance): IDataSource => { if (!client) { 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."); } @@ -21,117 +31,83 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => { getBaseUrl: () => client.getUri(), getList: async (resource: string, params?: Record): Promise => { - const { signal, ...rest } = (params ?? {}) as Record & { + const { signal, ...queryParams } = (params ?? {}) as Record & { signal?: AbortSignal; }; - const res = await client.get(resource, { - signal, - params: rest, - }); - - return res.data; + return executeRequest(() => + client.get(resource, { + signal, + params: queryParams, + }) + ); }, getOne: async ( resource: string, id: string | number, params?: Record - ): Promise => { - const res = await client.get(`${resource}/${id}`, params); - return res.data; - }, + ): Promise => + executeRequest(() => client.get(`${resource}/${id}`, params as AxiosRequestConfig)), - getMany: async (resource: string, ids: Array): Promise => { - const res = await client.get(resource, { - params: { ids }, - }); - - return res.data; - }, + getMany: async (resource: string, ids: Array): Promise => + executeRequest(() => + client.get(resource, { + params: { ids }, + }) + ), createOne: async ( resource: string, data: TData, params?: Record - ): Promise => { - const res = await client.post, TData>( - resource, - data, - params as AxiosRequestConfig - ); - - return res.data; - }, + ): Promise => + executeRequest(() => + client.post, TData>(resource, data, params as AxiosRequestConfig) + ), updateOne: async ( resource: string, id: string | number, data: TData, params?: Record - ): Promise => { - const url = `${resource}/${id}`; - - const res = await client.put, TData>( - url, - data, - params as AxiosRequestConfig - ); - - return res.data; - }, + ): Promise => + executeRequest(() => + client.put, TData>( + `${resource}/${id}`, + data, + params as AxiosRequestConfig + ) + ), deleteOne: async ( resource: string, id: string | number, params?: Record - ): Promise => { - const res = await client.delete(`${resource}/${id}`, params); - return res.data; - }, + ): Promise => + executeRequest(() => client.delete(`${resource}/${id}`, params as AxiosRequestConfig)), custom: async (customParams: ICustomParams): Promise => { const { url, path, method, responseType, headers, signal, data, ...payload } = customParams; 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 = { url: requestUrl, method, responseType, signal, - ...payload, ...defaultAxiosRequestConfig, + ...payload, headers, + data, }; - let customResponse: AxiosResponse; - - switch (method) { - case "put": - case "post": - case "patch": - customResponse = await client.request, TData>({ - ...config, - data, - }); - break; - - case "delete": - customResponse = await client.delete, TData>(requestUrl, { - ...config, - }); - break; - - default: - customResponse = await client.get, TData>(requestUrl, { - ...config, - }); - break; - } - - return customResponse.data; + return executeRequest(() => client.request, TData>(config)); }, }; }; diff --git a/modules/core/src/web/lib/data-source/axios/normalize-axios-error.ts b/modules/core/src/web/lib/data-source/axios/normalize-axios-error.ts new file mode 100644 index 00000000..a8443c00 --- /dev/null +++ b/modules/core/src/web/lib/data-source/axios/normalize-axios-error.ts @@ -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); +}; diff --git a/modules/core/src/web/lib/data-source/errors/api-client-error.ts b/modules/core/src/web/lib/data-source/errors/api-client-error.ts new file mode 100644 index 00000000..edc4f892 --- /dev/null +++ b/modules/core/src/web/lib/data-source/errors/api-client-error.ts @@ -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; diff --git a/modules/core/src/web/lib/data-source/errors/api-problem-details.ts b/modules/core/src/web/lib/data-source/errors/api-problem-details.ts new file mode 100644 index 00000000..88794fe5 --- /dev/null +++ b/modules/core/src/web/lib/data-source/errors/api-problem-details.ts @@ -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[]; +} diff --git a/modules/core/src/web/lib/data-source/errors/get-user-error-message.ts b/modules/core/src/web/lib/data-source/errors/get-user-error-message.ts new file mode 100644 index 00000000..c356f39a --- /dev/null +++ b/modules/core/src/web/lib/data-source/errors/get-user-error-message.ts @@ -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, + }; + } +}; diff --git a/modules/core/src/web/lib/data-source/errors/index.ts b/modules/core/src/web/lib/data-source/errors/index.ts new file mode 100644 index 00000000..0f36bbc8 --- /dev/null +++ b/modules/core/src/web/lib/data-source/errors/index.ts @@ -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"; diff --git a/modules/core/src/web/lib/data-source/errors/is-api-problem-details.ts b/modules/core/src/web/lib/data-source/errors/is-api-problem-details.ts new file mode 100644 index 00000000..e2c0056f --- /dev/null +++ b/modules/core/src/web/lib/data-source/errors/is-api-problem-details.ts @@ -0,0 +1,47 @@ +import type { + ApiProblemDetails, + ApiProblemIssue, + ApiProblemPathSegment, +} from "./api-problem-details"; + +const isRecord = (value: unknown): value is Record => + 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; +}; diff --git a/modules/core/src/web/lib/data-source/index.ts b/modules/core/src/web/lib/data-source/index.ts index 2f1724fa..f4411efe 100644 --- a/modules/core/src/web/lib/data-source/index.ts +++ b/modules/core/src/web/lib/data-source/index.ts @@ -1,3 +1,4 @@ export * from "./axios"; export * from "./build-text-filters"; export * from "./datasource.interface"; +export * from "./errors"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/models/issued-invoice-summary.ts b/modules/customer-invoices/src/api/application/issued-invoices/models/issued-invoice-summary.ts index 17b9174f..3ca46483 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/models/issued-invoice-summary.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/models/issued-invoice-summary.ts @@ -4,9 +4,9 @@ import type { Maybe } from "@repo/rdx-utils"; import type { InvoiceAmount, InvoiceNumber, - InvoiceRecipient, InvoiceSerie, InvoiceStatus, + IssuedInvoiceRecipient, VerifactuRecord, } from "../../../domain"; @@ -14,7 +14,6 @@ export type IssuedInvoiceSummary = { id: UniqueID; companyId: UniqueID; - isProforma: boolean; invoiceNumber: InvoiceNumber; status: InvoiceStatus; series: Maybe; @@ -26,7 +25,7 @@ export type IssuedInvoiceSummary = { description: Maybe; customerId: UniqueID; - recipient: InvoiceRecipient; + recipient: IssuedInvoiceRecipient; languageCode: LanguageCode; currencyCode: CurrencyCode; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/summary/issued-invoice-summary-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/summary/issued-invoice-summary-snapshot-builder.ts index fdb4f896..4998826c 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/summary/issued-invoice-summary-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/summary/issued-invoice-summary-snapshot-builder.ts @@ -31,16 +31,15 @@ export class IssuedInvoiceSummarySnapshotBuilder implements IIssuedInvoiceSummar return { id: invoice.id.toString(), company_id: invoice.companyId.toString(), - is_proforma: invoice.isProforma, invoice_number: invoice.invoiceNumber.toString(), status: invoice.status.toPrimitive(), series: maybeToEmptyString(invoice.series, (v) => v.toString()), invoice_date: invoice.invoiceDate.toDateString(), - operation_date: maybeToEmptyString(invoice.operationDate, (v) => v.toDateString()), - reference: maybeToEmptyString(invoice.reference, (v) => v.toString()), - description: maybeToEmptyString(invoice.description, (v) => v.toString()), + operation_date: maybeToNullable(invoice.operationDate, (v) => v.toDateString()), + reference: maybeToNullable(invoice.reference, (v) => v.toString()), + description: maybeToNullable(invoice.description, (v) => v.toString()), customer_id: invoice.customerId.toString(), diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/summary/issued-invoice-summary-snapshot.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/summary/issued-invoice-summary-snapshot.interface.ts index 3a8f2a59..ab2d3dbc 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/summary/issued-invoice-summary-snapshot.interface.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/summary/issued-invoice-summary-snapshot.interface.ts @@ -4,20 +4,19 @@ export interface IIssuedInvoiceSummarySnapshot { id: string; company_id: string; - is_proforma: boolean; invoice_number: string; status: string; series: string; invoice_date: string; - operation_date: string; + operation_date: string | null; language_code: string; currency_code: string; reference: string | null; - description: string; + description: string | null; customer_id: string; recipient: { diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts index b43184d4..bd168e35 100644 --- a/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts +++ b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts @@ -15,12 +15,12 @@ import { Maybe, NumberHelper, Result } from "@repo/rdx-utils"; import type { CreateProformaRequestDTO } from "../../../../common"; import { - type InvoiceRecipient, InvoiceSerie, InvoiceStatus, ItemAmount, ItemDescription, ItemQuantity, + type ProformaRecipient, } from "../../../domain"; import type { ProformaCreateInputProps, @@ -64,8 +64,8 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper { errors ); - const invoiceDate = extractOrPushError( - UtcDate.createFromISO(dto.invoice_date), + const proformaDate = extractOrPushError( + UtcDate.createFromISO(dto.proforma_date), "invoice_date", errors ); @@ -142,11 +142,11 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper { //invoiceNumber: invoiceNumber!, series: series!, - proformaDate: invoiceDate!, + proformaDate: proformaDate!, operationDate: operationDate!, customerId: customerId!, - recipient: Maybe.none(), + recipient: Maybe.none(), reference: reference!, description: description!, @@ -169,6 +169,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper { props, }); } catch (err: unknown) { + console.error(err); return Result.fail(new DomainError("Proforma props mapping failed", { cause: err })); } } diff --git a/modules/customer-invoices/src/api/application/proformas/models/proforma-create-input.model.ts b/modules/customer-invoices/src/api/application/proformas/models/proforma-create-input.model.ts index e81a2a14..decef7a1 100644 --- a/modules/customer-invoices/src/api/application/proformas/models/proforma-create-input.model.ts +++ b/modules/customer-invoices/src/api/application/proformas/models/proforma-create-input.model.ts @@ -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. */ -export type ProformaCreateInputProps = Omit & { +export type ProformaCreateInputProps = Omit & { items: ProformaItemCreateInputProps[]; }; diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts index 328e8043..85786624 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts @@ -77,8 +77,6 @@ export class ProformaUpdater implements IProformaUpdater { }): Promise> { const { patch, companyId, currentInvoiceDate } = params; - console.log(patch); - if (patch.taxRegimeCode !== undefined) { const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({ companyId, diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts index 8effdb2a..a575d051 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts @@ -10,7 +10,6 @@ export interface IProformaSummarySnapshotBuilder export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotBuilder { toOutput(proforma: ProformaSummary): ProformaSummaryDTO { const recipient = proforma.recipient.toObjectString(); - console.log("Proforma => ", proforma); return { id: proforma.id.toString(), diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-issued-invoice-item.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-issued-invoice-item.model.ts index eeb7ee59..1a8050de 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-issued-invoice-item.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-issued-invoice-item.model.ts @@ -19,48 +19,85 @@ export class IssuedInvoiceItemModel extends Model< InferAttributes, InferCreationAttributes > { - declare id: string; + declare item_id: string; declare issued_invoice_id: string; + declare position: number; + declare description: CreationOptional; + declare quantity_value: CreationOptional; declare quantity_scale: number; + declare unit_amount_value: CreationOptional; declare unit_amount_scale: number; + + // Subtotal (cantidad * importe unitario) declare subtotal_amount_value: number; declare subtotal_amount_scale: number; + + // Discount percentage declare item_discount_percentage_value: CreationOptional; declare item_discount_percentage_scale: number; + + // Discount amount declare item_discount_amount_value: 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_scale: number; + + // Importe del descuento global para esta línea declare global_discount_amount_value: 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_scale: number; + + // Taxable amount (base imponible tras los dos descuentos) declare taxable_amount_value: number; declare taxable_amount_scale: number; + + // IVA percentage declare iva_code: CreationOptional; + declare iva_percentage_value: CreationOptional; declare iva_percentage_scale: number; + + // IVA amount declare iva_amount_value: number; declare iva_amount_scale: number; + + // Recargo de equivalencia percentage declare rec_code: CreationOptional; + declare rec_percentage_value: CreationOptional; declare rec_percentage_scale: number; + + // Recargo de equivalencia amount declare rec_amount_value: number; declare rec_amount_scale: number; + + // Retention percentage declare retention_code: CreationOptional; + declare retention_percentage_value: CreationOptional; declare retention_percentage_scale: number; + declare retention_amount_value: number; declare retention_amount_scale: number; + + // Total taxes amount / taxes total declare taxes_amount_value: number; declare taxes_amount_scale: number; + + // Total declare total_amount_value: number; declare total_amount_scale: number; + // Relaciones declare issued_invoice: NonAttribute; static associate(database: Sequelize) { @@ -88,10 +125,9 @@ export class IssuedInvoiceItemModel extends Model< export default (database: Sequelize) => { IssuedInvoiceItemModel.init( { - id: { + item_id: { type: DataTypes.UUID, primaryKey: true, - field: "item_id", charset: "utf8mb4", collate: "utf8mb4_bin", } as any, @@ -112,96 +148,117 @@ export default (database: Sequelize) => { allowNull: true, defaultValue: null, }, + quantity_value: { type: new DataTypes.BIGINT(), allowNull: true, defaultValue: null, }, + quantity_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 2, }, + unit_amount_value: { type: new DataTypes.BIGINT(), allowNull: true, defaultValue: null, }, + unit_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 4, }, + subtotal_amount_value: { - type: new DataTypes.BIGINT(), + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: false, defaultValue: 0, }, + subtotal_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 4, }, + item_discount_percentage_value: { type: new DataTypes.SMALLINT(), allowNull: true, defaultValue: null, }, + item_discount_percentage_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 2, }, + item_discount_amount_value: { - type: new DataTypes.BIGINT(), + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: false, defaultValue: 0, }, + item_discount_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 4, }, + global_discount_percentage_value: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 0, }, + global_discount_percentage_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 2, }, + global_discount_amount_value: { type: new DataTypes.BIGINT(), allowNull: false, defaultValue: 0, }, + global_discount_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 4, }, + total_discount_amount_value: { - type: new DataTypes.BIGINT(), + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: false, defaultValue: 0, }, + total_discount_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 4, }, + taxable_amount_value: { - type: new DataTypes.BIGINT(), + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: false, defaultValue: 0, }, + taxable_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 4, }, + + // IVA % + iva_code: { type: DataTypes.STRING(40), allowNull: true, @@ -217,6 +274,7 @@ export default (database: Sequelize) => { allowNull: false, defaultValue: 2, }, + iva_amount_value: { type: DataTypes.BIGINT, allowNull: false, @@ -227,11 +285,14 @@ export default (database: Sequelize) => { allowNull: false, defaultValue: 4, }, + + // REC % rec_code: { type: DataTypes.STRING(40), allowNull: true, defaultValue: null, }, + rec_percentage_value: { type: DataTypes.SMALLINT, allowNull: true, @@ -242,6 +303,7 @@ export default (database: Sequelize) => { allowNull: false, defaultValue: 2, }, + rec_amount_value: { type: DataTypes.BIGINT, allowNull: false, @@ -252,11 +314,14 @@ export default (database: Sequelize) => { allowNull: false, defaultValue: 4, }, + + // Retención % retention_code: { type: DataTypes.STRING(40), allowNull: true, defaultValue: null, }, + retention_percentage_value: { type: DataTypes.SMALLINT, allowNull: true, @@ -267,6 +332,7 @@ export default (database: Sequelize) => { allowNull: false, defaultValue: 2, }, + retention_amount_value: { type: DataTypes.BIGINT, allowNull: false, @@ -277,21 +343,25 @@ export default (database: Sequelize) => { allowNull: false, defaultValue: 4, }, + taxes_amount_value: { - type: new DataTypes.BIGINT(), + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: false, defaultValue: 0, }, + taxes_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 4, }, + total_amount_value: { - type: new DataTypes.BIGINT(), + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: false, defaultValue: 0, }, + total_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, @@ -302,11 +372,15 @@ export default (database: Sequelize) => { sequelize: database, modelName: "IssuedInvoiceItemModel", tableName: "issued_invoice_items", + charset: "utf8mb4", collate: "utf8mb4_unicode_ci", underscored: true, + indexes: [{ fields: ["issued_invoice_id"] }, { fields: ["issued_invoice_id", "position"] }], - whereMergeStrategy: "and", + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + defaultScope: { order: [["position", "ASC"]], }, diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-issued-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-issued-invoice.model.ts index 3fc278f2..32d5b5b0 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-issued-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-issued-invoice.model.ts @@ -51,43 +51,68 @@ export class IssuedInvoiceModel extends Model< declare reference: CreationOptional; declare description: string; + declare notes: CreationOptional; declare language_code: CreationOptional; declare currency_code: CreationOptional; + // Método de pago declare payment_method_id: CreationOptional; declare payment_method_name: CreationOptional; declare payment_method_description: CreationOptional; + // Plazo de pago declare payment_term_id: CreationOptional; declare payment_term_name: CreationOptional; declare payment_term_description: CreationOptional; declare payment_term_snapshot_json: CreationOptional; + // Régimen fiscal declare tax_regime_code: string; declare tax_regime_description: string; + // Subtotal declare subtotal_amount_value: 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_scale: number; + + // Global/header discount percentage declare global_discount_percentage_value: number; declare global_discount_percentage_scale: number; + + // Global/header discount amount declare global_discount_amount_value: number; declare global_discount_amount_scale: number; + + // Total discount amount (subtotal - descuentos) declare total_discount_amount_value: number; declare total_discount_amount_scale: number; + + // Taxable amount (base imponible) declare taxable_amount_value: number; declare taxable_amount_scale: number; - declare iva_amount_value: CreationOptional; + + // IVA amount + declare iva_amount_value: number; declare iva_amount_scale: number; - declare rec_amount_value: CreationOptional; + + // Recargo de equivalencia amount + declare rec_amount_value: number; declare rec_amount_scale: number; - declare retention_amount_value: CreationOptional; + + // Retention amount + declare retention_amount_value: number; declare retention_amount_scale: number; + + // Total taxes amount / taxes total declare taxes_amount_value: number; declare taxes_amount_scale: number; + + // Total declare total_amount_value: number; declare total_amount_scale: number; diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-proforma.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-proforma.model.ts index 130e9897..790b38f3 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-proforma.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/sequelize-proforma.model.ts @@ -76,18 +76,18 @@ export class ProformaModel extends Model< declare total_amount_scale: number; declare customer_id: string; - declare customer_tin: CreationOptional; + /*declare customer_tin: CreationOptional; declare customer_name: CreationOptional; declare customer_street: CreationOptional; declare customer_street2: CreationOptional; declare customer_city: CreationOptional; declare customer_province: CreationOptional; declare customer_postal_code: CreationOptional; - declare customer_country: CreationOptional; + declare customer_country: CreationOptional;*/ declare items: NonAttribute; declare taxes: NonAttribute; - declare current_customer?: NonAttribute; + declare current_customer: NonAttribute; declare linked_invoice?: NonAttribute; static associate(database: Sequelize) { @@ -352,7 +352,7 @@ export default (database: Sequelize) => { collate: "utf8mb4_bin", } as any, - customer_tin: { + /*customer_tin: { type: DataTypes.STRING, allowNull: true, defaultValue: null, @@ -391,7 +391,7 @@ export default (database: Sequelize) => { type: DataTypes.STRING, allowNull: true, defaultValue: null, - }, + },*/ }, { sequelize: database, diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices.routes.ts index dd38a695..b0e20248 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices.routes.ts @@ -52,7 +52,7 @@ export const issuedInvoicesRouter = (params: StartParams) => { ); router.get( - "/:invoice_id/report", + "/:invoice_id/report/pdf", //checkTabContext, validateRequest(ReportIssueInvoiceByIdQueryRequestSchema, "query"), validateRequest(ReportIssueInvoiceByIdParamsRequestSchema, "params"), diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/index.ts index 1cb5cacd..b3a7af10 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/index.ts @@ -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-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-taxes-domain.mapper"; export * from "./sequelize-issued-invoice-v2-verifactu-domain.mapper"; +*/ diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-domain.mapper.ts index ea8858f8..9216cae6 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-domain.mapper.ts @@ -98,7 +98,11 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper< "reference", errors ); - const description = extractOrPushError(Result.ok(String(raw.description ?? "")), "description", errors); + const description = extractOrPushError( + Result.ok(String(raw.description ?? "")), + "description", + errors + ); const notes = extractOrPushError( maybeFromNullableResult(raw.notes, (value) => TextValue.create(value)), "notes", @@ -124,18 +128,18 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper< } const taxRegime = extractOrPushError( - !isNullishOrEmpty(raw.tax_regime_code) - ? InvoiceTaxRegime.create({ - code: String(raw.tax_regime_code), - description: String(raw.tax_regime_description ?? raw.tax_regime_code), - }) - : Result.fail( + isNullishOrEmpty(raw.tax_regime_code) + ? Result.fail( new DomainValidationError( "MISSING_TAX_REGIME", "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", errors ); @@ -167,7 +171,10 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper< errors ); 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", errors ); @@ -187,7 +194,10 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper< errors ); 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", errors ); @@ -233,8 +243,13 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper< }; } - public mapToDomain(raw: IssuedInvoiceModel, params?: MapperParamsType): Result { + public mapToDomain( + raw: IssuedInvoiceModel, + params?: MapperParamsType + ): Result { try { + console.debug(raw); + const errors: ValidationErrorDetail[] = []; const attributes = this._mapAttributesToDomain(raw, { errors, ...params }); const recipientResult = this._recipientMapper.mapToDomain(raw, { @@ -319,23 +334,27 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper< params?: MapperParamsType ): Result { const errors: ValidationErrorDetail[] = []; + const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, { errors, parent: source, ...params, }); + if (itemsResult.isFailure) errors.push({ path: "items", message: itemsResult.error.message }); const taxesResult = this._taxesMapper.mapToPersistenceArray(source.taxes, { errors, parent: source, ...params, }); + if (taxesResult.isFailure) errors.push({ path: "taxes", message: taxesResult.error.message }); const recipient = this._recipientMapper.mapToPersistence(source.recipient, { errors, parent: source, ...params, }); + const verifactuResult = this._verifactuMapper.mapToPersistence(source.verifactu, { errors, parent: source, diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-item-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-item-domain.mapper.ts index 695b97e7..b49561aa 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-item-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-item-domain.mapper.ts @@ -13,13 +13,14 @@ import { maybeFromNullableOrEmptyString, maybeFromNullableResult, maybeToNullable, + maybeToNullableString, } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { type IIssuedInvoiceCreateProps, - type IssuedInvoice, type IIssuedInvoiceItemCreateProps, + type IssuedInvoice, IssuedInvoiceItem, ItemAmount, ItemDescription, @@ -56,7 +57,11 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap attributes: Partial; }; - 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( maybeFromNullableResult(raw.description, (v) => ItemDescription.create(v)), `items[${index}].description`, @@ -75,7 +80,10 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap errors ); 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`, errors ); @@ -130,7 +138,10 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap errors ); 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`, errors ); @@ -141,13 +152,18 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap errors ); 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`, errors ); const retentionCode = maybeFromNullableOrEmptyString(raw.retention_code); 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`, errors ); @@ -160,12 +176,18 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap errors ); 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`, errors ); 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`, errors ); @@ -258,18 +280,25 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap }; return Result.ok({ - id: source.id.toPrimitive(), + item_id: source.id.toPrimitive(), issued_invoice_id: parent.id.toPrimitive(), position: index, + description: maybeToNullable(source.description, (v) => v.toPrimitive()), + quantity_value: maybeToNullable(source.quantity, (v) => v.toPrimitive().value), 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_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_scale: source.subtotalAmount.toPrimitive().scale, + item_discount_percentage_value: maybeToNullable( source.itemDiscountPercentage, (v) => v.toPrimitive().value @@ -277,36 +306,62 @@ export class SequelizeIssuedInvoiceV2ItemDomainMapper extends SequelizeDomainMap item_discount_percentage_scale: maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? DiscountPercentage.DEFAULT_SCALE, + item_discount_amount_value: source.itemDiscountAmount.toPrimitive().value, item_discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale, + global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale, + global_discount_amount_value: source.globalDiscountAmount.toPrimitive().value, global_discount_amount_scale: source.globalDiscountAmount.toPrimitive().scale, + total_discount_amount_value: source.totalDiscountAmount.toPrimitive().value, total_discount_amount_scale: source.totalDiscountAmount.toPrimitive().scale, + taxable_amount_value: source.taxableAmount.toPrimitive().value, taxable_amount_scale: source.taxableAmount.toPrimitive().scale, - iva_code: source.ivaCode ?? null, - iva_percentage_value: source.ivaPercentage?.toPrimitive().value ?? null, - iva_percentage_scale: source.ivaPercentage?.toPrimitive().scale ?? TaxPercentage.DEFAULT_SCALE, - iva_amount_value: source.ivaAmount.toPrimitive().value, - iva_amount_scale: source.ivaAmount.toPrimitive().scale, - rec_code: source.recCode ?? null, - rec_percentage_value: source.recPercentage?.toPrimitive().value ?? null, - rec_percentage_scale: source.recPercentage?.toPrimitive().scale ?? TaxPercentage.DEFAULT_SCALE, - rec_amount_value: source.recAmount.toPrimitive().value, - rec_amount_scale: source.recAmount.toPrimitive().scale, - retention_code: source.retentionCode ?? null, - retention_percentage_value: source.retentionPercentage?.toPrimitive().value ?? null, + + // IVA + iva_code: maybeToNullableString(source.ivaCode), + + iva_percentage_value: maybeToNullable(source.ivaPercentage, (v) => v.toPrimitive().value), + iva_percentage_scale: + maybeToNullable(source.ivaPercentage, (v) => v.toPrimitive().scale) ?? 2, + + iva_amount_value: source.ivaAmount.value, + iva_amount_scale: source.ivaAmount.scale, + + // 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: - source.retentionPercentage?.toPrimitive().scale ?? TaxPercentage.DEFAULT_SCALE, - retention_amount_value: source.retentionAmount.toPrimitive().value, - retention_amount_scale: source.retentionAmount.toPrimitive().scale, + maybeToNullable(source.retentionPercentage, (v) => v.toPrimitive().scale) ?? 2, + + retention_amount_value: source.retentionAmount.value, + retention_amount_scale: source.retentionAmount.scale, + + // taxes_amount_value: source.taxesAmount.toPrimitive().value, taxes_amount_scale: source.taxesAmount.toPrimitive().scale, + + // total_amount_value: source.totalAmount.toPrimitive().value, total_amount_scale: source.totalAmount.toPrimitive().scale, - }); + } satisfies IssuedInvoiceItemCreationAttributes); } } diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-recipient-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-recipient-domain.mapper.ts index ec6930be..3c15fe2e 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-recipient-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-v2-recipient-domain.mapper.ts @@ -28,15 +28,31 @@ export class SequelizeIssuedInvoiceV2RecipientDomainMapper { attributes: Partial; }; - const customerName = extractOrPushError(Name.create(source.customer_name!), "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 customerName = extractOrPushError( + Name.create(source.customer_name!), + "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( maybeFromNullableResult(source.customer_street2, (value) => Street.create(value)), "customer_street2", 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( maybeFromNullableResult(source.customer_province, (value) => Province.create(value)), "customer_province", @@ -75,7 +91,7 @@ export class SequelizeIssuedInvoiceV2RecipientDomainMapper { return Result.ok(createResult.data); } - mapToPersistence(source: IssuedInvoiceRecipient) { + mapToPersistence(source: IssuedInvoiceRecipient, params?: MapperParamsType) { return { customer_tin: source.tin.toString(), customer_name: source.name.toPrimitive(), diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/index.ts index c63284cf..3f4173ad 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/index.ts @@ -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-recipient-summary.mapper"; +//export * from "./sequelize-issued-invoice-v2-recipient-summary.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-v2-summary.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-v2-summary.mapper.ts index 7ca2427f..ce7fa101 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-v2-summary.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-v2-summary.mapper.ts @@ -21,8 +21,8 @@ import { } from "../../../../../../domain"; import type { IssuedInvoiceModel } from "../../../../../common"; -import { SequelizeVerifactuRecordSummaryMapper } from "./sequelize-verifactu-record-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< IssuedInvoiceModel, @@ -113,12 +113,17 @@ export class SequelizeIssuedInvoiceV2SummaryMapper extends SequelizeQueryMapper< "invoice_number", 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( maybeFromNullableResult(raw.operation_date, (value) => UtcDate.createFromISO(value)), "operation_date", errors ); + const reference = extractOrPushError( maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))), "reference", diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/get-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/get-proforma-by-id.controller.ts similarity index 92% rename from modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/get-proforma.controller.ts rename to modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/get-proforma-by-id.controller.ts index de06d707..2d064b66 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/get-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/get-proforma-by-id.controller.ts @@ -5,10 +5,10 @@ import { requireCompanyContextGuard, } from "@erp/core/api"; -import type { GetProformaByIdUseCase } from "../../../../application"; +import type { GetProformaByIdUseCase } from "../../../../application/index.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) { super(); this.errorMapper = proformasApiErrorMapper; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts index ee66f7a1..c69a1e68 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts @@ -1,7 +1,7 @@ //export * from "./change-status-proforma.controller"; //export * from "./create-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 "./list-proformas.controller"; export * from "./preview-proforma-report.controller"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts index 83457cc1..36d29cc8 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts @@ -3,7 +3,7 @@ import { requireIdentityTenant } from "@erp/identity/api"; import { type NextFunction, type Request, type Response, Router } from "express"; import { - GetProformaController, + GetProformaByIdController, IssueProformaController, ListProformasController, PreviewProformaReportController, @@ -15,9 +15,9 @@ import { ChangeStatusProformaByIdParamsRequestSchema, ChangeStatusProformaByIdRequestSchema, CreateProformaRequestSchema, - LegacyIssueProformaByIdParamsRequestSchema, GetProformaByIdRequestSchema, IssueProformaByIdParamsRequestSchema, + LegacyIssueProformaByIdParamsRequestSchema, ListProformasRequestSchema, PreviewProformaReportByIdParamsRequestSchema, ReportProformaPdfByIdParamsRequestSchema, @@ -28,8 +28,8 @@ import type { IIssuedInvoicePublicServices } from "../../../application"; import { ChangeStatusProformaController } from "./controllers/change-status-proforma.controller"; import { CreateProformaController } from "./controllers/create-proforma.controller"; -import { IssueProformaByIdRequestMapper } from "./mappers"; import { UpdateProformaController } from "./controllers/update-proforma.controller"; +import { IssueProformaByIdRequestMapper } from "./mappers"; export const proformasRouter = (params: StartParams) => { const { app, config, getService, getInternal } = params; @@ -67,7 +67,7 @@ export const proformasRouter = (params: StartParams) => { validateRequest(GetProformaByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { const useCase = deps.useCases.getProformaById(); - const controller = new GetProformaController(useCase); + const controller = new GetProformaByIdController(useCase); return controller.execute(req, res, next); } ); diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-v2-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-v2-domain.mapper.ts index 186ca674..7551fa2b 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-v2-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-v2-domain.mapper.ts @@ -218,11 +218,7 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper< errors.push({ path: "taxes", message: taxesResult.error.message }); } - const recipient = this._recipientMapper.mapToPersistence(source.recipient, { - errors, - parent: source, - ...params, - }); + const recipient = this._recipientMapper.mapToPersistence(source.recipient); if (errors.length > 0) { return Result.fail( @@ -233,8 +229,11 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper< const allAmounts = source.totals(); return Result.ok({ + // Identificación id: source.id.toPrimitive(), company_id: source.companyId.toPrimitive(), + + // Flags / estado / serie / número status: source.status.toPrimitive(), proforma_reference: source.proformaReference.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()), language_code: source.languageCode.toPrimitive(), currency_code: source.currencyCode.toPrimitive(), + reference: maybeToNullable(source.reference, (reference) => reference), description: maybeToNullable(source.description, (description) => description), notes: maybeToNullable(source.notes, (v) => v.toPrimitive()), payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()), payment_term_id: null, tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value), + subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_scale: allAmounts.subtotalAmount.scale, + items_discount_amount_value: allAmounts.itemsDiscountAmount.value, items_discount_amount_scale: allAmounts.itemsDiscountAmount.scale, + global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale, + global_discount_amount_value: allAmounts.globalDiscountAmount.value, global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, + total_discount_amount_value: allAmounts.totalDiscountAmount.value, total_discount_amount_scale: allAmounts.totalDiscountAmount.scale, + taxable_amount_value: allAmounts.taxableAmount.value, taxable_amount_scale: allAmounts.taxableAmount.scale, + iva_amount_value: allAmounts.ivaAmount.value, iva_amount_scale: allAmounts.ivaAmount.scale, + rec_amount_value: allAmounts.recAmount.value, rec_amount_scale: allAmounts.recAmount.scale, + retention_amount_value: allAmounts.retentionAmount.value, retention_amount_scale: allAmounts.retentionAmount.scale, + taxes_amount_value: allAmounts.taxesAmount.value, taxes_amount_scale: allAmounts.taxesAmount.scale, + total_amount_value: allAmounts.totalAmount.value, total_amount_scale: allAmounts.totalAmount.scale, + customer_id: source.customerId.toPrimitive(), - ...recipient, + taxes: taxesResult.data, items: itemsResult.data, } satisfies ProformaCreationAttributes); diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-v2-recipient-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-v2-recipient-domain.mapper.ts index 0ac7e367..472f4f3c 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-v2-recipient-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-v2-recipient-domain.mapper.ts @@ -11,7 +11,6 @@ import { type ValidationErrorDetail, extractOrPushError, maybeFromNullableResult, - maybeToNullable, } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; @@ -28,44 +27,49 @@ export class SequelizeProformaV2RecipientDomainMapper { parent: Partial; }; - if (!source.customer_name) { - return Result.ok(Maybe.none()); - } + const _name = source.current_customer.name; + 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( - maybeFromNullableResult(source.customer_tin, (value) => TINNumber.create(value)), - "customer_tin", + maybeFromNullableResult(_tin, (value) => TINNumber.create(value)), + "current_customer.tin", errors ); const customerStreet = extractOrPushError( - maybeFromNullableResult(source.customer_street, (value) => Street.create(value)), - "customer_street", + maybeFromNullableResult(_street, (value) => Street.create(value)), + "current_customer.street", errors ); const customerStreet2 = extractOrPushError( - maybeFromNullableResult(source.customer_street2, (value) => Street.create(value)), - "customer_street2", + maybeFromNullableResult(_street2, (value) => Street.create(value)), + "current_customer.street2", errors ); const customerCity = extractOrPushError( - maybeFromNullableResult(source.customer_city, (value) => City.create(value)), - "customer_city", + maybeFromNullableResult(_city, (value) => City.create(value)), + "current_customer.city", errors ); const customerProvince = extractOrPushError( - maybeFromNullableResult(source.customer_province, (value) => Province.create(value)), - "customer_province", + maybeFromNullableResult(_province, (value) => Province.create(value)), + "current_customer.province", errors ); const customerPostalCode = extractOrPushError( - maybeFromNullableResult(source.customer_postal_code, (value) => PostalCode.create(value)), - "customer_postal_code", + maybeFromNullableResult(_postal_code, (value) => PostalCode.create(value)), + "current_customer.postal_code", errors ); const customerCountry = extractOrPushError( - maybeFromNullableResult(source.customer_country, (value) => Country.create(value)), - "customer_country", + maybeFromNullableResult(_country, (value) => Country.create(value)), + "current_customer.country", errors ); @@ -92,7 +96,8 @@ export class SequelizeProformaV2RecipientDomainMapper { } mapToPersistence(source: Maybe) { - if (source.isNone()) { + return Result.ok(); + /*if (source.isNone()) { return { customer_tin: null, customer_name: null, @@ -117,5 +122,6 @@ export class SequelizeProformaV2RecipientDomainMapper { customer_postal_code: maybeToNullable(recipient.postalCode, (value) => value.toPrimitive()), customer_country: maybeToNullable(recipient.country, (value) => value.toPrimitive()), }; + */ } } diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma-v2.repository.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma-v2.repository.ts index 4cb18586..88f4f436 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma-v2.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma-v2.repository.ts @@ -203,6 +203,8 @@ export class SequelizeProformaRepositoryV2 transaction: Transaction, options: FindOptions> = {} ): Promise> { + const { CustomerModel } = this.database.models; + try { const normalizedOrder = Array.isArray(options.order) ? options.order @@ -225,6 +227,7 @@ export class SequelizeProformaRepositoryV2 order: [...normalizedOrder, [{ model: ProformaItemModel, as: "items" }, "position", "ASC"]], include: [ ...normalizedInclude, + { model: CustomerModel, as: "current_customer", required: false }, { model: ProformaItemModel, as: "items", required: false }, { model: ProformaTaxModel, as: "taxes", required: false }, { model: IssuedInvoiceModel, as: "linked_invoice", required: false, attributes: ["id"] }, diff --git a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts index fce79656..36df02c1 100644 --- a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts @@ -21,7 +21,6 @@ import { export const GetIssuedInvoiceByIdResponseSchema = z.object({ id: z.uuid(), company_id: z.uuid(), - is_proforma: z.boolean(), invoice_number: z.string(), status: IssuedInvoiceStatusSchema, diff --git a/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts b/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts index 64bca415..acc199c7 100644 --- a/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts @@ -1,50 +1,9 @@ -import { - CurrencyCodeSchema, - IsoDateSchema, - LanguageCodeSchema, - MoneySchema, - createPaginatedListSchema, -} from "@erp/core"; -import { z } from "zod/v4"; +import { createPaginatedListSchema } from "@erp/core"; +import type { z } from "zod/v4"; -import { - IssuedInvoiceRecipientSummarySchema, - IssuedInvoiceStatusSchema, - VerifactuRecordSchema, -} from "../../shared"; +import { IssuedInvoiceSummarySchema } from "../../shared"; export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema( - z.object({ - 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(), - }) + IssuedInvoiceSummarySchema ); - export type ListIssuedInvoicesResponseDTO = z.infer; diff --git a/modules/customer-invoices/src/common/dto/shared/issued-invoices/index.ts b/modules/customer-invoices/src/common/dto/shared/issued-invoices/index.ts index dae1bce1..c7087b55 100644 --- a/modules/customer-invoices/src/common/dto/shared/issued-invoices/index.ts +++ b/modules/customer-invoices/src/common/dto/shared/issued-invoices/index.ts @@ -1,4 +1,5 @@ export * from "./issued-invoice-item-detail.dto"; export * from "./issued-invoice-recipient-summary.dto"; export * from "./issued-invoice-status.dto"; +export * from "./issued-invoice-summary.dto"; export * from "./verifactu-record.dto"; diff --git a/modules/customer-invoices/src/common/dto/shared/issued-invoices/issued-invoice-summary.dto.ts b/modules/customer-invoices/src/common/dto/shared/issued-invoices/issued-invoice-summary.dto.ts new file mode 100644 index 00000000..bd87eba5 --- /dev/null +++ b/modules/customer-invoices/src/common/dto/shared/issued-invoices/issued-invoice-summary.dto.ts @@ -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; diff --git a/modules/customer-invoices/src/web/issued-invoices/list/controllers/use-issued-invoice-list.controller.ts b/modules/customer-invoices/src/web/issued-invoices/list/controllers/use-issued-invoice-list.controller.ts index 17321055..8c3a723a 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/controllers/use-issued-invoice-list.controller.ts +++ b/modules/customer-invoices/src/web/issued-invoices/list/controllers/use-issued-invoice-list.controller.ts @@ -1,3 +1,4 @@ +import { getUserErrorMessage } from "@erp/core/client"; import { useDebounce } from "@erp/core/hooks"; import { useMemo, useState } from "react"; @@ -37,13 +38,15 @@ export const useIssuedInvoiceListController = () => { filters: verifactuStatusFilter === "all" ? [] - : [{ field: "verifactu.status", operator: "eq", value: verifactuStatusFilter }], + : [{ field: "verifactu.status", operator: "EQUALS", value: verifactuStatusFilter }], }), [debouncedSearch, pageIndex, pageSize, verifactuStatusFilter] ); const query = useIssuedInvoiceListQuery({ criteria }); + const errorMessage = query.error ? getUserErrorMessage(query.error) : null; + const setStatusFilterValue = (value: string) => { const nextValue = (value || "all") as VerifactuRecordListStatusFilter; @@ -88,8 +91,8 @@ export const useIssuedInvoiceListController = () => { isError: query.isError, error: query.error, - - refetch: query.refetch, + errorMessage, + retry: query.refetch, pageIndex, pageSize, diff --git a/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/list-issued-invoices-page.tsx b/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/list-issued-invoices-page.tsx index 7d64c4dd..22a8a26c 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/list-issued-invoices-page.tsx +++ b/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/list-issued-invoices-page.tsx @@ -1,5 +1,5 @@ -import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components"; -import { AppContent, BackHistoryButton, LogoVerifactu } from "@repo/rdx-ui/components"; +import { ErrorState, PageHeader, SimpleSearchInput } from "@erp/core/components"; +import { LogoVerifactu } from "@repo/rdx-ui/components"; import { Alert, AlertDescription, @@ -44,8 +44,16 @@ export const ListIssuedInvoicesPage = () => { widthClass: "w-[500px]", });*/ - if (listCtrl.isError) { + if (listCtrl.isError && listCtrl.errorMessage) { return ( + + ); + /*return ( { /> - ); + );*/ } return ( diff --git a/modules/customer-invoices/src/web/issued-invoices/pages/index.ts b/modules/customer-invoices/src/web/issued-invoices/pages/index.ts deleted file mode 100644 index 491ccf0c..00000000 --- a/modules/customer-invoices/src/web/issued-invoices/pages/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./list"; diff --git a/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/index.ts b/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/index.ts deleted file mode 100644 index b7d6b472..00000000 --- a/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../../list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns"; - -export * from "./use-issued-invoices-list"; diff --git a/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-list.ts b/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-list.ts deleted file mode 100644 index 4a7b5665..00000000 --- a/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-list.ts +++ /dev/null @@ -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(() => { - 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, - }; -}; diff --git a/modules/customer-invoices/src/web/issued-invoices/pages/list/index.ts b/modules/customer-invoices/src/web/issued-invoices/pages/list/index.ts deleted file mode 100644 index 77624ddb..00000000 --- a/modules/customer-invoices/src/web/issued-invoices/pages/list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./issued-invoice-list-page"; diff --git a/modules/customer-invoices/src/web/issued-invoices/pages/list/ui/index.ts b/modules/customer-invoices/src/web/issued-invoices/pages/list/ui/index.ts deleted file mode 100644 index 096a6e62..00000000 --- a/modules/customer-invoices/src/web/issued-invoices/pages/list/ui/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./issued-invoices-grid"; diff --git a/modules/customer-invoices/src/web/issued-invoices/pages/list/ui/issued-invoices-grid.tsx b/modules/customer-invoices/src/web/issued-invoices/pages/list/ui/issued-invoices-grid.tsx deleted file mode 100644 index 87da3aaf..00000000 --- a/modules/customer-invoices/src/web/issued-invoices/pages/list/ui/issued-invoices-grid.tsx +++ /dev/null @@ -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 ( - - ); - - return ( -
-
- - -
- - onRowClick?.(row.id)} - pageIndex={pageIndex} - pageSize={pageSize} - totalItems={total_items} - /> -
- ); -}; diff --git a/modules/customer-invoices/src/web/issued-invoices/shared/hooks/use-issued-invoice-list-query.ts b/modules/customer-invoices/src/web/issued-invoices/shared/hooks/use-issued-invoice-list-query.ts index c2559d34..5655a197 100644 --- a/modules/customer-invoices/src/web/issued-invoices/shared/hooks/use-issued-invoice-list-query.ts +++ b/modules/customer-invoices/src/web/issued-invoices/shared/hooks/use-issued-invoice-list-query.ts @@ -1,4 +1,5 @@ import type { CriteriaDTO } from "@erp/core"; +import { isApiClientError } from "@erp/core/client"; import { useDataSource } from "@erp/core/hooks"; import { type DefaultError, useQuery } from "@tanstack/react-query"; @@ -27,5 +28,21 @@ export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesListQueryOptio }, enabled, 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; + }, }); }; diff --git a/modules/customer-invoices/src/web/proformas/update/utils/build-update-proforma-by-id-params.ts b/modules/customer-invoices/src/web/proformas/update/utils/build-update-proforma-by-id-params.ts index 68f41c40..b5014582 100644 --- a/modules/customer-invoices/src/web/proformas/update/utils/build-update-proforma-by-id-params.ts +++ b/modules/customer-invoices/src/web/proformas/update/utils/build-update-proforma-by-id-params.ts @@ -38,8 +38,6 @@ export const buildUpdateProformaByIdParams = ( throw new Error("proformaId is required"); } - console.log(patch); - const data: UpdateProformaByIdParams["data"] = {}; if (ObjectHelper.hasOwn(patch, "series")) { diff --git a/modules/customers/src/api/application/mappers/update-customer-input.mapper.ts b/modules/customers/src/api/application/mappers/update-customer-input.mapper.ts index a397fdbd..1d48087e 100644 --- a/modules/customers/src/api/application/mappers/update-customer-input.mapper.ts +++ b/modules/customers/src/api/application/mappers/update-customer-input.mapper.ts @@ -132,7 +132,6 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper { }); toPatchField(dto.tax_regime_code).ifSetOrNull((taxRegimeCode) => { - console.log("Hay taxRegimeCode", taxRegimeCode); customerPatchProps.taxRegimeCode = extractOrPushError( maybeFromNullableResult(taxRegimeCode, (value) => Result.ok(String(value))), "tax_regime_code", @@ -162,9 +161,6 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper { this.throwIfValidationErrors(errors); - console.log("dto => ", dto); - console.log("customerPatchProps => ", customerPatchProps); - return Result.ok(customerPatchProps); } catch (err: unknown) { console.error(err); diff --git a/modules/customers/src/api/infrastructure/persistence/sequelize/repositories/customer.repository.ts b/modules/customers/src/api/infrastructure/persistence/sequelize/repositories/customer.repository.ts index f7f02189..7f346b5a 100644 --- a/modules/customers/src/api/infrastructure/persistence/sequelize/repositories/customer.repository.ts +++ b/modules/customers/src/api/infrastructure/persistence/sequelize/repositories/customer.repository.ts @@ -68,8 +68,6 @@ export class CustomerRepository const { id, ...updatePayload } = dtoResult.data; - console.log(dtoResult.data); - const [affected] = await CustomerModel.update(updatePayload, { where: { id /*, version */ }, transaction, diff --git a/modules/customers/src/web/create/controllers/use-customer-create.controller.ts b/modules/customers/src/web/create/controllers/use-customer-create.controller.ts index 862ac08f..3a1fedf6 100644 --- a/modules/customers/src/web/create/controllers/use-customer-create.controller.ts +++ b/modules/customers/src/web/create/controllers/use-customer-create.controller.ts @@ -99,9 +99,7 @@ export const useCreateCustomerController = (options?: UseCustomerCreateControlle } options?.onCreated?.(created); } catch (error: unknown) { - console.log(error); const normalizedError = normalizeSubmitError(error); - console.debug(normalizedError); // No revierto el form para que no se pierdan los cambios que // ha hecho el usuario y no han sido guardados. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 165e2416..c4d1ce58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -617,6 +617,9 @@ importers: '@erp/catalogs': specifier: workspace:* version: link:../catalogs + '@erp/companies': + specifier: workspace:* + version: link:../companies '@erp/core': specifier: workspace:* version: link:../core diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator index df11ed0b..c3660fc4 100755 Binary files a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator and b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator differ diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe index 4c242670..1950a07e 100755 Binary files a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe and b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe differ