This commit is contained in:
David Arranz 2025-11-11 19:57:04 +01:00
parent 54e23899c4
commit 4d76919e44
98 changed files with 1505 additions and 577 deletions

View File

@ -1,3 +1,13 @@
{
"recommendations": ["biomejs.biome", "cweijan.vscode-mysql-client2"]
"recommendations": [
"biomejs.biome",
"cweijan.vscode-mysql-client2",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-typescript-next",
"yzhang.markdown-all-in-one",
"ms-vscode.vscode-json",
"formulahendry.auto-rename-tag",
"christian-kohler.path-intellisense"
]
}

21
.vscode/settings.json vendored
View File

@ -20,15 +20,6 @@
"strings": "on"
},
"editor.codeActionsOnSave": {
"source.fixAll": "always",
"source.fixAll.biome": "always",
"source.removeUnusedImports": "always"
},
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.formatOnPaste": false,
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
@ -48,8 +39,16 @@
"editor.defaultFormatter": "biomejs.biome"
},
"prettier.enable": false,
"eslint.enable": false,
// Biome
"biome.enabled": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.formatOnPaste": false,
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit",
"source.removeUnusedImports": "always"
},
// other vscode settings
"[handlebars]": {

View File

@ -1,7 +1,9 @@
import { DateTime } from "luxon";
import http from "node:http";
import os from "node:os";
import { DateTime } from "luxon";
import { z } from "zod/v4";
import { createApp } from "./app.ts";
import { tryConnectToDatabase } from "./config/database.ts";
import { ENV } from "./config/index.ts";

View File

@ -1,69 +1,366 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
"files": { "ignoreUnknown": false, "includes": ["**", "!**/dist"] },
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true,
"defaultBranch": "main"
},
"files": {
"ignoreUnknown": true,
"includes": [
"**/*.js",
"**/*.jsx",
"**/*.ts",
"**/*.tsx",
"**/*.json",
"**/*.css",
"**/*.scss"
],
"experimentalScannerIgnores": [
"**/node_modules/**",
"**/.next/**",
"**/dist/**",
"**/build/**",
"**/coverage/**",
"**/.turbo/**",
"**/out/**",
"**/.env*",
"**/public/**",
"**/*.d.ts",
"**/storybook-static/**",
"**/.vercel/**"
]
},
"formatter": {
"enabled": true,
"useEditorconfig": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100,
"attributePosition": "auto",
"bracketSpacing": true
"lineEnding": "lf",
"attributePosition": "auto"
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"useExhaustiveDependencies": "info",
"noUnreachable": "warn"
},
"complexity": {
"noForEach": "off",
"noBannedTypes": "info",
"noUselessFragments": "off",
"useOptionalChain": "off",
"noThisInStatic": "off"
},
"suspicious": {
"noImplicitAnyLet": "info",
"noExplicitAny": "info",
"noArrayIndexKey": "info"
},
"style": {
"useImportType": "off",
"noInferrableTypes": "off",
"noDefaultExport": "off",
"noImplicitBoolean": "off",
"noInferrableTypes": "error",
"noNamespace": "error",
"noNegationElse": "warn",
"noNonNullAssertion": "info",
"noUselessElse": "off",
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"noUnusedTemplateLiteral": "error",
"noUselessElse": "warn",
"useBlockStatements": "off",
"useCollapsedElseIf": "error",
"useConst": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useExportType": "error",
"useFilenamingConvention": {
"level": "error",
"options": {
"strictCase": false,
"requireAscii": true,
"filenameCases": ["kebab-case"]
}
},
"useForOf": "error",
"useFragmentSyntax": "error",
"useImportType": "error",
"useNamingConvention": {
"level": "off",
"options": {
"strictCase": false,
"conventions": [
{
"selector": {
"kind": "function"
},
"formats": ["camelCase", "PascalCase"]
},
{
"selector": {
"kind": "variable"
},
"formats": ["camelCase", "PascalCase", "CONSTANT_CASE"]
},
{
"selector": {
"kind": "typeLike"
},
"formats": ["PascalCase"]
}
]
}
},
"useNodejsImportProtocol": "error",
"useNumberNamespace": "error",
"useSelfClosingElements": "error",
"useShorthandAssign": "error",
"useShorthandFunctionType": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error"
"useTemplate": "error",
"useThrowOnlyError": "error"
},
"suspicious": {
"noExplicitAny": "error",
"noDebugger": "error",
"noDuplicateJsxProps": "error",
"noDuplicateObjectKeys": "error",
"noDuplicateParameters": "error",
"noShadowRestrictedNames": "error",
"noSparseArray": "error",
"noUnsafeNegation": "error",
"noArrayIndexKey": "warn",
"noAssignInExpressions": "error",
"noCatchAssign": "error",
"noClassAssign": "error",
"noCommentText": "error",
"noCompareNegZero": "error",
"noConsole": "warn",
"noConstEnum": "error",
"noControlCharactersInRegex": "error",
"noDoubleEquals": "error",
"noDuplicateCase": "error",
"noEmptyBlockStatements": "error",
"noFallthroughSwitchClause": "error",
"noFunctionAssign": "error",
"noGlobalAssign": "error",
"noLabelVar": "error",
"noMisleadingCharacterClass": "error",
"noPrototypeBuiltins": "error",
"noRedeclare": "error",
"noSelfCompare": "error",
"noUnknownAtRules": "off"
},
"correctness": {
"noConstAssign": "error",
"noConstructorReturn": "error",
"noEmptyPattern": "error",
"noInvalidConstructorSuper": "error",
"noInvalidUseBeforeDeclaration": "error",
"noSelfAssign": "error",
"noSetterReturn": "error",
"noSwitchDeclarations": "error",
"noUnreachable": "error",
"noUnreachableSuper": "error",
"noUnsafeFinally": "error",
"noUnsafeOptionalChaining": "error",
"noUnusedLabels": "error",
"noUnusedVariables": "warn",
"useExhaustiveDependencies": "error",
"useHookAtTopLevel": "error",
"useIsNan": "error",
"useJsxKeyInIterable": "error",
"useValidForDirection": "error",
"useYield": "error"
},
"complexity": {
"noBannedTypes": "error",
"noExcessiveCognitiveComplexity": {
"level": "warn",
"options": {
"maxAllowedComplexity": 15
}
},
"noForEach": "warn",
"noStaticOnlyClass": "error",
"noThisInStatic": "error",
"noUselessCatch": "error",
"noUselessConstructor": "error",
"noUselessFragments": "error",
"noUselessLabel": "error",
"noUselessRename": "error",
"noUselessSwitchCase": "error",
"noUselessTernary": "error",
"noUselessTypeConstraint": "error",
"noVoid": "error",
"useFlatMap": "error",
"useLiteralKeys": "error",
"useOptionalChain": "error",
"useSimpleNumberKeys": "error",
"useSimplifiedLogicExpression": "error"
},
"security": {
"noDangerouslySetInnerHtml": "error",
"noDangerouslySetInnerHtmlWithChildren": "error",
"noGlobalEval": "error"
},
"a11y": {
"useSemanticElements": "info"
"noAccessKey": "error",
"noAriaHiddenOnFocusable": "error",
"noAriaUnsupportedElements": "error",
"noAutofocus": "error",
"noDistractingElements": "error",
"noHeaderScope": "error",
"noInteractiveElementToNoninteractiveRole": "error",
"noNoninteractiveElementToInteractiveRole": "error",
"noNoninteractiveTabindex": "error",
"noPositiveTabindex": "error",
"noRedundantAlt": "error",
"noRedundantRoles": "error",
"useFocusableInteractive": "error",
"useIframeTitle": "error",
"useKeyWithClickEvents": "error",
"useKeyWithMouseEvents": "error",
"useMediaCaption": "error",
"useSemanticElements": "error",
"useValidAnchor": "error",
"useValidAriaProps": "error",
"useValidAriaValues": "error",
"useValidLang": "error"
},
"performance": {
"noAccumulatingSpread": "warn",
"noDelete": "error"
}
},
"domains": {
"next": "all",
"react": "recommended"
}
},
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
":URL:",
":BLANK_LINE:",
[":BUN:", ":NODE:"],
":BLANK_LINE:",
":PACKAGE_WITH_PROTOCOL:",
":BLANK_LINE:",
[":PACKAGE:"],
":BLANK_LINE:",
["!@/**", "!#*", "!~*", "!$*", "!%*"],
":BLANK_LINE:",
["@/**", "#*", "~*", "$*", "%*"],
":BLANK_LINE:",
[":PATH:", "!./**", "!../**"],
":BLANK_LINE:",
["../**"],
":BLANK_LINE:",
["./**"]
],
"identifierOrder": "lexicographic"
}
},
"useSortedAttributes": "on"
}
}
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "single",
"quoteProperties": "asNeeded",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"quoteStyle": "double",
"attributePosition": "auto",
"bracketSpacing": true
"semicolons": "always",
"trailingCommas": "es5"
},
"globals": ["console", "process", "__dirname", "__filename"]
},
"json": {
"formatter": {
"trailingCommas": "none",
"indentStyle": "space",
"indentWidth": 2
}
}
},
"css": {
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"quoteStyle": "double"
},
"linter": {
"enabled": true
}
},
"overrides": [
{
"includes": ["**/*.test.{js,ts,tsx}", "**/*.spec.{js,ts,tsx}", "**/__tests__/**"],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off",
"noConsole": "off"
},
"style": {
"noNonNullAssertion": "off"
}
}
}
},
{
"includes": ["**/next.config.{js,ts}", "**/tailwind.config.{js,ts}", "**/*.config.{js,ts}"],
"linter": {
"rules": {
"style": {
"noDefaultExport": "off"
},
"suspicious": {
"noExplicitAny": "off"
}
}
}
},
{
"includes": ["**/pages/**", "**/app/**/page.{tsx,jsx}", "**/app/**/layout.{tsx,jsx}"],
"linter": {
"rules": {
"style": {
"noDefaultExport": "off"
}
}
}
},
{
"includes": ["**/*.d.ts", "**/lib/env/*.ts"],
"linter": {
"rules": {
"style": {
"noNamespace": "off",
"useNamingConvention": {
"level": "error",
"options": {
"strictCase": false,
"conventions": [
{
"selector": {
"kind": "objectLiteralProperty"
},
"formats": ["CONSTANT_CASE", "camelCase"]
}
]
}
}
},
"suspicious": {
"noExplicitAny": "error"
},
"complexity": {
"noExcessiveCognitiveComplexity": {
"level": "error",
"options": {
"maxAllowedComplexity": 12
}
}
}
}
}
}
]
}

View File

@ -1,7 +1,7 @@
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import type { GetIssueInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
export function formatPaymentMethodDTO(
paymentMethod?: GetCustomerInvoiceByIdResponseDTO["payment_method"]
paymentMethod?: GetIssueInvoiceByIdResponseDTO["payment_method"]
) {
if (!paymentMethod) {
return null;

View File

@ -1,11 +1,12 @@
import { Presenter } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd";
import { ArrayElement } from "@repo/rdx-utils";
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
import { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../domain";
import type { ArrayElement } from "@repo/rdx-utils";
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto";
import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../domain";
type GetCustomerInvoiceItemByInvoiceIdResponseDTO = ArrayElement<
GetCustomerInvoiceByIdResponseDTO["items"]
GetIssueInvoiceByIdResponseDTO["items"]
>;
export class CustomerInvoiceItemsFullPresenter extends Presenter {
@ -48,7 +49,7 @@ export class CustomerInvoiceItemsFullPresenter extends Presenter {
};
}
toOutput(invoiceItems: CustomerInvoiceItems): GetCustomerInvoiceByIdResponseDTO["items"] {
toOutput(invoiceItems: CustomerInvoiceItems): GetIssueInvoiceByIdResponseDTO["items"] {
return invoiceItems.map(this._mapItem);
}
}

View File

@ -1,15 +1,17 @@
import { Presenter } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd";
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain";
import { CustomerInvoiceItemsFullPresenter } from "./customer-invoice-items.full.presenter";
import { RecipientInvoiceFullPresenter } from "./recipient-invoice.full.representer";
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto";
import type { CustomerInvoice } from "../../../domain";
import type { CustomerInvoiceItemsFullPresenter } from "./customer-invoice-items.full.presenter";
import type { RecipientInvoiceFullPresenter } from "./recipient-invoice.full.representer";
export class CustomerInvoiceFullPresenter extends Presenter<
CustomerInvoice,
GetCustomerInvoiceByIdResponseDTO
GetIssueInvoiceByIdResponseDTO
> {
toOutput(invoice: CustomerInvoice): GetCustomerInvoiceByIdResponseDTO {
toOutput(invoice: CustomerInvoice): GetIssueInvoiceByIdResponseDTO {
const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice-items",
projection: "FULL",

View File

@ -1,9 +1,10 @@
import { Presenter } from "@erp/core/api";
import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd";
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
import { CustomerInvoice, InvoiceRecipient } from "../../../domain";
type GetRecipientInvoiceByInvoiceIdResponseDTO = GetCustomerInvoiceByIdResponseDTO["recipient"];
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto";
import type { CustomerInvoice, InvoiceRecipient } from "../../../domain";
type GetRecipientInvoiceByInvoiceIdResponseDTO = GetIssueInvoiceByIdResponseDTO["recipient"];
export class RecipientInvoiceFullPresenter extends Presenter {
toOutput(invoice: CustomerInvoice): GetRecipientInvoiceByInvoiceIdResponseDTO {

View File

@ -1,9 +1,10 @@
import { IPresenterOutputParams, Presenter } from "@erp/core/api";
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import { ArrayElement } from "@repo/rdx-utils";
import { FormatMoneyOptions, formatMoneyDTO, formatQuantityDTO } from "../../helpers";
import { type IPresenterOutputParams, Presenter } from "@erp/core/api";
import type { GetIssueInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils";
type CustomerInvoiceItemsDTO = GetCustomerInvoiceByIdResponseDTO["items"];
import { type FormatMoneyOptions, formatMoneyDTO, formatQuantityDTO } from "../../helpers";
type CustomerInvoiceItemsDTO = GetIssueInvoiceByIdResponseDTO["items"];
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
export class CustomerInvoiceItemsReportPersenter extends Presenter<

View File

@ -1,7 +1,8 @@
import { Presenter } from "@erp/core/api";
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto";
import {
FormatMoneyOptions,
type FormatMoneyOptions,
formatDateDTO,
formatMoneyDTO,
formatPercentageDTO,
@ -9,10 +10,10 @@ import {
import { formatPaymentMethodDTO } from "../../helpers/format-payment_method-dto";
export class CustomerInvoiceReportPresenter extends Presenter<
GetCustomerInvoiceByIdResponseDTO,
GetIssueInvoiceByIdResponseDTO,
unknown
> {
toOutput(invoiceDTO: GetCustomerInvoiceByIdResponseDTO) {
toOutput(invoiceDTO: GetIssueInvoiceByIdResponseDTO) {
const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice-items",
projection: "REPORT",

View File

@ -1,15 +1,16 @@
import { Presenter } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import type { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd";
import { ArrayElement, Collection } from "@repo/rdx-utils";
import { ListCustomerInvoicesResponseDTO } from "../../../../common/dto";
import { CustomerInvoiceListDTO } from "../../../infrastructure";
import type { ArrayElement, Collection } from "@repo/rdx-utils";
import type { ListIssueInvoicesResponseDTO } from "../../../../common/dto";
import type { CustomerInvoiceListDTO } from "../../../infrastructure";
export class ListCustomerInvoicesPresenter extends Presenter {
protected _mapInvoice(invoice: CustomerInvoiceListDTO) {
const recipientDTO = invoice.recipient.toObjectString();
const invoiceDTO: ArrayElement<ListCustomerInvoicesResponseDTO["items"]> = {
const invoiceDTO: ArrayElement<ListIssueInvoicesResponseDTO["items"]> = {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
is_proforma: invoice.isProforma,
@ -49,7 +50,7 @@ export class ListCustomerInvoicesPresenter extends Presenter {
toOutput(params: {
customerInvoices: Collection<CustomerInvoiceListDTO>;
criteria: Criteria;
}): ListCustomerInvoicesResponseDTO {
}): ListIssueInvoicesResponseDTO {
const { customerInvoices, criteria } = params;
const invoices = customerInvoices.map((invoice) => this._mapInvoice(invoice));

View File

@ -1,8 +1,9 @@
import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Maybe, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize";
import {
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Maybe, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type {
CustomerInvoiceNumber,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
@ -10,11 +11,11 @@ import {
} from "../../domain";
import {
CustomerInvoice,
CustomerInvoicePatchProps,
CustomerInvoiceProps,
type CustomerInvoicePatchProps,
type CustomerInvoiceProps,
} from "../../domain/aggregates";
import { ICustomerInvoiceRepository } from "../../domain/repositories";
import { CustomerInvoiceListDTO } from "../../infrastructure";
import type { ICustomerInvoiceRepository } from "../../domain/repositories";
import type { CustomerInvoiceListDTO } from "../../infrastructure";
export class CustomerInvoiceApplicationService {
constructor(
@ -57,57 +58,57 @@ export class CustomerInvoiceApplicationService {
*
* @param companyId - Identificador de la empresa a la que pertenece la proforma.
* @param props - Las propiedades ya validadas para crear la proforma.
* @param invoiceId - Identificador UUID de la proforma (opcional).
* @param proformaId - Identificador UUID de la proforma (opcional).
* @returns Result<CustomerInvoice, Error> - El agregado construido o un error si falla la creación.
*/
buildProformaInCompany(
companyId: UniqueID,
props: Omit<CustomerInvoiceProps, "companyId">,
invoiceId?: UniqueID
proformaId?: UniqueID
): Result<CustomerInvoice, Error> {
return CustomerInvoice.create({ ...props, companyId }, invoiceId);
return CustomerInvoice.create({ ...props, companyId }, proformaId);
}
/**
* Guarda una nueva factura y devuelve la factura guardada.
* Guarda una nueva proforma y devuelve la proforma guardada.
*
* @param companyId - Identificador de la empresa a la que pertenece la factura.
* @param invoice - El agregado a guardar.
* @param companyId - Identificador de la empresa a la que pertenece la proforma.
* @param proforma - La proforma a guardar.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - El agregado guardado o un error si falla la operación.
* @returns Result<CustomerInvoice, Error> - La proforma guardada o un error si falla la operación.
*/
async createInvoiceInCompany(
companyId: UniqueID,
invoice: CustomerInvoice,
proforma: CustomerInvoice,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
const result = await this.repository.create(invoice, transaction);
const result = await this.repository.create(proforma, transaction);
if (result.isFailure) {
return Result.fail(result.error);
}
return this.getInvoiceByIdInCompany(companyId, invoice.id, transaction);
return this.getProformaByIdInCompany(companyId, proforma.id, transaction);
}
/**
* Actualiza una factura existente y devuelve la factura actualizada.
* Actualiza una proforma existente y devuelve la proforma actualizada.
*
* @param companyId - Identificador de la empresa a la que pertenece la factura.
* @param invoice - El agregado a guardar.
* @param proforma - La proforma a guardar.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - El agregado guardado o un error si falla la operación.
* @returns Result<CustomerInvoice, Error> - La proforma guardada o un error si falla la operación.
*/
async updateInvoiceInCompany(
async updateProformaInCompany(
companyId: UniqueID,
invoice: CustomerInvoice,
proforma: CustomerInvoice,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
const result = await this.repository.update(invoice, transaction);
const result = await this.repository.update(proforma, transaction);
if (result.isFailure) {
return Result.fail(result.error);
}
return this.getInvoiceByIdInCompany(companyId, invoice.id, transaction);
return this.getProformaByIdInCompany(companyId, proforma.id, transaction);
}
/**
@ -151,12 +152,35 @@ export class CustomerInvoiceApplicationService {
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura encontrada o error.
*/
async getInvoiceByIdInCompany(
async getIssueInvoiceByIdInCompany(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<CustomerInvoice>> {
return await this.repository.getByIdInCompany(companyId, invoiceId, transaction);
return await this.repository.getByIdInCompany(companyId, invoiceId, transaction, {
where: {
is_proforma: false,
},
});
}
/**
* Recupera una proforma por su identificador único.
*
* @param proformaId - Identificador UUID de la proforma.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - proforma encontrada o error.
*/
async getProformaByIdInCompany(
companyId: UniqueID,
proformaId: UniqueID,
transaction?: Transaction
): Promise<Result<CustomerInvoice>> {
return await this.repository.getByIdInCompany(companyId, proformaId, transaction, {
where: {
is_proforma: true,
},
});
}
/**
@ -164,18 +188,18 @@ export class CustomerInvoiceApplicationService {
* No lo guarda en el repositorio.
*
* @param companyId - Identificador de la empresa a la que pertenece la factura.
* @param invoiceId - Identificador de la factura a actualizar.
* @param proformaId - Identificador de la factura a actualizar.
* @param changes - Subconjunto de props válidas para aplicar.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura actualizada o error.
*/
async patchInvoiceByIdInCompany(
async patchProformaByIdInCompany(
companyId: UniqueID,
invoiceId: UniqueID,
proformaId: UniqueID,
changes: CustomerInvoicePatchProps,
transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> {
const invoiceResult = await this.getInvoiceByIdInCompany(companyId, invoiceId, transaction);
const invoiceResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction);
if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error);

View File

@ -1 +0,0 @@
export * from "./create-customer-invoice.use-case";

View File

@ -1,7 +1,2 @@
export * from "./change-status-customer-invoice.use-case";
export * from "./create";
export * from "./get-customer-invoice.use-case";
export * from "./issue-customer-invoice.use-case";
export * from "./list-customer-invoices.use-case";
export * from "./report";
export * from "./update";
export * from "./issue-invoices";
export * from "./proformas";

View File

@ -1,22 +1,23 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceFullPresenter } from "../presenters/domain";
import { CustomerInvoiceApplicationService } from "../services";
type GetCustomerInvoiceUseCaseInput = {
import type { CustomerInvoiceFullPresenter } from "../../presenters/domain";
import type { CustomerInvoiceApplicationService } from "../../services";
type GetIssueInvoiceUseCaseInput = {
companyId: UniqueID;
invoice_id: string;
};
export class GetCustomerInvoiceUseCase {
export class GetIssueInvoiceUseCase {
constructor(
private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry
) {}
public execute(params: GetCustomerInvoiceUseCaseInput) {
public execute(params: GetIssueInvoiceUseCaseInput) {
const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id);
@ -32,17 +33,18 @@ export class GetCustomerInvoiceUseCase {
return this.transactionManager.complete(async (transaction) => {
try {
const invoiceOrError = await this.service.getInvoiceByIdInCompany(
const invoiceOrError = await this.service.getIssueInvoiceByIdInCompany(
companyId,
invoiceId,
transaction
);
if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error);
}
const customerInvoice = invoiceOrError.data;
const dto = presenter.toOutput(customerInvoice);
const invoice = invoiceOrError.data;
const dto = presenter.toOutput(invoice);
return Result.ok(dto);
} catch (error: unknown) {

View File

@ -0,0 +1,2 @@
export * from "./get-issue-invoice.use-case";
export * from "./report-issue-invoice.use-case";

View File

@ -1,22 +1,23 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceApplicationService } from "../../services/customer-invoice-application.service";
import { CustomerInvoiceReportPDFPresenter } from "./reporter";
type ReportCustomerInvoiceUseCaseInput = {
import type { CustomerInvoiceApplicationService } from "../../services";
import type { CustomerInvoiceReportPDFPresenter } from "../proformas";
type ReportIssueInvoiceUseCaseInput = {
companyId: UniqueID;
invoice_id: string;
};
export class ReportCustomerInvoiceUseCase {
export class ReportIssueInvoiceUseCase {
constructor(
private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry
) {}
public async execute(params: ReportCustomerInvoiceUseCaseInput) {
public async execute(params: ReportIssueInvoiceUseCaseInput) {
const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id);
@ -34,11 +35,12 @@ export class ReportCustomerInvoiceUseCase {
return this.transactionManager.complete(async (transaction) => {
try {
const invoiceOrError = await this.service.getInvoiceByIdInCompany(
const invoiceOrError = await this.service.getIssueInvoiceByIdInCompany(
companyId,
invoiceId,
transaction
);
if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error);
}

View File

@ -1,17 +1,18 @@
import { ITransactionManager } from "@erp/core/api";
import { ChangeStatusCustomerInvoiceByIdRequestDTO } from "@erp/customer-invoices/common";
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { ProformaCustomerInvoiceDomainService } from "../../domain";
import { CustomerInvoiceApplicationService } from "../services";
type ChangeStatusCustomerInvoiceUseCaseInput = {
import type { ChangeStatusProformaByIdRequestDTO } from "../../../../common";
import { ProformaCustomerInvoiceDomainService } from "../../../domain";
import type { CustomerInvoiceApplicationService } from "../../services";
type ChangeStatusProformaUseCaseInput = {
companyId: UniqueID;
proforma_id: string;
dto: ChangeStatusCustomerInvoiceByIdRequestDTO;
dto: ChangeStatusProformaByIdRequestDTO;
};
export class ChangeStatusCustomerInvoiceUseCase {
export class ChangeStatusProformaUseCase {
private readonly proformaDomainService: ProformaCustomerInvoiceDomainService;
constructor(
@ -21,7 +22,7 @@ export class ChangeStatusCustomerInvoiceUseCase {
this.proformaDomainService = new ProformaCustomerInvoiceDomainService();
}
public execute(params: ChangeStatusCustomerInvoiceUseCaseInput) {
public execute(params: ChangeStatusProformaUseCaseInput) {
const {
proforma_id,
companyId,

View File

@ -1,19 +1,25 @@
import { JsonTaxCatalogProvider } from "@erp/core";
import { DuplicateEntityError, IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize";
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
import { CustomerInvoiceFullPresenter } from "../../presenters";
import { CustomerInvoiceApplicationService } from "../../services/customer-invoice-application.service";
import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props";
import type { JsonTaxCatalogProvider } from "@erp/core";
import {
DuplicateEntityError,
type IPresenterRegistry,
type ITransactionManager,
} from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
type CreateCustomerInvoiceUseCaseInput = {
import type { CreateProformaRequestDTO } from "../../../../../common";
import type { CustomerInvoiceFullPresenter } from "../../../presenters";
import type { CustomerInvoiceApplicationService } from "../../../services";
import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-proforma-props";
type CreateProformaUseCaseInput = {
companyId: UniqueID;
dto: CreateCustomerInvoiceRequestDTO;
dto: CreateProformaRequestDTO;
};
export class CreateCustomerInvoiceUseCase {
export class CreateProformaUseCase {
constructor(
private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager,
@ -21,7 +27,7 @@ export class CreateCustomerInvoiceUseCase {
private readonly taxCatalog: JsonTaxCatalogProvider
) {}
public async execute(params: CreateCustomerInvoiceUseCaseInput) {
public async execute(params: CreateProformaUseCaseInput) {
const { dto, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",
@ -51,7 +57,7 @@ export class CreateCustomerInvoiceUseCase {
// 3) Construir entidad de dominio
const proformaProps = {
...props,
invoiceNumber: Maybe.some(newProformaNumber),
invoiceNumber: newProformaNumber,
};
const buildResult = this.service.buildProformaInCompany(companyId, proformaProps, id);

View File

@ -0,0 +1 @@
export * from "./create-proforma.use-case";

View File

@ -1,39 +1,37 @@
import { JsonTaxCatalogProvider } from "@erp/core";
import type { JsonTaxCatalogProvider } from "@erp/core";
import { Tax } from "@erp/core/api";
import {
CurrencyCode,
DomainError,
extractOrPushError,
LanguageCode,
maybeFromNullableVO,
Percentage,
TextValue,
UniqueID,
UtcDate,
ValidationErrorCollection,
ValidationErrorDetail,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import {
CreateCustomerInvoiceItemRequestDTO,
CreateCustomerInvoiceRequestDTO,
} from "../../../../common/dto";
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
import {
CustomerInvoiceItem,
CustomerInvoiceItemDescription,
CustomerInvoiceItemProps,
type CustomerInvoiceItemProps,
CustomerInvoiceItems,
CustomerInvoiceNumber,
CustomerInvoiceProps,
type CustomerInvoiceProps,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
InvoicePaymentMethod,
InvoiceRecipient,
type InvoiceRecipient,
ItemAmount,
ItemDiscount,
ItemQuantity,
ItemTaxes,
} from "../../../domain";
} from "../../../../domain";
/**
* Convierte el DTO a las props validadas (CustomerProps).
@ -56,13 +54,13 @@ export class CreateCustomerInvoicePropsMapper {
this.errors = [];
}
public map(dto: CreateCustomerInvoiceRequestDTO, companyId: UniqueID) {
public map(dto: CreateProformaRequestDTO, companyId: UniqueID) {
try {
this.errors = [];
const defaultStatus = CustomerInvoiceStatus.createDraft();
const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
const isProforma = true;
@ -74,8 +72,8 @@ export class CreateCustomerInvoicePropsMapper {
const recipient = Maybe.none<InvoiceRecipient>();
const invoiceNumber = extractOrPushError(
maybeFromNullableVO(dto.invoice_number, (value) => CustomerInvoiceNumber.create(value)),
const proformaNumber = extractOrPushError(
CustomerInvoiceNumber.create(dto.invoice_number),
"invoice_number",
this.errors
);
@ -153,13 +151,14 @@ export class CreateCustomerInvoicePropsMapper {
);
}
const invoiceProps: CustomerInvoiceProps = {
const proformaProps: CustomerInvoiceProps = {
companyId,
isProforma,
proformaId: Maybe.none(),
status: defaultStatus!,
invoiceNumber: proformaNumber!,
series: series!,
invoiceNumber: invoiceNumber!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
@ -181,13 +180,13 @@ export class CreateCustomerInvoicePropsMapper {
discountPercentage: discountPercentage!,
};
return Result.ok({ id: invoiceId!, props: invoiceProps });
return Result.ok({ id: proformaId!, props: proformaProps });
} catch (err: unknown) {
return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err }));
}
}
private mapItems(items: CreateCustomerInvoiceItemRequestDTO[]) {
private mapItems(items: CreateProformaItemRequestDTO[]) {
const invoiceItems = CustomerInvoiceItems.create({
currencyCode: this.currencyCode!,
languageCode: this.languageCode!,
@ -246,10 +245,10 @@ export class CreateCustomerInvoicePropsMapper {
return invoiceItems;
}
private mapTaxes(item: CreateCustomerInvoiceItemRequestDTO, itemIndex: number) {
private mapTaxes(item: CreateProformaItemRequestDTO, itemIndex: number) {
const taxes = ItemTaxes.create([]);
item.taxes.split(",").every((tax_code, taxIndex) => {
item.taxes.split(",").forEach((tax_code, taxIndex) => {
const taxResult = Tax.createFromCode(tax_code, this.taxCatalog);
if (taxResult.isSuccess) {
taxes.add(taxResult.data);

View File

@ -1,23 +1,24 @@
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
import { EntityNotFoundError, type ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceApplicationService } from "../services";
type DeleteCustomerInvoiceUseCaseInput = {
import type { CustomerInvoiceApplicationService } from "../../services";
type DeleteProformaUseCaseInput = {
companyId: UniqueID;
invoice_id: string;
proforma_id: string;
};
export class DeleteCustomerInvoiceUseCase {
export class DeleteProformaUseCase {
constructor(
private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager
) {}
public execute(params: DeleteCustomerInvoiceUseCaseInput) {
const { invoice_id, companyId } = params;
public execute(params: DeleteProformaUseCaseInput) {
const { proforma_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id);
const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
@ -40,9 +41,7 @@ export class DeleteCustomerInvoiceUseCase {
const invoiceExists = existsCheck.data;
if (!invoiceExists) {
return Result.fail(
new EntityNotFoundError("Customer invoice", "id", invoiceId.toString())
);
return Result.fail(new EntityNotFoundError("Proforma", "id", invoiceId.toString()));
}
return await this.service.deleteInvoiceByIdInCompany(companyId, invoiceId, transaction);

View File

@ -0,0 +1,54 @@
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { CustomerInvoiceFullPresenter } from "../../presenters/domain";
import type { CustomerInvoiceApplicationService } from "../../services";
type GetProformaUseCaseInput = {
companyId: UniqueID;
proforma_id: string;
};
export class GetProformaUseCase {
constructor(
private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry
) {}
public execute(params: GetProformaUseCaseInput) {
const { proforma_id, companyId } = params;
const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const proformaId = idOrError.data;
const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",
projection: "FULL",
}) as CustomerInvoiceFullPresenter;
return this.transactionManager.complete(async (transaction) => {
try {
const proformaOrError = await this.service.getProformaByIdInCompany(
companyId,
proformaId,
transaction
);
if (proformaOrError.isFailure) {
return Result.fail(proformaOrError.error);
}
const proforma = proformaOrError.data;
const dto = presenter.toOutput(proforma);
return Result.ok(dto);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,8 @@
export * from "./change-status-proforma.use-case";
export * from "./create-proforma";
export * from "./delete-proforma.use-case";
export * from "./get-proforma.use-case";
export * from "./issue-proforma.use-case";
export * from "./list-proformas.use-case";
export * from "./report-proforma";
export * from "./update-proforma";

View File

@ -1,12 +1,13 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import {
IssueCustomerInvoiceDomainService,
ProformaCustomerInvoiceDomainService,
} from "../../domain";
import { CustomerInvoiceFullPresenter } from "../presenters";
import { CustomerInvoiceApplicationService } from "../services";
} from "../../../domain";
import type { CustomerInvoiceFullPresenter } from "../../presenters";
import type { CustomerInvoiceApplicationService } from "../../services";
type IssueCustomerInvoiceUseCaseInput = {
companyId: UniqueID;
@ -22,7 +23,7 @@ type IssueCustomerInvoiceUseCaseInput = {
* - Marca la proforma como "issued"
* - Persiste ambas dentro de la misma transacción
*/
export class IssueCustomerInvoiceUseCase {
export class IssueProformaInvoiceUseCase {
private readonly issueDomainService: IssueCustomerInvoiceDomainService;
private readonly proformaDomainService: ProformaCustomerInvoiceDomainService;

View File

@ -1,18 +1,19 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize";
import { ListCustomerInvoicesResponseDTO } from "../../../common/dto";
import { ListCustomerInvoicesPresenter } from "../presenters";
import { CustomerInvoiceApplicationService } from "../services";
import type { Transaction } from "sequelize";
type ListCustomerInvoicesUseCaseInput = {
import type { ListIssueInvoicesResponseDTO } from "../../../../common/dto";
import type { ListCustomerInvoicesPresenter } from "../../presenters";
import type { CustomerInvoiceApplicationService } from "../../services";
type ListProformasUseCaseInput = {
companyId: UniqueID;
criteria: Criteria;
};
export class ListCustomerInvoicesUseCase {
export class ListProformasUseCase {
constructor(
private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager,
@ -20,8 +21,8 @@ export class ListCustomerInvoicesUseCase {
) {}
public execute(
params: ListCustomerInvoicesUseCaseInput
): Promise<Result<ListCustomerInvoicesResponseDTO, Error>> {
params: ListProformasUseCaseInput
): Promise<Result<ListIssueInvoicesResponseDTO, Error>> {
const { criteria, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",

View File

@ -0,0 +1,2 @@
export * from "./report-proforma.use-case";
export * from "./reporter";

View File

@ -0,0 +1,59 @@
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service";
import type { CustomerInvoiceReportPDFPresenter } from "./reporter";
type ReportProformaUseCaseInput = {
companyId: UniqueID;
proforma_id: string;
};
export class ReportProformaUseCase {
constructor(
private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry
) {}
public async execute(params: ReportProformaUseCaseInput) {
const { proforma_id, companyId } = params;
const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const invoiceId = idOrError.data;
const pdfPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",
projection: "REPORT",
format: "PDF",
}) as CustomerInvoiceReportPDFPresenter;
return this.transactionManager.complete(async (transaction) => {
try {
const proformaOrError = await this.service.getProformaByIdInCompany(
companyId,
invoiceId,
transaction
);
if (proformaOrError.isFailure) {
return Result.fail(proformaOrError.error);
}
const proforma = proformaOrError.data;
const pdfData = await pdfPresenter.toOutput(proforma);
return Result.ok({
data: pdfData,
filename: `proforma-${proforma.invoiceNumber}.pdf`,
});
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -1,9 +1,14 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { Presenter } from "@erp/core/api";
import * as handlebars from "handlebars";
import { CustomerInvoice } from "../../../../domain";
import { CustomerInvoiceFullPresenter, CustomerInvoiceReportPresenter } from "../../../presenters";
import type { CustomerInvoice } from "../../../../../domain";
import type {
CustomerInvoiceFullPresenter,
CustomerInvoiceReportPresenter,
} from "../../../../presenters";
export class CustomerInvoiceReportHTMLPresenter extends Presenter {
toOutput(customerInvoice: CustomerInvoice): string {

View File

@ -1,8 +1,10 @@
import { Presenter } from "@erp/core/api";
import puppeteer from "puppeteer";
import report from "puppeteer-report";
import { CustomerInvoice } from "../../../../domain";
import { CustomerInvoiceReportHTMLPresenter } from "./customer-invoice.report.html";
import type { CustomerInvoice } from "../../../../../domain";
import type { CustomerInvoiceReportHTMLPresenter } from "./customer-invoice.report.html";
// https://plnkr.co/edit/lWk6Yd?preview

View File

@ -6,14 +6,14 @@ import {
UniqueID,
UtcDate,
ValidationErrorCollection,
ValidationErrorDetail,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common/dto";
import { CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../domain";
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto";
import { type CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../../domain";
/**
* mapDTOToUpdateCustomerInvoicePatchProps
@ -29,7 +29,7 @@ import { CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../domain
*
*/
export function mapDTOToUpdateCustomerInvoicePatchProps(dto: UpdateCustomerInvoiceByIdRequestDTO) {
export function mapDTOToUpdateCustomerInvoicePatchProps(dto: UpdateProformaByIdRequestDTO) {
try {
const errors: ValidationErrorDetail[] = [];
const props: CustomerInvoicePatchProps = {};

View File

@ -1,17 +1,19 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize";
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common";
import { CustomerInvoicePatchProps } from "../../../domain";
import { CustomerInvoiceFullPresenter } from "../../presenters";
import { CustomerInvoiceApplicationService } from "../../services/customer-invoice-application.service";
import type { Transaction } from "sequelize";
import type { UpdateProformaByIdRequestDTO } from "../../../../../common";
import type { CustomerInvoicePatchProps } from "../../../../domain";
import type { CustomerInvoiceFullPresenter } from "../../../presenters";
import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service";
import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props";
type UpdateCustomerInvoiceUseCaseInput = {
companyId: UniqueID;
invoice_id: string;
dto: UpdateCustomerInvoiceByIdRequestDTO;
proforma_id: string;
dto: UpdateProformaByIdRequestDTO;
};
export class UpdateCustomerInvoiceUseCase {
@ -22,9 +24,9 @@ export class UpdateCustomerInvoiceUseCase {
) {}
public execute(params: UpdateCustomerInvoiceUseCaseInput) {
const { companyId, invoice_id, dto } = params;
const { companyId, proforma_id, dto } = params;
const idOrError = UniqueID.create(invoice_id);
const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
@ -45,7 +47,7 @@ export class UpdateCustomerInvoiceUseCase {
return this.transactionManager.complete(async (transaction: Transaction) => {
try {
const updatedInvoice = await this.service.patchInvoiceByIdInCompany(
const updatedInvoice = await this.service.patchProformaByIdInCompany(
companyId,
invoiceId,
patchProps,
@ -56,7 +58,7 @@ export class UpdateCustomerInvoiceUseCase {
return Result.fail(updatedInvoice.error);
}
const invoiceOrError = await this.service.updateInvoiceInCompany(
const invoiceOrError = await this.service.updateProformaInCompany(
companyId,
updatedInvoice.data,
transaction

View File

@ -1,2 +0,0 @@
export * from "./report-customer-invoice.use-case";
export * from "./reporter";

View File

@ -1,21 +1,22 @@
import {
AggregateRoot,
CurrencyCode,
type CurrencyCode,
DomainValidationError,
LanguageCode,
Percentage,
TextValue,
UniqueID,
UtcDate,
type LanguageCode,
type Percentage,
type TextValue,
type UniqueID,
type UtcDate,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoiceItems, InvoicePaymentMethod, InvoiceTaxTotal } from "../entities";
import { type Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoiceItems, type InvoicePaymentMethod, type InvoiceTaxTotal } from "../entities";
import {
CustomerInvoiceNumber,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
type CustomerInvoiceNumber,
type CustomerInvoiceSerie,
type CustomerInvoiceStatus,
InvoiceAmount,
InvoiceRecipient,
type InvoiceRecipient,
} from "../value-objects";
export interface CustomerInvoiceProps {
@ -24,7 +25,7 @@ export interface CustomerInvoiceProps {
isProforma: boolean;
status: CustomerInvoiceStatus;
proformaId: Maybe<UniqueID>;
proformaId: Maybe<UniqueID>; // <- proforma padre en caso de issue
series: Maybe<CustomerInvoiceSerie>;
invoiceNumber: CustomerInvoiceNumber;
@ -106,7 +107,7 @@ export class CustomerInvoice
// Reglas de negocio / validaciones
if (!customerInvoice.isProforma && !customerInvoice.hasRecipient) {
if (!(customerInvoice.isProforma || customerInvoice.hasRecipient)) {
return Result.fail(
new DomainValidationError(
"MISSING_CUSTOMER_DATA",

View File

@ -1,9 +1,10 @@
import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoiceListDTO } from "../../infrastructure";
import { CustomerInvoice } from "../aggregates";
import { CustomerInvoiceStatus } from "../value-objects";
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { CustomerInvoiceListDTO } from "../../infrastructure";
import type { CustomerInvoice } from "../aggregates";
import type { CustomerInvoiceStatus } from "../value-objects";
/**
* Interfaz del repositorio para el agregado `CustomerInvoice`.
@ -45,7 +46,8 @@ export interface ICustomerInvoiceRepository {
getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
transaction: unknown,
options: unknown
): Promise<Result<CustomerInvoice, Error>>;
/**

View File

@ -1,7 +1,9 @@
import { IModuleServer, ModuleParams } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Transaction } from "sequelize";
import { buildCustomerInvoiceDependencies, customerInvoicesRouter, models } from "./infrastructure";
import type { IModuleServer, ModuleParams } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Transaction } from "sequelize";
import { buildCustomerInvoiceDependencies, models, proformasRouter } from "./infrastructure";
import { issueInvoicesRouter } from "./infrastructure/express/issue-invoices.routes";
export const customerInvoicesAPIModule: IModuleServer = {
name: "customer-invoices",
@ -11,7 +13,8 @@ export const customerInvoicesAPIModule: IModuleServer = {
async init(params: ModuleParams) {
// const contacts = getService<ContactsService>("contacts");
const { logger } = params;
customerInvoicesRouter(params);
proformasRouter(params);
issueInvoicesRouter(params);
logger.info("🚀 CustomerInvoices module initialized", { label: this.name });
},

View File

@ -1,15 +1,16 @@
// modules/invoice/infrastructure/invoice-dependencies.factory.ts
import { JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core";
import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core";
import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api";
import {
InMemoryMapperRegistry,
InMemoryPresenterRegistry,
SequelizeTransactionManager,
} from "@erp/core/api";
import {
ChangeStatusCustomerInvoiceUseCase,
CreateCustomerInvoiceUseCase,
ChangeStatusProformaUseCase,
CreateProformaUseCase,
CustomerInvoiceApplicationService,
CustomerInvoiceFullPresenter,
CustomerInvoiceItemsFullPresenter,
@ -17,14 +18,15 @@ import {
CustomerInvoiceReportHTMLPresenter,
CustomerInvoiceReportPDFPresenter,
CustomerInvoiceReportPresenter,
GetCustomerInvoiceUseCase,
IssueCustomerInvoiceUseCase,
GetProformaUseCase,
IssueProformaInvoiceUseCase,
ListCustomerInvoicesPresenter,
ListCustomerInvoicesUseCase,
ListProformasUseCase,
RecipientInvoiceFullPresenter,
ReportCustomerInvoiceUseCase,
ReportProformaUseCase,
UpdateCustomerInvoiceUseCase,
} from "../application";
import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers";
import { CustomerInvoiceRepository } from "./sequelize";
import { SequelizeInvoiceNumberGenerator } from "./services";
@ -39,14 +41,14 @@ export type CustomerInvoiceDeps = {
taxes: JsonTaxCatalogProvider;
};
useCases: {
list: () => ListCustomerInvoicesUseCase;
get: () => GetCustomerInvoiceUseCase;
create: () => CreateCustomerInvoiceUseCase;
list: () => ListProformasUseCase;
get: () => GetProformaUseCase;
create: () => CreateProformaUseCase;
update: () => UpdateCustomerInvoiceUseCase;
//delete: () => DeleteCustomerInvoiceUseCase;
report: () => ReportCustomerInvoiceUseCase;
issue: () => IssueCustomerInvoiceUseCase;
changeStatus: () => ChangeStatusCustomerInvoiceUseCase;
report: () => ReportProformaUseCase;
issue: () => IssueProformaInvoiceUseCase;
changeStatus: () => ChangeStatusProformaUseCase;
};
};
@ -117,21 +119,15 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
]);
const useCases = {
list: () => new ListCustomerInvoicesUseCase(appService, transactionManager, presenterRegistry),
get: () => new GetCustomerInvoiceUseCase(appService, transactionManager, presenterRegistry),
list: () => new ListProformasUseCase(appService, transactionManager, presenterRegistry),
get: () => new GetProformaUseCase(appService, transactionManager, presenterRegistry),
create: () =>
new CreateCustomerInvoiceUseCase(
appService,
transactionManager,
presenterRegistry,
catalogs.taxes
),
new CreateProformaUseCase(appService, transactionManager, presenterRegistry, catalogs.taxes),
update: () =>
new UpdateCustomerInvoiceUseCase(appService, transactionManager, presenterRegistry),
report: () =>
new ReportCustomerInvoiceUseCase(appService, transactionManager, presenterRegistry),
issue: () => new IssueCustomerInvoiceUseCase(appService, transactionManager, presenterRegistry),
changeStatus: () => new ChangeStatusCustomerInvoiceUseCase(appService, transactionManager),
report: () => new ReportProformaUseCase(appService, transactionManager, presenterRegistry),
issue: () => new IssueProformaInvoiceUseCase(appService, transactionManager, presenterRegistry),
changeStatus: () => new ChangeStatusProformaUseCase(appService, transactionManager),
};
return {

View File

@ -1,28 +0,0 @@
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { DeleteCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
export class DeleteCustomerInvoiceController extends ExpressController {
public constructor(
private readonly useCase: DeleteCustomerInvoiceUseCase
/* private readonly presenter: any */
) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
async executeImpl() {
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
const { id } = this.req.params;
const result = await this.useCase.execute({ id, tenantId });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -1,8 +1,2 @@
export * from "./change-status-customer-invoice.controller";
export * from "./create-customer-invoice.controller";
//export * from "./delete-customer-invoice.controller";
export * from "./get-customer-invoice.controller";
export * from "./issue-customer-invoice.controller";
export * from "./list-customer-invoices.controller";
export * from "./report-customer-invoice.controller";
export * from "./update-customer-invoice.controller";
export * from "./issue-invoices";
export * from "./proformas";

View File

@ -1,9 +1,10 @@
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { GetCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
export class GetCustomerInvoiceController extends ExpressController {
public constructor(private readonly useCase: GetCustomerInvoiceUseCase) {
import type { GetProformaUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class GetIssueInvoiceController extends ExpressController {
public constructor(private readonly useCase: GetProformaUseCase) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;
@ -18,7 +19,7 @@ export class GetCustomerInvoiceController extends ExpressController {
}
const { invoice_id } = this.req.params;
const result = await this.useCase.execute({ invoice_id, companyId });
const result = await this.useCase.execute({ proforma_id: invoice_id, companyId });
return result.match(
(data) => this.ok(data),

View File

@ -0,0 +1,2 @@
export * from "./get-issue-invoice.controller";
export * from "./list-issue-invoices.controller";

View File

@ -1,10 +1,11 @@
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import { ListCustomerInvoicesUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
export class ListCustomerInvoicesController extends ExpressController {
public constructor(private readonly useCase: ListCustomerInvoicesUseCase) {
import type { ListProformasUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class ListIssueInvoicesController extends ExpressController {
public constructor(private readonly useCase: ListProformasUseCase) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;

View File

@ -1,10 +1,11 @@
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { ChangeStatusCustomerInvoiceByIdRequestDTO } from "@erp/customer-invoices/common";
import { ChangeStatusCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
export class ChangeStatusCustomerInvoiceController extends ExpressController {
public constructor(private readonly useCase: ChangeStatusCustomerInvoiceUseCase) {
import type { ChangeStatusProformaByIdRequestDTO } from "../../../../../common/dto";
import type { ChangeStatusProformaUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class ChangeStatusProformaController extends ExpressController {
public constructor(private readonly useCase: ChangeStatusProformaUseCase) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;
@ -12,7 +13,7 @@ export class ChangeStatusCustomerInvoiceController extends ExpressController {
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
async executeImpl(): Promise<any> {
protected async executeImpl() {
const companyId = this.getTenantId(); // garantizado por tenantGuard
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
@ -23,7 +24,7 @@ export class ChangeStatusCustomerInvoiceController extends ExpressController {
return this.invalidInputError("Proforma ID missing");
}
const dto = this.req.body as ChangeStatusCustomerInvoiceByIdRequestDTO;
const dto = this.req.body as ChangeStatusProformaByIdRequestDTO;
const result = await this.useCase.execute({ proforma_id, dto, companyId });
return result.match(

View File

@ -1,11 +1,11 @@
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
import { CreateCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
import type { CreateProformaRequestDTO } from "../../../../../common/dto";
import type { CreateProformaUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class CreateCustomerInvoiceController extends ExpressController {
public constructor(private readonly useCase: CreateCustomerInvoiceUseCase) {
export class CreateProformaController extends ExpressController {
public constructor(private readonly useCase: CreateProformaUseCase) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;
@ -18,7 +18,7 @@ export class CreateCustomerInvoiceController extends ExpressController {
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const dto = this.req.body as CreateCustomerInvoiceRequestDTO;
const dto = this.req.body as CreateProformaRequestDTO;
const result = await this.useCase.execute({ dto, companyId });

View File

@ -0,0 +1,33 @@
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import type { DeleteProformaUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class DeleteProformaController extends ExpressController {
public constructor(
private readonly useCase: DeleteProformaUseCase
/* private readonly presenter: any */
) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { proforma_id } = this.req.params;
const result = await this.useCase.execute({ proforma_id, companyId });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,29 @@
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import type { GetProformaUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class GetProformaController extends ExpressController {
public constructor(private readonly useCase: GetProformaUseCase) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { proforma_id } = this.req.params;
const result = await this.useCase.execute({ proforma_id, companyId });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,8 @@
export * from "./change-status-proforma.controller";
export * from "./create-proforma.controller";
export * from "./delete-proforma.controller";
export * from "./get-proforma.controller";
export * from "./issue-proforma.controller";
export * from "./list-proformas.controller";
export * from "./report-proforma.controller";
export * from "./update-proforma.controller";

View File

@ -1,9 +1,10 @@
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { IssueCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
export class IssueCustomerInvoiceController extends ExpressController {
public constructor(private readonly useCase: IssueCustomerInvoiceUseCase) {
import type { IssueProformaInvoiceUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class IssueProformaController extends ExpressController {
public constructor(private readonly useCase: IssueProformaInvoiceUseCase) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;
@ -11,7 +12,7 @@ export class IssueCustomerInvoiceController extends ExpressController {
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
async executeImpl(): Promise<any> {
protected async executeImpl() {
const companyId = this.getTenantId(); // garantizado por tenantGuard
if (!companyId) {
return this.forbiddenError("Tenant ID not found");

View File

@ -0,0 +1,52 @@
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import type { ListProformasUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class ListProformasController extends ExpressController {
public constructor(private readonly useCase: ListProformasUseCase) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
private getCriteriaWithDefaultOrder() {
if (this.criteria.hasOrder()) {
return this.criteria;
}
const { q: quicksearch, filters, pageSize, pageNumber } = this.criteria.toPrimitives();
return Criteria.fromPrimitives(
filters,
"invoice_date",
"DESC",
pageSize,
pageNumber,
quicksearch
);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const criteria = this.getCriteriaWithDefaultOrder();
const result = await this.useCase.execute({ criteria, companyId });
return result.match(
(data) =>
this.ok(data, {
"X-Total-Count": String(data.total_items),
"Pagination-Count": String(data.total_pages),
"Pagination-Page": String(data.page),
"Pagination-Limit": String(data.per_page),
}),
(err) => this.handleError(err)
);
}
}

View File

@ -1,9 +1,10 @@
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { ReportCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
export class ReportCustomerInvoiceController extends ExpressController {
public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) {
import type { ReportProformaUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class ReportProformaController extends ExpressController {
public constructor(private readonly useCase: ReportProformaUseCase) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;
@ -16,9 +17,9 @@ export class ReportCustomerInvoiceController extends ExpressController {
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { invoice_id } = this.req.params;
const { proforma_id } = this.req.params;
const result = await this.useCase.execute({ invoice_id, companyId });
const result = await this.useCase.execute({ proforma_id, companyId });
return result.match(
({ data, filename }) => this.downloadPDF(data, filename),

View File

@ -1,9 +1,10 @@
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common/dto";
import { UpdateCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
export class UpdateCustomerInvoiceController extends ExpressController {
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto";
import type { UpdateCustomerInvoiceUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class UpdateProformaController extends ExpressController {
public constructor(private readonly useCase: UpdateCustomerInvoiceUseCase) {
super();
this.errorMapper = customerInvoicesApiErrorMapper;
@ -12,7 +13,7 @@ export class UpdateCustomerInvoiceController extends ExpressController {
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
async executeImpl(): Promise<any> {
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
@ -23,9 +24,9 @@ export class UpdateCustomerInvoiceController extends ExpressController {
return this.invalidInputError("Proforma ID missing");
}
const dto = this.req.body as UpdateCustomerInvoiceByIdRequestDTO;
const dto = this.req.body as UpdateProformaByIdRequestDTO;
const result = await this.useCase.execute({ invoice_id: proforma_id, companyId, dto });
const result = await this.useCase.execute({ proforma_id, companyId, dto });
return result.match(
(data) => this.ok(data),

View File

@ -4,16 +4,17 @@
import {
ApiErrorMapper,
ConflictApiError,
ErrorToApiRule,
type ErrorToApiRule,
ValidationApiError,
} from "@erp/core/api";
import {
CustomerInvoiceIdAlreadyExistsError,
EntityIsNotProformaError,
type CustomerInvoiceIdAlreadyExistsError,
type EntityIsNotProformaError,
type ProformaCannotBeConvertedToInvoiceError,
isCustomerInvoiceIdAlreadyExistsError,
isEntityIsNotProformaError,
isProformaCannotBeConvertedToInvoiceError,
ProformaCannotBeConvertedToInvoiceError,
} from "../../domain";
// Crea una regla específica (prioridad alta para sobreescribir mensajes)

View File

@ -1 +1,2 @@
export * from "./customer-invoices.routes";
export * from "./issue-invoices.routes";
export * from "./proformas.routes";

View File

@ -0,0 +1,83 @@
import { type RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { type ModuleParams, validateRequest } from "@erp/core/api";
import type { ILogger } from "@repo/rdx-logger";
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
import type { Sequelize } from "sequelize";
import {
GetIssueInvoiceByIdRequestSchema,
ListIssueInvoicesRequestSchema,
ReportIssueInvoiceByIdRequestSchema,
} from "../../../common/dto";
import { buildCustomerInvoiceDependencies } from "../dependencies";
import {
GetProformaController,
ListProformasController,
ReportProformaController,
} from "./controllers";
export const issueInvoicesRouter = (params: ModuleParams) => {
const { app, baseRoutePath, logger } = params as {
app: Application;
database: Sequelize;
baseRoutePath: string;
logger: ILogger;
};
const deps = buildCustomerInvoiceDependencies(params);
const router: Router = Router({ mergeParams: true });
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
// 🔐 Autenticación + Tenancy para TODO el router
router.use(
(req: Request, res: Response, next: NextFunction) =>
mockUser(req as RequestWithAuth, res, next) // Debe ir antes de las rutas protegidas
);
}
router.use([
(req: Request, res: Response, next: NextFunction) =>
enforceUser()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
(req: Request, res: Response, next: NextFunction) =>
enforceTenant()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
]);
// ----------------------------------------------
router.get(
"/",
//checkTabContext,
validateRequest(ListIssueInvoicesRequestSchema, "params"),
async (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.list();
const controller = new ListProformasController(useCase /*, deps.presenters.list */);
return controller.execute(req, res, next);
}
);
router.get(
"/:invoice_id",
//checkTabContext,
validateRequest(GetIssueInvoiceByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.get();
const controller = new GetProformaController(useCase);
return controller.execute(req, res, next);
}
);
router.get(
"/:invoice_id/report",
//checkTabContext,
validateRequest(ReportIssueInvoiceByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.report();
const controller = new ReportProformaController(useCase);
return controller.execute(req, res, next);
}
);
app.use(`${baseRoutePath}/customer-invoices`, router);
};

View File

@ -1,30 +1,32 @@
import { enforceTenant, enforceUser, mockUser, RequestWithAuth } from "@erp/auth/api";
import { ModuleParams, validateRequest } from "@erp/core/api";
import { ILogger } from "@repo/rdx-logger";
import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize";
import { type RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { type ModuleParams, validateRequest } from "@erp/core/api";
import {
ChangeStatusCustomerInvoiceByIdParamsRequestSchema,
ChangeStatusCustomerInvoiceByIdRequestSchema,
CreateCustomerInvoiceRequestSchema,
CustomerInvoiceListRequestSchema,
GetCustomerInvoiceByIdRequestSchema,
ReportCustomerInvoiceByIdRequestSchema,
UpdateCustomerInvoiceByIdParamsRequestSchema,
UpdateCustomerInvoiceByIdRequestSchema,
} from "../../../common/dto";
import { buildCustomerInvoiceDependencies } from "../dependencies";
import {
ChangeStatusCustomerInvoiceController,
CreateCustomerInvoiceController,
GetCustomerInvoiceController,
ListCustomerInvoicesController,
ReportCustomerInvoiceController,
UpdateCustomerInvoiceController,
} from "./controllers";
import { IssueCustomerInvoiceController } from "./controllers/issue-customer-invoice.controller";
ChangeStatusProformaByIdParamsRequestSchema,
ChangeStatusProformaByIdRequestSchema,
CreateProformaRequestSchema,
GetProformaByIdRequestSchema,
ListProformasRequestSchema,
ReportProformaByIdRequestSchema,
UpdateProformaByIdParamsRequestSchema,
UpdateProformaByIdRequestSchema,
} from "@erp/customer-invoices/common";
import type { ILogger } from "@repo/rdx-logger";
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
import type { Sequelize } from "sequelize";
export const customerInvoicesRouter = (params: ModuleParams) => {
import { buildCustomerInvoiceDependencies } from "../dependencies";
import {
ChangeStatusProformaController,
CreateProformaController,
GetProformaController,
ListProformasController,
ReportProformaController,
UpdateProformaController,
} from "./controllers/proformas";
import { IssueProformaController } from "./controllers/proformas/issue-proforma.controller";
export const proformasRouter = (params: ModuleParams) => {
const { app, baseRoutePath, logger } = params as {
app: Application;
database: Sequelize;
@ -56,21 +58,21 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
router.get(
"/",
//checkTabContext,
validateRequest(CustomerInvoiceListRequestSchema, "params"),
validateRequest(ListProformasRequestSchema, "params"),
async (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.list();
const controller = new ListCustomerInvoicesController(useCase /*, deps.presenters.list */);
const controller = new ListProformasController(useCase /*, deps.presenters.list */);
return controller.execute(req, res, next);
}
);
router.get(
"/:invoice_id",
"/:proforma_id",
//checkTabContext,
validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"),
validateRequest(GetProformaByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.get();
const controller = new GetCustomerInvoiceController(useCase);
const controller = new GetProformaController(useCase);
return controller.execute(req, res, next);
}
);
@ -79,10 +81,10 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
"/",
//checkTabContext,
validateRequest(CreateCustomerInvoiceRequestSchema, "body"),
validateRequest(CreateProformaRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.create();
const controller = new CreateCustomerInvoiceController(useCase);
const controller = new CreateProformaController(useCase);
return controller.execute(req, res, next);
}
);
@ -91,17 +93,17 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
"/:proforma_id",
//checkTabContext,
validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"),
validateRequest(UpdateCustomerInvoiceByIdRequestSchema, "body"),
validateRequest(UpdateProformaByIdParamsRequestSchema, "params"),
validateRequest(UpdateProformaByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.update();
const controller = new UpdateCustomerInvoiceController(useCase);
const controller = new UpdateProformaController(useCase);
return controller.execute(req, res, next);
}
);
/*router.delete(
"/:invoice_id",
"/:proforma_id",
//checkTabContext,
validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"),
@ -113,12 +115,12 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
);*/
router.get(
"/:invoice_id/report",
"/:proforma_id/report",
//checkTabContext,
validateRequest(ReportCustomerInvoiceByIdRequestSchema, "params"),
validateRequest(ReportProformaByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.report();
const controller = new ReportCustomerInvoiceController(useCase);
const controller = new ReportProformaController(useCase);
return controller.execute(req, res, next);
}
);
@ -127,12 +129,12 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
"/:proforma_id/status",
//checkTabContext,
validateRequest(ChangeStatusCustomerInvoiceByIdParamsRequestSchema, "params"),
validateRequest(ChangeStatusCustomerInvoiceByIdRequestSchema, "body"),
validateRequest(ChangeStatusProformaByIdParamsRequestSchema, "params"),
validateRequest(ChangeStatusProformaByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.changeStatus();
const controller = new ChangeStatusCustomerInvoiceController(useCase);
const controller = new ChangeStatusProformaController(useCase);
return controller.execute(req, res, next);
}
);
@ -146,10 +148,10 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.issue();
const controller = new IssueCustomerInvoiceController(useCase);
const controller = new IssueProformaController(useCase);
return controller.execute(req, res, next);
}
);
app.use(`${baseRoutePath}/customer-invoices`, router);
app.use(`${baseRoutePath}/proformas`, router);
};

View File

@ -4,16 +4,22 @@ import {
SequelizeRepository,
translateSequelizeError,
} from "@erp/core/api";
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize";
import { CustomerInvoice, CustomerInvoiceStatus, ICustomerInvoiceRepository } from "../../domain";
import {
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Result } from "@repo/rdx-utils";
import type { FindOptions, InferAttributes, Transaction } from "sequelize";
import type {
CustomerInvoice,
CustomerInvoiceStatus,
ICustomerInvoiceRepository,
} from "../../domain";
import type {
CustomerInvoiceListDTO,
ICustomerInvoiceDomainMapper,
ICustomerInvoiceListMapper,
} from "../mappers";
import { CustomerInvoiceModel } from "./models/customer-invoice.model";
import { CustomerInvoiceItemModel } from "./models/customer-invoice-item.model";
import { CustomerInvoiceItemTaxModel } from "./models/customer-invoice-item-tax.model";
@ -185,12 +191,14 @@ export class CustomerInvoiceRepository
* @param companyId - Identificador UUID de la empresa a la que pertenece la factura.
* @param id - UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @params options - Opciones adicionales para la consulta (Sequelize FindOptions)
* @returns Result<CustomerInvoice, Error>
*/
async getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
transaction: Transaction,
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<CustomerInvoice, Error>> {
const { CustomerModel } = this._database.models;
@ -199,14 +207,36 @@ export class CustomerInvoiceRepository
resource: "customer-invoice",
});
const row = await CustomerInvoiceModel.findOne({
where: { id: id.toString(), company_id: companyId.toString() },
order: [[{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"]],
// Normalización defensiva de order/include
const normalizedOrder = Array.isArray(options.order)
? options.order
: options.order
? [options.order]
: [];
const normalizedInclude = Array.isArray(options.include)
? options.include
: options.include
? [options.include]
: [];
const mergedOptions: FindOptions<InferAttributes<CustomerInvoiceModel>> = {
...options,
where: {
id: id.toString(),
company_id: companyId.toString(),
...(options.where ?? {}),
},
order: [
...normalizedOrder,
[{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"],
],
include: [
...normalizedInclude,
{
model: CustomerModel,
as: "current_customer",
required: false, // false => LEFT JOIN
required: false,
},
{
model: CustomerInvoiceItemModel,
@ -227,7 +257,9 @@ export class CustomerInvoiceRepository
},
],
transaction,
});
};
const row = await CustomerInvoiceModel.findOne(mergedOptions);
if (!row) {
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));

View File

@ -1,13 +0,0 @@
import { z } from "zod/v4";
export const ChangeStatusCustomerInvoiceByIdParamsRequestSchema = z.object({
proforma_id: z.string(),
});
export const ChangeStatusCustomerInvoiceByIdRequestSchema = z.object({
new_status: z.string(),
});
export type ChangeStatusCustomerInvoiceByIdRequestDTO = Partial<
z.infer<typeof ChangeStatusCustomerInvoiceByIdRequestSchema>
>;

View File

@ -1,5 +0,0 @@
import { CriteriaSchema } from "@erp/core";
import { z } from "zod/v4";
export const CustomerInvoiceListRequestSchema = CriteriaSchema;
export type CustomerInvoiceListRequestDTO = z.infer<typeof CustomerInvoiceListRequestSchema>;

View File

@ -1,15 +0,0 @@
import { z } from "zod/v4";
/**
* Este DTO es utilizado por el endpoint:
* `DELETE /customer-invoices/:id` (eliminar una factura por ID).
*
*/
export const DeleteCustomerInvoiceByIdRequestSchema = z.object({
id: z.string(),
});
export type DeleteCustomerInvoiceByIdRequestDTO = z.infer<
typeof DeleteCustomerInvoiceByIdRequestSchema
>;

View File

@ -1,7 +0,0 @@
import { z } from "zod/v4";
export const GetCustomerInvoiceByIdRequestSchema = z.object({
invoice_id: z.string(),
});
export type GetCustomerInvoiceByIdRequestDTO = z.infer<typeof GetCustomerInvoiceByIdRequestSchema>;

View File

@ -1,7 +1,2 @@
export * from "./change-status-customer-invoice-by-id.request.dto";
export * from "./create-customer-invoice.request.dto";
export * from "./customer-invoices-list.request.dto";
export * from "./delete-customer-invoice-by-id.request.dto";
export * from "./get-customer-invoice-by-id.request.dto";
export * from "./report-customer-invoice-by-id.request.dto";
export * from "./update-customer-invoice-by-id.request.dto";
export * from "./issue-invoices";
export * from "./proformas";

View File

@ -0,0 +1,7 @@
import { z } from "zod/v4";
export const GetIssueInvoiceByIdRequestSchema = z.object({
invoice_id: z.string(),
});
export type GetIssueInvoiceByIdRequestDTO = z.infer<typeof GetIssueInvoiceByIdRequestSchema>;

View File

@ -0,0 +1,3 @@
export * from "./get-issue-invoice-by-id.request.dto";
export * from "./list-issue-invoices.request.dto";
export * from "./report-issue-invoice-by-id.request.dto";

View File

@ -0,0 +1,5 @@
import { CriteriaSchema } from "@erp/core";
import { z } from "zod/v4";
export const ListIssueInvoicesRequestSchema = CriteriaSchema;
export type ListIssueInvoicesRequestDTO = z.infer<typeof ListIssueInvoicesRequestSchema>;

View File

@ -0,0 +1,7 @@
import { z } from "zod/v4";
export const ReportIssueInvoiceByIdRequestSchema = z.object({
invoice_id: z.string(),
});
export type ReportIssueInvoiceByIdRequestDTO = z.infer<typeof ReportIssueInvoiceByIdRequestSchema>;

View File

@ -0,0 +1,13 @@
import { z } from "zod/v4";
export const ChangeStatusProformaByIdParamsRequestSchema = z.object({
proforma_id: z.string(),
});
export const ChangeStatusProformaByIdRequestSchema = z.object({
new_status: z.string(),
});
export type ChangeStatusProformaByIdRequestDTO = Partial<
z.infer<typeof ChangeStatusProformaByIdRequestSchema>
>;

View File

@ -1,7 +1,7 @@
import { NumericStringSchema, PercentageSchema } from "@erp/core";
import { z } from "zod/v4";
export const CreateCustomerInvoiceItemRequestSchema = z.object({
export const CreateProformaItemRequestSchema = z.object({
id: z.uuid(),
position: z.string(),
description: z.string().default(""),
@ -10,8 +10,9 @@ export const CreateCustomerInvoiceItemRequestSchema = z.object({
discount_percentage: NumericStringSchema.default(""),
taxes: z.string().default(""),
});
export type CreateProformaItemRequestDTO = z.infer<typeof CreateProformaItemRequestSchema>;
export const CreateCustomerInvoiceRequestSchema = z.object({
export const CreateProformaRequestSchema = z.object({
id: z.uuid(),
invoice_number: z.string(),
@ -35,10 +36,6 @@ export const CreateCustomerInvoiceRequestSchema = z.object({
payment_method: z.string().default(""),
items: z.array(CreateCustomerInvoiceItemRequestSchema).default([]),
items: z.array(CreateProformaItemRequestSchema).default([]),
});
export type CreateCustomerInvoiceItemRequestDTO = z.infer<
typeof CreateCustomerInvoiceItemRequestSchema
>;
export type CreateCustomerInvoiceRequestDTO = z.infer<typeof CreateCustomerInvoiceRequestSchema>;
export type CreateProformaRequestDTO = z.infer<typeof CreateProformaRequestSchema>;

View File

@ -0,0 +1,13 @@
import { z } from "zod/v4";
/**
* Este DTO es utilizado por el endpoint:
* `DELETE /customer-invoices/:id` (eliminar una factura por ID).
*
*/
export const DeleteProformaByIdRequestSchema = z.object({
id: z.string(),
});
export type DeleteProformaByIdRequestDTO = z.infer<typeof DeleteProformaByIdRequestSchema>;

View File

@ -0,0 +1,7 @@
import { z } from "zod/v4";
export const GetProformaByIdRequestSchema = z.object({
proforma_id: z.string(),
});
export type GetProformaByIdRequestDTO = z.infer<typeof GetProformaByIdRequestSchema>;

View File

@ -0,0 +1,7 @@
export * from "./change-status-proforma-by-id.request.dto";
export * from "./create-proforma.request.dto";
export * from "./delete-proforma-by-id.request.dto";
export * from "./get-proforma-by-id.request.dto";
export * from "./list-proformas.request.dto";
export * from "./report-proforma-by-id.request.dto";
export * from "./update-proforma-by-id.request.dto";

View File

@ -0,0 +1,5 @@
import { CriteriaSchema } from "@erp/core";
import type { z } from "zod/v4";
export const ListProformasRequestSchema = CriteriaSchema;
export type ProformasListRequestDTO = z.infer<typeof ListProformasRequestSchema>;

View File

@ -0,0 +1,7 @@
import { z } from "zod/v4";
export const ReportProformaByIdRequestSchema = z.object({
proforma_id: z.string(),
});
export type ReportProformaByIdRequestDTO = z.infer<typeof ReportProformaByIdRequestSchema>;

View File

@ -1,11 +1,11 @@
import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
import { z } from "zod/v4";
export const UpdateCustomerInvoiceByIdParamsRequestSchema = z.object({
export const UpdateProformaByIdParamsRequestSchema = z.object({
proforma_id: z.string(),
});
export const UpdateCustomerInvoiceByIdRequestSchema = z.object({
export const UpdateProformaByIdRequestSchema = z.object({
series: z.string().optional(),
invoice_date: z.string().optional(),
@ -38,6 +38,4 @@ export const UpdateCustomerInvoiceByIdRequestSchema = z.object({
.default([]),
});
export type UpdateCustomerInvoiceByIdRequestDTO = Partial<
z.infer<typeof UpdateCustomerInvoiceByIdRequestSchema>
>;
export type UpdateProformaByIdRequestDTO = Partial<z.infer<typeof UpdateProformaByIdRequestSchema>>;

View File

@ -1,9 +0,0 @@
import { z } from "zod/v4";
export const ReportCustomerInvoiceByIdRequestSchema = z.object({
invoice_id: z.string(),
});
export type ReportCustomerInvoiceByIdRequestDTO = z.infer<
typeof ReportCustomerInvoiceByIdRequestSchema
>;

View File

@ -1,4 +1,2 @@
export * from "./create-customer-invoice.response.dto";
export * from "./get-customer-invoice-by-id.response.dto";
export * from "./list-customer-invoices.response.dto";
export * from "./update-customer-invoice-by-id.response.dto";
export * from "./issue-invoices";
export * from "./proformas";

View File

@ -1,7 +1,7 @@
import { MetadataSchema, MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
import { z } from "zod/v4";
export const GetCustomerInvoiceByIdResponseSchema = z.object({
export const GetIssueInvoiceByIdResponseSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
@ -79,6 +79,4 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
metadata: MetadataSchema.optional(),
});
export type GetCustomerInvoiceByIdResponseDTO = z.infer<
typeof GetCustomerInvoiceByIdResponseSchema
>;
export type GetIssueInvoiceByIdResponseDTO = z.infer<typeof GetIssueInvoiceByIdResponseSchema>;

View File

@ -0,0 +1,2 @@
export * from "./get-issue-invoice-by-id.response.dto";
export * from "./list-issue-invoices.response.dto";

View File

@ -6,7 +6,7 @@ import {
} from "@erp/core";
import { z } from "zod/v4";
export const ListCustomerInvoicesResponseSchema = createPaginatedListSchema(
export const ListIssueInvoicesResponseSchema = createPaginatedListSchema(
z.object({
id: z.uuid(),
company_id: z.uuid(),
@ -51,4 +51,4 @@ export const ListCustomerInvoicesResponseSchema = createPaginatedListSchema(
})
);
export type ListCustomerInvoicesResponseDTO = z.infer<typeof ListCustomerInvoicesResponseSchema>;
export type ListIssueInvoicesResponseDTO = z.infer<typeof ListIssueInvoicesResponseSchema>;

View File

@ -7,7 +7,7 @@ import {
} from "@erp/core";
import { z } from "zod/v4";
export const CreateCustomerInvoiceResponseSchema = z.object({
export const CreateProformaResponseSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
@ -52,4 +52,4 @@ export const CreateCustomerInvoiceResponseSchema = z.object({
metadata: MetadataSchema.optional(),
});
export type CreateCustomerInvoiceResponseDTO = z.infer<typeof CreateCustomerInvoiceResponseSchema>;
export type CreateProformaResponseDTO = z.infer<typeof CreateProformaResponseSchema>;

View File

@ -0,0 +1,82 @@
import { MetadataSchema, MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
import { z } from "zod/v4";
export const GetProformaByIdResponseSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
is_proforma: z.string(),
invoice_number: z.string(),
status: z.string(),
series: z.string(),
invoice_date: z.string(),
operation_date: z.string(),
reference: z.string(),
description: z.string(),
notes: z.string(),
language_code: z.string(),
currency_code: z.string(),
customer_id: z.string(),
recipient: z.object({
id: z.string(),
name: z.string(),
tin: z.string(),
street: z.string(),
street2: z.string(),
city: z.string(),
province: z.string(),
postal_code: z.string(),
country: z.string(),
}),
taxes: z.array(
z.object({
tax_code: z.string(),
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
})
),
payment_method: z
.object({
payment_id: z.string(),
payment_description: z.string(),
})
.optional(),
subtotal_amount: MoneySchema,
items_discount_amount: MoneySchema,
discount_percentage: PercentageSchema,
discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
items: z.array(
z.object({
id: z.uuid(),
is_valued: z.string(),
position: z.string(),
description: z.string(),
quantity: QuantitySchema,
unit_amount: MoneySchema,
tax_codes: z.array(z.string()),
subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema,
discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
})
),
metadata: MetadataSchema.optional(),
});
export type GetProformaByIdResponseDTO = z.infer<typeof GetProformaByIdResponseSchema>;

View File

@ -0,0 +1,3 @@
export * from "./create-proforma.response.dto";
export * from "./get-proforma-by-id.response.dto";
export * from "./list-proformas.response.dto";

View File

@ -0,0 +1,54 @@
import {
MetadataSchema,
MoneySchema,
PercentageSchema,
createPaginatedListSchema,
} from "@erp/core";
import { z } from "zod/v4";
export const ListProformasResponseSchema = createPaginatedListSchema(
z.object({
id: z.uuid(),
company_id: z.uuid(),
is_proforma: z.boolean(),
customer_id: z.string(),
invoice_number: z.string(),
status: z.string(),
series: z.string(),
invoice_date: z.string(),
operation_date: z.string(),
language_code: z.string(),
currency_code: z.string(),
reference: z.string(),
description: z.string(),
recipient: z.object({
tin: z.string(),
name: z.string(),
street: z.string(),
street2: z.string(),
city: z.string(),
postal_code: z.string(),
province: z.string(),
country: z.string(),
}),
taxes: z.string(),
subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema,
discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
metadata: MetadataSchema.optional(),
})
);
export type ListProformasResponseDTO = z.infer<typeof ListProformasResponseSchema>;

View File

@ -1,44 +0,0 @@
import { AmountSchema, MetadataSchema, PercentageSchema, QuantitySchema } from "@erp/core";
import { z } from "zod/v4";
export const UpdateCustomerInvoiceByIdResponseSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
invoice_number: z.string(),
status: z.string(),
series: z.string(),
invoice_date: z.string(),
operation_date: z.string(),
notes: z.string(),
language_code: z.string(),
currency_code: z.string(),
subtotal_amount: AmountSchema,
discount_percentage: PercentageSchema,
discount_amount: AmountSchema,
taxable_amount: AmountSchema,
tax_amount: AmountSchema,
total_amount: AmountSchema,
items: z.array(
z.object({
id: z.uuid(),
position: z.string(),
description: z.string(),
quantity: QuantitySchema,
unit_amount: AmountSchema,
discount_percentage: PercentageSchema,
total_amount: AmountSchema,
})
),
metadata: MetadataSchema.optional(),
});
export type UpdateCustomerInvoiceByIdResponseDTO = z.infer<
typeof UpdateCustomerInvoiceByIdResponseSchema
>;

View File

@ -1,15 +1,13 @@
import { ModuleClientParams } from "@erp/core/client";
import type { ModuleClientParams } from "@erp/core/client";
import { lazy } from "react";
import { Outlet, RouteObject } from "react-router-dom";
import { Outlet, type RouteObject } from "react-router-dom";
// Lazy load components
const InvoicesLayout = lazy(() =>
import("./components").then((m) => ({ default: m.InvoicesLayout }))
);
const InvoiceListPage = lazy(() =>
import("./pages").then((m) => ({ default: m.InvoiceListPage }))
);
const InvoiceListPage = lazy(() => import("./pages").then((m) => ({ default: m.InvoiceListPage })));
const CustomerInvoiceAdd = lazy(() =>
import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate }))
@ -20,6 +18,20 @@ const InvoiceUpdatePage = lazy(() =>
export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => {
return [
{
path: "proformas",
element: (
<InvoicesLayout>
<Outlet context={params} />
</InvoicesLayout>
),
children: [
{ path: "", index: true, element: <ProformasListPage /> }, // index
{ path: "list", element: <ProformasListPage /> },
{ path: "create", element: <CustomerInvoiceAdd /> },
{ path: ":id/edit", element: <InvoiceUpdatePage /> },
],
},
{
path: "customer-invoices",
element: (
@ -30,8 +42,6 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
children: [
{ path: "", index: true, element: <InvoiceListPage /> }, // index
{ path: "list", element: <InvoiceListPage /> },
{ path: "create", element: <CustomerInvoiceAdd /> },
{ path: ":id/edit", element: <InvoiceUpdatePage /> },
//
/*{ path: "create", element: <CustomerInvoicesList /> },

View File

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

View File

@ -0,0 +1,111 @@
import { PageHeader } from "@erp/core/components";
import { ErrorAlert } from "@erp/customers/components";
import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useInvoicesQuery } from "../../hooks";
import { useTranslation } from "../../i18n";
import { invoiceResumeDtoToFormAdapter } from "../../schemas/invoice-resume-dto.adapter";
import { InvoicesListGrid } from "./invoices-list-grid";
export const ProformaListPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [search, setSearch] = useState("");
const debouncedQ = useDebounce(search, 300);
const criteria = useMemo(
() => ({
q: debouncedQ || "",
pageSize,
pageNumber: pageIndex,
}),
[pageSize, pageIndex, debouncedQ]
);
const { data, isLoading, isError, error } = useInvoicesQuery({
criteria,
});
const invoicesPageData = useMemo(() => {
if (!data) return undefined;
return {
...data,
items: invoiceResumeDtoToFormAdapter.fromDto(data.items),
};
}, [data]);
const handlePageChange = (newPageIndex: number) => {
setPageIndex(newPageIndex);
};
const handlePageSizeChange = (newSize: number) => {
setPageSize(newSize);
setPageIndex(0);
};
const handleSearchChange = (value: string) => {
// Normalización ligera: recorta y colapsa espacios internos
const cleaned = value.trim().replace(/\s+/g, " ");
setSearch(cleaned);
setPageIndex(0);
};
if (isError || !invoicesPageData) {
return (
<AppContent>
<ErrorAlert
title={t("pages.list.loadErrorTitle")}
message={(error as Error)?.message || "Error al cargar el listado"}
/>
<BackHistoryButton />
</AppContent>
);
}
return (
<>
<AppHeader>
<PageHeader
title={t("pages.list.title")}
description={t("pages.list.description")}
rightSlot={
<div className='flex items-center space-x-2'>
<Button
onClick={() => navigate("/customer-invoices/create")}
variant={"default"}
aria-label={t("pages.create.title")}
className='cursor-pointer'
>
<PlusIcon className='mr-2 h-4 w-4' aria-hidden />
{t("pages.create.title")}
</Button>
</div>
}
/>
</AppHeader>
<AppContent>
<div className='flex flex-col w-full h-full py-3'>
<div className={"flex-1"}>
<InvoicesListGrid
invoicesPage={invoicesPageData}
loading={isLoading}
pageIndex={pageIndex}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
searchValue={search}
onSearchChange={handleSearchChange}
/>
</div>
</div>
</AppContent>
</>
);
};

View File

@ -1,5 +1,5 @@
export * from "./invoice-dto.adapter";
export * from "./invoice-resume-dto.adapter";
export * from "./invoice-resume.form.schema";
export * from "./invoice.form.schema";
export * from "./invoice-dto.adapter";
export * from "./invoice-resume.form.schema";
export * from "./invoice-resume-dto.adapter";
export * from "./invoices.api.schema";

View File

@ -1,16 +1,18 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import {
GetCustomerInvoiceByIdResponseDTO,
import type {
GetIssueInvoiceByIdResponseDTO,
UpdateCustomerInvoiceByIdRequestDTO,
} from "../../common";
import { InvoiceContextValue } from "../context";
import { InvoiceFormData } from "./invoice.form.schema";
import type { InvoiceContextValue } from "../context";
import type { InvoiceFormData } from "./invoice.form.schema";
/**
* Convierte el DTO completo de API a datos numéricos para el formulario.
*/
export const invoiceDtoToFormAdapter = {
fromDto(dto: GetCustomerInvoiceByIdResponseDTO, context: InvoiceContextValue): InvoiceFormData {
fromDto(dto: GetIssueInvoiceByIdResponseDTO, context: InvoiceContextValue): InvoiceFormData {
const { taxCatalog } = context;
return {
invoice_number: dto.invoice_number,
@ -30,6 +32,7 @@ export const invoiceDtoToFormAdapter = {
currency_code: dto.currency_code,
subtotal_amount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
items_discount_amount: 0,
discount_percentage: PercentageDTOHelper.toNumber(dto.discount_percentage),
discount_amount: MoneyDTOHelper.toNumber(dto.discount_amount),
taxable_amount: MoneyDTOHelper.toNumber(dto.taxable_amount),

View File

@ -1,15 +1,15 @@
import { z } from "zod/v4";
import type { PaginationSchema } from "@erp/core";
import type { ArrayElement } from "@repo/rdx-utils";
import type { z } from "zod/v4";
import { PaginationSchema } from "@erp/core";
import { ArrayElement } from "@repo/rdx-utils";
import {
CreateCustomerInvoiceRequestSchema,
GetCustomerInvoiceByIdResponseSchema,
ListCustomerInvoicesResponseSchema,
GetIssueInvoiceByIdResponseSchema,
ListIssueInvoicesResponseSchema,
UpdateCustomerInvoiceByIdRequestSchema,
} from "../../common";
export const CustomerInvoiceSchema = GetCustomerInvoiceByIdResponseSchema.omit({
export const CustomerInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({
metadata: true,
});
export const CustomerInvoiceCreateSchema = CreateCustomerInvoiceRequestSchema;
@ -24,7 +24,7 @@ export type CustomerInvoiceCreateInput = z.infer<typeof CustomerInvoiceCreateSch
export type CustomerInvoiceUpdateInput = z.infer<typeof CustomerInvoiceUpdateSchema>; // Cuerpo para actualizar
// Resultado de consulta con criteria (paginado, etc.)
export const CustomerInvoicesPageSchema = ListCustomerInvoicesResponseSchema.omit({
export const CustomerInvoicesPageSchema = ListIssueInvoicesResponseSchema.omit({
metadata: true,
});

View File

@ -1,6 +1,7 @@
import { FindOptions, Op, OrderItem, Sequelize, WhereOptions } from "sequelize";
import { Criteria } from "./critera";
import { type ConvertParams, type CriteriaMappings, ICriteriaToOrmConverter } from "./types";
import { type FindOptions, Op, type OrderItem, Sequelize, type WhereOptions } from "sequelize";
import type { Criteria } from "./critera";
import type { ConvertParams, CriteriaMappings, ICriteriaToOrmConverter } from "./types";
import { appendOrder, prependOrder } from "./utils";
/**
@ -146,7 +147,7 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
}
}
private transformValue(operator: symbol, value: any): any {
private transformValue(operator: symbol, value: unknown): unknown {
if (operator === Op.like || operator === Op.notLike) return `%${value}%`;
return value;
}