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" "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]": { "[typescript]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome"
}, },
@ -48,8 +39,16 @@
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome"
}, },
"prettier.enable": false, // Biome
"eslint.enable": false, "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 // other vscode settings
"[handlebars]": { "[handlebars]": {

View File

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

View File

@ -1,69 +1,366 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, "vcs": {
"files": { "ignoreUnknown": false, "includes": ["**", "!**/dist"] }, "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": { "formatter": {
"enabled": true, "enabled": true,
"useEditorconfig": true,
"formatWithErrors": false, "formatWithErrors": false,
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 2, "indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100, "lineWidth": 100,
"attributePosition": "auto", "lineEnding": "lf",
"bracketSpacing": true "attributePosition": "auto"
}, },
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true, "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": { "style": {
"useImportType": "off", "noDefaultExport": "off",
"noInferrableTypes": "off", "noImplicitBoolean": "off",
"noInferrableTypes": "error",
"noNamespace": "error",
"noNegationElse": "warn",
"noNonNullAssertion": "info", "noNonNullAssertion": "info",
"noUselessElse": "off",
"noParameterAssign": "error", "noParameterAssign": "error",
"useAsConstAssertion": "error", "noUnusedTemplateLiteral": "error",
"noUselessElse": "warn",
"useBlockStatements": "off",
"useCollapsedElseIf": "error",
"useConst": "error",
"useDefaultParameterLast": "error", "useDefaultParameterLast": "error",
"useEnumInitializers": "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", "useSelfClosingElements": "error",
"useShorthandAssign": "error",
"useShorthandFunctionType": "error",
"useSingleVarDeclarator": "error", "useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error", "useTemplate": "error",
"useNumberNamespace": "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": { "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": { "javascript": {
"formatter": { "formatter": {
"jsxQuoteStyle": "single",
"quoteProperties": "asNeeded",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always", "arrowParentheses": "always",
"bracketSameLine": false, "bracketSameLine": false,
"bracketSpacing": true,
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"quoteStyle": "double", "quoteStyle": "double",
"attributePosition": "auto", "semicolons": "always",
"bracketSpacing": true "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( export function formatPaymentMethodDTO(
paymentMethod?: GetCustomerInvoiceByIdResponseDTO["payment_method"] paymentMethod?: GetIssueInvoiceByIdResponseDTO["payment_method"]
) { ) {
if (!paymentMethod) { if (!paymentMethod) {
return null; return null;

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; 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 { export class RecipientInvoiceFullPresenter extends Presenter {
toOutput(invoice: CustomerInvoice): GetRecipientInvoiceByInvoiceIdResponseDTO { toOutput(invoice: CustomerInvoice): GetRecipientInvoiceByInvoiceIdResponseDTO {

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Collection, Maybe, Result } from "@repo/rdx-utils"; import { type Collection, Maybe, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import {
import type {
CustomerInvoiceNumber, CustomerInvoiceNumber,
CustomerInvoiceSerie, CustomerInvoiceSerie,
CustomerInvoiceStatus, CustomerInvoiceStatus,
@ -10,11 +11,11 @@ import {
} from "../../domain"; } from "../../domain";
import { import {
CustomerInvoice, CustomerInvoice,
CustomerInvoicePatchProps, type CustomerInvoicePatchProps,
CustomerInvoiceProps, type CustomerInvoiceProps,
} from "../../domain/aggregates"; } from "../../domain/aggregates";
import { ICustomerInvoiceRepository } from "../../domain/repositories"; import type { ICustomerInvoiceRepository } from "../../domain/repositories";
import { CustomerInvoiceListDTO } from "../../infrastructure"; import type { CustomerInvoiceListDTO } from "../../infrastructure";
export class CustomerInvoiceApplicationService { export class CustomerInvoiceApplicationService {
constructor( constructor(
@ -57,57 +58,57 @@ export class CustomerInvoiceApplicationService {
* *
* @param companyId - Identificador de la empresa a la que pertenece la proforma. * @param companyId - Identificador de la empresa a la que pertenece la proforma.
* @param props - Las propiedades ya validadas para crear 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. * @returns Result<CustomerInvoice, Error> - El agregado construido o un error si falla la creación.
*/ */
buildProformaInCompany( buildProformaInCompany(
companyId: UniqueID, companyId: UniqueID,
props: Omit<CustomerInvoiceProps, "companyId">, props: Omit<CustomerInvoiceProps, "companyId">,
invoiceId?: UniqueID proformaId?: UniqueID
): Result<CustomerInvoice, Error> { ): 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 companyId - Identificador de la empresa a la que pertenece la proforma.
* @param invoice - El agregado a guardar. * @param proforma - La proforma a guardar.
* @param transaction - Transacción activa para la operación. * @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( async createInvoiceInCompany(
companyId: UniqueID, companyId: UniqueID,
invoice: CustomerInvoice, proforma: CustomerInvoice,
transaction: Transaction transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
const result = await this.repository.create(invoice, transaction); const result = await this.repository.create(proforma, transaction);
if (result.isFailure) { if (result.isFailure) {
return Result.fail(result.error); 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 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. * @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, companyId: UniqueID,
invoice: CustomerInvoice, proforma: CustomerInvoice,
transaction: Transaction transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
const result = await this.repository.update(invoice, transaction); const result = await this.repository.update(proforma, transaction);
if (result.isFailure) { if (result.isFailure) {
return Result.fail(result.error); 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. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura encontrada o error. * @returns Result<CustomerInvoice, Error> - Factura encontrada o error.
*/ */
async getInvoiceByIdInCompany( async getIssueInvoiceByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: Transaction
): Promise<Result<CustomerInvoice>> { ): 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. * No lo guarda en el repositorio.
* *
* @param companyId - Identificador de la empresa a la que pertenece la factura. * @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 changes - Subconjunto de props válidas para aplicar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura actualizada o error. * @returns Result<CustomerInvoice, Error> - Factura actualizada o error.
*/ */
async patchInvoiceByIdInCompany( async patchProformaByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, proformaId: UniqueID,
changes: CustomerInvoicePatchProps, changes: CustomerInvoicePatchProps,
transaction?: Transaction transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
const invoiceResult = await this.getInvoiceByIdInCompany(companyId, invoiceId, transaction); const invoiceResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction);
if (invoiceResult.isFailure) { if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error); 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 "./issue-invoices";
export * from "./create"; export * from "./proformas";
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";

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 { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; 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; companyId: UniqueID;
invoice_id: string; invoice_id: string;
}; };
export class GetCustomerInvoiceUseCase { export class GetIssueInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceApplicationService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
public execute(params: GetCustomerInvoiceUseCaseInput) { public execute(params: GetIssueInvoiceUseCaseInput) {
const { invoice_id, companyId } = params; const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id); const idOrError = UniqueID.create(invoice_id);
@ -32,17 +33,18 @@ export class GetCustomerInvoiceUseCase {
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
const invoiceOrError = await this.service.getInvoiceByIdInCompany( const invoiceOrError = await this.service.getIssueInvoiceByIdInCompany(
companyId, companyId,
invoiceId, invoiceId,
transaction transaction
); );
if (invoiceOrError.isFailure) { if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error); return Result.fail(invoiceOrError.error);
} }
const customerInvoice = invoiceOrError.data; const invoice = invoiceOrError.data;
const dto = presenter.toOutput(customerInvoice); const dto = presenter.toOutput(invoice);
return Result.ok(dto); return Result.ok(dto);
} catch (error: unknown) { } 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 { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; 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; companyId: UniqueID;
invoice_id: string; invoice_id: string;
}; };
export class ReportCustomerInvoiceUseCase { export class ReportIssueInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceApplicationService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
public async execute(params: ReportCustomerInvoiceUseCaseInput) { public async execute(params: ReportIssueInvoiceUseCaseInput) {
const { invoice_id, companyId } = params; const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id); const idOrError = UniqueID.create(invoice_id);
@ -34,11 +35,12 @@ export class ReportCustomerInvoiceUseCase {
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
const invoiceOrError = await this.service.getInvoiceByIdInCompany( const invoiceOrError = await this.service.getIssueInvoiceByIdInCompany(
companyId, companyId,
invoiceId, invoiceId,
transaction transaction
); );
if (invoiceOrError.isFailure) { if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error); return Result.fail(invoiceOrError.error);
} }

View File

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

View File

@ -1,19 +1,25 @@
import { JsonTaxCatalogProvider } from "@erp/core"; import type { JsonTaxCatalogProvider } from "@erp/core";
import { DuplicateEntityError, IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import {
import { UniqueID } from "@repo/rdx-ddd"; DuplicateEntityError,
import { Maybe, Result } from "@repo/rdx-utils"; type IPresenterRegistry,
import { Transaction } from "sequelize"; type ITransactionManager,
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto"; } from "@erp/core/api";
import { CustomerInvoiceFullPresenter } from "../../presenters"; import type { UniqueID } from "@repo/rdx-ddd";
import { CustomerInvoiceApplicationService } from "../../services/customer-invoice-application.service"; import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props"; 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; companyId: UniqueID;
dto: CreateCustomerInvoiceRequestDTO; dto: CreateProformaRequestDTO;
}; };
export class CreateCustomerInvoiceUseCase { export class CreateProformaUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceApplicationService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
@ -21,7 +27,7 @@ export class CreateCustomerInvoiceUseCase {
private readonly taxCatalog: JsonTaxCatalogProvider private readonly taxCatalog: JsonTaxCatalogProvider
) {} ) {}
public async execute(params: CreateCustomerInvoiceUseCaseInput) { public async execute(params: CreateProformaUseCaseInput) {
const { dto, companyId } = params; const { dto, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({ const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice", resource: "customer-invoice",
@ -51,7 +57,7 @@ export class CreateCustomerInvoiceUseCase {
// 3) Construir entidad de dominio // 3) Construir entidad de dominio
const proformaProps = { const proformaProps = {
...props, ...props,
invoiceNumber: Maybe.some(newProformaNumber), invoiceNumber: newProformaNumber,
}; };
const buildResult = this.service.buildProformaInCompany(companyId, proformaProps, id); 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 { Tax } from "@erp/core/api";
import { import {
CurrencyCode, CurrencyCode,
DomainError, DomainError,
extractOrPushError,
LanguageCode, LanguageCode,
maybeFromNullableVO,
Percentage, Percentage,
TextValue, TextValue,
UniqueID, UniqueID,
UtcDate, UtcDate,
ValidationErrorCollection, ValidationErrorCollection,
ValidationErrorDetail, type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import {
CreateCustomerInvoiceItemRequestDTO, import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
CreateCustomerInvoiceRequestDTO,
} from "../../../../common/dto";
import { import {
CustomerInvoiceItem, CustomerInvoiceItem,
CustomerInvoiceItemDescription, CustomerInvoiceItemDescription,
CustomerInvoiceItemProps, type CustomerInvoiceItemProps,
CustomerInvoiceItems, CustomerInvoiceItems,
CustomerInvoiceNumber, CustomerInvoiceNumber,
CustomerInvoiceProps, type CustomerInvoiceProps,
CustomerInvoiceSerie, CustomerInvoiceSerie,
CustomerInvoiceStatus, CustomerInvoiceStatus,
InvoicePaymentMethod, InvoicePaymentMethod,
InvoiceRecipient, type InvoiceRecipient,
ItemAmount, ItemAmount,
ItemDiscount, ItemDiscount,
ItemQuantity, ItemQuantity,
ItemTaxes, ItemTaxes,
} from "../../../domain"; } from "../../../../domain";
/** /**
* Convierte el DTO a las props validadas (CustomerProps). * Convierte el DTO a las props validadas (CustomerProps).
@ -56,13 +54,13 @@ export class CreateCustomerInvoicePropsMapper {
this.errors = []; this.errors = [];
} }
public map(dto: CreateCustomerInvoiceRequestDTO, companyId: UniqueID) { public map(dto: CreateProformaRequestDTO, companyId: UniqueID) {
try { try {
this.errors = []; this.errors = [];
const defaultStatus = CustomerInvoiceStatus.createDraft(); 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; const isProforma = true;
@ -74,8 +72,8 @@ export class CreateCustomerInvoicePropsMapper {
const recipient = Maybe.none<InvoiceRecipient>(); const recipient = Maybe.none<InvoiceRecipient>();
const invoiceNumber = extractOrPushError( const proformaNumber = extractOrPushError(
maybeFromNullableVO(dto.invoice_number, (value) => CustomerInvoiceNumber.create(value)), CustomerInvoiceNumber.create(dto.invoice_number),
"invoice_number", "invoice_number",
this.errors this.errors
); );
@ -153,13 +151,14 @@ export class CreateCustomerInvoicePropsMapper {
); );
} }
const invoiceProps: CustomerInvoiceProps = { const proformaProps: CustomerInvoiceProps = {
companyId, companyId,
isProforma, isProforma,
proformaId: Maybe.none(),
status: defaultStatus!, status: defaultStatus!,
invoiceNumber: proformaNumber!,
series: series!, series: series!,
invoiceNumber: invoiceNumber!,
invoiceDate: invoiceDate!, invoiceDate: invoiceDate!,
operationDate: operationDate!, operationDate: operationDate!,
@ -181,13 +180,13 @@ export class CreateCustomerInvoicePropsMapper {
discountPercentage: discountPercentage!, discountPercentage: discountPercentage!,
}; };
return Result.ok({ id: invoiceId!, props: invoiceProps }); return Result.ok({ id: proformaId!, props: proformaProps });
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err })); return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err }));
} }
} }
private mapItems(items: CreateCustomerInvoiceItemRequestDTO[]) { private mapItems(items: CreateProformaItemRequestDTO[]) {
const invoiceItems = CustomerInvoiceItems.create({ const invoiceItems = CustomerInvoiceItems.create({
currencyCode: this.currencyCode!, currencyCode: this.currencyCode!,
languageCode: this.languageCode!, languageCode: this.languageCode!,
@ -246,10 +245,10 @@ export class CreateCustomerInvoicePropsMapper {
return invoiceItems; return invoiceItems;
} }
private mapTaxes(item: CreateCustomerInvoiceItemRequestDTO, itemIndex: number) { private mapTaxes(item: CreateProformaItemRequestDTO, itemIndex: number) {
const taxes = ItemTaxes.create([]); 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); const taxResult = Tax.createFromCode(tax_code, this.taxCatalog);
if (taxResult.isSuccess) { if (taxResult.isSuccess) {
taxes.add(taxResult.data); 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 { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceApplicationService } from "../services";
type DeleteCustomerInvoiceUseCaseInput = { import type { CustomerInvoiceApplicationService } from "../../services";
type DeleteProformaUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
invoice_id: string; proforma_id: string;
}; };
export class DeleteCustomerInvoiceUseCase { export class DeleteProformaUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceApplicationService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager private readonly transactionManager: ITransactionManager
) {} ) {}
public execute(params: DeleteCustomerInvoiceUseCaseInput) { public execute(params: DeleteProformaUseCaseInput) {
const { invoice_id, companyId } = params; const { proforma_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id); const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
@ -40,9 +41,7 @@ export class DeleteCustomerInvoiceUseCase {
const invoiceExists = existsCheck.data; const invoiceExists = existsCheck.data;
if (!invoiceExists) { if (!invoiceExists) {
return Result.fail( return Result.fail(new EntityNotFoundError("Proforma", "id", invoiceId.toString()));
new EntityNotFoundError("Customer invoice", "id", invoiceId.toString())
);
} }
return await this.service.deleteInvoiceByIdInCompany(companyId, invoiceId, transaction); 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 { UniqueID, UtcDate } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import {
IssueCustomerInvoiceDomainService, IssueCustomerInvoiceDomainService,
ProformaCustomerInvoiceDomainService, ProformaCustomerInvoiceDomainService,
} from "../../domain"; } from "../../../domain";
import { CustomerInvoiceFullPresenter } from "../presenters"; import type { CustomerInvoiceFullPresenter } from "../../presenters";
import { CustomerInvoiceApplicationService } from "../services"; import type { CustomerInvoiceApplicationService } from "../../services";
type IssueCustomerInvoiceUseCaseInput = { type IssueCustomerInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
@ -22,7 +23,7 @@ type IssueCustomerInvoiceUseCaseInput = {
* - Marca la proforma como "issued" * - Marca la proforma como "issued"
* - Persiste ambas dentro de la misma transacción * - Persiste ambas dentro de la misma transacción
*/ */
export class IssueCustomerInvoiceUseCase { export class IssueProformaInvoiceUseCase {
private readonly issueDomainService: IssueCustomerInvoiceDomainService; private readonly issueDomainService: IssueCustomerInvoiceDomainService;
private readonly proformaDomainService: ProformaCustomerInvoiceDomainService; private readonly proformaDomainService: ProformaCustomerInvoiceDomainService;

View File

@ -1,18 +1,19 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import { ListCustomerInvoicesResponseDTO } from "../../../common/dto";
import { ListCustomerInvoicesPresenter } from "../presenters";
import { CustomerInvoiceApplicationService } from "../services";
type ListCustomerInvoicesUseCaseInput = { import type { ListIssueInvoicesResponseDTO } from "../../../../common/dto";
import type { ListCustomerInvoicesPresenter } from "../../presenters";
import type { CustomerInvoiceApplicationService } from "../../services";
type ListProformasUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
criteria: Criteria; criteria: Criteria;
}; };
export class ListCustomerInvoicesUseCase { export class ListProformasUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceApplicationService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
@ -20,8 +21,8 @@ export class ListCustomerInvoicesUseCase {
) {} ) {}
public execute( public execute(
params: ListCustomerInvoicesUseCaseInput params: ListProformasUseCaseInput
): Promise<Result<ListCustomerInvoicesResponseDTO, Error>> { ): Promise<Result<ListIssueInvoicesResponseDTO, Error>> {
const { criteria, companyId } = params; const { criteria, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({ const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice", 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 { readFileSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import * as handlebars from "handlebars"; 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 { export class CustomerInvoiceReportHTMLPresenter extends Presenter {
toOutput(customerInvoice: CustomerInvoice): string { toOutput(customerInvoice: CustomerInvoice): string {

View File

@ -1,8 +1,10 @@
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import puppeteer from "puppeteer"; import puppeteer from "puppeteer";
import report from "puppeteer-report"; 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 // https://plnkr.co/edit/lWk6Yd?preview

View File

@ -6,14 +6,14 @@ import {
UniqueID, UniqueID,
UtcDate, UtcDate,
ValidationErrorCollection, ValidationErrorCollection,
ValidationErrorDetail, type ValidationErrorDetail,
extractOrPushError, extractOrPushError,
maybeFromNullableVO, maybeFromNullableVO,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils"; import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common/dto"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto";
import { CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../domain"; import { type CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../../domain";
/** /**
* mapDTOToUpdateCustomerInvoicePatchProps * mapDTOToUpdateCustomerInvoicePatchProps
@ -29,7 +29,7 @@ import { CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../domain
* *
*/ */
export function mapDTOToUpdateCustomerInvoicePatchProps(dto: UpdateCustomerInvoiceByIdRequestDTO) { export function mapDTOToUpdateCustomerInvoicePatchProps(dto: UpdateProformaByIdRequestDTO) {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const props: CustomerInvoicePatchProps = {}; 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 { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common";
import { CustomerInvoicePatchProps } from "../../../domain"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common";
import { CustomerInvoiceFullPresenter } from "../../presenters"; import type { CustomerInvoicePatchProps } from "../../../../domain";
import { CustomerInvoiceApplicationService } from "../../services/customer-invoice-application.service"; import type { CustomerInvoiceFullPresenter } from "../../../presenters";
import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service";
import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props"; import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props";
type UpdateCustomerInvoiceUseCaseInput = { type UpdateCustomerInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
invoice_id: string; proforma_id: string;
dto: UpdateCustomerInvoiceByIdRequestDTO; dto: UpdateProformaByIdRequestDTO;
}; };
export class UpdateCustomerInvoiceUseCase { export class UpdateCustomerInvoiceUseCase {
@ -22,9 +24,9 @@ export class UpdateCustomerInvoiceUseCase {
) {} ) {}
public execute(params: UpdateCustomerInvoiceUseCaseInput) { 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) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
} }
@ -45,7 +47,7 @@ export class UpdateCustomerInvoiceUseCase {
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { try {
const updatedInvoice = await this.service.patchInvoiceByIdInCompany( const updatedInvoice = await this.service.patchProformaByIdInCompany(
companyId, companyId,
invoiceId, invoiceId,
patchProps, patchProps,
@ -56,7 +58,7 @@ export class UpdateCustomerInvoiceUseCase {
return Result.fail(updatedInvoice.error); return Result.fail(updatedInvoice.error);
} }
const invoiceOrError = await this.service.updateInvoiceInCompany( const invoiceOrError = await this.service.updateProformaInCompany(
companyId, companyId,
updatedInvoice.data, updatedInvoice.data,
transaction transaction

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { GetCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
export class GetCustomerInvoiceController extends ExpressController { import type { GetProformaUseCase } from "../../../../application";
public constructor(private readonly useCase: GetCustomerInvoiceUseCase) { import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class GetIssueInvoiceController extends ExpressController {
public constructor(private readonly useCase: GetProformaUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
@ -18,7 +19,7 @@ export class GetCustomerInvoiceController extends ExpressController {
} }
const { invoice_id } = this.req.params; 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( return result.match(
(data) => this.ok(data), (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 { Criteria } from "@repo/rdx-criteria/server";
import { ListCustomerInvoicesUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
export class ListCustomerInvoicesController extends ExpressController { import type { ListProformasUseCase } from "../../../../application";
public constructor(private readonly useCase: ListCustomerInvoicesUseCase) { import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class ListIssueInvoicesController extends ExpressController {
public constructor(private readonly useCase: ListProformasUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;

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 { ChangeStatusCustomerInvoiceByIdRequestDTO } from "@erp/customer-invoices/common";
import { ChangeStatusCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
export class ChangeStatusCustomerInvoiceController extends ExpressController { import type { ChangeStatusProformaByIdRequestDTO } from "../../../../../common/dto";
public constructor(private readonly useCase: ChangeStatusCustomerInvoiceUseCase) { 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(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
@ -12,7 +13,7 @@ export class ChangeStatusCustomerInvoiceController extends ExpressController {
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
async executeImpl(): Promise<any> { protected async executeImpl() {
const companyId = this.getTenantId(); // garantizado por tenantGuard const companyId = this.getTenantId(); // garantizado por tenantGuard
if (!companyId) { if (!companyId) {
return this.forbiddenError("Tenant ID not found"); return this.forbiddenError("Tenant ID not found");
@ -23,7 +24,7 @@ export class ChangeStatusCustomerInvoiceController extends ExpressController {
return this.invalidInputError("Proforma ID missing"); 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 }); const result = await this.useCase.execute({ proforma_id, dto, companyId });
return result.match( 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 type { CreateProformaRequestDTO } from "../../../../../common/dto";
import { CreateCustomerInvoiceUseCase } from "../../../application"; import type { CreateProformaUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class CreateCustomerInvoiceController extends ExpressController { export class CreateProformaController extends ExpressController {
public constructor(private readonly useCase: CreateCustomerInvoiceUseCase) { public constructor(private readonly useCase: CreateProformaUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
@ -18,7 +18,7 @@ export class CreateCustomerInvoiceController extends ExpressController {
if (!companyId) { if (!companyId) {
return this.forbiddenError("Tenant ID not found"); 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 }); 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 { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { IssueCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
export class IssueCustomerInvoiceController extends ExpressController { import type { IssueProformaInvoiceUseCase } from "../../../../application";
public constructor(private readonly useCase: IssueCustomerInvoiceUseCase) { import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class IssueProformaController extends ExpressController {
public constructor(private readonly useCase: IssueProformaInvoiceUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
@ -11,7 +12,7 @@ export class IssueCustomerInvoiceController extends ExpressController {
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
async executeImpl(): Promise<any> { protected async executeImpl() {
const companyId = this.getTenantId(); // garantizado por tenantGuard const companyId = this.getTenantId(); // garantizado por tenantGuard
if (!companyId) { if (!companyId) {
return this.forbiddenError("Tenant ID not found"); 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 { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { ReportCustomerInvoiceUseCase } from "../../../application";
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
export class ReportCustomerInvoiceController extends ExpressController { import type { ReportProformaUseCase } from "../../../../application";
public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) { import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class ReportProformaController extends ExpressController {
public constructor(private readonly useCase: ReportProformaUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
@ -16,9 +17,9 @@ export class ReportCustomerInvoiceController extends ExpressController {
if (!companyId) { if (!companyId) {
return this.forbiddenError("Tenant ID not found"); 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( return result.match(
({ data, filename }) => this.downloadPDF(data, filename), ({ data, filename }) => this.downloadPDF(data, filename),

View File

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

View File

@ -4,16 +4,17 @@
import { import {
ApiErrorMapper, ApiErrorMapper,
ConflictApiError, ConflictApiError,
ErrorToApiRule, type ErrorToApiRule,
ValidationApiError, ValidationApiError,
} from "@erp/core/api"; } from "@erp/core/api";
import { import {
CustomerInvoiceIdAlreadyExistsError, type CustomerInvoiceIdAlreadyExistsError,
EntityIsNotProformaError, type EntityIsNotProformaError,
type ProformaCannotBeConvertedToInvoiceError,
isCustomerInvoiceIdAlreadyExistsError, isCustomerInvoiceIdAlreadyExistsError,
isEntityIsNotProformaError, isEntityIsNotProformaError,
isProformaCannotBeConvertedToInvoiceError, isProformaCannotBeConvertedToInvoiceError,
ProformaCannotBeConvertedToInvoiceError,
} from "../../domain"; } from "../../domain";
// Crea una regla específica (prioridad alta para sobreescribir mensajes) // 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 { type RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { ModuleParams, validateRequest } from "@erp/core/api"; import { type 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 { import {
ChangeStatusCustomerInvoiceByIdParamsRequestSchema, ChangeStatusProformaByIdParamsRequestSchema,
ChangeStatusCustomerInvoiceByIdRequestSchema, ChangeStatusProformaByIdRequestSchema,
CreateCustomerInvoiceRequestSchema, CreateProformaRequestSchema,
CustomerInvoiceListRequestSchema, GetProformaByIdRequestSchema,
GetCustomerInvoiceByIdRequestSchema, ListProformasRequestSchema,
ReportCustomerInvoiceByIdRequestSchema, ReportProformaByIdRequestSchema,
UpdateCustomerInvoiceByIdParamsRequestSchema, UpdateProformaByIdParamsRequestSchema,
UpdateCustomerInvoiceByIdRequestSchema, UpdateProformaByIdRequestSchema,
} from "../../../common/dto"; } from "@erp/customer-invoices/common";
import { buildCustomerInvoiceDependencies } from "../dependencies"; import type { ILogger } from "@repo/rdx-logger";
import { import { type Application, type NextFunction, type Request, type Response, Router } from "express";
ChangeStatusCustomerInvoiceController, import type { Sequelize } from "sequelize";
CreateCustomerInvoiceController,
GetCustomerInvoiceController,
ListCustomerInvoicesController,
ReportCustomerInvoiceController,
UpdateCustomerInvoiceController,
} from "./controllers";
import { IssueCustomerInvoiceController } from "./controllers/issue-customer-invoice.controller";
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 { const { app, baseRoutePath, logger } = params as {
app: Application; app: Application;
database: Sequelize; database: Sequelize;
@ -56,21 +58,21 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
router.get( router.get(
"/", "/",
//checkTabContext, //checkTabContext,
validateRequest(CustomerInvoiceListRequestSchema, "params"), validateRequest(ListProformasRequestSchema, "params"),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.list(); 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); return controller.execute(req, res, next);
} }
); );
router.get( router.get(
"/:invoice_id", "/:proforma_id",
//checkTabContext, //checkTabContext,
validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"), validateRequest(GetProformaByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.get(); const useCase = deps.useCases.get();
const controller = new GetCustomerInvoiceController(useCase); const controller = new GetProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
@ -79,10 +81,10 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
"/", "/",
//checkTabContext, //checkTabContext,
validateRequest(CreateCustomerInvoiceRequestSchema, "body"), validateRequest(CreateProformaRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.create(); const useCase = deps.useCases.create();
const controller = new CreateCustomerInvoiceController(useCase); const controller = new CreateProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
@ -91,17 +93,17 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
"/:proforma_id", "/:proforma_id",
//checkTabContext, //checkTabContext,
validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"), validateRequest(UpdateProformaByIdParamsRequestSchema, "params"),
validateRequest(UpdateCustomerInvoiceByIdRequestSchema, "body"), validateRequest(UpdateProformaByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.update(); const useCase = deps.useCases.update();
const controller = new UpdateCustomerInvoiceController(useCase); const controller = new UpdateProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
/*router.delete( /*router.delete(
"/:invoice_id", "/:proforma_id",
//checkTabContext, //checkTabContext,
validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"), validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"),
@ -113,12 +115,12 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
);*/ );*/
router.get( router.get(
"/:invoice_id/report", "/:proforma_id/report",
//checkTabContext, //checkTabContext,
validateRequest(ReportCustomerInvoiceByIdRequestSchema, "params"), validateRequest(ReportProformaByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.report(); const useCase = deps.useCases.report();
const controller = new ReportCustomerInvoiceController(useCase); const controller = new ReportProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
@ -127,12 +129,12 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
"/:proforma_id/status", "/:proforma_id/status",
//checkTabContext, //checkTabContext,
validateRequest(ChangeStatusCustomerInvoiceByIdParamsRequestSchema, "params"), validateRequest(ChangeStatusProformaByIdParamsRequestSchema, "params"),
validateRequest(ChangeStatusCustomerInvoiceByIdRequestSchema, "body"), validateRequest(ChangeStatusProformaByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.changeStatus(); const useCase = deps.useCases.changeStatus();
const controller = new ChangeStatusCustomerInvoiceController(useCase); const controller = new ChangeStatusProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
@ -146,10 +148,10 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.issue(); const useCase = deps.useCases.issue();
const controller = new IssueCustomerInvoiceController(useCase); const controller = new IssueProformaController(useCase);
return controller.execute(req, res, next); 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, SequelizeRepository,
translateSequelizeError, translateSequelizeError,
} from "@erp/core/api"; } from "@erp/core/api";
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { type Collection, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import type { FindOptions, InferAttributes, Transaction } from "sequelize";
import { CustomerInvoice, CustomerInvoiceStatus, ICustomerInvoiceRepository } from "../../domain";
import { import type {
CustomerInvoice,
CustomerInvoiceStatus,
ICustomerInvoiceRepository,
} from "../../domain";
import type {
CustomerInvoiceListDTO, CustomerInvoiceListDTO,
ICustomerInvoiceDomainMapper, ICustomerInvoiceDomainMapper,
ICustomerInvoiceListMapper, ICustomerInvoiceListMapper,
} from "../mappers"; } from "../mappers";
import { CustomerInvoiceModel } from "./models/customer-invoice.model"; import { CustomerInvoiceModel } from "./models/customer-invoice.model";
import { CustomerInvoiceItemModel } from "./models/customer-invoice-item.model"; import { CustomerInvoiceItemModel } from "./models/customer-invoice-item.model";
import { CustomerInvoiceItemTaxModel } from "./models/customer-invoice-item-tax.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 companyId - Identificador UUID de la empresa a la que pertenece la factura.
* @param id - UUID de la factura. * @param id - UUID de la factura.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @params options - Opciones adicionales para la consulta (Sequelize FindOptions)
* @returns Result<CustomerInvoice, Error> * @returns Result<CustomerInvoice, Error>
*/ */
async getByIdInCompany( async getByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction: Transaction transaction: Transaction,
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
const { CustomerModel } = this._database.models; const { CustomerModel } = this._database.models;
@ -199,14 +207,36 @@ export class CustomerInvoiceRepository
resource: "customer-invoice", resource: "customer-invoice",
}); });
const row = await CustomerInvoiceModel.findOne({ // Normalización defensiva de order/include
where: { id: id.toString(), company_id: companyId.toString() }, const normalizedOrder = Array.isArray(options.order)
order: [[{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"]], ? 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: [ include: [
...normalizedInclude,
{ {
model: CustomerModel, model: CustomerModel,
as: "current_customer", as: "current_customer",
required: false, // false => LEFT JOIN required: false,
}, },
{ {
model: CustomerInvoiceItemModel, model: CustomerInvoiceItemModel,
@ -227,7 +257,9 @@ export class CustomerInvoiceRepository
}, },
], ],
transaction, transaction,
}); };
const row = await CustomerInvoiceModel.findOne(mergedOptions);
if (!row) { if (!row) {
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); 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 "./issue-invoices";
export * from "./create-customer-invoice.request.dto"; export * from "./proformas";
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";

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

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

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 "./issue-invoices";
export * from "./get-customer-invoice-by-id.response.dto"; export * from "./proformas";
export * from "./list-customer-invoices.response.dto";
export * from "./update-customer-invoice-by-id.response.dto";

View File

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

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"; } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
export const ListCustomerInvoicesResponseSchema = createPaginatedListSchema( export const ListIssueInvoicesResponseSchema = createPaginatedListSchema(
z.object({ z.object({
id: z.uuid(), id: z.uuid(),
company_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"; } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
export const CreateCustomerInvoiceResponseSchema = z.object({ export const CreateProformaResponseSchema = z.object({
id: z.uuid(), id: z.uuid(),
company_id: z.uuid(), company_id: z.uuid(),
@ -52,4 +52,4 @@ export const CreateCustomerInvoiceResponseSchema = z.object({
metadata: MetadataSchema.optional(), 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 { lazy } from "react";
import { Outlet, RouteObject } from "react-router-dom"; import { Outlet, type RouteObject } from "react-router-dom";
// Lazy load components // Lazy load components
const InvoicesLayout = lazy(() => const InvoicesLayout = lazy(() =>
import("./components").then((m) => ({ default: m.InvoicesLayout })) import("./components").then((m) => ({ default: m.InvoicesLayout }))
); );
const InvoiceListPage = lazy(() => const InvoiceListPage = lazy(() => import("./pages").then((m) => ({ default: m.InvoiceListPage })));
import("./pages").then((m) => ({ default: m.InvoiceListPage }))
);
const CustomerInvoiceAdd = lazy(() => const CustomerInvoiceAdd = lazy(() =>
import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate })) import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate }))
@ -20,6 +18,20 @@ const InvoiceUpdatePage = lazy(() =>
export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => { export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => {
return [ 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", path: "customer-invoices",
element: ( element: (
@ -30,8 +42,6 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
children: [ children: [
{ path: "", index: true, element: <InvoiceListPage /> }, // index { path: "", index: true, element: <InvoiceListPage /> }, // index
{ path: "list", element: <InvoiceListPage /> }, { path: "list", element: <InvoiceListPage /> },
{ path: "create", element: <CustomerInvoiceAdd /> },
{ path: ":id/edit", element: <InvoiceUpdatePage /> },
// //
/*{ path: "create", element: <CustomerInvoicesList /> }, /*{ 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.form.schema";
export * from "./invoice-dto.adapter";
export * from "./invoice-resume.form.schema";
export * from "./invoice-resume-dto.adapter";
export * from "./invoices.api.schema"; export * from "./invoices.api.schema";

View File

@ -1,16 +1,18 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core"; import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import {
GetCustomerInvoiceByIdResponseDTO, import type {
GetIssueInvoiceByIdResponseDTO,
UpdateCustomerInvoiceByIdRequestDTO, UpdateCustomerInvoiceByIdRequestDTO,
} from "../../common"; } from "../../common";
import { InvoiceContextValue } from "../context"; import type { InvoiceContextValue } from "../context";
import { InvoiceFormData } from "./invoice.form.schema";
import type { InvoiceFormData } from "./invoice.form.schema";
/** /**
* Convierte el DTO completo de API a datos numéricos para el formulario. * Convierte el DTO completo de API a datos numéricos para el formulario.
*/ */
export const invoiceDtoToFormAdapter = { export const invoiceDtoToFormAdapter = {
fromDto(dto: GetCustomerInvoiceByIdResponseDTO, context: InvoiceContextValue): InvoiceFormData { fromDto(dto: GetIssueInvoiceByIdResponseDTO, context: InvoiceContextValue): InvoiceFormData {
const { taxCatalog } = context; const { taxCatalog } = context;
return { return {
invoice_number: dto.invoice_number, invoice_number: dto.invoice_number,
@ -30,6 +32,7 @@ export const invoiceDtoToFormAdapter = {
currency_code: dto.currency_code, currency_code: dto.currency_code,
subtotal_amount: MoneyDTOHelper.toNumber(dto.subtotal_amount), subtotal_amount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
items_discount_amount: 0,
discount_percentage: PercentageDTOHelper.toNumber(dto.discount_percentage), discount_percentage: PercentageDTOHelper.toNumber(dto.discount_percentage),
discount_amount: MoneyDTOHelper.toNumber(dto.discount_amount), discount_amount: MoneyDTOHelper.toNumber(dto.discount_amount),
taxable_amount: MoneyDTOHelper.toNumber(dto.taxable_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 { import {
CreateCustomerInvoiceRequestSchema, CreateCustomerInvoiceRequestSchema,
GetCustomerInvoiceByIdResponseSchema, GetIssueInvoiceByIdResponseSchema,
ListCustomerInvoicesResponseSchema, ListIssueInvoicesResponseSchema,
UpdateCustomerInvoiceByIdRequestSchema, UpdateCustomerInvoiceByIdRequestSchema,
} from "../../common"; } from "../../common";
export const CustomerInvoiceSchema = GetCustomerInvoiceByIdResponseSchema.omit({ export const CustomerInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({
metadata: true, metadata: true,
}); });
export const CustomerInvoiceCreateSchema = CreateCustomerInvoiceRequestSchema; 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 export type CustomerInvoiceUpdateInput = z.infer<typeof CustomerInvoiceUpdateSchema>; // Cuerpo para actualizar
// Resultado de consulta con criteria (paginado, etc.) // Resultado de consulta con criteria (paginado, etc.)
export const CustomerInvoicesPageSchema = ListCustomerInvoicesResponseSchema.omit({ export const CustomerInvoicesPageSchema = ListIssueInvoicesResponseSchema.omit({
metadata: true, metadata: true,
}); });

View File

@ -1,6 +1,7 @@
import { FindOptions, Op, OrderItem, Sequelize, WhereOptions } from "sequelize"; import { type FindOptions, Op, type OrderItem, Sequelize, type WhereOptions } from "sequelize";
import { Criteria } from "./critera";
import { type ConvertParams, type CriteriaMappings, ICriteriaToOrmConverter } from "./types"; import type { Criteria } from "./critera";
import type { ConvertParams, CriteriaMappings, ICriteriaToOrmConverter } from "./types";
import { appendOrder, prependOrder } from "./utils"; 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}%`; if (operator === Op.like || operator === Op.notLike) return `%${value}%`;
return value; return value;
} }