diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7b4957cc..6f618606 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -60,5 +60,12 @@
// other vscode settings
"[sql]": {
"editor.defaultFormatter": "cweijan.vscode-mysql-client2"
+ },
+ "chat.tools.terminal.autoApprove": {
+ "/^cd /home/rodax/Documentos/uecko-erp/apps/web/src/layout && python3 - <<'PY'\nfrom pathlib import Path\nfiles = \\{\n 'app-layout\\.tsx': '''import type \\* as React from \"react\"\nimport \\{ SidebarInset, SidebarProvider \\} from \"@repo/shadcn-ui/components\"\nimport \\{ Outlet \\} from \"react-router\"\n\nimport \\{ AppMain \\} from \"\\./app-main\"\nimport \\{ AppSidebar \\} from \"\\./app-sidebar\"\nimport \\{ AppTopbar \\} from \"\\./app-topbar\"\n\nexport const AppLayout = \\(\\) => \\{\n return \\(\n \n \n \n \n \n \n \n \n \n \\)\n\\}\n''',\n 'app-main\\.tsx': '''import type \\* as React from \"react\"\n\nimport \\{ cn \\} from \"@repo/shadcn-ui/utils\"\n\ninterface AppMainProps \\{\n children: React\\.ReactNode\n className\\?: string\n\\}\n\nexport const AppMain = \\(\\{ children, className \\}: AppMainProps\\) => \\{\n return \\(\n \n \\{children\\}\n \n \\)\n\\}\n''',\n 'app-sidebar\\.config\\.ts': '''import \\{\n BanknoteIcon,\n BarChart3Icon,\n BoxesIcon,\n Building2Icon,\n ClipboardListIcon,\n CoinsIcon,\n CreditCardIcon,\n FileTextIcon,\n LandmarkIcon,\n PackageIcon,\n PercentIcon,\n ReceiptIcon,\n RefreshCcwIcon,\n SettingsIcon,\n ShoppingCartIcon,\n SlidersHorizontalIcon,\n TruckIcon,\n UserCogIcon,\n UsersIcon,\n WarehouseIcon,\n\\} from \"lucide-react\"\nimport type \\{ LucideIcon \\} from \"lucide-react\"\n\nexport interface AppSidebarNavItem \\{\n title: string\n href: string\n icon: LucideIcon\n\\}\n\nexport interface AppSidebarNavSection \\{\n title: string\n items: AppSidebarNavItem\\[\\]\n\\}\n\nexport const appSidebarNavSections: AppSidebarNavSection\\[\\] = \\[\n \\{\n title: \"Ventas\",\n items: \\[\n \\{ title: \"Proformas\", href: \"/proformas\", icon: FileTextIcon \\},\n \\{ title: \"Pedidos de venta\", href: \"/sales-orders\", icon: ShoppingCartIcon \\},\n \\{ title: \"Clientes\", href: \"/customers\", icon: UsersIcon \\},\n \\{ title: \"Facturación\", href: \"/customer-invoices\", icon: ReceiptIcon \\},\n \\{ title: \"Cobros\", href: \"/collections\", icon: CreditCardIcon \\},\n \\{ title: \"Devoluciones\", href: \"/sales-returns\", icon: RefreshCcwIcon \\},\n \\],\n \\},\n \\{\n title: \"Compras\",\n items: \\[\n \\{ title: \"Pedidos de compra\", href: \"/purchase-orders\", icon: ClipboardListIcon \\},\n \\{ title: \"Proveedores\", href: \"/suppliers\", icon: TruckIcon \\},\n \\{ title: \"Facturas de compra\", href: \"/supplier-invoices\", icon: ReceiptIcon \\},\n \\{ title: \"Pagos\", href: \"/payments\", icon: BanknoteIcon \\},\n \\{ title: \"Devoluciones\", href: \"/purchase-returns\", icon: RefreshCcwIcon \\},\n \\],\n \\},\n \\{\n title: \"Inventario\",\n items: \\[\n \\{ title: \"Productos\", href: \"/products\", icon: PackageIcon \\},\n \\{ title: \"Almacenes\", href: \"/warehouses\", icon: WarehouseIcon \\},\n \\{ title: \"Movimientos\", href: \"/stock-movements\", icon: BoxesIcon \\},\n \\{ title: \"Ajustes de stock\", href: \"/stock-adjustments\", icon: SlidersHorizontalIcon \\},\n \\{ title: \"Categorías\", href: \"/product-categories\", icon: ClipboardListIcon \\},\n \\],\n \\},\n \\{\n title: \"Contabilidad\",\n items: \\[\n \\{ title: \"Plan contable\", href: \"/accounting/chart\", icon: LandmarkIcon \\},\n \\{ title: \"Asientos contables\", href: \"/accounting/entries\", icon: FileTextIcon \\},\n \\{ title: \"Diario\", href: \"/accounting/journal\", icon: ClipboardListIcon \\},\n \\{ title: \"Informes financieros\", href: \"/accounting/reports\", icon: BarChart3Icon \\},\n \\],\n \\},\n \\{\n title: \"Configuración\",\n items: \\[\n \\{ title: \"Usuarios y roles\", href: \"/settings/users\", icon: UserCogIcon \\},\n \\{ title: \"Impuestos\", href: \"/settings/taxes\", icon: PercentIcon \\},\n \\{ title: \"Monedas\", href: \"/settings/currencies\", icon: CoinsIcon \\},\n \\{ title: \"Empresas\", href: \"/settings/companies\", icon: Building2Icon \\},\n \\{ title: \"Parámetros\", href: \"/settings\", icon: SettingsIcon \\},\n \\],\n \\},\n\\]\n''',\n 'app-sidebar\\.tsx': '''import type \\* as React from \"react\"\nimport \\{\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarHeader,\n SidebarRail,\n SidebarTrigger,\n\\} from \"@repo/shadcn-ui/components\"\nimport \\{ cn \\} from \"@repo/shadcn-ui/utils\"\n\nimport \\{ AppSidebarNav \\} from \"\\./app-sidebar-nav\"\nimport \\{ appSidebarNavSections \\} from \"\\./app-sidebar\\.config\"\n\ninterface AppSidebarProps \\{\n className\\?: string\n\\}\n\nexport const AppSidebar = \\(\\{ className \\}: AppSidebarProps\\) => \\{\n return \\(\n \n \n \n \n\n \n \n \n\n \n \n
\n AM\n
\n
\n
Ana Martínez
\n
\n Administrador\n
\n
\n
\n \n\n \n \n \\)\n\\}\n''',\n 'app-sidebar-nav\\.tsx': '''import type \\{ AppSidebarNavSection \\} from \"\\./app-sidebar\\.config\"\nimport \\{ NavLink \\} from \"react-router\"\nimport \\{\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n\\} from \"@repo/shadcn-ui/components\"\nimport \\{ cn \\} from \"@repo/shadcn-ui/utils\"\n\ninterface AppSidebarNavProps \\{\n sections: AppSidebarNavSection\\[\\]\n\\}\n\nexport const AppSidebarNav = \\(\\{ sections \\}: AppSidebarNavProps\\) => \\{\n return \\(\n \n \\)\n\\}\n''',\n 'app-topbar\\.tsx': '''import \\{\n BellIcon,\n ChevronDownIcon,\n MessageSquareIcon,\n SearchIcon,\n\\} from \"lucide-react\"\nimport \\{\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Input,\n SidebarTrigger,\n\\} from \"@repo/shadcn-ui/components\"\n\nexport const AppTopbar = \\(\\) => \\{\n return \\(\n \n \\)\n\\}\n'''\n\\}\nfor name, content in files\\.items\\(\\):\n Path\\(name\\)\\.write_text\\(content, encoding='utf-8'\\)\nPY$/": {
+ "approve": true,
+ "matchCommandLine": true
+ },
+ "pnpm": true
} // <- your root font size here
}
diff --git a/apps/server/src/lib/modules/module-loader.ts b/apps/server/src/lib/modules/module-loader.ts
index ebcdf767..8f6a2b40 100644
--- a/apps/server/src/lib/modules/module-loader.ts
+++ b/apps/server/src/lib/modules/module-loader.ts
@@ -208,7 +208,7 @@ function validateModuleServices(moduleName: string, services: Record = {};
* Registra un objeto de servicio (API) bajo un nombre.
*/
export function registerService(name: string, api: unknown) {
+ console.debug(`Registering service: ${name}`);
if (services[name]) {
throw new Error(`❌ Servicio "${name}" ya fue registrado.`);
}
diff --git a/modules/catalogs/src/api/application/index.ts b/modules/catalogs/src/api/application/index.ts
index 7b68c32a..1702efcf 100644
--- a/modules/catalogs/src/api/application/index.ts
+++ b/modules/catalogs/src/api/application/index.ts
@@ -1,5 +1,4 @@
export * from "./payment-methods";
export * from "./payment-terms";
-export * from "./services";
export * from "./tax-definitions";
export * from "./tax-regimes";
diff --git a/modules/catalogs/src/api/application/payment-methods/index.ts b/modules/catalogs/src/api/application/payment-methods/index.ts
index b68ea0dc..77d7c040 100644
--- a/modules/catalogs/src/api/application/payment-methods/index.ts
+++ b/modules/catalogs/src/api/application/payment-methods/index.ts
@@ -1,6 +1,7 @@
export * from "./di";
export * from "./mappers";
export * from "./models";
+export * from "./public";
export * from "./repositories";
export * from "./services";
export * from "./snapshot-builders";
diff --git a/modules/catalogs/src/api/application/payment-methods/public/errors/index.ts b/modules/catalogs/src/api/application/payment-methods/public/errors/index.ts
new file mode 100644
index 00000000..7fedd188
--- /dev/null
+++ b/modules/catalogs/src/api/application/payment-methods/public/errors/index.ts
@@ -0,0 +1 @@
+export * from './payment-method-not-active.error';
diff --git a/modules/catalogs/src/api/application/payment-methods/public/errors/payment-method-not-active.error.ts b/modules/catalogs/src/api/application/payment-methods/public/errors/payment-method-not-active.error.ts
new file mode 100644
index 00000000..3025dd0c
--- /dev/null
+++ b/modules/catalogs/src/api/application/payment-methods/public/errors/payment-method-not-active.error.ts
@@ -0,0 +1,9 @@
+import { DomainError, UniqueID } from "@repo/rdx-ddd";
+
+export class PaymentMethodNotActiveError extends DomainError {
+ public readonly code = "PAYMENT_METHOD_NOT_ACTIVE" as const;
+
+ public constructor(public readonly id: UniqueID) {
+ super(`Payment method with id ${id.toString()} is not active`);
+ }
+}
diff --git a/modules/catalogs/src/api/application/payment-methods/public/index.ts b/modules/catalogs/src/api/application/payment-methods/public/index.ts
new file mode 100644
index 00000000..0035998c
--- /dev/null
+++ b/modules/catalogs/src/api/application/payment-methods/public/index.ts
@@ -0,0 +1,4 @@
+export * from "./mappers";
+export * from "./models/";
+export * from "./services";
+export * from "./services/";
diff --git a/modules/catalogs/src/api/application/payment-methods/public/mappers/index.ts b/modules/catalogs/src/api/application/payment-methods/public/mappers/index.ts
new file mode 100644
index 00000000..d7046b81
--- /dev/null
+++ b/modules/catalogs/src/api/application/payment-methods/public/mappers/index.ts
@@ -0,0 +1 @@
+export * from './payment-method-public-model.mapper';
diff --git a/modules/catalogs/src/api/application/payment-methods/public/mappers/payment-method-public-model.mapper.ts b/modules/catalogs/src/api/application/payment-methods/public/mappers/payment-method-public-model.mapper.ts
new file mode 100644
index 00000000..e0f25e01
--- /dev/null
+++ b/modules/catalogs/src/api/application/payment-methods/public/mappers/payment-method-public-model.mapper.ts
@@ -0,0 +1,18 @@
+
+import type {
+ PaymentMethodPublicModel
+} from "../models";
+import { PaymentMethod } from '../../../../domain';
+
+export class PaymentMethodPublicModelMapper {
+ public toPublicModel(paymentMethod: PaymentMethod): PaymentMethodPublicModel {
+ return {
+ id: paymentMethod.id,
+ companyId: paymentMethod.companyId,
+ name: paymentMethod.name.toPrimitive(),
+ description: paymentMethod.description.map((value) => value.toPrimitive()),
+ isActive: paymentMethod.isActive,
+ };
+ }
+
+}
diff --git a/modules/catalogs/src/api/application/payment-methods/public/models/index.ts b/modules/catalogs/src/api/application/payment-methods/public/models/index.ts
new file mode 100644
index 00000000..45d01dad
--- /dev/null
+++ b/modules/catalogs/src/api/application/payment-methods/public/models/index.ts
@@ -0,0 +1 @@
+export * from './payment-method-public.model';
diff --git a/modules/catalogs/src/api/application/payment-methods/public/models/payment-method-public.model.ts b/modules/catalogs/src/api/application/payment-methods/public/models/payment-method-public.model.ts
new file mode 100644
index 00000000..2fa25788
--- /dev/null
+++ b/modules/catalogs/src/api/application/payment-methods/public/models/payment-method-public.model.ts
@@ -0,0 +1,14 @@
+import type { UniqueID } from "@repo/rdx-ddd";
+import type { Maybe } from "@repo/rdx-utils";
+
+export interface PaymentMethodPublicModel {
+ id: UniqueID;
+ companyId: UniqueID;
+
+ name: string;
+ description: Maybe;
+
+ isActive: boolean;
+
+
+}
diff --git a/modules/catalogs/src/api/application/payment-methods/public/services/index.ts b/modules/catalogs/src/api/application/payment-methods/public/services/index.ts
new file mode 100644
index 00000000..928428f5
--- /dev/null
+++ b/modules/catalogs/src/api/application/payment-methods/public/services/index.ts
@@ -0,0 +1,2 @@
+export * from './payment-method-public-finder.interface';
+export * from './payment-method-public-finder';
diff --git a/modules/catalogs/src/api/application/payment-methods/public/services/payment-method-public-finder.interface.ts b/modules/catalogs/src/api/application/payment-methods/public/services/payment-method-public-finder.interface.ts
new file mode 100644
index 00000000..52c9fa06
--- /dev/null
+++ b/modules/catalogs/src/api/application/payment-methods/public/services/payment-method-public-finder.interface.ts
@@ -0,0 +1,24 @@
+import type { UniqueID } from "@repo/rdx-ddd";
+import type { Maybe, Result } from "@repo/rdx-utils";
+
+import type { PaymentMethodPublicModel } from "../models/payment-method-public.model";
+
+export interface FindPaymentMethodByIdInCompanyParams {
+ companyId: UniqueID;
+ id: UniqueID;
+ transaction?: unknown;
+}
+
+export interface IPaymentMethodPublicFinder {
+ existsByIdInCompany(
+ params: FindPaymentMethodByIdInCompanyParams,
+ ): Promise>;
+
+ getByIdInCompany(
+ params: FindPaymentMethodByIdInCompanyParams,
+ ): Promise>;
+
+ findByIdInCompany(
+ params: FindPaymentMethodByIdInCompanyParams,
+ ): Promise, Error>>;
+}
\ No newline at end of file
diff --git a/modules/catalogs/src/api/application/payment-methods/public/services/payment-method-public-finder.ts b/modules/catalogs/src/api/application/payment-methods/public/services/payment-method-public-finder.ts
new file mode 100644
index 00000000..e9cddc78
--- /dev/null
+++ b/modules/catalogs/src/api/application/payment-methods/public/services/payment-method-public-finder.ts
@@ -0,0 +1,71 @@
+import { Maybe, Result } from "@repo/rdx-utils";
+
+import type { IPaymentMethodRepository } from "../../repositories";
+import type { PaymentMethodPublicModelMapper } from "../mappers/payment-method-public-model.mapper";
+import type { PaymentMethodPublicModel } from "../models/payment-method-public.model";
+import type {
+ FindPaymentMethodByIdInCompanyParams,
+ IPaymentMethodPublicFinder,
+} from "./payment-method-public-finder.interface";
+
+export class PaymentMethodPublicFinder implements IPaymentMethodPublicFinder {
+ public constructor(
+ private readonly deps: {
+ repository: IPaymentMethodRepository;
+ mapper: PaymentMethodPublicModelMapper;
+ },
+ ) {}
+
+ public async existsByIdInCompany(
+ params: FindPaymentMethodByIdInCompanyParams,
+ ): Promise> {
+ const result = await this.deps.repository.existsByIdInCompany(
+ params.companyId,
+ params.id,
+ params.transaction,
+ );
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ return Result.ok(result.data);
+ }
+
+ public async getByIdInCompany(
+ params: FindPaymentMethodByIdInCompanyParams,
+ ): Promise> {
+ const result = await this.deps.repository.getByIdInCompany(
+ params.companyId,
+ params.id,
+ params.transaction,
+ );
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ return Result.ok(this.deps.mapper.toPublicModel(result.data));
+ }
+
+ public async findByIdInCompany(
+ params: FindPaymentMethodByIdInCompanyParams,
+ ): Promise, Error>> {
+ const result = await this.deps.repository.findByIdInCompany(
+ params.companyId,
+ params.id,
+ params.transaction,
+ );
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ return Result.ok(
+ result.data.match(
+ (paymentMethod) => Maybe.some(this.deps.mapper.toPublicModel(paymentMethod)),
+ () => Maybe.none(),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/modules/catalogs/src/api/application/payment-methods/repositories/payment-method-repository.interface.ts b/modules/catalogs/src/api/application/payment-methods/repositories/payment-method-repository.interface.ts
index 946b9998..c2726f05 100644
--- a/modules/catalogs/src/api/application/payment-methods/repositories/payment-method-repository.interface.ts
+++ b/modules/catalogs/src/api/application/payment-methods/repositories/payment-method-repository.interface.ts
@@ -1,6 +1,6 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
-import type { Collection, Result } from "@repo/rdx-utils";
+import type { Collection, Maybe, Result } from "@repo/rdx-utils";
import type { PaymentMethod } from "../../../domain";
import type { PaymentMethodSummary } from "../models";
@@ -18,6 +18,11 @@ export interface IPaymentMethodRepository {
id: UniqueID,
transaction?: unknown
): Promise>;
+ findByIdInCompany(
+ companyId: UniqueID,
+ id: UniqueID,
+ transaction?: unknown
+ ): Promise, Error>>;
findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
diff --git a/modules/catalogs/src/api/application/payment-methods/services/payment-method-finder.ts b/modules/catalogs/src/api/application/payment-methods/services/payment-method-finder.ts
index 43fb3703..dba7cec9 100644
--- a/modules/catalogs/src/api/application/payment-methods/services/payment-method-finder.ts
+++ b/modules/catalogs/src/api/application/payment-methods/services/payment-method-finder.ts
@@ -9,13 +9,13 @@ import type { IPaymentMethodRepository } from "../repositories";
export interface IPaymentMethodFinder {
findPaymentMethodById(
companyId: UniqueID,
- invoiceId: UniqueID,
+ paymentMethodId: UniqueID,
transaction?: unknown
): Promise>;
paymentmethodExists(
companyId: UniqueID,
- invoiceId: UniqueID,
+ paymentMethodId: UniqueID,
transaction?: unknown
): Promise>;
@@ -31,18 +31,18 @@ export class PaymentMethodFinder implements IPaymentMethodFinder {
async findPaymentMethodById(
companyId: UniqueID,
- paymentmethodId: UniqueID,
+ paymentMethodId: UniqueID,
transaction?: unknown
): Promise> {
- return this.repository.getByIdInCompany(companyId, paymentmethodId, transaction);
+ return this.repository.getByIdInCompany(companyId, paymentMethodId, transaction);
}
async paymentmethodExists(
companyId: UniqueID,
- paymentmethodId: UniqueID,
+ paymentMethodId: UniqueID,
transaction?: unknown
): Promise> {
- return this.repository.existsByIdInCompany(companyId, paymentmethodId, transaction);
+ return this.repository.existsByIdInCompany(companyId, paymentMethodId, transaction);
}
async findPaymentMethodsByCriteria(
diff --git a/modules/catalogs/src/api/application/services/catalog-public-services.interface.ts b/modules/catalogs/src/api/application/services/catalog-public-services.interface.ts
deleted file mode 100644
index aac260c3..00000000
--- a/modules/catalogs/src/api/application/services/catalog-public-services.interface.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { UniqueID } from "@repo/rdx-ddd";
-
-import type { IPaymentMethodPublicServices } from "../payment-methods";
-import type { IPaymentTermPublicServices } from "../payment-terms";
-import type { ITaxRegimePublicServices } from "../tax-regimes";
-
-export type { IPaymentMethodPublicServices } from "../payment-methods";
-export type { IPaymentTermPublicServices } from "../payment-terms";
-export type { ITaxRegimePublicServices } from "../tax-regimes";
-
-export interface ICatalogServicesContext {
- transaction: unknown;
- companyId: UniqueID;
-}
-
-export interface ICatalogPublicServices {
- paymentMethods: IPaymentMethodPublicServices;
- paymentTerms: IPaymentTermPublicServices;
- taxRegimes: ITaxRegimePublicServices;
-}
diff --git a/modules/catalogs/src/api/application/services/index.ts b/modules/catalogs/src/api/application/services/index.ts
deleted file mode 100644
index 81746ced..00000000
--- a/modules/catalogs/src/api/application/services/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './catalog-public-services.interface';
diff --git a/modules/catalogs/src/api/application/tax-definitions/index.ts b/modules/catalogs/src/api/application/tax-definitions/index.ts
index f9c2a070..b6a7cf59 100644
--- a/modules/catalogs/src/api/application/tax-definitions/index.ts
+++ b/modules/catalogs/src/api/application/tax-definitions/index.ts
@@ -1,5 +1,6 @@
export * from "./mappers";
export * from "./models";
+export * from "./public";
export * from "./repositories";
export * from "./services";
export * from "./snapshot-builders";
diff --git a/modules/catalogs/src/api/application/tax-definitions/public/errors/index.ts b/modules/catalogs/src/api/application/tax-definitions/public/errors/index.ts
new file mode 100644
index 00000000..4d07a60f
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/public/errors/index.ts
@@ -0,0 +1 @@
+export * from './tax-definitions-not-found.error';
diff --git a/modules/catalogs/src/api/application/tax-definitions/public/errors/tax-definitions-not-found.error.ts b/modules/catalogs/src/api/application/tax-definitions/public/errors/tax-definitions-not-found.error.ts
new file mode 100644
index 00000000..49679ccf
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/public/errors/tax-definitions-not-found.error.ts
@@ -0,0 +1,10 @@
+import { DomainError } from "@repo/rdx-ddd";
+import type { Collection } from "@repo/rdx-utils";
+
+export class TaxDefinitionsNotFoundError extends DomainError {
+ public readonly code = "TAX_DEFINITIONS_NOT_FOUND" as const;
+
+ public constructor(public readonly missingCodes: Collection) {
+ super(`Tax definitions not found for codes: ${missingCodes.getAll().join(", ")}`);
+ }
+}
diff --git a/modules/catalogs/src/api/application/tax-definitions/public/index.ts b/modules/catalogs/src/api/application/tax-definitions/public/index.ts
new file mode 100644
index 00000000..4e9a8fb1
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/public/index.ts
@@ -0,0 +1,5 @@
+export * from "./errors/";
+export * from "./mappers";
+export * from "./models/";
+export * from "./services";
+export * from "./services/";
diff --git a/modules/catalogs/src/api/application/tax-definitions/public/mappers/index.ts b/modules/catalogs/src/api/application/tax-definitions/public/mappers/index.ts
new file mode 100644
index 00000000..bb57fdb7
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/public/mappers/index.ts
@@ -0,0 +1 @@
+export * from './tax-definition-public-model.mapper';
diff --git a/modules/catalogs/src/api/application/tax-definitions/public/mappers/tax-definition-public-model.mapper.ts b/modules/catalogs/src/api/application/tax-definitions/public/mappers/tax-definition-public-model.mapper.ts
new file mode 100644
index 00000000..4da9fae6
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/public/mappers/tax-definition-public-model.mapper.ts
@@ -0,0 +1,80 @@
+import { Collection, Maybe } from "@repo/rdx-utils";
+
+import type { TaxDefinition } from "../../../../domain/tax-definitions";
+import type {
+ TaxDefinitionPublicCalculationBehavior,
+ TaxDefinitionPublicFamily,
+ TaxDefinitionPublicModel,
+ TaxDefinitionPublicScope,
+} from "../models/tax-definition-public.model";
+
+export class TaxDefinitionPublicModelMapper {
+ public toPublicModel(taxDefinition: TaxDefinition): TaxDefinitionPublicModel {
+ return {
+ id: taxDefinition.id,
+ companyId: Maybe.some(taxDefinition.companyId),
+ code: taxDefinition.code.toPrimitive(),
+ name: taxDefinition.name.toPrimitive(),
+ description: taxDefinition.description.map((value) => value.toPrimitive()),
+ rate: taxDefinition.rate,
+ taxFamily: this.mapTaxFamily(taxDefinition.taxFamily.toPrimitive()),
+ calculationBehavior: this.mapCalculationBehavior(
+ taxDefinition.calculationBehavior.toPrimitive()
+ ),
+ taxScope: this.mapTaxScope(taxDefinition.taxScope.toPrimitive()),
+ jurisdictionCountryCode: taxDefinition.jurisdictionCountryCode,
+ jurisdictionRegionCode: taxDefinition.jurisdictionRegionCode,
+ invoiceNote: taxDefinition.invoiceNote.map((value) => value.toPrimitive()),
+ allowedSurchargeCodes: new Collection(
+ taxDefinition.allowedSurchargeCodes.match(
+ (codes) => codes.map((code) => code.toPrimitive()),
+ () => []
+ )
+ ),
+ isSystem: taxDefinition.isSystem,
+ isActive: taxDefinition.isActive,
+ validFrom: taxDefinition.validFrom,
+ validTo: taxDefinition.validTo,
+ };
+ }
+
+ private mapTaxFamily(value: string): TaxDefinitionPublicFamily {
+ switch (value) {
+ case "iva":
+ case "igic":
+ case "ipsi":
+ return value;
+ case "equivalence_surcharge":
+ return "surcharge";
+ case "withholding":
+ return "retention";
+ default:
+ throw new Error(`Unsupported tax family for public model: ${value}`);
+ }
+ }
+
+ private mapCalculationBehavior(value: string): TaxDefinitionPublicCalculationBehavior {
+ switch (value) {
+ case "additive":
+ case "subtractive":
+ return value;
+ default:
+ throw new Error(`Unsupported calculation behavior for public model: ${value}`);
+ }
+ }
+
+ private mapTaxScope(value: string): TaxDefinitionPublicScope {
+ switch (value) {
+ case "domestic":
+ return "sales";
+ case "intra_eu":
+ case "import":
+ return "purchases";
+ case "export":
+ case "international":
+ return "both";
+ default:
+ throw new Error(`Unsupported tax scope for public model: ${value}`);
+ }
+ }
+}
diff --git a/modules/catalogs/src/api/application/tax-definitions/public/models/index.ts b/modules/catalogs/src/api/application/tax-definitions/public/models/index.ts
new file mode 100644
index 00000000..fa5957bf
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/public/models/index.ts
@@ -0,0 +1 @@
+export * from './tax-definition-public.model';
diff --git a/modules/catalogs/src/api/application/tax-definitions/public/models/tax-definition-public.model.ts b/modules/catalogs/src/api/application/tax-definitions/public/models/tax-definition-public.model.ts
new file mode 100644
index 00000000..ec31c928
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/public/models/tax-definition-public.model.ts
@@ -0,0 +1,72 @@
+import type {
+ CountryCode,
+ CountryRegionCode, Percentage, UniqueID,
+ UtcDate
+} from "@repo/rdx-ddd";
+import type { Collection, Maybe } from "@repo/rdx-utils";
+
+/**
+ * Familia fiscal pública expuesta por `catalogs`.
+ *
+ * No se expone el Value Object interno de dominio para evitar acoplar
+ * consumidores externos a las invariantes privadas de `tax-definitions`.
+ */
+export type TaxDefinitionPublicFamily = "iva" | "igic" | "ipsi" | "surcharge" | "retention";
+
+/**
+ * Comportamiento de cálculo público de un impuesto.
+ *
+ * `additive` suma al total.
+ * `subtractive` resta del total, por ejemplo retenciones.
+ */
+export type TaxDefinitionPublicCalculationBehavior = "additive" | "subtractive";
+
+/**
+ * Ámbito público de aplicación fiscal.
+ */
+export type TaxDefinitionPublicScope = "sales" | "purchases" | "both";
+
+/**
+ * Modelo público de lectura expuesto por el módulo `catalogs`.
+ *
+ * Es un contrato backend entre módulos, no un DTO HTTP y no una entidad
+ * de dominio. Puede usar Value Objects comunes del ERP, pero no debe
+ * exponer Value Objects internos de `tax-definitions`.
+ */
+export interface TaxDefinitionPublicModel {
+ id: UniqueID;
+
+ /**
+ * `none` si la definición es global/sistema.
+ * `some(companyId)` si la definición está sobrescrita para una empresa.
+ */
+ companyId: Maybe;
+
+ code: string;
+ name: string;
+ description: Maybe;
+
+ rate: Percentage;
+
+ taxFamily: TaxDefinitionPublicFamily;
+ calculationBehavior: TaxDefinitionPublicCalculationBehavior;
+ taxScope: TaxDefinitionPublicScope;
+
+ jurisdictionCountryCode: CountryCode;
+ jurisdictionRegionCode: Maybe;
+
+ invoiceNote: Maybe;
+
+ /**
+ * Códigos de recargo compatibles con esta definición.
+ *
+ * Colección vacía => no permite recargos.
+ */
+ allowedSurchargeCodes: Collection;
+
+ isSystem: boolean;
+ isActive: boolean;
+
+ validFrom: Maybe;
+ validTo: Maybe;
+}
diff --git a/modules/catalogs/src/api/application/tax-definitions/public/services/index.ts b/modules/catalogs/src/api/application/tax-definitions/public/services/index.ts
new file mode 100644
index 00000000..c78c790b
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/public/services/index.ts
@@ -0,0 +1,2 @@
+export * from './tax-definition-public-finder.interface';
+export * from './tax-definition-public-finder';
diff --git a/modules/catalogs/src/api/application/tax-definitions/public/services/tax-definition-public-finder.interface.ts b/modules/catalogs/src/api/application/tax-definitions/public/services/tax-definition-public-finder.interface.ts
new file mode 100644
index 00000000..01227d04
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/public/services/tax-definition-public-finder.interface.ts
@@ -0,0 +1,30 @@
+import type { UniqueID, UtcDate } from "@repo/rdx-ddd";
+import type { Collection, Maybe, Result } from "@repo/rdx-utils";
+
+import type { TaxDefinitionPublicModel } from "../models/tax-definition-public.model";
+
+export interface FindActiveTaxDefinitionByCodeParams {
+ companyId: UniqueID;
+ code: string;
+ atDate: UtcDate;
+}
+
+export interface FindActiveTaxDefinitionsByCodesParams {
+ companyId: UniqueID;
+ codes: Collection;
+ atDate: UtcDate;
+}
+
+export interface ITaxDefinitionPublicFinder {
+ findActiveByCode(
+ params: FindActiveTaxDefinitionByCodeParams
+ ): Promise, Error>>;
+
+ findActiveByCodes(
+ params: FindActiveTaxDefinitionsByCodesParams
+ ): Promise, Error>>;
+
+ ensureActiveByCodes(
+ params: FindActiveTaxDefinitionsByCodesParams
+ ): Promise, Error>>;
+}
diff --git a/modules/catalogs/src/api/application/tax-definitions/public/services/tax-definition-public-finder.ts b/modules/catalogs/src/api/application/tax-definitions/public/services/tax-definition-public-finder.ts
new file mode 100644
index 00000000..5c4576d1
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/public/services/tax-definition-public-finder.ts
@@ -0,0 +1,64 @@
+import { Collection, Maybe, Result } from "@repo/rdx-utils";
+
+import type { ITaxDefinitionRepository } from "../../repositories";
+import { TaxDefinitionsNotFoundError } from "../errors/tax-definitions-not-found.error";
+import type { TaxDefinitionPublicModelMapper } from "../mappers/tax-definition-public-model.mapper";
+import type {
+ FindActiveTaxDefinitionByCodeParams,
+ FindActiveTaxDefinitionsByCodesParams,
+ ITaxDefinitionPublicFinder,
+} from "./tax-definition-public-finder.interface";
+import { TaxDefinitionPublicModel } from '../models';
+
+export class TaxDefinitionPublicFinder implements ITaxDefinitionPublicFinder {
+ public constructor(
+ private readonly deps: {
+ repository: ITaxDefinitionRepository;
+ mapper: TaxDefinitionPublicModelMapper;
+ }
+ ) {}
+
+ public async findActiveByCode(params: FindActiveTaxDefinitionByCodeParams): Promise, Error>> {
+ const result = await this.deps.repository.findActiveByCodeInCompany(params);
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ return Result.ok(result.data.map((taxDefinition) => this.deps.mapper.toPublicModel(taxDefinition)));
+ }
+
+ public async findActiveByCodes(params: FindActiveTaxDefinitionsByCodesParams): Promise, Error>> {
+ const result = await this.deps.repository.findActiveByCodesInCompany(params);
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ return Result.ok(new Collection(result.data.map((taxDefinition) => this.deps.mapper.toPublicModel(taxDefinition))));
+ }
+
+ public async ensureActiveByCodes(params: FindActiveTaxDefinitionsByCodesParams):Promise, Error>> {
+ const result = await this.findActiveByCodes(params);
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ const requestedCodes = Array.from(
+ new Set(params.codes.getAll().map((code) => this.normalizeCode(code)))
+ );
+ const foundCodes = new Set(result.data.getAll().map((taxDefinition) => taxDefinition.code));
+ const missingCodes = requestedCodes.filter((code) => !foundCodes.has(code));
+
+ if (missingCodes.length > 0) {
+ return Result.fail(new TaxDefinitionsNotFoundError(new Collection(missingCodes)));
+ }
+
+ return result;
+ }
+
+ private normalizeCode(code: string): string {
+ return code.trim().toLowerCase();
+ }
+}
diff --git a/modules/catalogs/src/api/application/tax-definitions/repositories/tax-definition-repository.interface.ts b/modules/catalogs/src/api/application/tax-definitions/repositories/tax-definition-repository.interface.ts
index 389294ba..c239ab91 100644
--- a/modules/catalogs/src/api/application/tax-definitions/repositories/tax-definition-repository.interface.ts
+++ b/modules/catalogs/src/api/application/tax-definitions/repositories/tax-definition-repository.interface.ts
@@ -1,6 +1,6 @@
import type { Criteria } from "@repo/rdx-criteria/server";
-import type { UniqueID } from "@repo/rdx-ddd";
-import type { Collection, Result } from "@repo/rdx-utils";
+import type { UniqueID, UtcDate } from "@repo/rdx-ddd";
+import type { Collection, Maybe, Result } from "@repo/rdx-utils";
import type { TaxDefinition, TaxDefinitionCode } from "../../../domain";
import type { TaxDefinitionSummary } from "../../tax-definitions/models";
@@ -32,6 +32,20 @@ export interface ITaxDefinitionRepository {
transaction?: unknown
): Promise>;
+ findActiveByCodeInCompany(params: {
+ companyId: UniqueID;
+ code: string;
+ atDate: UtcDate;
+ transaction?: unknown;
+ }): Promise, Error>>;
+
+ findActiveByCodesInCompany(params: {
+ companyId: UniqueID;
+ codes: Collection;
+ atDate: UtcDate;
+ transaction?: unknown;
+ }): Promise, Error>>;
+
findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
diff --git a/modules/catalogs/src/api/application/tax-definitions/services/index.ts b/modules/catalogs/src/api/application/tax-definitions/services/index.ts
index fa60b209..cb32ad1e 100644
--- a/modules/catalogs/src/api/application/tax-definitions/services/index.ts
+++ b/modules/catalogs/src/api/application/tax-definitions/services/index.ts
@@ -1,5 +1,6 @@
export * from "./tax-definition-creator.service";
export * from "./tax-definition-deleter.service";
export * from "./tax-definition-finder.service";
+export * from "./tax-definition-public-services";
export * from "./tax-definition-status-changer.service";
export * from "./tax-definition-updater.service";
diff --git a/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-public-services.ts b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-public-services.ts
new file mode 100644
index 00000000..17974738
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-public-services.ts
@@ -0,0 +1,11 @@
+import type { ITaxDefinitionPublicFinder } from "../public";
+
+export interface ITaxDefinitionPublicServices {
+ finder: ITaxDefinitionPublicFinder;
+}
+
+export const buildTaxDefinitionsPublicServices = (
+ finder: ITaxDefinitionPublicFinder
+): ITaxDefinitionPublicServices => ({
+ finder,
+});
diff --git a/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/full-tax-definition-snapshot.builder.ts b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/full-tax-definition-snapshot.builder.ts
index 1d058f4a..4570e8e3 100644
--- a/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/full-tax-definition-snapshot.builder.ts
+++ b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/full-tax-definition-snapshot.builder.ts
@@ -1,4 +1,3 @@
-import type { TaxDefinitionDetailDTO } from "@erp/catalogs/common";
import type { ISnapshotBuilder } from "@erp/core/api";
import type { TaxDefinitionDetail } from "../../models";
diff --git a/modules/catalogs/src/api/application/tax-regimes/index.ts b/modules/catalogs/src/api/application/tax-regimes/index.ts
index f9c2a070..b6a7cf59 100644
--- a/modules/catalogs/src/api/application/tax-regimes/index.ts
+++ b/modules/catalogs/src/api/application/tax-regimes/index.ts
@@ -1,5 +1,6 @@
export * from "./mappers";
export * from "./models";
+export * from "./public";
export * from "./repositories";
export * from "./services";
export * from "./snapshot-builders";
diff --git a/modules/catalogs/src/api/application/tax-regimes/public/errors/index.ts b/modules/catalogs/src/api/application/tax-regimes/public/errors/index.ts
new file mode 100644
index 00000000..515a81c3
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-regimes/public/errors/index.ts
@@ -0,0 +1 @@
+export * from './tax-regime-not-found.error';
diff --git a/modules/catalogs/src/api/application/tax-regimes/public/errors/tax-regime-not-found.error.ts b/modules/catalogs/src/api/application/tax-regimes/public/errors/tax-regime-not-found.error.ts
new file mode 100644
index 00000000..84633786
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-regimes/public/errors/tax-regime-not-found.error.ts
@@ -0,0 +1,10 @@
+import { DomainError } from "@repo/rdx-ddd";
+import type { Collection } from "@repo/rdx-utils";
+
+export class TaxRegimesNotFoundError extends DomainError {
+ public readonly code = "TAX_REGIME_NOT_FOUND" as const;
+
+ public constructor(public readonly missingCodes: Collection) {
+ super(`Tax regime not found for codes: ${missingCodes.getAll().join(", ")}`);
+ }
+}
diff --git a/modules/catalogs/src/api/application/tax-regimes/public/index.ts b/modules/catalogs/src/api/application/tax-regimes/public/index.ts
new file mode 100644
index 00000000..a3edecb3
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-regimes/public/index.ts
@@ -0,0 +1,14 @@
+export { TaxRegimesNotFoundError } from "./errors/";
+export { TaxRegimePublicModelMapper } from "./mappers";
+export type {
+ TaxRegimePublicCalculationBehavior,
+ TaxRegimePublicFamily,
+ TaxRegimePublicModel,
+ TaxRegimePublicScope,
+} from "./models/";
+export type {
+ FindActiveTaxRegimeByCodeParams,
+ FindActiveTaxRegimesByCodesParams,
+ ITaxRegimePublicFinder,
+} from "./services";
+export { TaxRegimePublicFinder } from "./services/";
diff --git a/modules/catalogs/src/api/application/tax-regimes/public/mappers/index.ts b/modules/catalogs/src/api/application/tax-regimes/public/mappers/index.ts
new file mode 100644
index 00000000..6b8ad82f
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-regimes/public/mappers/index.ts
@@ -0,0 +1 @@
+export * from './tax-regime-public-model.mapper';
diff --git a/modules/catalogs/src/api/application/tax-regimes/public/mappers/tax-regime-public-model.mapper.ts b/modules/catalogs/src/api/application/tax-regimes/public/mappers/tax-regime-public-model.mapper.ts
new file mode 100644
index 00000000..50ff84c1
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-regimes/public/mappers/tax-regime-public-model.mapper.ts
@@ -0,0 +1,20 @@
+
+import type { TaxRegime } from "../../../../domain/tax-regimes";
+import type {
+ TaxRegimePublicModel
+} from "../models/tax-regime-public.model";
+
+export class TaxRegimePublicModelMapper {
+ public toPublicModel(TaxRegime: TaxRegime): TaxRegimePublicModel {
+ return {
+ id: TaxRegime.id,
+ companyId: TaxRegime.companyId,
+ code: TaxRegime.code.toPrimitive(),
+ description: TaxRegime.description.toPrimitive(),
+ isSystem: TaxRegime.isSystem,
+ isActive: TaxRegime.isActive,
+ };
+ }
+
+
+}
diff --git a/modules/catalogs/src/api/application/tax-regimes/public/models/index.ts b/modules/catalogs/src/api/application/tax-regimes/public/models/index.ts
new file mode 100644
index 00000000..cca6b57e
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-regimes/public/models/index.ts
@@ -0,0 +1 @@
+export * from './tax-regime-public.model';
diff --git a/modules/catalogs/src/api/application/tax-regimes/public/models/tax-regime-public.model.ts b/modules/catalogs/src/api/application/tax-regimes/public/models/tax-regime-public.model.ts
new file mode 100644
index 00000000..7cec6da6
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-regimes/public/models/tax-regime-public.model.ts
@@ -0,0 +1,20 @@
+import type { UniqueID } from "@repo/rdx-ddd";
+
+/**
+ * Modelo público de lectura expuesto por el módulo `catalogs`.
+ *
+ * Es un contrato backend entre módulos, no un DTO HTTP y no una entidad
+ * de dominio. Puede usar Value Objects comunes del ERP, pero no debe
+ * exponer Value Objects internos de `tax-regimes`.
+ */
+export interface TaxRegimePublicModel {
+ id: UniqueID;
+ companyId: UniqueID;
+
+ code: string;
+ description: string;
+
+ isSystem: boolean;
+ isActive: boolean;
+
+}
diff --git a/modules/catalogs/src/api/application/tax-regimes/public/services/index.ts b/modules/catalogs/src/api/application/tax-regimes/public/services/index.ts
new file mode 100644
index 00000000..e5063181
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-regimes/public/services/index.ts
@@ -0,0 +1,2 @@
+export * from './tax-regime-public-finder.interface';
+export * from './tax-regime-public-finder';
diff --git a/modules/catalogs/src/api/application/tax-regimes/public/services/tax-regime-public-finder.interface.ts b/modules/catalogs/src/api/application/tax-regimes/public/services/tax-regime-public-finder.interface.ts
new file mode 100644
index 00000000..413ad6ac
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-regimes/public/services/tax-regime-public-finder.interface.ts
@@ -0,0 +1,24 @@
+import type { UniqueID } from "@repo/rdx-ddd";
+import type { Maybe, Result } from "@repo/rdx-utils";
+
+import type { TaxRegimePublicModel } from "../models";
+
+export interface FindTaxRegimeByCodeInCompanyParams {
+ companyId: UniqueID;
+ code: string;
+ transaction?: unknown;
+}
+
+export interface ITaxRegimePublicFinder {
+ existsByCodeInCompany(
+ params: FindTaxRegimeByCodeInCompanyParams,
+ ): Promise>;
+
+ getByCodeInCompany(
+ params: FindTaxRegimeByCodeInCompanyParams,
+ ): Promise>;
+
+ findByCodeInCompany(
+ params: FindTaxRegimeByCodeInCompanyParams,
+ ): Promise, Error>>;
+}
\ No newline at end of file
diff --git a/modules/catalogs/src/api/application/tax-regimes/public/services/tax-regime-public-finder.ts b/modules/catalogs/src/api/application/tax-regimes/public/services/tax-regime-public-finder.ts
new file mode 100644
index 00000000..52ab6cb1
--- /dev/null
+++ b/modules/catalogs/src/api/application/tax-regimes/public/services/tax-regime-public-finder.ts
@@ -0,0 +1,96 @@
+import { Maybe, Result } from "@repo/rdx-utils";
+
+import { TaxRegimeCode } from "../../../../domain/tax-regimes";
+import type { ITaxRegimeRepository } from "../../repositories";
+import type { TaxRegimePublicModelMapper } from "../mappers/tax-regime-public-model.mapper";
+import type { TaxRegimePublicModel } from "../models";
+import type {
+ FindTaxRegimeByCodeInCompanyParams,
+ ITaxRegimePublicFinder,
+} from "./tax-regime-public-finder.interface";
+
+export class TaxRegimePublicFinder implements ITaxRegimePublicFinder {
+ public constructor(
+ private readonly deps: {
+ repository: ITaxRegimeRepository;
+ mapper: TaxRegimePublicModelMapper;
+ },
+ ) {}
+
+ public async existsByCodeInCompany(
+ params: FindTaxRegimeByCodeInCompanyParams,
+ ): Promise> {
+ const code = TaxRegimeCode.create(params.code);
+
+ if (code.isFailure) {
+ return Result.fail(code.error);
+ }
+
+ const result = await this.deps.repository.existsByCodeInCompany(
+ params.companyId,
+ code.data,
+ params.transaction,
+ );
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ return Result.ok(result.data);
+ }
+
+ public async getByCodeInCompany(
+ params: FindTaxRegimeByCodeInCompanyParams,
+ ): Promise> {
+ const code = TaxRegimeCode.create(params.code);
+
+ if (code.isFailure) {
+ return Result.fail(code.error);
+ }
+
+ const result = await this.deps.repository.getByCodeInCompany(
+ params.companyId,
+ code.data,
+ params.transaction,
+ );
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ return Result.ok(this.deps.mapper.toPublicModel(result.data));
+ }
+
+ /**
+ * Busca un régimen fiscal por código dentro de una empresa.
+ *
+ * No falla si el régimen fiscal no existe. En ese caso devuelve `Maybe.none()`.
+ * Usar `getByCodeInCompany` cuando la ausencia deba tratarse como error.
+ */
+ public async findByCodeInCompany(
+ params: FindTaxRegimeByCodeInCompanyParams,
+ ): Promise, Error>> {
+ const code = TaxRegimeCode.create(params.code);
+
+ if (code.isFailure) {
+ return Result.fail(code.error);
+ }
+
+ const result = await this.deps.repository.findByCodeInCompany(
+ params.companyId,
+ code.data,
+ params.transaction,
+ );
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ return Result.ok(
+ result.data.match(
+ (taxRegime) => Maybe.some(this.deps.mapper.toPublicModel(taxRegime)),
+ () => Maybe.none(),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/modules/catalogs/src/api/application/tax-regimes/repositories/tax-regime-repository.interface.ts b/modules/catalogs/src/api/application/tax-regimes/repositories/tax-regime-repository.interface.ts
index 21253aec..0eaf3509 100644
--- a/modules/catalogs/src/api/application/tax-regimes/repositories/tax-regime-repository.interface.ts
+++ b/modules/catalogs/src/api/application/tax-regimes/repositories/tax-regime-repository.interface.ts
@@ -1,6 +1,6 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
-import type { Collection, Result } from "@repo/rdx-utils";
+import type { Collection, Maybe, Result } from "@repo/rdx-utils";
import type { TaxRegime, TaxRegimeCode } from "../../../domain";
import type { TaxRegimeSummary } from "../models";
@@ -20,6 +20,12 @@ export interface ITaxRegimeRepository {
transaction?: unknown
): Promise>;
+ existsByCodeInCompany(
+ companyId: UniqueID,
+ code: TaxRegimeCode,
+ transaction?: unknown
+ ): Promise>;
+
getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
@@ -32,6 +38,12 @@ export interface ITaxRegimeRepository {
transaction?: unknown
): Promise>;
+ findByCodeInCompany(
+ companyId: UniqueID,
+ code: TaxRegimeCode,
+ transaction?: unknown
+ ): Promise, Error>>;
+
findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
diff --git a/modules/catalogs/src/api/domain/tax-definitions/errors.ts b/modules/catalogs/src/api/domain/tax-definitions/errors.ts
index 903b427c..731ad620 100644
--- a/modules/catalogs/src/api/domain/tax-definitions/errors.ts
+++ b/modules/catalogs/src/api/domain/tax-definitions/errors.ts
@@ -61,24 +61,6 @@ export const isInvalidTaxDefinitionCalculationBehaviorError = (
): e is InvalidTaxDefinitionCalculationBehaviorError =>
e instanceof InvalidTaxDefinitionCalculationBehaviorError;
-export class InvalidTaxDefinitionJurisdictionCountryCodeError extends DomainError {
- public readonly code = "TAX_DEFINITION_INVALID_JURISDICTION_COUNTRY_CODE" as const;
-}
-
-export const isInvalidTaxDefinitionJurisdictionCountryCodeError = (
- e: unknown
-): e is InvalidTaxDefinitionJurisdictionCountryCodeError =>
- e instanceof InvalidTaxDefinitionJurisdictionCountryCodeError;
-
-export class InvalidTaxDefinitionJurisdictionRegionCodeError extends DomainError {
- public readonly code = "TAX_DEFINITION_INVALID_JURISDICTION_REGION_CODE" as const;
-}
-
-export const isInvalidTaxDefinitionJurisdictionRegionCodeError = (
- e: unknown
-): e is InvalidTaxDefinitionJurisdictionRegionCodeError =>
- e instanceof InvalidTaxDefinitionJurisdictionRegionCodeError;
-
export class InvalidTaxDefinitionScopeError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_SCOPE" as const;
}
diff --git a/modules/catalogs/src/api/domain/tax-definitions/index.ts b/modules/catalogs/src/api/domain/tax-definitions/index.ts
index 47ec912b..dfd50c16 100644
--- a/modules/catalogs/src/api/domain/tax-definitions/index.ts
+++ b/modules/catalogs/src/api/domain/tax-definitions/index.ts
@@ -1,7 +1,5 @@
export * from "./calculation-behavior";
export * from "./errors";
-export * from "./jurisdiction-country-code";
-export * from "./jurisdiction-region-code";
export * from "./tax-definition.aggregate";
export * from "./tax-definition-code";
export * from "./tax-definition-name";
diff --git a/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-country-code.ts b/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-country-code.ts
deleted file mode 100644
index a3fbda89..00000000
--- a/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-country-code.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Result } from "@repo/rdx-utils";
-
-import { InvalidTaxDefinitionJurisdictionCountryCodeError } from "./errors";
-
-export class TaxJurisdictionCountryCode {
- private constructor(private readonly value: string) {}
-
- public static create(code: string): Result {
- const trimmed = code?.trim() ?? "";
- if (trimmed.length !== 2) {
- return Result.fail(
- new InvalidTaxDefinitionJurisdictionCountryCodeError("Country code must be 2 letters")
- );
- }
-
- const normalized = trimmed.toUpperCase();
- const regex = /^[A-Z]{2}$/;
- if (!regex.test(normalized)) {
- return Result.fail(
- new InvalidTaxDefinitionJurisdictionCountryCodeError(
- "Country code must be 2 uppercase letters"
- )
- );
- }
-
- // NOTE: We allow two-letter codes including 'EU' to support union-level definitions like reverse charge.
- return Result.ok(new TaxJurisdictionCountryCode(normalized));
- }
-
- public static fromPersistence(code: string): TaxJurisdictionCountryCode {
- return new TaxJurisdictionCountryCode(code);
- }
-
- public toPrimitive(): string {
- return this.value;
- }
-
- public toString(): string {
- return this.value;
- }
-}
diff --git a/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-region-code.ts b/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-region-code.ts
deleted file mode 100644
index 58a60136..00000000
--- a/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-region-code.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Result } from "@repo/rdx-utils";
-
-import { InvalidTaxDefinitionJurisdictionRegionCodeError } from "./errors";
-
-export class TaxJurisdictionRegionCode {
- private constructor(private readonly value: string) {}
-
- public static create(code: string): Result {
- const trimmed = code?.trim() ?? "";
- if (trimmed.length === 0) {
- return Result.fail(
- new InvalidTaxDefinitionJurisdictionRegionCodeError("Region code cannot be empty")
- );
- }
-
- const normalized = trimmed.toUpperCase();
-
- // basic pattern: CC-... (we won't validate full ISO-3166-2 list)
- const regex = /^[A-Z]{2}-[A-Z0-9-]+$/;
- if (!regex.test(normalized)) {
- return Result.fail(
- new InvalidTaxDefinitionJurisdictionRegionCodeError(
- "Region code must follow ISO-3166-2 pattern COUNTRY-REGION"
- )
- );
- }
-
- return Result.ok(new TaxJurisdictionRegionCode(normalized));
- }
-
- public static fromPersistence(code: string): TaxJurisdictionRegionCode {
- return new TaxJurisdictionRegionCode(code);
- }
-
- public toPrimitive(): string {
- return this.value;
- }
-
- public toString(): string {
- return this.value;
- }
-}
diff --git a/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.ts b/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.ts
index ab97167c..fda4c524 100644
--- a/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.ts
+++ b/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.ts
@@ -1,14 +1,18 @@
-import { AggregateRoot, type TextValue, type UniqueID, type UtcDate } from "@repo/rdx-ddd";
+import {
+ AggregateRoot,
+ type CountryCode,
+ type CountryRegionCode,
+ type TextValue,
+ type UniqueID,
+ type UtcDate,
+} from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import type { TaxCalculationBehavior } from "./calculation-behavior";
import {
InvalidTaxDefinitionAllowedSurchargeCodesError,
- InvalidTaxDefinitionJurisdictionRegionCodeError,
InvalidTaxDefinitionValidityPeriodError,
} from "./errors";
-import type { TaxJurisdictionCountryCode } from "./jurisdiction-country-code";
-import type { TaxJurisdictionRegionCode } from "./jurisdiction-region-code";
import type { TaxDefinitionCode } from "./tax-definition-code";
import type { TaxDefinitionName } from "./tax-definition-name";
import type { TaxFamily } from "./tax-family";
@@ -23,8 +27,8 @@ export interface ITaxDefinitionCreateProps {
rate: TaxRate;
taxFamily: TaxFamily;
calculationBehavior: TaxCalculationBehavior;
- jurisdictionCountryCode: TaxJurisdictionCountryCode;
- jurisdictionRegionCode: Maybe;
+ jurisdictionCountryCode: CountryCode;
+ jurisdictionRegionCode: Maybe;
taxScope: TaxScope;
invoiceNote: Maybe; // Texto fiscal que aparece en el documento
allowedSurchargeCodes: Maybe;
@@ -146,19 +150,6 @@ export class TaxDefinition extends AggregateRoot {
}
}
- // region coherence with country
- if (props.jurisdictionRegionCode && props.jurisdictionRegionCode.isSome()) {
- const region = props.jurisdictionRegionCode.unwrap().toPrimitive();
- const country = props.jurisdictionCountryCode.toPrimitive();
- if (!region.startsWith(`${country}-`)) {
- return Result.fail(
- new InvalidTaxDefinitionJurisdictionRegionCodeError(
- "Region code must start with country code"
- )
- );
- }
- }
-
return Result.ok();
}
@@ -198,11 +189,11 @@ export class TaxDefinition extends AggregateRoot {
return this.props.calculationBehavior;
}
- public get jurisdictionCountryCode(): TaxJurisdictionCountryCode {
+ public get jurisdictionCountryCode(): CountryCode {
return this.props.jurisdictionCountryCode;
}
- public get jurisdictionRegionCode(): Maybe {
+ public get jurisdictionRegionCode(): Maybe {
return this.props.jurisdictionRegionCode;
}
diff --git a/modules/catalogs/src/api/index.ts b/modules/catalogs/src/api/index.ts
index 72d49956..ad825e3c 100644
--- a/modules/catalogs/src/api/index.ts
+++ b/modules/catalogs/src/api/index.ts
@@ -5,6 +5,8 @@ import {
paymentMethodsRouter,
paymentTermModels,
paymentTermsRouter,
+ taxDefinitionModels,
+ taxDefinitionsRouter,
taxRegimeModels,
taxRegimesRouter,
} from "./infrastructure";
@@ -13,10 +15,14 @@ import {
buildCatalogsPublicServices,
} from "./infrastructure/di/catalogs.di";
-export * from "./application/services/catalog-public-services.interface"; // <- exportamos la interfaz de los servicios públicos para que otros módulos puedan usarla en sus dependencias
+export * from "./application/payment-methods/public";
+export * from "./application/tax-definitions/public";
+export * from "./application/tax-regimes/public";
//export * from "./infrastructure/payment-methods/persistence/sequelize"; <- ???
+export type CatalogsPublicServicesType = ReturnType;
+
export const catalogsAPIModule: IModuleServer = {
name: "catalogs",
version: "1.0.0",
@@ -44,13 +50,16 @@ export const catalogsAPIModule: IModuleServer = {
return {
// Modelos Sequelize del módulo
- models: [...paymentMethodModels, ...paymentTermModels, ...taxRegimeModels],
+ models: [
+ ...paymentMethodModels,
+ ...paymentTermModels,
+ ...taxDefinitionModels,
+ ...taxRegimeModels,
+ ],
// Servicios expuestos a otros módulos
services: {
- paymentMethods: publicServices.paymentMethods,
- paymentTerms: publicServices.paymentTerms,
- taxRegimes: publicServices.taxRegimes,
+ ...publicServices,
},
// Implementación privada del módulo
@@ -72,6 +81,7 @@ export const catalogsAPIModule: IModuleServer = {
paymentMethodsRouter(params);
paymentTermsRouter(params);
taxRegimesRouter(params);
+ taxDefinitionsRouter(params);
logger.info("🚀 Catalogs module started", {
label: this.name,
diff --git a/modules/catalogs/src/api/infrastructure/di/catalogs.di.ts b/modules/catalogs/src/api/infrastructure/di/catalogs.di.ts
index ebf601e4..ed8cc9ab 100644
--- a/modules/catalogs/src/api/infrastructure/di/catalogs.di.ts
+++ b/modules/catalogs/src/api/infrastructure/di/catalogs.di.ts
@@ -10,6 +10,11 @@ import {
buildPaymentTermsDependencies,
buildPaymentTermsPublicServices,
} from "../payment-terms/di";
+import {
+ type TaxDefinitionsInternalDeps,
+ buildTaxDefinitionsDependencies,
+ buildTaxDefinitionsPublicServices,
+} from "../tax-definitions/di";
import {
type TaxRegimesInternalDeps,
buildTaxRegimesDependencies,
@@ -19,6 +24,7 @@ import {
export type CatalogsInternalDeps = {
paymentMethods: PaymentMethodsInternalDeps;
paymentTerms: PaymentTermsInternalDeps;
+ taxDefinitions: TaxDefinitionsInternalDeps;
taxRegimes: TaxRegimesInternalDeps;
};
@@ -26,6 +32,7 @@ export const buildCatalogsDependencies = (params: ModuleParams): CatalogsInterna
return {
paymentMethods: buildPaymentMethodsDependencies(params),
paymentTerms: buildPaymentTermsDependencies(params),
+ taxDefinitions: buildTaxDefinitionsDependencies(params),
taxRegimes: buildTaxRegimesDependencies(params),
};
};
@@ -34,6 +41,7 @@ export const buildCatalogsPublicServices = (params: SetupParams, deps: CatalogsI
return {
paymentMethods: buildPaymentMethodsPublicServices(params, deps.paymentMethods),
paymentTerms: buildPaymentTermsPublicServices(params, deps.paymentTerms),
+ taxDefinitions: buildTaxDefinitionsPublicServices(params, deps.taxDefinitions),
taxRegimes: buildTaxRegimesPublicServices(params, deps.taxRegimes),
};
};
diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/repositories/sequelize-payment-method.repository.ts b/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/repositories/sequelize-payment-method.repository.ts
index 5b5a1576..97338523 100644
--- a/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/repositories/sequelize-payment-method.repository.ts
+++ b/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/repositories/sequelize-payment-method.repository.ts
@@ -6,7 +6,7 @@ import {
} from "@erp/core/api";
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
-import { type Collection, Result } from "@repo/rdx-utils";
+import { type Collection, Maybe, Result } from "@repo/rdx-utils";
import type { Sequelize, Transaction } from "sequelize";
import type { IPaymentMethodRepository } from "../../../../../application";
@@ -145,6 +145,35 @@ export class SequelizePaymentMethodRepository
}
}
+ async findByIdInCompany(
+ companyId: UniqueID,
+ id: UniqueID,
+ transaction?: Transaction
+ ): Promise, Error>> {
+ try {
+ const row = await PaymentMethodModel.findOne({
+ where: {
+ id: id.toString(),
+ company_id: companyId.toString(),
+ },
+ transaction,
+ });
+
+ if (!row) {
+ return Result.ok(Maybe.none());
+ }
+
+ const mappedResult = this.domainMapper.mapToDomain(row);
+ if (mappedResult.isFailure) {
+ return Result.fail(mappedResult.error);
+ }
+
+ return Result.ok(Maybe.some(mappedResult.data));
+ } catch (err: unknown) {
+ return Result.fail(translateSequelizeError(err));
+ }
+ }
+
/**
* Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.).
*
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/di/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/di/index.ts
index 6b086d9c..f7fb465a 100644
--- a/modules/catalogs/src/api/infrastructure/tax-definitions/di/index.ts
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/di/index.ts
@@ -1,2 +1,3 @@
export * from "./tax-definition-persistence-mappers.di";
export * from "./tax-definition-repositories.di";
+export * from "./tax-definitions.di";
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definitions.di.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definitions.di.ts
new file mode 100644
index 00000000..18040f4a
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definitions.di.ts
@@ -0,0 +1,125 @@
+import { type ModuleParams, type SetupParams, buildTransactionManager } from "@erp/core/api";
+import type { Sequelize } from "sequelize";
+
+import {
+ CreateTaxDefinitionInputMapper,
+ CreateTaxDefinitionUseCase,
+ DeleteTaxDefinitionByIdUseCase,
+ DisableTaxDefinitionByIdUseCase,
+ EnableTaxDefinitionByIdUseCase,
+ GetTaxDefinitionByIdUseCase,
+ type ITaxDefinitionPublicFinder,
+ type ITaxDefinitionRepository,
+ ListTaxDefinitionsUseCase,
+ TaxDefinitionCreator,
+ TaxDefinitionDeleter,
+ TaxDefinitionFinder,
+ TaxDefinitionFullSnapshotBuilder,
+ TaxDefinitionPublicFinder,
+ TaxDefinitionPublicModelMapper,
+ TaxDefinitionStatusChanger,
+ TaxDefinitionSummarySnapshotBuilder,
+ TaxDefinitionUpdater,
+ UpdateTaxDefinitionByIdInputMapper,
+ UpdateTaxDefinitionByIdUseCase,
+} from "../../../application";
+
+import { buildTaxDefinitionPersistenceMappers } from "./tax-definition-persistence-mappers.di";
+import { buildTaxDefinitionRepository } from "./tax-definition-repositories.di";
+
+export type TaxDefinitionsInternalDeps = {
+ repository: ITaxDefinitionRepository;
+ useCases: {
+ listTaxDefinitions: () => ListTaxDefinitionsUseCase;
+ getTaxDefinitionById: () => GetTaxDefinitionByIdUseCase;
+ createTaxDefinition: () => CreateTaxDefinitionUseCase;
+ updateTaxDefinitionById: () => UpdateTaxDefinitionByIdUseCase;
+ deleteTaxDefinitionById: () => DeleteTaxDefinitionByIdUseCase;
+ disableTaxDefinitionById: () => DisableTaxDefinitionByIdUseCase;
+ enableTaxDefinitionById: () => EnableTaxDefinitionByIdUseCase;
+ };
+};
+
+export const buildTaxDefinitionsDependencies = (
+ params: ModuleParams
+): TaxDefinitionsInternalDeps => {
+ const { database } = params;
+
+ const transactionManager = buildTransactionManager(database as Sequelize);
+ const persistenceMappers = buildTaxDefinitionPersistenceMappers();
+
+ const repository = buildTaxDefinitionRepository({ database, mappers: persistenceMappers });
+
+ const finder = new TaxDefinitionFinder(repository);
+ const creator = new TaxDefinitionCreator(repository);
+ const updater = new TaxDefinitionUpdater(repository);
+ const deleter = new TaxDefinitionDeleter(repository);
+ const statusChanger = new TaxDefinitionStatusChanger(repository);
+
+ const createInputMapper = new CreateTaxDefinitionInputMapper();
+ const updateInputMapper = new UpdateTaxDefinitionByIdInputMapper();
+
+ const fullSnapshotBuilder = new TaxDefinitionFullSnapshotBuilder();
+ const summarySnapshotBuilder = new TaxDefinitionSummarySnapshotBuilder();
+
+ return {
+ repository,
+ useCases: {
+ listTaxDefinitions: () =>
+ new ListTaxDefinitionsUseCase({
+ repository,
+ summarySnapshotBuilder,
+ }),
+
+ getTaxDefinitionById: () =>
+ new GetTaxDefinitionByIdUseCase({
+ finder,
+ fullSnapshotBuilder,
+ }),
+
+ createTaxDefinition: () =>
+ new CreateTaxDefinitionUseCase({
+ dtoMapper: createInputMapper,
+ creator,
+ fullSnapshotBuilder,
+ transactionManager,
+ }),
+
+ updateTaxDefinitionById: () =>
+ new UpdateTaxDefinitionByIdUseCase({
+ dtoMapper: updateInputMapper,
+ updater,
+ fullSnapshotBuilder,
+ }),
+
+ deleteTaxDefinitionById: () =>
+ new DeleteTaxDefinitionByIdUseCase({
+ deleter,
+ }),
+
+ disableTaxDefinitionById: () =>
+ new DisableTaxDefinitionByIdUseCase({
+ statusChanger,
+ }),
+
+ enableTaxDefinitionById: () =>
+ new EnableTaxDefinitionByIdUseCase({
+ statusChanger,
+ }),
+ },
+ };
+};
+
+export const buildTaxDefinitionsPublicServices = (
+ _params: SetupParams,
+ deps: TaxDefinitionsInternalDeps
+): { finder: ITaxDefinitionPublicFinder } => {
+ const mapper = new TaxDefinitionPublicModelMapper();
+
+ return {
+ finder: new TaxDefinitionPublicFinder({
+ repository: deps.repository,
+ mapper,
+ }),
+ };
+};
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/create-tax-definition.controller.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/create-tax-definition.controller.ts
new file mode 100644
index 00000000..90db7576
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/create-tax-definition.controller.ts
@@ -0,0 +1,39 @@
+import type { CreateTaxDefinitionRequestDTO } from "@erp/catalogs/common";
+import {
+ ExpressController,
+ forbidQueryFieldGuard,
+ requireAuthenticatedGuard,
+ requireCompanyContextGuard,
+} from "@erp/core/api";
+
+import type { CreateTaxDefinitionUseCase } from "../../../../application/tax-definitions";
+import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
+
+export class CreateTaxDefinitionController extends ExpressController {
+ constructor(private readonly useCase: CreateTaxDefinitionUseCase) {
+ super();
+
+ this.errorMapper = taxDefinitionsApiErrorMapper;
+
+ this.registerGuards(
+ requireAuthenticatedGuard(),
+ requireCompanyContextGuard(),
+ forbidQueryFieldGuard("companyId")
+ );
+ }
+
+ protected async executeImpl() {
+ const companyId = this.getTenantId();
+ if (!companyId) {
+ return this.forbiddenError("Tenant ID not found");
+ }
+
+ const dto = this.req.body satisfies CreateTaxDefinitionRequestDTO;
+ const result = await this.useCase.execute({ dto, companyId });
+
+ return result.match(
+ (data: unknown) => this.created(data),
+ (err: Error) => this.handleError(err)
+ );
+ }
+}
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/delete-tax-definition-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/delete-tax-definition-by-id.controller.ts
new file mode 100644
index 00000000..8d436c58
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/delete-tax-definition-by-id.controller.ts
@@ -0,0 +1,38 @@
+import {
+ ExpressController,
+ forbidQueryFieldGuard,
+ requireAuthenticatedGuard,
+ requireCompanyContextGuard,
+} from "@erp/core/api";
+
+import type { DeleteTaxDefinitionByIdUseCase } from "../../../../application/tax-definitions";
+import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
+
+export class DeleteTaxDefinitionByIdController extends ExpressController {
+ constructor(private readonly useCase: DeleteTaxDefinitionByIdUseCase) {
+ super();
+
+ this.errorMapper = taxDefinitionsApiErrorMapper;
+
+ this.registerGuards(
+ requireAuthenticatedGuard(),
+ requireCompanyContextGuard(),
+ forbidQueryFieldGuard("companyId")
+ );
+ }
+
+ protected async executeImpl() {
+ const companyId = this.getTenantId();
+ if (!companyId) {
+ return this.forbiddenError("Tenant ID not found");
+ }
+
+ const { tax_definition_id } = this.req.params;
+ const result = await this.useCase.execute(companyId, tax_definition_id as any);
+
+ return result.match(
+ (data: unknown) => this.ok(data),
+ (err: Error) => this.handleError(err)
+ );
+ }
+}
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/disable-tax-definition-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/disable-tax-definition-by-id.controller.ts
new file mode 100644
index 00000000..24df8959
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/disable-tax-definition-by-id.controller.ts
@@ -0,0 +1,38 @@
+import {
+ ExpressController,
+ forbidQueryFieldGuard,
+ requireAuthenticatedGuard,
+ requireCompanyContextGuard,
+} from "@erp/core/api";
+
+import type { DisableTaxDefinitionByIdUseCase } from "../../../../application/tax-definitions";
+import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
+
+export class DisableTaxDefinitionByIdController extends ExpressController {
+ constructor(private readonly useCase: DisableTaxDefinitionByIdUseCase) {
+ super();
+
+ this.errorMapper = taxDefinitionsApiErrorMapper;
+
+ this.registerGuards(
+ requireAuthenticatedGuard(),
+ requireCompanyContextGuard(),
+ forbidQueryFieldGuard("companyId")
+ );
+ }
+
+ protected async executeImpl() {
+ const companyId = this.getTenantId();
+ if (!companyId) {
+ return this.forbiddenError("Tenant ID not found");
+ }
+
+ const { tax_definition_id } = this.req.params;
+ const result = await this.useCase.execute(companyId, tax_definition_id as any);
+
+ return result.match(
+ (data: unknown) => this.ok(data),
+ (err: Error) => this.handleError(err)
+ );
+ }
+}
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/enable-tax-definition-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/enable-tax-definition-by-id.controller.ts
new file mode 100644
index 00000000..3dc47a27
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/enable-tax-definition-by-id.controller.ts
@@ -0,0 +1,38 @@
+import {
+ ExpressController,
+ forbidQueryFieldGuard,
+ requireAuthenticatedGuard,
+ requireCompanyContextGuard,
+} from "@erp/core/api";
+
+import type { EnableTaxDefinitionByIdUseCase } from "../../../../application/tax-definitions";
+import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
+
+export class EnableTaxDefinitionByIdController extends ExpressController {
+ constructor(private readonly useCase: EnableTaxDefinitionByIdUseCase) {
+ super();
+
+ this.errorMapper = taxDefinitionsApiErrorMapper;
+
+ this.registerGuards(
+ requireAuthenticatedGuard(),
+ requireCompanyContextGuard(),
+ forbidQueryFieldGuard("companyId")
+ );
+ }
+
+ protected async executeImpl() {
+ const companyId = this.getTenantId();
+ if (!companyId) {
+ return this.forbiddenError("Tenant ID not found");
+ }
+
+ const { tax_definition_id } = this.req.params;
+ const result = await this.useCase.execute(companyId, tax_definition_id as any);
+
+ return result.match(
+ (data: unknown) => this.ok(data),
+ (err: Error) => this.handleError(err)
+ );
+ }
+}
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/get-tax-definition-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/get-tax-definition-by-id.controller.ts
new file mode 100644
index 00000000..44070907
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/get-tax-definition-by-id.controller.ts
@@ -0,0 +1,38 @@
+import {
+ ExpressController,
+ forbidQueryFieldGuard,
+ requireAuthenticatedGuard,
+ requireCompanyContextGuard,
+} from "@erp/core/api";
+
+import type { GetTaxDefinitionByIdUseCase } from "../../../../application/tax-definitions";
+import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
+
+export class GetTaxDefinitionByIdController extends ExpressController {
+ constructor(private readonly useCase: GetTaxDefinitionByIdUseCase) {
+ super();
+
+ this.errorMapper = taxDefinitionsApiErrorMapper;
+
+ this.registerGuards(
+ requireAuthenticatedGuard(),
+ requireCompanyContextGuard(),
+ forbidQueryFieldGuard("companyId")
+ );
+ }
+
+ protected async executeImpl() {
+ const companyId = this.getTenantId();
+ if (!companyId) {
+ return this.forbiddenError("Tenant ID not found");
+ }
+
+ const { tax_definition_id } = this.req.params;
+ const result = await this.useCase.execute(companyId, tax_definition_id as any);
+
+ return result.match(
+ (data) => this.ok(data),
+ (err: Error) => this.handleError(err)
+ );
+ }
+}
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/index.ts
new file mode 100644
index 00000000..4543a68f
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/index.ts
@@ -0,0 +1,7 @@
+export * from "./create-tax-definition.controller";
+export * from "./delete-tax-definition-by-id.controller";
+export * from "./disable-tax-definition-by-id.controller";
+export * from "./enable-tax-definition-by-id.controller";
+export * from "./get-tax-definition-by-id.controller";
+export * from "./list-tax-definitions.controller";
+export * from "./update-tax-definition-by-id.controller";
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/list-tax-definitions.controller.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/list-tax-definitions.controller.ts
new file mode 100644
index 00000000..98049570
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/list-tax-definitions.controller.ts
@@ -0,0 +1,54 @@
+import {
+ ExpressController,
+ forbidQueryFieldGuard,
+ requireAuthenticatedGuard,
+ requireCompanyContextGuard,
+} from "@erp/core/api";
+import { Criteria } from "@repo/rdx-criteria/server";
+
+import type { ListTaxDefinitionsUseCase } from "../../../../application/tax-definitions";
+import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
+
+export class ListTaxDefinitionsController extends ExpressController {
+ constructor(private readonly useCase: ListTaxDefinitionsUseCase) {
+ super();
+
+ this.errorMapper = taxDefinitionsApiErrorMapper;
+
+ this.registerGuards(
+ requireAuthenticatedGuard(),
+ requireCompanyContextGuard(),
+ forbidQueryFieldGuard("companyId")
+ );
+ }
+
+ private getCriteriaWithDefaultOrder() {
+ if (this.criteria.hasOrder()) {
+ return this.criteria;
+ }
+
+ const { q: quicksearch, filters, pageSize, pageNumber } = this.criteria.toPrimitives();
+ return Criteria.fromPrimitives(filters, "code", "ASC", 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(companyId, criteria);
+
+ return result.match(
+ (data: any) =>
+ 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: Error) => this.handleError(err)
+ );
+ }
+}
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/update-tax-definition-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/update-tax-definition-by-id.controller.ts
new file mode 100644
index 00000000..c2aba57c
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/controllers/update-tax-definition-by-id.controller.ts
@@ -0,0 +1,44 @@
+import type { UpdateTaxDefinitionByIdRequestDTO } from "@erp/catalogs/common";
+import {
+ ExpressController,
+ forbidQueryFieldGuard,
+ requireAuthenticatedGuard,
+ requireCompanyContextGuard,
+} from "@erp/core/api";
+
+import type { UpdateTaxDefinitionByIdUseCase } from "../../../../application/tax-definitions";
+import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
+
+export class UpdateTaxDefinitionByIdController extends ExpressController {
+ constructor(private readonly useCase: UpdateTaxDefinitionByIdUseCase) {
+ super();
+
+ this.errorMapper = taxDefinitionsApiErrorMapper;
+
+ this.registerGuards(
+ requireAuthenticatedGuard(),
+ requireCompanyContextGuard(),
+ forbidQueryFieldGuard("companyId")
+ );
+ }
+
+ protected async executeImpl() {
+ const companyId = this.getTenantId();
+ if (!companyId) {
+ return this.forbiddenError("Tenant ID not found");
+ }
+
+ const { tax_definition_id } = this.req.params;
+ if (!tax_definition_id) {
+ return this.invalidInputError("Tax definition ID missing");
+ }
+
+ const dto = this.req.body as UpdateTaxDefinitionByIdRequestDTO;
+ const result = await this.useCase.execute(companyId, { id: tax_definition_id as any, dto });
+
+ return result.match(
+ (data: unknown) => this.ok(data),
+ (err: Error) => this.handleError(err)
+ );
+ }
+}
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/index.ts
new file mode 100644
index 00000000..9a210359
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/index.ts
@@ -0,0 +1,2 @@
+export * from "./controllers";
+export * from "./tax-definitions.routes";
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/tax-definitions-api-error-mapper.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/tax-definitions-api-error-mapper.ts
new file mode 100644
index 00000000..704be02e
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/tax-definitions-api-error-mapper.ts
@@ -0,0 +1,186 @@
+import {
+ ApiErrorMapper,
+ ConflictApiError,
+ EntityNotFoundError,
+ type ErrorToApiRule,
+ NotFoundApiError,
+ ValidationApiError,
+} from "@erp/core/api";
+
+import {
+ type InvalidTaxDefinitionAllowedSurchargeCodesError,
+ type InvalidTaxDefinitionCalculationBehaviorError,
+ type InvalidTaxDefinitionCodeError,
+ type InvalidTaxDefinitionDescriptionError,
+ type InvalidTaxDefinitionFamilyError,
+ type InvalidTaxDefinitionIdError,
+ type InvalidTaxDefinitionInvoiceNoteError,
+ type InvalidTaxDefinitionNameError,
+ type InvalidTaxDefinitionRateError,
+ type InvalidTaxDefinitionScopeError,
+ type InvalidTaxDefinitionValidityPeriodError,
+ type TaxDefinitionCannotBeDisabledError,
+ type TaxDefinitionCannotBeEnabledError,
+ isInvalidTaxDefinitionAllowedSurchargeCodesError,
+ isInvalidTaxDefinitionCalculationBehaviorError,
+ isInvalidTaxDefinitionCodeError,
+ isInvalidTaxDefinitionDescriptionError,
+ isInvalidTaxDefinitionFamilyError,
+ isInvalidTaxDefinitionIdError,
+ isInvalidTaxDefinitionInvoiceNoteError,
+ isInvalidTaxDefinitionNameError,
+ isInvalidTaxDefinitionRateError,
+ isInvalidTaxDefinitionScopeError,
+ isInvalidTaxDefinitionValidityPeriodError,
+ isTaxDefinitionCannotBeDisabledError,
+ isTaxDefinitionCannotBeEnabledError,
+} from "../../../domain/tax-definitions";
+
+const invalidTaxDefinitionIdRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionIdError,
+ build: (error) =>
+ new ConflictApiError(
+ (error as InvalidTaxDefinitionIdError).message ||
+ "Tax definition with the provided id already exists."
+ ),
+};
+
+const invalidTaxDefinitionCodeRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionCodeError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as InvalidTaxDefinitionCodeError).message || "Tax definition code is invalid."
+ ),
+};
+
+const invalidTaxDefinitionNameRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionNameError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as InvalidTaxDefinitionNameError).message || "Tax definition name is invalid."
+ ),
+};
+
+const invalidTaxDefinitionDescriptionRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionDescriptionError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as InvalidTaxDefinitionDescriptionError).message ||
+ "Tax definition description is invalid."
+ ),
+};
+
+const invalidTaxDefinitionRateRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionRateError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as InvalidTaxDefinitionRateError).message || "Tax definition rate is invalid."
+ ),
+};
+
+const invalidTaxDefinitionFamilyRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionFamilyError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as InvalidTaxDefinitionFamilyError).message || "Tax definition family is invalid."
+ ),
+};
+
+const invalidTaxDefinitionCalculationBehaviorRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionCalculationBehaviorError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as InvalidTaxDefinitionCalculationBehaviorError).message ||
+ "Tax definition calculation behavior is invalid."
+ ),
+};
+
+const invalidTaxDefinitionScopeRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionScopeError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as InvalidTaxDefinitionScopeError).message || "Tax definition scope is invalid."
+ ),
+};
+
+const invalidTaxDefinitionInvoiceNoteRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionInvoiceNoteError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as InvalidTaxDefinitionInvoiceNoteError).message ||
+ "Tax definition invoice note is invalid."
+ ),
+};
+
+const invalidTaxDefinitionAllowedSurchargeCodesRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionAllowedSurchargeCodesError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as InvalidTaxDefinitionAllowedSurchargeCodesError).message ||
+ "Tax definition allowed surcharge codes are invalid."
+ ),
+};
+
+const invalidTaxDefinitionValidityPeriodRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isInvalidTaxDefinitionValidityPeriodError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as InvalidTaxDefinitionValidityPeriodError).message ||
+ "Tax definition validity period is invalid."
+ ),
+};
+
+const taxDefinitionNotFoundRule: ErrorToApiRule = {
+ priority: 120,
+ matches: (error) =>
+ error instanceof EntityNotFoundError &&
+ ((error as EntityNotFoundError).message.includes("TaxDefinition") ||
+ (error as EntityNotFoundError).message.includes("Tax definition")),
+ build: (error) =>
+ new NotFoundApiError((error as EntityNotFoundError).message || "Tax definition not found."),
+};
+
+const taxDefinitionCannotBeDisabledRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isTaxDefinitionCannotBeDisabledError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as TaxDefinitionCannotBeDisabledError).message || "Tax definition cannot be disabled."
+ ),
+};
+
+const taxDefinitionCannotBeEnabledRule: ErrorToApiRule = {
+ priority: 120,
+ matches: isTaxDefinitionCannotBeEnabledError,
+ build: (error) =>
+ new ValidationApiError(
+ (error as TaxDefinitionCannotBeEnabledError).message || "Tax definition cannot be enabled."
+ ),
+};
+
+export const taxDefinitionsApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
+ .register(invalidTaxDefinitionIdRule)
+ .register(invalidTaxDefinitionCodeRule)
+ .register(invalidTaxDefinitionNameRule)
+ .register(invalidTaxDefinitionDescriptionRule)
+ .register(invalidTaxDefinitionRateRule)
+ .register(invalidTaxDefinitionFamilyRule)
+ .register(invalidTaxDefinitionCalculationBehaviorRule)
+ .register(invalidTaxDefinitionScopeRule)
+ .register(invalidTaxDefinitionInvoiceNoteRule)
+ .register(invalidTaxDefinitionAllowedSurchargeCodesRule)
+ .register(invalidTaxDefinitionValidityPeriodRule)
+ .register(taxDefinitionNotFoundRule)
+ .register(taxDefinitionCannotBeDisabledRule)
+ .register(taxDefinitionCannotBeEnabledRule);
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/express/tax-definitions.routes.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/express/tax-definitions.routes.ts
new file mode 100644
index 00000000..186de34e
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/express/tax-definitions.routes.ts
@@ -0,0 +1,120 @@
+import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
+import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api";
+import type { NextFunction, Request, Response } from "express";
+import { Router } from "express";
+
+import {
+ CreateTaxDefinitionRequestSchema,
+ DeleteTaxDefinitionByIdRequestSchema,
+ GetTaxDefinitionByIdRequestSchema,
+ ListTaxDefinitionsRequestSchema,
+ UpdateTaxDefinitionByIdParamsRequestSchema,
+ UpdateTaxDefinitionByIdRequestSchema,
+} from "../../../../common";
+import type { CatalogsInternalDeps } from "../../di";
+
+import {
+ CreateTaxDefinitionController,
+ DeleteTaxDefinitionByIdController,
+ DisableTaxDefinitionByIdController,
+ EnableTaxDefinitionByIdController,
+ GetTaxDefinitionByIdController,
+ ListTaxDefinitionsController,
+ UpdateTaxDefinitionByIdController,
+} from "./controllers";
+
+export const taxDefinitionsRouter = (params: StartParams) => {
+ const { app, config, getInternal } = params;
+ const deps = getInternal("catalogs").taxDefinitions;
+
+ const router = Router({ mergeParams: true });
+
+ if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
+ router.use((req: Request, res: Response, next: NextFunction) =>
+ mockUser(req as RequestWithAuth, res, next)
+ );
+ }
+
+ router.use([
+ (req: Request, res: Response, next: NextFunction) =>
+ requireAuthenticated()(req as RequestWithAuth, res, next),
+ (req: Request, res: Response, next: NextFunction) =>
+ requireCompanyContext()(req as RequestWithAuth, res, next),
+ ]);
+
+ router.get(
+ "/",
+ validateRequest(ListTaxDefinitionsRequestSchema, "query"),
+ (req, res, next) => {
+ const controller = new ListTaxDefinitionsController(deps.useCases.listTaxDefinitions());
+ return controller.execute(req, res, next);
+ }
+ );
+
+ router.post(
+ "/",
+ validateRequest(CreateTaxDefinitionRequestSchema, "body"),
+ (req, res, next) => {
+ const controller = new CreateTaxDefinitionController(deps.useCases.createTaxDefinition());
+ return controller.execute(req, res, next);
+ }
+ );
+
+ router.get(
+ "/:tax_definition_id",
+ validateRequest(GetTaxDefinitionByIdRequestSchema, "params"),
+ (req, res, next) => {
+ const controller = new GetTaxDefinitionByIdController(
+ deps.useCases.getTaxDefinitionById()
+ );
+ return controller.execute(req, res, next);
+ }
+ );
+
+ router.delete(
+ "/:tax_definition_id",
+ validateRequest(DeleteTaxDefinitionByIdRequestSchema, "params"),
+ (req, res, next) => {
+ const controller = new DeleteTaxDefinitionByIdController(
+ deps.useCases.deleteTaxDefinitionById()
+ );
+ return controller.execute(req, res, next);
+ }
+ );
+
+ router.put(
+ "/:tax_definition_id",
+ validateRequest(UpdateTaxDefinitionByIdParamsRequestSchema, "params"),
+ validateRequest(UpdateTaxDefinitionByIdRequestSchema, "body"),
+ (req, res, next) => {
+ const controller = new UpdateTaxDefinitionByIdController(
+ deps.useCases.updateTaxDefinitionById()
+ );
+ return controller.execute(req, res, next);
+ }
+ );
+
+ router.patch(
+ "/:tax_definition_id/disable",
+ validateRequest(GetTaxDefinitionByIdRequestSchema, "params"),
+ (req, res, next) => {
+ const controller = new DisableTaxDefinitionByIdController(
+ deps.useCases.disableTaxDefinitionById()
+ );
+ return controller.execute(req, res, next);
+ }
+ );
+
+ router.patch(
+ "/:tax_definition_id/enable",
+ validateRequest(GetTaxDefinitionByIdRequestSchema, "params"),
+ (req, res, next) => {
+ const controller = new EnableTaxDefinitionByIdController(
+ deps.useCases.enableTaxDefinitionById()
+ );
+ return controller.execute(req, res, next);
+ }
+ );
+
+ app.use(`${config.server.apiBasePath}/catalogs/tax-definitions`, router);
+};
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/index.ts
index 96e04610..d1dab3b3 100644
--- a/modules/catalogs/src/api/infrastructure/tax-definitions/index.ts
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/index.ts
@@ -1,2 +1,3 @@
+export * from "./express";
export * from "./di";
export * from "./persistence";
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts
index f7469a5e..93910d08 100644
--- a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts
@@ -1,5 +1,7 @@
-import { SequelizeQueryMapper } from "@erp/core/api";
+import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
+ CountryCode,
+ CountryRegionCode,
TextValue,
UniqueID,
ValidationErrorCollection,
@@ -15,18 +17,20 @@ import {
TaxDefinitionCode as TaxDefinitionCodeVO,
TaxDefinitionName as TaxDefinitionNameVO,
TaxFamily,
- TaxJurisdictionCountryCode,
- TaxJurisdictionRegionCode,
TaxRate as TaxRateVO,
TaxScope,
} from "../../../../../domain";
-import type { TaxDefinitionModel } from "../models";
+import type { TaxDefinitionCreationAttributes, TaxDefinitionModel } from "../models";
-export class SequelizeTaxDefinitionDomainMapper extends SequelizeQueryMapper<
+export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
TaxDefinitionModel,
+ TaxDefinitionCreationAttributes,
TaxDefinition
> {
- public mapToDomain(raw: TaxDefinitionModel): Result {
+ public mapToDomain(
+ raw: TaxDefinitionModel,
+ params?: MapperParamsType
+ ): Result {
const errors: ValidationErrorDetail[] = [];
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
@@ -51,13 +55,13 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeQueryMapper<
);
const jurisdictionCountryCode = extractOrPushError(
- TaxJurisdictionCountryCode.create(raw.jurisdiction_country_code),
+ CountryCode.create(raw.jurisdiction_country_code),
"jurisdiction_country_code",
errors
);
const jurisdictionRegionCode = maybeFromNullableResult(raw.jurisdiction_region_code, (v) =>
- TaxJurisdictionRegionCode.create(v)
+ CountryRegionCode.create(v)
);
const taxScope = extractOrPushError(TaxScope.create(raw.tax_scope), "tax_scope", errors);
@@ -69,9 +73,10 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeQueryMapper<
const arr: any[] = [];
for (const el of v) {
- const r = TaxDefinitionCodeVO.create(String(el));
- if (r.isFailure) return Result.fail(new Error("Invalid allowed_surcharge_codes element"));
- arr.push(r.getValue());
+ const _result = TaxDefinitionCodeVO.create(String(el));
+ if (_result.isFailure)
+ return Result.fail(new Error("Invalid allowed_surcharge_codes element"));
+ arr.push(_result.data);
}
return Result.ok(arr);
@@ -112,46 +117,51 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeQueryMapper<
return Result.ok(domainOrError);
}
- public mapToPersistence(domain: TaxDefinition): Result, Error> {
- const dto: Record = {
- id: domain.id.toPrimitive(),
- company_id: domain.companyId.toPrimitive(),
- code: domain.code.toPrimitive(),
- name: domain.name.toPrimitive(),
- description: domain.description.match(
+ public mapToPersistence(
+ source: TaxDefinition,
+ params?: MapperParamsType
+ ): Result {
+ const dto: TaxDefinitionCreationAttributes = {
+ id: source.id.toPrimitive(),
+ company_id: source.companyId.toPrimitive(),
+ code: source.code.toPrimitive(),
+ name: source.name.toPrimitive(),
+ description: source.description.match(
(v) => v.toPrimitive(),
() => null
),
- rate_value: domain.rate.toPrimitive(),
- rate_scale: (domain.rate as any).scale ?? 2,
- tax_family: domain.taxFamily.toPrimitive(),
- calculation_behavior: domain.calculationBehavior.toPrimitive(),
- jurisdiction_country_code: domain.jurisdictionCountryCode.toPrimitive(),
- jurisdiction_region_code: domain.jurisdictionRegionCode.match(
+ rate_value: source.rate.toPrimitive(),
+ rate_scale: (source.rate as any).scale ?? 2,
+ tax_family: source.taxFamily.toPrimitive(),
+ calculation_behavior: source.calculationBehavior.toPrimitive(),
+ jurisdiction_country_code: source.jurisdictionCountryCode.toPrimitive(),
+ jurisdiction_region_code: source.jurisdictionRegionCode.match(
(v) => v.toPrimitive(),
() => null
),
- tax_scope: domain.taxScope.toPrimitive(),
- invoice_note: domain.invoiceNote.match(
+ tax_scope: source.taxScope.toPrimitive(),
+ invoice_note: source.invoiceNote.match(
(v) => v.toPrimitive(),
() => null
),
- allowed_surcharge_codes: domain.allowedSurchargeCodes.match(
+ allowed_surcharge_codes: source.allowedSurchargeCodes.match(
(arr) => arr.map((c) => c.toPrimitive()),
() => null
),
- is_system: domain.isSystem,
- is_active: domain.isActive,
- valid_from: domain.validFrom.match(
+ is_system: source.isSystem,
+ is_active: source.isActive,
+ valid_from: source.validFrom.match(
(d) => d.toPrimitive(),
() => null
),
- valid_to: domain.validTo.match(
+ valid_to: source.validTo.match(
(d) => d.toPrimitive(),
() => null
),
};
- return Result.ok(dto);
+ return Result.ok(
+ dto satisfies TaxDefinitionCreationAttributes
+ );
}
}
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/models/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/models/index.ts
new file mode 100644
index 00000000..144c6f5c
--- /dev/null
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/models/index.ts
@@ -0,0 +1 @@
+export * from "./sequelize-tax-definition.model";
diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/repositories/sequelize-tax-definition.repository.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/repositories/sequelize-tax-definition.repository.ts
index 05d16ce6..7aa65a2e 100644
--- a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/repositories/sequelize-tax-definition.repository.ts
+++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/repositories/sequelize-tax-definition.repository.ts
@@ -5,9 +5,9 @@ import {
translateSequelizeError,
} from "@erp/core/api";
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
-import type { UniqueID } from "@repo/rdx-ddd";
-import { type Collection, Result } from "@repo/rdx-utils";
-import type { Sequelize, Transaction } from "sequelize";
+import type { UniqueID, UtcDate } from "@repo/rdx-ddd";
+import { Collection, Maybe, Result } from "@repo/rdx-utils";
+import { Op, type Sequelize, type Transaction, type WhereOptions } from "sequelize";
import type { ITaxDefinitionRepository, TaxDefinitionSummary } from "../../../../../application";
import type { TaxDefinition, TaxDefinitionCode } from "../../../../../domain";
@@ -123,6 +123,62 @@ export class SequelizeTaxDefinitionRepository
}
}
+ async findActiveByCodeInCompany(params: {
+ companyId: UniqueID;
+ code: string;
+ atDate: UtcDate;
+ transaction?: Transaction;
+ }): Promise, Error>> {
+ try {
+ const row = await TaxDefinitionModel.findOne({
+ where: {
+ ...this.buildActiveWhereClause(params.companyId, params.atDate),
+ code: this.normalizeCode(params.code),
+ },
+ transaction: params.transaction,
+ });
+
+ if (!row) {
+ return Result.ok(Maybe.none());
+ }
+
+ return this.domainMapper.mapToDomain(row).map((taxDefinition) => Maybe.some(taxDefinition));
+ } catch (err: unknown) {
+ return Result.fail(translateSequelizeError(err));
+ }
+ }
+
+ async findActiveByCodesInCompany(params: {
+ companyId: UniqueID;
+ codes: Collection;
+ atDate: UtcDate;
+ transaction?: Transaction;
+ }): Promise, Error>> {
+ try {
+ const normalizedCodes = Array.from(
+ new Set(params.codes.getAll().map((code) => this.normalizeCode(code)))
+ );
+
+ if (normalizedCodes.length === 0) {
+ return Result.ok(new Collection([]));
+ }
+
+ const rows = await TaxDefinitionModel.findAll({
+ where: {
+ ...this.buildActiveWhereClause(params.companyId, params.atDate),
+ code: {
+ [Op.in]: normalizedCodes,
+ },
+ },
+ transaction: params.transaction,
+ });
+
+ return this.domainMapper.mapToDomainCollection(rows, rows.length);
+ } catch (err: unknown) {
+ return Result.fail(translateSequelizeError(err));
+ }
+ }
+
async findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
@@ -171,4 +227,25 @@ export class SequelizeTaxDefinitionRepository
return Result.fail(translateSequelizeError(err));
}
}
+
+ private buildActiveWhereClause(companyId: UniqueID, atDate: UtcDate): WhereOptions {
+ const atDateISO = atDate.toPrimitive();
+
+ return {
+ company_id: companyId.toString(),
+ is_active: true,
+ [Op.and]: [
+ {
+ [Op.or]: [{ valid_from: null }, { valid_from: { [Op.lte]: atDateISO } }],
+ },
+ {
+ [Op.or]: [{ valid_to: null }, { valid_to: { [Op.gte]: atDateISO } }],
+ },
+ ],
+ };
+ }
+
+ private normalizeCode(code: string): string {
+ return code.trim().toLowerCase();
+ }
}
diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regimes.di.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regimes.di.ts
index 7e110bff..2e7ed0cc 100644
--- a/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regimes.di.ts
+++ b/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regimes.di.ts
@@ -2,8 +2,7 @@ import type { ModuleParams, SetupParams } from "@erp/core/api";
import { buildTransactionManager } from "@erp/core/api";
import type { Sequelize } from "sequelize";
-import type { ITaxRegimeRepository } from "../../../application/";
-import { TaxRegimeFinder } from "../../../application/";
+import { type ITaxRegimeRepository, TaxRegimePublicFinder } from "../../../application/";
import {
CreateTaxRegimeUseCase,
DeleteTaxRegimeByIdUseCase,
@@ -11,6 +10,7 @@ import {
EnableTaxRegimeByIdUseCase,
GetTaxRegimeByIdUseCase,
ListTaxRegimesUseCase,
+ TaxRegimePublicModelMapper,
UpdateTaxRegimeByIdUseCase,
} from "../../../application/tax-regimes";
import {
@@ -112,8 +112,13 @@ export const buildTaxRegimesDependencies = (params: ModuleParams): TaxRegimesInt
export const buildTaxRegimesPublicServices = (
_params: SetupParams,
deps: TaxRegimesInternalDeps
-): { finder: TaxRegimeFinder } => {
+): { finder: TaxRegimePublicFinder } => {
+ const mapper = new TaxRegimePublicModelMapper();
+
return {
- finder: new TaxRegimeFinder(deps.repository),
+ finder: new TaxRegimePublicFinder({
+ repository: deps.repository,
+ mapper,
+ }),
};
};
diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/models/sequelize-tax-regime.model.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/models/sequelize-tax-regime.model.ts
index ac3875e4..07fdf017 100644
--- a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/models/sequelize-tax-regime.model.ts
+++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/models/sequelize-tax-regime.model.ts
@@ -37,7 +37,7 @@ export default (database: Sequelize) => {
allowNull: false,
},
code: {
- type: DataTypes.STRING,
+ type: DataTypes.STRING(2),
allowNull: false,
unique: true,
},
diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/repositories/sequelize-tax-regime.repository.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/repositories/sequelize-tax-regime.repository.ts
index 25be1046..3ac7dfb7 100644
--- a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/repositories/sequelize-tax-regime.repository.ts
+++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/repositories/sequelize-tax-regime.repository.ts
@@ -6,8 +6,8 @@ import {
} from "@erp/core/api";
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
-import { type Collection, Result } from "@repo/rdx-utils";
-import type { Sequelize, Transaction } from "sequelize";
+import { Collection, Maybe, Result } from "@repo/rdx-utils";
+import { Op, type Sequelize, type Transaction } from "sequelize";
import type { ITaxRegimeRepository, TaxRegimeSummary } from "../../../../../application";
import type { TaxRegime, TaxRegimeCode } from "../../../../../domain";
@@ -105,6 +105,22 @@ export class SequelizeTaxRegimeRepository
}
}
+ async existsByCodeInCompany(
+ companyId: UniqueID,
+ code: TaxRegimeCode,
+ transaction?: Transaction
+ ): Promise> {
+ try {
+ const count = await TaxRegimeModel.count({
+ where: { code: code.toString(), company_id: companyId.toString() },
+ transaction,
+ });
+ return Result.ok(Boolean(count > 0));
+ } catch (error: unknown) {
+ return Result.fail(translateSequelizeError(error));
+ }
+ }
+
/**
* Recupera un método de pago por su ID y companyId.
*
@@ -250,4 +266,55 @@ export class SequelizeTaxRegimeRepository
return Result.fail(translateSequelizeError(err));
}
}
+
+ async findByCodeInCompany(
+ companyId: UniqueID,
+ code: TaxRegimeCode,
+ transaction?: Transaction
+ ): Promise, Error>> {
+ try {
+ const row = await TaxRegimeModel.findOne({
+ where: { code: code.toString(), company_id: companyId.toString() },
+ transaction,
+ });
+
+ if (!row) {
+ return Result.ok(Maybe.none());
+ }
+
+ return this.domainMapper.mapToDomain(row).map((taxDefinition) => Maybe.some(taxDefinition));
+ } catch (err: unknown) {
+ return Result.fail(translateSequelizeError(err));
+ }
+ }
+
+ async findByCodesInCompany(params: {
+ companyId: UniqueID;
+ codes: Collection;
+ transaction?: Transaction;
+ }): Promise, Error>> {
+ try {
+ const normalizedCodes = Array.from(
+ new Set(params.codes.getAll().map((code) => code.toString()))
+ );
+
+ if (normalizedCodes.length === 0) {
+ return Result.ok(new Collection());
+ }
+
+ const rows = await TaxRegimeModel.findAll({
+ where: {
+ company_id: params.companyId.toString(),
+ code: {
+ [Op.in]: normalizedCodes,
+ },
+ },
+ transaction: params.transaction,
+ });
+
+ return this.domainMapper.mapToDomainCollection(rows, rows.length);
+ } catch (err: unknown) {
+ return Result.fail(translateSequelizeError(err));
+ }
+ }
}
diff --git a/modules/catalogs/src/common/dto/index.ts b/modules/catalogs/src/common/dto/index.ts
index 23f8279d..1702efcf 100644
--- a/modules/catalogs/src/common/dto/index.ts
+++ b/modules/catalogs/src/common/dto/index.ts
@@ -1,3 +1,4 @@
export * from "./payment-methods";
export * from "./payment-terms";
+export * from "./tax-definitions";
export * from "./tax-regimes";
diff --git a/modules/catalogs/src/common/dto/tax-definitions/index.ts b/modules/catalogs/src/common/dto/tax-definitions/index.ts
new file mode 100644
index 00000000..32497d2a
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/index.ts
@@ -0,0 +1,3 @@
+export * from "./request";
+export * from "./response";
+export * from "./shared";
diff --git a/modules/catalogs/src/common/dto/tax-definitions/request/create-tax-definition.request.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/request/create-tax-definition.request.dto.ts
new file mode 100644
index 00000000..f4ce0f3a
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/request/create-tax-definition.request.dto.ts
@@ -0,0 +1,43 @@
+import { IsoDateSchema, PercentageSchema } from "@erp/core";
+import { z } from "zod/v4";
+
+const TaxDefinitionCodeSchema = z.string().regex(/^[a-z0-9][a-z0-9_]*$/);
+const TaxDefinitionFamilySchema = z.enum([
+ "iva",
+ "igic",
+ "ipsi",
+ "equivalence_surcharge",
+ "withholding",
+ "vat",
+ "gst",
+ "sales_tax",
+ "reverse_charge",
+ "exempt",
+ "not_subject",
+ "custom",
+]);
+const TaxCalculationBehaviorSchema = z.enum(["additive", "subtractive", "neutral"]);
+const TaxScopeSchema = z.enum(["domestic", "intra_eu", "export", "import", "international"]);
+const JurisdictionCountryCodeSchema = z.string().regex(/^[A-Z]{2}$/);
+const JurisdictionRegionCodeSchema = z.string().regex(/^[A-Z]{2}-[A-Z0-9-]+$/);
+
+export const CreateTaxDefinitionRequestSchema = z.object({
+ id: z.uuid(),
+ code: TaxDefinitionCodeSchema,
+ name: z.string(),
+ description: z.string().nullable().optional(),
+ rate: PercentageSchema,
+ tax_family: TaxDefinitionFamilySchema,
+ calculation_behavior: TaxCalculationBehaviorSchema,
+ jurisdiction_country_code: JurisdictionCountryCodeSchema,
+ jurisdiction_region_code: JurisdictionRegionCodeSchema.nullable().optional(),
+ tax_scope: TaxScopeSchema,
+ invoice_note: z.string().nullable().optional(),
+ allowed_surcharge_codes: z.array(TaxDefinitionCodeSchema).nullable().optional(),
+ is_system: z.boolean().optional(),
+ is_active: z.boolean(),
+ valid_from: IsoDateSchema.nullable().optional(),
+ valid_to: IsoDateSchema.nullable().optional(),
+});
+
+export type CreateTaxDefinitionRequestDTO = z.infer;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/request/delete-tax-definition-by-id.request.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/request/delete-tax-definition-by-id.request.dto.ts
new file mode 100644
index 00000000..196609f0
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/request/delete-tax-definition-by-id.request.dto.ts
@@ -0,0 +1,9 @@
+import { z } from "zod/v4";
+
+export const DeleteTaxDefinitionByIdRequestSchema = z.object({
+ tax_definition_id: z.uuid(),
+});
+
+export type DeleteTaxDefinitionByIdRequestDTO = z.infer<
+ typeof DeleteTaxDefinitionByIdRequestSchema
+>;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/request/disable-tax-definition-by-id.request.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/request/disable-tax-definition-by-id.request.dto.ts
new file mode 100644
index 00000000..68bd12b8
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/request/disable-tax-definition-by-id.request.dto.ts
@@ -0,0 +1,9 @@
+import { z } from "zod/v4";
+
+export const DisableTaxDefinitionByIdRequestSchema = z.object({
+ tax_definition_id: z.uuid(),
+});
+
+export type DisableTaxDefinitionByIdRequestDTO = z.infer<
+ typeof DisableTaxDefinitionByIdRequestSchema
+>;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/request/enable-tax-definition-by-id.request.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/request/enable-tax-definition-by-id.request.dto.ts
new file mode 100644
index 00000000..5227fa05
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/request/enable-tax-definition-by-id.request.dto.ts
@@ -0,0 +1,9 @@
+import { z } from "zod/v4";
+
+export const EnableTaxDefinitionByIdRequestSchema = z.object({
+ tax_definition_id: z.uuid(),
+});
+
+export type EnableTaxDefinitionByIdRequestDTO = z.infer<
+ typeof EnableTaxDefinitionByIdRequestSchema
+>;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/request/get-tax-definition-by-id.request.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/request/get-tax-definition-by-id.request.dto.ts
new file mode 100644
index 00000000..d4b0082b
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/request/get-tax-definition-by-id.request.dto.ts
@@ -0,0 +1,7 @@
+import { z } from "zod/v4";
+
+export const GetTaxDefinitionByIdRequestSchema = z.object({
+ tax_definition_id: z.uuid(),
+});
+
+export type GetTaxDefinitionByIdRequestDTO = z.infer;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/request/index.ts b/modules/catalogs/src/common/dto/tax-definitions/request/index.ts
new file mode 100644
index 00000000..ceebef93
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/request/index.ts
@@ -0,0 +1,7 @@
+export * from "./create-tax-definition.request.dto";
+export * from "./delete-tax-definition-by-id.request.dto";
+export * from "./disable-tax-definition-by-id.request.dto";
+export * from "./enable-tax-definition-by-id.request.dto";
+export * from "./get-tax-definition-by-id.request.dto";
+export * from "./list-tax-definitions.request.dto";
+export * from "./update-tax-definition-by-id.request.dto";
diff --git a/modules/catalogs/src/common/dto/tax-definitions/request/list-tax-definitions.request.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/request/list-tax-definitions.request.dto.ts
new file mode 100644
index 00000000..4edae6cf
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/request/list-tax-definitions.request.dto.ts
@@ -0,0 +1,5 @@
+import { CriteriaSchema } from "@erp/core";
+import type { z } from "zod/v4";
+
+export const ListTaxDefinitionsRequestSchema = CriteriaSchema;
+export type ListTaxDefinitionsRequestDTO = z.infer;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/request/update-tax-definition-by-id.request.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/request/update-tax-definition-by-id.request.dto.ts
new file mode 100644
index 00000000..f02e160c
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/request/update-tax-definition-by-id.request.dto.ts
@@ -0,0 +1,26 @@
+import { IsoDateSchema, PercentageSchema } from "@erp/core";
+import { z } from "zod/v4";
+
+const TaxDefinitionCodeSchema = z.string().regex(/^[a-z0-9][a-z0-9_]*$/);
+
+export const UpdateTaxDefinitionByIdParamsRequestSchema = z.object({
+ tax_definition_id: z.uuid(),
+});
+
+export const UpdateTaxDefinitionByIdRequestSchema = z.object({
+ name: z.string().optional(),
+ description: z.string().nullable().optional(),
+ rate: PercentageSchema.optional(),
+ invoice_note: z.string().nullable().optional(),
+ allowed_surcharge_codes: z.array(TaxDefinitionCodeSchema).nullable().optional(),
+ is_active: z.boolean().optional(),
+ valid_from: IsoDateSchema.nullable().optional(),
+ valid_to: IsoDateSchema.nullable().optional(),
+});
+
+export type UpdateTaxDefinitionByIdParamsRequestDTO = z.infer<
+ typeof UpdateTaxDefinitionByIdParamsRequestSchema
+>;
+export type UpdateTaxDefinitionByIdRequestDTO = z.infer<
+ typeof UpdateTaxDefinitionByIdRequestSchema
+>;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/response/create-tax-definition.response.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/response/create-tax-definition.response.dto.ts
new file mode 100644
index 00000000..887fe5a2
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/response/create-tax-definition.response.dto.ts
@@ -0,0 +1,6 @@
+import type { z } from "zod/v4";
+
+import { TaxDefinitionDetailSchema } from "../shared";
+
+export const CreateTaxDefinitionResponseSchema = TaxDefinitionDetailSchema;
+export type CreateTaxDefinitionResponseDTO = z.infer;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/response/disable-tax-definition-by-id.response.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/response/disable-tax-definition-by-id.response.dto.ts
new file mode 100644
index 00000000..98026fa8
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/response/disable-tax-definition-by-id.response.dto.ts
@@ -0,0 +1,8 @@
+import type { z } from "zod/v4";
+
+import { TaxDefinitionDetailSchema } from "../shared";
+
+export const DisableTaxDefinitionByIdResponseSchema = TaxDefinitionDetailSchema;
+export type DisableTaxDefinitionByIdResponseDTO = z.infer<
+ typeof DisableTaxDefinitionByIdResponseSchema
+>;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/response/enable-tax-definition-by-id.response.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/response/enable-tax-definition-by-id.response.dto.ts
new file mode 100644
index 00000000..120a1505
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/response/enable-tax-definition-by-id.response.dto.ts
@@ -0,0 +1,8 @@
+import type { z } from "zod/v4";
+
+import { TaxDefinitionDetailSchema } from "../shared";
+
+export const EnableTaxDefinitionByIdResponseSchema = TaxDefinitionDetailSchema;
+export type EnableTaxDefinitionByIdResponseDTO = z.infer<
+ typeof EnableTaxDefinitionByIdResponseSchema
+>;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/response/get-tax-definition-by-id.response.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/response/get-tax-definition-by-id.response.dto.ts
new file mode 100644
index 00000000..f1df0fbe
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/response/get-tax-definition-by-id.response.dto.ts
@@ -0,0 +1,8 @@
+import type { z } from "zod/v4";
+
+import { TaxDefinitionDetailSchema } from "../shared";
+
+export const GetTaxDefinitionByIdResponseSchema = TaxDefinitionDetailSchema;
+export type GetTaxDefinitionByIdResponseDTO = z.infer<
+ typeof GetTaxDefinitionByIdResponseSchema
+>;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/response/index.ts b/modules/catalogs/src/common/dto/tax-definitions/response/index.ts
new file mode 100644
index 00000000..823ee1aa
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/response/index.ts
@@ -0,0 +1,6 @@
+export * from "./create-tax-definition.response.dto";
+export * from "./disable-tax-definition-by-id.response.dto";
+export * from "./enable-tax-definition-by-id.response.dto";
+export * from "./get-tax-definition-by-id.response.dto";
+export * from "./list-tax-definitions.response.dto";
+export * from "./update-tax-definition-by-id.response.dto";
diff --git a/modules/catalogs/src/common/dto/tax-definitions/response/list-tax-definitions.response.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/response/list-tax-definitions.response.dto.ts
new file mode 100644
index 00000000..3d3cd221
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/response/list-tax-definitions.response.dto.ts
@@ -0,0 +1,9 @@
+import { createPaginatedListSchema } from "@erp/core";
+import type { z } from "zod/v4";
+
+import { TaxDefinitionSummarySchema } from "../shared";
+
+export const ListTaxDefinitionsResponseSchema = createPaginatedListSchema(
+ TaxDefinitionSummarySchema
+);
+export type ListTaxDefinitionsResponseDTO = z.infer;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/response/update-tax-definition-by-id.response.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/response/update-tax-definition-by-id.response.dto.ts
new file mode 100644
index 00000000..65133716
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/response/update-tax-definition-by-id.response.dto.ts
@@ -0,0 +1,8 @@
+import type { z } from "zod/v4";
+
+import { TaxDefinitionDetailSchema } from "../shared";
+
+export const UpdateTaxDefinitionByIdResponseSchema = TaxDefinitionDetailSchema;
+export type UpdateTaxDefinitionByIdResponseDTO = z.infer<
+ typeof UpdateTaxDefinitionByIdResponseSchema
+>;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/shared/index.ts b/modules/catalogs/src/common/dto/tax-definitions/shared/index.ts
new file mode 100644
index 00000000..a2a9449c
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/shared/index.ts
@@ -0,0 +1,2 @@
+export * from "./tax-definition-detail.dto";
+export * from "./tax-definition-summary.dto";
diff --git a/modules/catalogs/src/common/dto/tax-definitions/shared/tax-definition-detail.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/shared/tax-definition-detail.dto.ts
new file mode 100644
index 00000000..baaa67d7
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/shared/tax-definition-detail.dto.ts
@@ -0,0 +1,44 @@
+import { IsoDateSchema, PercentageSchema } from "@erp/core";
+import { z } from "zod/v4";
+
+const TaxDefinitionCodeSchema = z.string().regex(/^[a-z0-9][a-z0-9_]*$/);
+const TaxDefinitionFamilySchema = z.enum([
+ "iva",
+ "igic",
+ "ipsi",
+ "equivalence_surcharge",
+ "withholding",
+ "vat",
+ "gst",
+ "sales_tax",
+ "reverse_charge",
+ "exempt",
+ "not_subject",
+ "custom",
+]);
+const TaxCalculationBehaviorSchema = z.enum(["additive", "subtractive", "neutral"]);
+const TaxScopeSchema = z.enum(["domestic", "intra_eu", "export", "import", "international"]);
+const JurisdictionCountryCodeSchema = z.string().regex(/^[A-Z]{2}$/);
+const JurisdictionRegionCodeSchema = z.string().regex(/^[A-Z]{2}-[A-Z0-9-]+$/);
+
+export const TaxDefinitionDetailSchema = z.object({
+ id: z.uuid(),
+ company_id: z.uuid(),
+ code: TaxDefinitionCodeSchema,
+ name: z.string(),
+ description: z.string().nullable(),
+ rate: PercentageSchema,
+ tax_family: TaxDefinitionFamilySchema,
+ calculation_behavior: TaxCalculationBehaviorSchema,
+ jurisdiction_country_code: JurisdictionCountryCodeSchema,
+ jurisdiction_region_code: JurisdictionRegionCodeSchema.nullable(),
+ tax_scope: TaxScopeSchema,
+ invoice_note: z.string().nullable(),
+ allowed_surcharge_codes: z.array(TaxDefinitionCodeSchema).nullable(),
+ is_system: z.boolean(),
+ is_active: z.boolean(),
+ valid_from: IsoDateSchema.nullable(),
+ valid_to: IsoDateSchema.nullable(),
+});
+
+export type TaxDefinitionDetailDTO = z.infer;
diff --git a/modules/catalogs/src/common/dto/tax-definitions/shared/tax-definition-summary.dto.ts b/modules/catalogs/src/common/dto/tax-definitions/shared/tax-definition-summary.dto.ts
new file mode 100644
index 00000000..8973a5aa
--- /dev/null
+++ b/modules/catalogs/src/common/dto/tax-definitions/shared/tax-definition-summary.dto.ts
@@ -0,0 +1,14 @@
+import { z } from "zod/v4";
+
+const TaxDefinitionCodeSchema = z.string().regex(/^[a-z0-9][a-z0-9_]*$/);
+
+export const TaxDefinitionSummarySchema = z.object({
+ id: z.uuid(),
+ company_id: z.uuid(),
+ code: TaxDefinitionCodeSchema,
+ name: z.string(),
+ is_active: z.boolean(),
+ is_system: z.boolean(),
+});
+
+export type TaxDefinitionSummaryDTO = z.infer;
diff --git a/modules/core/src/api/application/snapshot-builders/snapshot-builder.interface.ts b/modules/core/src/api/application/snapshot-builders/snapshot-builder.interface.ts
index 4bae855c..9636447d 100644
--- a/modules/core/src/api/application/snapshot-builders/snapshot-builder.interface.ts
+++ b/modules/core/src/api/application/snapshot-builders/snapshot-builder.interface.ts
@@ -1,4 +1,4 @@
-export type ISnapshotBuilderParams = Readonly>;
+export type ISnapshotBuilderParams = {}; //Readonly>;
export interface ISnapshotBuilder {
toOutput(source: TSource, params?: ISnapshotBuilderParams): TSnapshot;
diff --git a/modules/core/src/api/domain/value-objects/tax-percentage.vo.ts b/modules/core/src/api/domain/value-objects/tax-percentage.vo.ts
index 6ac212f2..a601d334 100644
--- a/modules/core/src/api/domain/value-objects/tax-percentage.vo.ts
+++ b/modules/core/src/api/domain/value-objects/tax-percentage.vo.ts
@@ -1,19 +1,25 @@
import { Percentage, type PercentageProps } from "@repo/rdx-ddd";
-import type { Result } from "@repo/rdx-utils";
+import { Result } from "@repo/rdx-utils";
type TaxPercentageProps = Pick;
export class TaxPercentage extends Percentage {
- static DEFAULT_SCALE = 2;
+ public static readonly DEFAULT_SCALE = 2;
- static create({ value }: TaxPercentageProps): Result {
- return Percentage.create({
+ public static create({ value }: TaxPercentageProps): Result {
+ const result = Percentage.create({
value,
scale: TaxPercentage.DEFAULT_SCALE,
});
+
+ if (result.isFailure) {
+ return result;
+ }
+
+ return Result.ok(new TaxPercentage(result.data.getProps()));
}
- static zero() {
+ public static zero(): TaxPercentage {
return TaxPercentage.create({ value: 0 }).data;
}
}
diff --git a/modules/core/src/api/domain/value-objects/tax.vo.ts b/modules/core/src/api/domain/value-objects/tax.vo.ts
index 609ced39..8e32c7a3 100644
--- a/modules/core/src/api/domain/value-objects/tax.vo.ts
+++ b/modules/core/src/api/domain/value-objects/tax.vo.ts
@@ -1,178 +1,183 @@
-import type { TaxCatalogProvider } from "@erp/core";
import { ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
-import { TaxPercentage } from "./tax-percentage.vo";
+import { TaxPercentage } from "./tax-percentage.vo.js";
-const TAX_GROUPS = ["IVA", "IPSI", "IGIC", "retention", "rec"] as const;
-type TaxGroup = (typeof TAX_GROUPS)[number];
+export const TAX_GROUPS = ["iva", "ipsi", "igic", "retention", "surcharge"] as const;
+
+export type TaxGroup = (typeof TAX_GROUPS)[number];
+
+export type TaxCalculationBehavior = "additive" | "subtractive";
export interface TaxProps {
- code: string; // iva_21
- name: string; // 21% IVA
- value: number; // 2100
+ code: string;
+ name: string;
+ rate: TaxPercentage;
group: TaxGroup;
+ calculationBehavior: TaxCalculationBehavior;
+}
+
+export interface CreateTaxProps {
+ code: string;
+ name: string;
+ rate: TaxPercentage;
+ group: TaxGroup;
+ calculationBehavior: TaxCalculationBehavior;
}
export class Tax extends ValueObject {
- static readonly DEFAULT_SCALE = TaxPercentage.DEFAULT_SCALE;
- static readonly MIN_VALUE = TaxPercentage.MIN_VALUE;
- static readonly MAX_VALUE = TaxPercentage.MAX_VALUE;
- static readonly MIN_SCALE = TaxPercentage.MIN_SCALE;
- static readonly MAX_SCALE = TaxPercentage.MAX_SCALE;
+ public static readonly DEFAULT_SCALE = TaxPercentage.DEFAULT_SCALE;
+ public static readonly MIN_VALUE = TaxPercentage.MIN_VALUE;
+ public static readonly MAX_VALUE = TaxPercentage.MAX_VALUE;
+ public static readonly MIN_SCALE = TaxPercentage.MIN_SCALE;
+ public static readonly MAX_SCALE = TaxPercentage.MAX_SCALE;
- private static CODE_REGEX = /^[a-z0-9_:-]+$/;
+ private static readonly CODE_REGEX = /^[a-z0-9_:-]+$/;
- private _percentage!: TaxPercentage;
+ public static create(props: CreateTaxProps): Result {
+ const validationResult = Tax.validate(props);
- protected static validate(values: TaxProps) {
+ if (!validationResult.success) {
+ return Result.fail(
+ new Error(validationResult.error.issues.map((issue) => issue.message).join(", "))
+ );
+ }
+
+ return Result.ok(
+ new Tax({
+ code: props.code.trim(),
+ name: props.name.trim(),
+ rate: props.rate,
+ group: props.group,
+ calculationBehavior: props.calculationBehavior,
+ })
+ );
+ }
+
+ protected static validate(values: CreateTaxProps) {
const schema = z.object({
- value: z
- .number()
- .int()
- .min(Tax.MIN_VALUE, "La tasa de impuesto no puede ser negativa.")
- .max(Tax.MAX_VALUE * 10 ** Tax.MAX_SCALE, "La tasa de impuesto es demasiado alta."),
name: z
.string()
+ .trim()
.min(1, "El nombre del impuesto es obligatorio.")
.max(100, "El nombre del impuesto no puede exceder 100 caracteres."),
+
code: z
.string()
+ .trim()
.min(1, "El código del impuesto es obligatorio.")
.max(40, "El código del impuesto no puede exceder 40 caracteres.")
.regex(Tax.CODE_REGEX, "El código contiene caracteres no permitidos."),
- group: z.enum(TAX_GROUPS, "El impuesto debe ser un IVA, retención o rec. equivalencia"),
+
+ group: z.enum(TAX_GROUPS),
+
+ calculationBehavior: z.enum(["additive", "subtractive"]),
});
return schema.safeParse(values);
}
- static create(props: TaxProps): Result {
- const { value, name, code, group } = props;
-
- const validationResult = Tax.validate({ value, name, code, group });
- if (!validationResult.success) {
- return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", ")));
- }
-
- return Result.ok(new Tax({ value, name, code, group }));
- }
-
- /**
- * Crea un Tax usando solo el 'code', resolviendo el resto de datos desde el catálogo.
- * @param code Código del impuesto (p.ej. "iva_21")
- * @param provider Proveedor del catálogo de impuestos
- */
- static createFromCode(code: string, provider: TaxCatalogProvider): Result {
- const normalized = (code ?? "").trim().toLowerCase();
-
- const schema = z
- .string()
- .min(1, "El código del impuesto es obligatorio.")
- .max(40, "El código del impuesto no puede exceder 40 caracteres.")
- .regex(Tax.CODE_REGEX, "El código contiene caracteres no permitidos.");
-
- const validationResult = schema.safeParse(normalized);
-
- if (!validationResult.success) {
- return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", ")));
- }
-
- const maybeItem = provider.findByCode(normalized);
- if (maybeItem.isNone()) {
- return Result.fail(
- new Error(`Código de impuesto no encontrado en el catálogo: "${normalized}"`)
- );
- }
-
- const item = maybeItem.unwrap();
-
- // Delegamos en create para reusar validación y límites
- return Tax.create({
- value: Number(item.value),
- name: item.name,
- code: item.code, // guardamos el code tal cual viene del catálogo
- group: item.group as TaxGroup,
- });
- }
-
- get value(): number {
- return this.props.value;
- }
- get scale(): number {
- return Tax.DEFAULT_SCALE;
- }
- get name(): string {
- return this.props.name;
- }
- get code(): string {
+ public get code(): string {
return this.props.code;
}
- get group(): string {
+ public get name(): string {
+ return this.props.name;
+ }
+
+ public get rate(): TaxPercentage {
+ return this.props.rate;
+ }
+
+ public get group(): TaxGroup {
return this.props.group;
}
- get percentage(): TaxPercentage {
- return TaxPercentage.create({ value: this.value }).data;
+ public get calculationBehavior(): TaxCalculationBehavior {
+ return this.props.calculationBehavior;
}
- isVATLike(): boolean {
- return this.group === "IVA" || this.group === "IGIC" || this.group === "IPSI";
+ public get value(): number {
+ return this.props.rate.value;
}
- isRetention(): boolean {
- return this.group === "retention";
+ public get scale(): number {
+ return this.props.rate.scale;
}
- isRec(): boolean {
- return this.group === "rec";
+ public get percentage(): TaxPercentage {
+ return this.props.rate;
}
- getProps(): TaxProps {
+ public isAdditive(): boolean {
+ return this.props.calculationBehavior === "additive";
+ }
+
+ public isSubtractive(): boolean {
+ return this.props.calculationBehavior === "subtractive";
+ }
+
+ public isIva(): boolean {
+ return this.props.group === "iva";
+ }
+
+ public isIgic(): boolean {
+ return this.props.group === "igic";
+ }
+
+ public isIpsi(): boolean {
+ return this.props.group === "ipsi";
+ }
+
+ public isSurcharge(): boolean {
+ return this.props.group === "surcharge";
+ }
+
+ public isRetention(): boolean {
+ return this.props.group === "retention";
+ }
+
+ public getProps(): TaxProps {
return this.props;
}
- toPrimitive() {
+ public toPrimitive(): TaxProps {
return this.getProps();
}
- toNumber(): number {
+ public toNumber(): number {
return this.value / 10 ** this.scale;
}
- toString(): string {
- return `${this.toNumber().toFixed(this.scale)}%`;
+ public toString(): string {
+ return this.rate.toString();
}
- isZero(): boolean {
+ public isZero(): boolean {
return this.value === 0;
}
- isPositive(): boolean {
+ public isPositive(): boolean {
return this.value > 0;
}
- equalsTo(other: Tax): boolean {
- return this.equals(other);
+ public equalsTo(other: Tax): boolean {
+ return (
+ this.code === other.code &&
+ this.name === other.name &&
+ this.value === other.value &&
+ this.scale === other.scale &&
+ this.group === other.group &&
+ this.calculationBehavior === other.calculationBehavior
+ );
}
- greaterThan(other: Tax): boolean {
+
+ public greaterThan(other: Tax): boolean {
return this.toNumber() > other.toNumber();
}
- lessThan(other: Tax): boolean {
+ public lessThan(other: Tax): boolean {
return this.toNumber() < other.toNumber();
}
-
- toJSON() {
- return {
- value: this.value,
- scale: this.scale,
- name: this.name,
- code: this.code,
- percentage: this.toNumber(),
- formatted: this.toString(),
- };
- }
}
diff --git a/modules/core/src/api/infrastructure/di/catalogs.di.ts b/modules/core/src/api/infrastructure/di/catalogs.di.ts
index e98d2b17..ca7689f2 100644
--- a/modules/core/src/api/infrastructure/di/catalogs.di.ts
+++ b/modules/core/src/api/infrastructure/di/catalogs.di.ts
@@ -1,21 +1,16 @@
-import {
- FactuGESPaymentCatalogProvider,
- type JsonPaymentCatalogProvider,
- type JsonTaxCatalogProvider,
- SpainTaxCatalogProvider,
-} from "../../../common";
+import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "../../../common";
export interface ICatalogs {
taxCatalog: JsonTaxCatalogProvider;
- paymentCatalog: JsonPaymentCatalogProvider;
+ //paymentCatalog: JsonPaymentCatalogProvider;
}
export const buildCatalogs = (): ICatalogs => {
const taxCatalog = SpainTaxCatalogProvider();
- const paymentCatalog = FactuGESPaymentCatalogProvider();
+ //const paymentCatalog = FactuGESPaymentCatalogProvider();
return {
taxCatalog,
- paymentCatalog,
+ //paymentCatalog,
};
};
diff --git a/modules/core/src/api/infrastructure/express/api-error-mapper.ts b/modules/core/src/api/infrastructure/express/api-error-mapper.ts
index 9b3e9596..69ae86d0 100644
--- a/modules/core/src/api/infrastructure/express/api-error-mapper.ts
+++ b/modules/core/src/api/infrastructure/express/api-error-mapper.ts
@@ -17,9 +17,8 @@ import {
isDomainValidationError,
isValidationErrorCollection,
} from "@repo/rdx-ddd";
-import type { ZodError } from "zod";
+import { ZodError } from "zod";
-import { isSchemaError } from "../../../common/schemas";
import { type DocumentGenerationError, isDocumentGenerationError } from "../../application";
import {
type DuplicateEntityError,
@@ -46,6 +45,8 @@ import {
ValidationApiError,
} from "./errors";
+export const isSchemaError = (e: unknown): e is ZodError => e instanceof ZodError;
+
// ────────────────────────────────────────────────────────────────────────────────
// Contexto opcional para enriquecer Problem+JSON (útil en middleware Express)
// ────────────────────────────────────────────────────────────────────────────────
diff --git a/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-domain-mapper.ts b/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-domain-mapper.ts
index f1212aca..4a01c84d 100644
--- a/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-domain-mapper.ts
+++ b/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-domain-mapper.ts
@@ -12,7 +12,7 @@ export abstract class SequelizeDomainMapper;
+ ): Result | Promise>;
public mapToDomainCollection(
raws: (TModel | TModelAttributes)[],
diff --git a/modules/core/src/api/modules/types.ts b/modules/core/src/api/modules/types.ts
index 8e3a8022..11d9c49c 100644
--- a/modules/core/src/api/modules/types.ts
+++ b/modules/core/src/api/modules/types.ts
@@ -6,6 +6,12 @@ export interface SequelizeModel extends Model {
}
export type ModuleParams = {
+ /**
+ * Acceso a servicios expuestos por otros módulos.
+ * Todas las dependencias ya están resueltas.
+ */
+ getService: (serviceName: string) => T;
+
[key: string]: any;
};
@@ -25,12 +31,6 @@ export type ModuleSetupResult = {
export type SetupParams = ModuleParams;
export type StartParams = ModuleParams & {
- /**
- * Acceso a servicios expuestos por otros módulos.
- * Todas las dependencias ya están resueltas.
- */
- getService: (serviceName: string) => T;
-
/**
* Acceso a internal del propio módulo.
* No debe usarse para consumir otros módulos.
diff --git a/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json b/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json
index 020bb32f..c892bd42 100644
--- a/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json
+++ b/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json
@@ -6,7 +6,8 @@
"scale": "2",
"group": "IVA",
"description": "IVA general. Tipo estándar nacional.",
- "aeat_code": "01"
+ "aeat_code": "01",
+ "req_equals": ["rec_5_2"]
},
{
"name": "IVA 10%",
diff --git a/modules/core/src/common/index.ts b/modules/core/src/common/index.ts
index 541d6f57..2dab51a5 100644
--- a/modules/core/src/common/index.ts
+++ b/modules/core/src/common/index.ts
@@ -1,5 +1,4 @@
export * from "./catalogs";
export * from "./dto";
export * from "./helpers";
-export * from "./schemas";
export * from "./types";
diff --git a/modules/core/src/common/schemas/core.schemas.ts b/modules/core/src/common/schemas/core.schemas.ts
deleted file mode 100644
index 38db200d..00000000
--- a/modules/core/src/common/schemas/core.schemas.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { z } from "zod";
-
-/***
- * Cantidad
- * – admite decimales hasta 2 cifras
- * – el campo `amount` puede ser null
- */
-export const makeAmountSchema = ({ maxScale = 2, nullable = false } = {}) => {
- const amount = z.number().refine((v) => Number.isFinite(v), "Amount must be a finite number");
-
- return z.object({
- amount: nullable ? amount.nullable() : amount,
- scale: z.number().int().nonnegative().max(maxScale, `Scale cannot exceed ${maxScale}`),
- });
-};
-
-/**
- * Porcentaje ≥ 0 ≤ 100
- * – admite decimales hasta 2 cifras
- * – el campo `amount` puede ser null
- */
-export const makePercentageSchema = ({ maxScale = 2, min = 0, max = 100, nullable = true } = {}) =>
- makeAmountSchema({ maxScale, nullable }).extend({
- amount: z
- .number()
- .min(min, `El porcentaje debe ser ≥ ${min}`)
- .max(max, `El porcentaje no puede superar ${max}`)
- .nullable(),
- });
diff --git a/modules/core/src/common/schemas/index.ts b/modules/core/src/common/schemas/index.ts
deleted file mode 100644
index e5649dba..00000000
--- a/modules/core/src/common/schemas/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./core.schemas";
-export * from "./schema-error";
diff --git a/modules/core/src/common/schemas/schema-error.ts b/modules/core/src/common/schemas/schema-error.ts
deleted file mode 100644
index 46d4961c..00000000
--- a/modules/core/src/common/schemas/schema-error.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { ZodError } from "zod";
-
-export const isSchemaError = (e: unknown): e is ZodError => e instanceof ZodError;
diff --git a/modules/core/src/common/types/dto.d.ts b/modules/core/src/common/types/dto.d.ts
index cad06e4f..f00d117f 100644
--- a/modules/core/src/common/types/dto.d.ts
+++ b/modules/core/src/common/types/dto.d.ts
@@ -1,2 +1 @@
-// biome-ignore lint/style/useNamingConvention: false positive
export type DTO> = T;
diff --git a/modules/customer-invoices/src/api/application/common/index.ts b/modules/customer-invoices/src/api/application/common/index.ts
new file mode 100644
index 00000000..c7468d98
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/common/index.ts
@@ -0,0 +1 @@
+export * from "./mappers";
diff --git a/modules/customer-invoices/src/api/application/common/mappers/index.ts b/modules/customer-invoices/src/api/application/common/mappers/index.ts
new file mode 100644
index 00000000..cdd12e27
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/common/mappers/index.ts
@@ -0,0 +1 @@
+export * from "./tax-definition-to-tax.mapper";
diff --git a/modules/customer-invoices/src/api/application/common/mappers/tax-definition-to-tax.mapper.ts b/modules/customer-invoices/src/api/application/common/mappers/tax-definition-to-tax.mapper.ts
new file mode 100644
index 00000000..bac6589f
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/common/mappers/tax-definition-to-tax.mapper.ts
@@ -0,0 +1,44 @@
+import type { TaxDefinitionPublicFamily, TaxDefinitionPublicModel } from "@erp/catalogs/api";
+import { Tax, type TaxGroup, TaxPercentage } from "@erp/core/api";
+import { Result } from "@repo/rdx-utils";
+
+/**
+ * Sirve para mapear TaxDefinitionPublicModel a Tax,
+ * que es el modelo de dominio que se para proforma e issued-invoices.
+ */
+
+export class TaxDefinitionToTaxMapper {
+ public toTax(taxDefinition: TaxDefinitionPublicModel): Result {
+ const group = this.toTaxGroup(taxDefinition.taxFamily);
+
+ if (group.isFailure) {
+ return Result.fail(group.error);
+ }
+
+ return Tax.create({
+ code: taxDefinition.code,
+ name: taxDefinition.name,
+ rate: TaxPercentage.create({
+ value: taxDefinition.rate.value,
+ }).data,
+ group: group.data,
+ calculationBehavior: taxDefinition.calculationBehavior,
+ });
+ }
+
+ private toTaxGroup(taxFamily: TaxDefinitionPublicFamily): Result {
+ switch (taxFamily) {
+ case "iva":
+ case "igic":
+ case "ipsi":
+ case "retention":
+ return Result.ok(taxFamily);
+
+ case "surcharge":
+ return Result.ok("surcharge");
+
+ default:
+ return Result.fail(new Error(`Unsupported tax family: ${String(taxFamily)}`));
+ }
+ }
+}
diff --git a/modules/customer-invoices/src/api/application/common/models/index.ts b/modules/customer-invoices/src/api/application/common/models/index.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/customer-invoices/src/api/application/common/services/index.ts b/modules/customer-invoices/src/api/application/common/services/index.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts
index 3dafcc3d..33b83f05 100644
--- a/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts
+++ b/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts
@@ -1,12 +1,10 @@
-import type { ICatalogs } from "@erp/core/api";
+import { buildCatalogs } from "@erp/core/api";
import {
type IProformaToIssuedInvoiceConverter,
ProformaToIssuedInvoiceConverter,
} from "../services";
-export function buildProformaToIssuedInvoicePropsConverter(
- catalogs: ICatalogs
-): IProformaToIssuedInvoiceConverter {
- return new ProformaToIssuedInvoiceConverter(catalogs);
+export function buildProformaToIssuedInvoicePropsConverter(): IProformaToIssuedInvoiceConverter {
+ return new ProformaToIssuedInvoiceConverter(buildCatalogs());
}
diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts
index 4428548f..85db04ab 100644
--- a/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts
+++ b/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts
@@ -222,7 +222,7 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
const paymentMethodOrError = InvoicePaymentMethod.create(
{
- paymentDescription: existingPaymentResult.unwrap().description,
+ name: existingPaymentResult.unwrap().description,
},
paymentId
);
diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts
index 850d08a2..c70aa9aa 100644
--- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts
+++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts
@@ -28,7 +28,7 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
const payment_method = {
payment_id: invoice.paymentMethod.id.toString(),
- payment_description: invoice.paymentMethod.paymentDescription.toString(),
+ payment_description: invoice.paymentMethod.name.toString(),
};
return {
diff --git a/modules/customer-invoices/src/api/application/proformas/di/index.ts b/modules/customer-invoices/src/api/application/proformas/di/index.ts
index 99d4b6f4..db4cc1c3 100644
--- a/modules/customer-invoices/src/api/application/proformas/di/index.ts
+++ b/modules/customer-invoices/src/api/application/proformas/di/index.ts
@@ -1,7 +1,10 @@
+export * from "./proforma-catalog-resolvers.di";
export * from "./proforma-creator.di";
export * from "./proforma-finder.di";
export * from "./proforma-input-mappers.di";
export * from "./proforma-issuer.di";
+export * from "./proforma-read-model-assemblers.di";
export * from "./proforma-snapshot-builders.di";
+export * from "./proforma-status-changer.di";
export * from "./proforma-updater.di";
export * from "./proforma-use-cases.di";
diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-catalog-resolvers.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-catalog-resolvers.di.ts
new file mode 100644
index 00000000..85424326
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-catalog-resolvers.di.ts
@@ -0,0 +1,33 @@
+import type {
+ IPaymentMethodPublicFinder,
+ ITaxDefinitionPublicFinder,
+ ITaxRegimePublicFinder,
+} from "@erp/catalogs/api";
+
+import {
+ ProformaPaymentResolver,
+ type ProformaTaxResolver,
+ buildProformaTaxResolver,
+} from "../services";
+
+export interface IProformaCatalogResolvers {
+ taxResolver: ProformaTaxResolver;
+ paymentResolver: ProformaPaymentResolver;
+}
+
+export function buildProformaCatalogResolvers(params: {
+ taxDefinitionFinder: ITaxDefinitionPublicFinder;
+ taxRegimeFinder: ITaxRegimePublicFinder;
+ paymentMethodFinder: IPaymentMethodPublicFinder;
+}): IProformaCatalogResolvers {
+ return {
+ taxResolver: buildProformaTaxResolver({
+ taxDefinitionFinder: params.taxDefinitionFinder,
+ taxRegimeFinder: params.taxRegimeFinder,
+ }),
+
+ paymentResolver: new ProformaPaymentResolver({
+ paymentMethodFinder: params.paymentMethodFinder,
+ }),
+ };
+}
diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-creator.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-creator.di.ts
index c20d2565..26c8e18f 100644
--- a/modules/customer-invoices/src/api/application/proformas/di/proforma-creator.di.ts
+++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-creator.di.ts
@@ -1,14 +1,17 @@
import type { IProformaRepository } from "../repositories";
-import { type IProformaCreator, type IProformaNumberGenerator, ProformaCreator } from "../services";
+import type { ProformaPaymentResolver } from "../services";
+import {
+ type IProformaCreator,
+ type IProformaNumberGenerator,
+ ProformaCreator,
+ type ProformaTaxResolver,
+} from "../services";
export const buildProformaCreator = (params: {
+ taxResolver: ProformaTaxResolver;
+ paymentResolver: ProformaPaymentResolver;
numberService: IProformaNumberGenerator;
repository: IProformaRepository;
}): IProformaCreator => {
- const { numberService, repository } = params;
-
- return new ProformaCreator({
- numberService,
- repository,
- });
+ return new ProformaCreator(params);
};
diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-input-mappers.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-input-mappers.di.ts
index 534520ae..2dbee9f7 100644
--- a/modules/customer-invoices/src/api/application/proformas/di/proforma-input-mappers.di.ts
+++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-input-mappers.di.ts
@@ -1,23 +1,18 @@
-import type { ICatalogs } from "@erp/core/api";
-
import {
CreateProformaInputMapper,
type ICreateProformaInputMapper,
+ type IUpdateProformaInputMapper,
UpdateProformaInputMapper,
} from "../mappers";
export interface IProformaInputMappers {
createInputMapper: ICreateProformaInputMapper;
- updateInputMapper: UpdateProformaInputMapper;
+ updateInputMapper: IUpdateProformaInputMapper;
}
-export const buildProformaInputMappers = (catalogs: ICatalogs): IProformaInputMappers => {
- // Mappers el DTO a las props validadas (ProformaProps) y luego construir agregado
- const createInputMapper = new CreateProformaInputMapper(catalogs);
- const updateInputMapper = new UpdateProformaInputMapper(catalogs);
-
+export const buildProformaInputMappers = (): IProformaInputMappers => {
return {
- createInputMapper,
- updateInputMapper,
+ createInputMapper: new CreateProformaInputMapper(),
+ updateInputMapper: new UpdateProformaInputMapper(),
};
};
diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-read-model-assemblers.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-read-model-assemblers.di.ts
new file mode 100644
index 00000000..32d834a9
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-read-model-assemblers.di.ts
@@ -0,0 +1,19 @@
+import type { IPaymentMethodPublicFinder, ITaxRegimePublicFinder } from "@erp/catalogs/api";
+
+import { type IProformaFullReadModelAssembler, ProformaFullReadModelAssembler } from "../services";
+
+export interface IProformaReadModelAssemblers {
+ full: IProformaFullReadModelAssembler;
+}
+
+export function buildProformaReadModelAssemblers(params: {
+ paymentMethodFinder: IPaymentMethodPublicFinder;
+ taxRegimeFinder: ITaxRegimePublicFinder;
+}): IProformaReadModelAssemblers {
+ return {
+ full: new ProformaFullReadModelAssembler({
+ paymentMethodFinder: params.paymentMethodFinder,
+ taxRegimeFinder: params.taxRegimeFinder,
+ }),
+ };
+}
diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-status-changer.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-status-changer.di.ts
new file mode 100644
index 00000000..d53b5595
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-status-changer.di.ts
@@ -0,0 +1,8 @@
+import type { IProformaRepository } from "../repositories";
+import { type IProformaStatusChanger, ProformaStatusChanger } from "../services";
+
+export const buildProformaStatusChanger = (params: {
+ repository: IProformaRepository;
+}): IProformaStatusChanger => {
+ return new ProformaStatusChanger(params);
+};
diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-updater.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-updater.di.ts
index 5f746d44..18e0ec65 100644
--- a/modules/customer-invoices/src/api/application/proformas/di/proforma-updater.di.ts
+++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-updater.di.ts
@@ -1,12 +1,12 @@
+// modules/customer-invoices/src/api/application/proformas/di/proforma-updater.di.ts
import type { IProformaRepository } from "../repositories";
-import { type IProformaUpdater, ProformaUpdater } from "../services";
+import type { ProformaPaymentResolver } from "../services";
+import { type IProformaUpdater, type ProformaTaxResolver, ProformaUpdater } from "../services";
export const buildProformaUpdater = (params: {
repository: IProformaRepository;
+ taxResolver: ProformaTaxResolver;
+ paymentResolver: ProformaPaymentResolver;
}): IProformaUpdater => {
- const { repository } = params;
-
- return new ProformaUpdater({
- repository,
- });
+ return new ProformaUpdater(params);
};
diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts
index 9eeeff21..a3330ffd 100644
--- a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts
+++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts
@@ -1,19 +1,21 @@
import type { ITransactionManager } from "@erp/core/api";
import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
-import type { ICreateProformaInputMapper, UpdateProformaInputMapper } from "../mappers";
+import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers";
import type {
IProformaCreator,
IProformaFinder,
+ IProformaFullReadModelAssembler,
IProformaIssuer,
+ IProformaStatusChanger,
IProformaUpdater,
ProformaDocumentGeneratorService,
} from "../services";
import type {
+ IProformaFullSnapshotBuilder,
IProformaReportSnapshotBuilder,
IProformaSummarySnapshotBuilder,
} from "../snapshot-builders";
-import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full";
import {
ChangeStatusProformaUseCase,
CreateProformaUseCase,
@@ -21,15 +23,21 @@ import {
IssueProformaUseCase,
ListProformasUseCase,
ReportProformaUseCase,
+ UpdateProformaByIdUseCase,
} from "../use-cases";
-import { UpdateProformaByIdUseCase } from "../use-cases/update-proforma-by-id.use-case";
export function buildGetProformaByIdUseCase(deps: {
finder: IProformaFinder;
+ fullReadModelAssembler: IProformaFullReadModelAssembler;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
- return new GetProformaByIdUseCase(deps.finder, deps.fullSnapshotBuilder, deps.transactionManager);
+ return new GetProformaByIdUseCase({
+ finder: deps.finder,
+ fullReadModelAssembler: deps.fullReadModelAssembler,
+ fullSnapshotBuilder: deps.fullSnapshotBuilder,
+ transactionManager: deps.transactionManager,
+ });
}
export function buildListProformasUseCase(deps: {
@@ -46,29 +54,33 @@ export function buildListProformasUseCase(deps: {
export function buildReportProformaUseCase(deps: {
finder: IProformaFinder;
+ fullReadModelAssembler: IProformaFullReadModelAssembler;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
reportSnapshotBuilder: IProformaReportSnapshotBuilder;
documentService: ProformaDocumentGeneratorService;
transactionManager: ITransactionManager;
}) {
- return new ReportProformaUseCase(
- deps.finder,
- deps.fullSnapshotBuilder,
- deps.reportSnapshotBuilder,
- deps.documentService,
- deps.transactionManager
- );
+ return new ReportProformaUseCase({
+ finder: deps.finder,
+ fullReadModelAssembler: deps.fullReadModelAssembler,
+ fullSnapshotBuilder: deps.fullSnapshotBuilder,
+ reportSnapshotBuilder: deps.reportSnapshotBuilder,
+ documentService: deps.documentService,
+ transactionManager: deps.transactionManager,
+ });
}
export function buildCreateProformaUseCase(deps: {
creator: IProformaCreator;
dtoMapper: ICreateProformaInputMapper;
+ fullReadModelAssembler: IProformaFullReadModelAssembler;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new CreateProformaUseCase({
dtoMapper: deps.dtoMapper,
creator: deps.creator,
+ fullReadModelAssembler: deps.fullReadModelAssembler,
fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager,
});
@@ -98,13 +110,15 @@ export function buildIssueProformaUseCase(deps: {
export function buildUpdateProformaUseCase(deps: {
updater: IProformaUpdater;
- dtoMapper: UpdateProformaInputMapper;
+ dtoMapper: IUpdateProformaInputMapper;
+ fullReadModelAssembler: IProformaFullReadModelAssembler;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new UpdateProformaByIdUseCase({
dtoMapper: deps.dtoMapper,
updater: deps.updater,
+ fullReadModelAssembler: deps.fullReadModelAssembler,
fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager,
});
@@ -116,14 +130,14 @@ export function buildDeleteProformaUseCase(deps: { finder: IProformaFinder }) {
}*/
export function buildChangeStatusProformaUseCase(deps: {
- finder: IProformaFinder;
- updater: IProformaUpdater;
+ statusChanger: IProformaStatusChanger;
+ fullReadModelAssembler: IProformaFullReadModelAssembler;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new ChangeStatusProformaUseCase({
- finder: deps.finder,
- updater: deps.updater,
+ statusChanger: deps.statusChanger,
+ fullReadModelAssembler: deps.fullReadModelAssembler,
fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager,
});
diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts
index 948b525b..54efaccc 100644
--- a/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts
+++ b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts
@@ -1,5 +1,4 @@
-import type { JsonTaxCatalogProvider } from "@erp/core";
-import { DiscountPercentage, type ICatalogs, Tax } from "@erp/core/api";
+import { DiscountPercentage } from "@erp/core/api";
import {
CurrencyCode,
DomainError,
@@ -16,8 +15,6 @@ import { Maybe, NumberHelper, Result } from "@repo/rdx-utils";
import type { CreateProformaRequestDTO } from "../../../../common";
import {
- type IProformaCreateProps,
- type IProformaItemCreateProps,
InvoiceNumber,
type InvoiceRecipient,
InvoiceSerie,
@@ -25,34 +22,32 @@ import {
ItemAmount,
ItemDescription,
ItemQuantity,
- ProformaItemTaxes,
- type ProformaItemTaxesProps,
} from "../../../domain";
+import type {
+ ProformaCreateInputProps,
+ ProformaItemCreateInputProps,
+ ProformaItemTaxCodesInput,
+} from "../models";
export interface ICreateProformaInputMapper {
map(
dto: CreateProformaRequestDTO,
params: { companyId: UniqueID }
- ): Result<{ id: UniqueID; props: IProformaCreateProps }>;
+ ): Result<{ id: UniqueID; props: ProformaCreateInputProps }>;
}
/**
- * @summary Convierte el DTO de creación de proforma en props de dominio.
+ * @summary Convierte el DTO de creación de proforma en props de aplicación.
* @remarks
- * No construye el agregado. Solo valida y convierte primitivas de transporte
- * a Value Objects y props necesarias para `Proforma.create`.
+ * No construye el agregado y no resuelve impuestos contra catálogos.
+ * Solo valida y convierte primitivas de transporte a Value Objects o códigos
+ * fiscales pendientes de resolver por `ProformaCreator`.
*/
-
export class CreateProformaInputMapper implements ICreateProformaInputMapper {
- private readonly taxCatalog: JsonTaxCatalogProvider;
-
- constructor(catalogs: ICatalogs) {
- this.taxCatalog = catalogs.taxCatalog;
- }
public map(
dto: CreateProformaRequestDTO,
params: { companyId: UniqueID }
- ): Result<{ id: UniqueID; props: IProformaCreateProps }> {
+ ): Result<{ id: UniqueID; props: ProformaCreateInputProps }> {
const errors: ValidationErrorDetail[] = [];
try {
@@ -132,6 +127,12 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
errors
);
+ const taxRegimeCode = extractOrPushError(
+ maybeFromNullableResult(dto.tax_regime_code, (value) => Result.ok(String(value))),
+ "tax_regime_code",
+ errors
+ );
+
const items = this.mapItemsProps(dto.items, {
languageCode: languageCode!,
currencyCode: currencyCode!,
@@ -141,7 +142,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
this.throwIfValidationErrors(errors);
- const props: IProformaCreateProps = {
+ const props: ProformaCreateInputProps = {
companyId: params.companyId,
status: InvoiceStatus.draft(),
@@ -163,6 +164,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
linkedInvoiceId: Maybe.none(),
+ taxRegimeCode: taxRegimeCode!,
paymentMethodId: paymentMethodId!,
globalDiscountPercentage: globalDiscountPercentage!,
@@ -186,7 +188,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
globalDiscountPercentage: DiscountPercentage;
errors: ValidationErrorDetail[];
}
- ): IProformaItemCreateProps[] {
+ ): ProformaItemCreateInputProps[] {
return itemsDTO.map((item, index) => {
const description = extractOrPushError(
maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
@@ -243,36 +245,43 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
private mapTaxesProps(
taxesDTO: CreateProformaRequestDTO["items"][number]["taxes"],
params: { itemIndex: number; errors: ValidationErrorDetail[] }
- ): ProformaItemTaxesProps {
+ ): ProformaItemTaxCodesInput {
const parts = taxesDTO.split(";");
+
if (parts.length !== 3) {
params.errors.push({
path: `items[${params.itemIndex}].taxes`,
message: "Tax combination must contain exactly three elements",
});
- return ProformaItemTaxes.empty().getProps();
+
+ return this.emptyTaxCodes();
}
const [ivaCode, recCode, retentionCode] = parts;
- const iva = this.mapTaxCode(ivaCode, `items[${params.itemIndex}].taxes.iva`, params.errors);
- const rec = this.mapTaxCode(recCode, `items[${params.itemIndex}].taxes.rec`, params.errors);
- const retention = this.mapTaxCode(
- retentionCode,
- `items[${params.itemIndex}].taxes.retention`,
- params.errors
- );
-
- return ProformaItemTaxes.create({ iva, rec, retention }).data.getProps();
+ return {
+ ivaCode: this.mapNullableTaxCode(ivaCode),
+ recCode: this.mapNullableTaxCode(recCode),
+ retentionCode: this.mapNullableTaxCode(retentionCode),
+ };
}
- private mapTaxCode(code: string, path: string, errors: ValidationErrorDetail[]): Maybe {
- if (code === "#") {
+ private mapNullableTaxCode(code: string): Maybe {
+ const normalizedCode = code.trim();
+
+ if (normalizedCode === "" || normalizedCode === "#") {
return Maybe.none();
}
- const tax = extractOrPushError(Tax.createFromCode(code, this.taxCatalog), path, errors);
- return tax ? Maybe.some(tax) : Maybe.none();
+ return Maybe.some(normalizedCode);
+ }
+
+ private emptyTaxCodes(): ProformaItemTaxCodesInput {
+ return {
+ ivaCode: Maybe.none(),
+ recCode: Maybe.none(),
+ retentionCode: Maybe.none(),
+ };
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts b/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts
index a0e365d0..c836e44a 100644
--- a/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts
+++ b/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts
@@ -1,5 +1,5 @@
-import type { JsonTaxCatalogProvider } from "@erp/core";
-import { DiscountPercentage, type ICatalogs, Tax } from "@erp/core/api";
+// modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts
+import { DiscountPercentage } from "@erp/core/api";
import {
CurrencyCode,
DomainError,
@@ -12,28 +12,25 @@ import {
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
-import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
+import { Maybe, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
import type { UpdateProformaByIdRequestDTO } from "../../../../common/dto";
-import {
- InvoiceSerie,
- ItemAmount,
- ItemDescription,
- ItemQuantity,
- type ProformaItemPatchProps,
- type ProformaItemTaxesProps,
- type ProformaPatchProps,
-} from "../../../domain";
+import { InvoiceSerie, ItemAmount, ItemDescription, ItemQuantity } from "../../../domain";
+import type {
+ ProformaItemPatchInputProps,
+ ProformaPatchInputProps,
+ ProformaItemTaxCodesInput,
+} from "../models";
export interface IUpdateProformaInputMapper {
map(
dto: UpdateProformaByIdRequestDTO,
params: { companyId: UniqueID }
- ): Result;
+ ): Result;
}
/**
- * @summary Convierte el DTO de update de proforma en props de dominio.
+ * @summary Convierte el DTO de update de proforma en props de aplicación.
* @remarks
* Respeta semántica PATCH en cabecera:
* - omitido: no modificar
@@ -44,21 +41,19 @@ export interface IUpdateProformaInputMapper {
* - undefined: no tocar líneas
* - []: borrar todas las líneas
* - [...]: reemplazar colección completa
+ *
+ * No resuelve impuestos contra catálogos. Solo conserva los códigos fiscales
+ * para que `ProformaUpdater` los resuelva usando la fecha efectiva:
+ * `patch.invoiceDate ?? current.invoiceDate`.
*/
-
export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
- private readonly taxCatalog: JsonTaxCatalogProvider;
- constructor(catalogs: ICatalogs) {
- this.taxCatalog = catalogs.taxCatalog;
- }
-
public map(
dto: UpdateProformaByIdRequestDTO,
_params: { companyId: UniqueID }
- ): Result {
+ ): Result {
try {
const errors: ValidationErrorDetail[] = [];
- const proformaPatchProps: ProformaPatchProps = {};
+ const proformaPatchProps: ProformaPatchInputProps = {};
toPatchField(dto.series).ifSet((series) => {
proformaPatchProps.series = extractOrPushError(
@@ -196,7 +191,6 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
return Result.ok(proformaPatchProps);
} catch (err: unknown) {
- console.error(err);
return Result.fail(new DomainError("Proforma props mapping failed (update)", { cause: err }));
}
}
@@ -204,7 +198,7 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
private mapItemsProps(
itemsDTO: NonNullable,
params: { errors: ValidationErrorDetail[] }
- ): ProformaItemPatchProps[] {
+ ): ProformaItemPatchInputProps[] {
return itemsDTO.map((item, index) => {
const description = extractOrPushError(
maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
@@ -232,53 +226,37 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
params.errors
);
- const taxes = this.mapTaxesProps(item, {
- itemIndex: index,
- errors: params.errors,
- });
-
return {
position: item.position,
+
description: description!,
quantity: quantity!,
unitAmount: unitAmount!,
itemDiscountPercentage: itemDiscountPercentage!,
- taxes,
+
+ taxes: this.mapTaxesProps(item),
};
});
}
private mapTaxesProps(
- itemDTO: NonNullable[number],
- params: { itemIndex: number; errors: ValidationErrorDetail[] }
- ): ProformaItemTaxesProps {
- const iva = extractOrPushError(
- maybeFromNullableResult(itemDTO.iva_code, (v) => Tax.createFromCode(v, this.taxCatalog)),
- `items[${params.itemIndex}].iva_code`,
- params.errors
- );
-
- const rec = extractOrPushError(
- maybeFromNullableResult(itemDTO.rec_code, (v) => Tax.createFromCode(v, this.taxCatalog)),
- `items[${params.itemIndex}].rec_code`,
- params.errors
- );
-
- const retention = extractOrPushError(
- maybeFromNullableResult(itemDTO.retention_code, (v) =>
- Tax.createFromCode(v, this.taxCatalog)
- ),
- `items[${params.itemIndex}].retention_code`,
- params.errors
- );
-
+ itemDTO: NonNullable[number]
+ ): ProformaItemTaxCodesInput {
return {
- iva: iva!,
- rec: rec!,
- retention: retention!,
+ ivaCode: this.mapNullableTaxCode(itemDTO.iva_code),
+ recCode: this.mapNullableTaxCode(itemDTO.rec_code),
+ retentionCode: this.mapNullableTaxCode(itemDTO.retention_code),
};
}
+ private mapNullableTaxCode(code: string | null | undefined): Maybe {
+ if (isNullishOrEmpty(code)) {
+ return Maybe.none();
+ }
+
+ return Maybe.some(String(code).trim());
+ }
+
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
diff --git a/modules/customer-invoices/src/api/application/proformas/models/index.ts b/modules/customer-invoices/src/api/application/proformas/models/index.ts
index 3030fae7..cc215b9b 100644
--- a/modules/customer-invoices/src/api/application/proformas/models/index.ts
+++ b/modules/customer-invoices/src/api/application/proformas/models/index.ts
@@ -1 +1,5 @@
+export * from "./proforma-create-input.model";
+export * from "./proforma-full-read.model";
+export * from "./proforma-item-tax-codes-input.model";
export * from "./proforma-summary";
+export * from "./proforma-update-input.model";
diff --git a/modules/customer-invoices/src/api/application/proformas/models/proforma-create-input.model.ts b/modules/customer-invoices/src/api/application/proformas/models/proforma-create-input.model.ts
new file mode 100644
index 00000000..dfb7d6aa
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/models/proforma-create-input.model.ts
@@ -0,0 +1,21 @@
+import type { IProformaCreateProps, IProformaItemCreateProps } from "../../../domain";
+
+import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.model";
+
+/**
+ * @summary Modelos de entrada para creación de proformas.
+ * @remarks
+ * Estos modelos representan la forma en que el cliente envía los datos para crear una proforma.
+ * Se diferencian de los modelos de dominio en que:
+ * - No incluyen lógica de negocio ni métodos, solo datos.
+ * - Pueden tener una estructura diferente a la de dominio, adaptada a las necesidades de la API.
+ * - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
+ */
+
+export type ProformaCreateInputProps = Omit & {
+ items: ProformaItemCreateInputProps[];
+};
+
+export type ProformaItemCreateInputProps = Omit & {
+ taxes: ProformaItemTaxCodesInput;
+};
diff --git a/modules/customer-invoices/src/api/application/proformas/models/proforma-full-read.model.ts b/modules/customer-invoices/src/api/application/proformas/models/proforma-full-read.model.ts
new file mode 100644
index 00000000..85305760
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/models/proforma-full-read.model.ts
@@ -0,0 +1,32 @@
+import type { Maybe } from "@repo/rdx-utils";
+
+import type { Proforma } from "../../../domain";
+
+/**
+ * Datos del método de pago que completan a la proforma.
+ */
+export interface ProformaPaymentMethodReadModel {
+ id: string;
+ name: string;
+ description: Maybe;
+}
+
+/**
+ * Datos del régimen de pago que completan a la proforma.
+ */
+export interface ProformaTaxRegimeFullReadModel {
+ code: string;
+ description: string;
+}
+
+/**
+ * Modelo de una proforma con datos accesorios.
+ *
+ * Combina el agregado con datos auxiliares necesarios para
+ * construir la respuesta API generada por el snapshot builder.
+ */
+export interface ProformaFullReadModel {
+ proforma: Proforma;
+ paymentMethod: Maybe;
+ taxRegime: Maybe;
+}
diff --git a/modules/customer-invoices/src/api/application/proformas/models/proforma-item-tax-codes-input.model.ts b/modules/customer-invoices/src/api/application/proformas/models/proforma-item-tax-codes-input.model.ts
new file mode 100644
index 00000000..606acc68
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/models/proforma-item-tax-codes-input.model.ts
@@ -0,0 +1,7 @@
+import type { Maybe } from "@repo/rdx-utils";
+
+export interface ProformaItemTaxCodesInput {
+ ivaCode: Maybe;
+ recCode: Maybe;
+ retentionCode: Maybe;
+}
diff --git a/modules/customer-invoices/src/api/application/proformas/models/proforma-update-input.model.ts b/modules/customer-invoices/src/api/application/proformas/models/proforma-update-input.model.ts
new file mode 100644
index 00000000..8978e52d
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/models/proforma-update-input.model.ts
@@ -0,0 +1,21 @@
+import type { ProformaItemPatchProps, ProformaPatchProps } from "../../../domain";
+
+import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.model";
+
+/**
+ * @summary Modelos de entrada para edición de proformas.
+ * @remarks
+ * Estos modelos representan la forma en que el cliente envía los datos para editar una proforma.
+ * Se diferencian de los modelos de dominio en que:
+ * - No incluyen lógica de negocio ni métodos, solo datos.
+ * - Pueden tener una estructura diferente a la de dominio, adaptada a las necesidades de la API.
+ * - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
+ */
+
+export type ProformaPatchInputProps = Omit & {
+ items?: ProformaItemPatchInputProps[];
+};
+
+export type ProformaItemPatchInputProps = Omit & {
+ taxes: ProformaItemTaxCodesInput;
+};
diff --git a/modules/customer-invoices/src/api/application/proformas/services/assemblers/index.ts b/modules/customer-invoices/src/api/application/proformas/services/assemblers/index.ts
new file mode 100644
index 00000000..6123aa1c
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/services/assemblers/index.ts
@@ -0,0 +1 @@
+export * from "./proforma-full-read-model.assembler";
diff --git a/modules/customer-invoices/src/api/application/proformas/services/assemblers/proforma-full-read-model.assembler.ts b/modules/customer-invoices/src/api/application/proformas/services/assemblers/proforma-full-read-model.assembler.ts
new file mode 100644
index 00000000..5d843313
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/services/assemblers/proforma-full-read-model.assembler.ts
@@ -0,0 +1,142 @@
+import type { IPaymentMethodPublicFinder, ITaxRegimePublicFinder } from "@erp/catalogs/api";
+import type { UniqueID } from "@repo/rdx-ddd";
+import { Maybe, Result } from "@repo/rdx-utils";
+
+import type { Proforma } from "../../../../domain";
+import type {
+ ProformaFullReadModel,
+ ProformaPaymentMethodReadModel,
+ ProformaTaxRegimeFullReadModel,
+} from "../../models";
+
+export interface IProformaFullReadModelAssembler {
+ assemble(params: {
+ companyId: UniqueID;
+ proforma: Proforma;
+ transaction?: unknown;
+ }): Promise>;
+}
+
+/**
+ * Enriquece una proforma recuperada con
+ * datos accesorios recuperados del catálogo.
+ *
+ * No modifica el agregado. Su responsabilidad es
+ * preparar el modelo completo que recibirá el
+ * snapshot builder después.
+ */
+
+export class ProformaFullReadModelAssembler implements IProformaFullReadModelAssembler {
+ public constructor(
+ private readonly deps: {
+ paymentMethodFinder: IPaymentMethodPublicFinder;
+ taxRegimeFinder: ITaxRegimePublicFinder;
+ }
+ ) {}
+
+ public async assemble(params: {
+ companyId: UniqueID;
+ proforma: Proforma;
+ transaction?: unknown;
+ }): Promise> {
+ const paymentMethod = await this.resolvePaymentMethod(params);
+
+ if (paymentMethod.isFailure) {
+ return Result.fail(paymentMethod.error);
+ }
+
+ const taxRegime = await this.resolveTaxRegime(params);
+
+ if (taxRegime.isFailure) {
+ return Result.fail(taxRegime.error);
+ }
+
+ return Result.ok({
+ proforma: params.proforma,
+ paymentMethod: paymentMethod.data,
+ taxRegime: taxRegime.data,
+ });
+ }
+
+ private async resolvePaymentMethod(params: {
+ companyId: UniqueID;
+ proforma: Proforma;
+ transaction?: unknown;
+ }): Promise, Error>> {
+ if (params.proforma.paymentMethodId.isNone()) {
+ return Result.ok(Maybe.none());
+ }
+
+ const paymentMethodId = params.proforma.paymentMethodId.unwrap();
+
+ const result = await this.deps.paymentMethodFinder.findByIdInCompany({
+ companyId: params.companyId,
+ id: paymentMethodId,
+ transaction: params.transaction,
+ });
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ if (result.data.isNone()) {
+ return Result.ok(
+ Maybe.some({
+ id: paymentMethodId.toString(),
+ name: "",
+ description: Maybe.none(),
+ })
+ );
+ }
+
+ const paymentMethod = result.data.unwrap();
+
+ return Result.ok(
+ Maybe.some({
+ id: paymentMethod.id.toString(),
+ name: paymentMethod.name.toString(),
+ description: paymentMethod.description ?? null,
+ })
+ );
+ }
+
+ private async resolveTaxRegime(params: {
+ companyId: UniqueID;
+ proforma: Proforma;
+ transaction?: unknown;
+ }): Promise, Error>> {
+ if (params.proforma.taxRegimeCode.isNone()) {
+ return Result.ok(Maybe.none());
+ }
+
+ const taxRegimeCode = params.proforma.taxRegimeCode.unwrap();
+
+ const result = await this.deps.taxRegimeFinder.findByCodeInCompany({
+ companyId: params.companyId,
+ code: taxRegimeCode,
+ transaction: params.transaction,
+ });
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ if (result.data.isNone()) {
+ return Result.ok(
+ Maybe.some({
+ code: taxRegimeCode,
+ description: "",
+ })
+ );
+ }
+
+ const taxRegime = result.data.unwrap();
+
+ return Result.ok(
+ Maybe.some({
+ code: taxRegime.code,
+ description: taxRegime.description,
+ })
+ );
+ }
+}
diff --git a/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/build-proforma-tax-resolver.ts b/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/build-proforma-tax-resolver.ts
new file mode 100644
index 00000000..21c68bf7
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/build-proforma-tax-resolver.ts
@@ -0,0 +1,16 @@
+import type { ITaxDefinitionPublicFinder, ITaxRegimePublicFinder } from "@erp/catalogs/api";
+
+import { TaxDefinitionToTaxMapper } from "../../../common/mappers";
+
+import { ProformaTaxResolver } from "./proforma-tax-resolver";
+
+export function buildProformaTaxResolver(deps: {
+ taxDefinitionFinder: ITaxDefinitionPublicFinder;
+ taxRegimeFinder: ITaxRegimePublicFinder;
+}): ProformaTaxResolver {
+ return new ProformaTaxResolver({
+ taxDefinitionFinder: deps.taxDefinitionFinder,
+ taxRegimeFinder: deps.taxRegimeFinder,
+ taxMapper: new TaxDefinitionToTaxMapper(),
+ });
+}
diff --git a/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/index.ts b/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/index.ts
new file mode 100644
index 00000000..085f8aa1
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/index.ts
@@ -0,0 +1,3 @@
+export * from "./build-proforma-tax-resolver";
+export * from "./proforma-payment-resolver";
+export * from "./proforma-tax-resolver";
diff --git a/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/proforma-payment-resolver.ts b/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/proforma-payment-resolver.ts
new file mode 100644
index 00000000..53d8b32b
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/proforma-payment-resolver.ts
@@ -0,0 +1,62 @@
+import type { IPaymentMethodPublicFinder } from "@erp/catalogs/api";
+import type { UniqueID } from "@repo/rdx-ddd";
+import { type Maybe, Result } from "@repo/rdx-utils";
+
+export interface EnsureProformaPaymentMethodByIdParams {
+ companyId: UniqueID;
+ id: Maybe;
+}
+
+/**
+ * Resuelve formas de pago externas al agregado de proforma usando servicios públicos de `catalogs`.
+ *
+ * Este servicio pertenece a Application porque coordina dependencias entre módulos
+ * antes de crear o actualizar una proforma.
+ *
+ * No accede a repositorios de `catalogs`, no usa Sequelize y no debe utilizarse
+ * desde mappers de persistencia. Su responsabilidad es convertir contratos públicos
+ * de catálogos (`payment-methods`) en Value Objects propios del
+ * dominio de `customer-invoices`.
+ *
+ * Las facturas emitidas no deben depender de este resolver para reconstruirse:
+ * son documentos históricos y deben contener en persistencia los snapshots fiscales
+ * necesarios.
+ */
+export class ProformaPaymentResolver {
+ public constructor(
+ private readonly deps: {
+ paymentMethodFinder: IPaymentMethodPublicFinder;
+ }
+ ) {}
+
+ // Comprobar si existe un método de pago (boolean)
+
+ // Recuperar un método de pago si existe (Maybe)
+
+ // Solo asegurarnos que existe ese método de pago
+ public async ensurePaymentMethodById(params: {
+ companyId: UniqueID;
+ id: Maybe;
+ }): Promise> {
+ if (params.id.isNone()) {
+ return Result.ok(undefined);
+ }
+
+ const id = params.id.unwrap();
+
+ const result = await this.deps.paymentMethodFinder.findByIdInCompany({
+ companyId: params.companyId,
+ id,
+ });
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ if (result.data.isNone()) {
+ return Result.fail(new Error(`Payment method not found: ${id.toString()}`));
+ }
+
+ return Result.ok(undefined);
+ }
+}
diff --git a/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/proforma-tax-resolver.ts b/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/proforma-tax-resolver.ts
new file mode 100644
index 00000000..2a10b122
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/services/catalog-resolver/proforma-tax-resolver.ts
@@ -0,0 +1,149 @@
+import type {
+ ITaxDefinitionPublicFinder,
+ ITaxRegimePublicFinder,
+ TaxDefinitionPublicModel,
+} from "@erp/catalogs/api";
+import type { Tax } from "@erp/core/api";
+import type { UniqueID, UtcDate } from "@repo/rdx-ddd";
+import { Collection, Maybe, Result } from "@repo/rdx-utils";
+
+import { ProformaItemTaxes, type ProformaItemTaxesProps } from "../../../../domain";
+import type { TaxDefinitionToTaxMapper } from "../../../common/mappers";
+import type { ProformaItemTaxCodesInput } from "../../models";
+
+export interface EnsureProformaTaxRegimeByCodeParams {
+ companyId: UniqueID;
+ code: Maybe;
+}
+
+export interface EnsureProformaItemTaxesParams {
+ companyId: UniqueID;
+ atDate: UtcDate;
+ taxCodes: ProformaItemTaxCodesInput;
+}
+
+/**
+ * Resuelve datos fiscales externos al agregado de proforma usando servicios públicos de `catalogs`.
+ *
+ * Este servicio pertenece a Application porque coordina dependencias entre módulos
+ * antes de crear o actualizar una proforma.
+ *
+ * No accede a repositorios de `catalogs`, no usa Sequelize y no debe utilizarse
+ * desde mappers de persistencia. Su responsabilidad es convertir contratos públicos
+ * de catálogos (`tax-definitions` y `tax-regimes`) en Value Objects propios del
+ * dominio de `customer-invoices`.
+ *
+ * Las facturas emitidas no deben depender de este resolver para reconstruirse:
+ * son documentos históricos y deben contener en persistencia los snapshots fiscales
+ * necesarios.
+ */
+export class ProformaTaxResolver {
+ public constructor(
+ private readonly deps: {
+ taxDefinitionFinder: ITaxDefinitionPublicFinder;
+ taxRegimeFinder: ITaxRegimePublicFinder;
+ taxMapper: TaxDefinitionToTaxMapper;
+ }
+ ) {}
+
+ // Solo asegurarnos que existe un TaxRegime con ese "code"
+ public async ensureTaxRegimeByCode(params: {
+ companyId: UniqueID;
+ code: Maybe;
+ }): Promise> {
+ if (params.code.isNone()) {
+ return Result.ok(undefined);
+ }
+
+ const code = params.code.unwrap();
+
+ const result = await this.deps.taxRegimeFinder.findByCodeInCompany({
+ companyId: params.companyId,
+ code,
+ });
+
+ if (result.isFailure) {
+ return Result.fail(result.error);
+ }
+
+ if (result.data.isNone()) {
+ return Result.fail(new Error(`Tax regime not found: ${code}`));
+ }
+
+ return Result.ok(undefined);
+ }
+
+ public async ensureItemTaxes(
+ params: EnsureProformaItemTaxesParams
+ ): Promise> {
+ const codes = this.collectCodes(params.taxCodes);
+
+ if (codes.size() === 0) {
+ return Result.ok(ProformaItemTaxes.empty().getProps());
+ }
+
+ const definitions = await this.deps.taxDefinitionFinder.ensureActiveByCodes({
+ companyId: params.companyId,
+ codes,
+ atDate: params.atDate,
+ });
+
+ if (definitions.isFailure) {
+ return Result.fail(definitions.error);
+ }
+
+ const iva = this.toMaybeTax(definitions.data, params.taxCodes.ivaCode);
+ const rec = this.toMaybeTax(definitions.data, params.taxCodes.recCode);
+ const retention = this.toMaybeTax(definitions.data, params.taxCodes.retentionCode);
+
+ if (iva.isFailure) {
+ return Result.fail(iva.error);
+ }
+
+ if (rec.isFailure) {
+ return Result.fail(rec.error);
+ }
+
+ if (retention.isFailure) {
+ return Result.fail(retention.error);
+ }
+
+ return Result.ok({
+ iva: iva.data,
+ rec: rec.data,
+ retention: retention.data,
+ });
+ }
+
+ private collectCodes(taxCodes: ProformaItemTaxCodesInput): Collection {
+ const codes = [taxCodes.ivaCode, taxCodes.recCode, taxCodes.retentionCode]
+ .filter((code) => code.isSome())
+ .map((code) => code.unwrap());
+
+ return new Collection([...new Set(codes)]);
+ }
+
+ private toMaybeTax(
+ taxDefinitions: Collection,
+ code: Maybe
+ ): Result, Error> {
+ if (code.isNone()) {
+ return Result.ok(Maybe.none());
+ }
+
+ const rawCode = code.unwrap();
+ const taxDefinition = taxDefinitions.find((item) => item.code === rawCode);
+
+ if (!taxDefinition) {
+ return Result.fail(new Error(`Tax definition not found: ${rawCode}`));
+ }
+
+ const tax = this.deps.taxMapper.toTax(taxDefinition);
+
+ if (tax.isFailure) {
+ return Result.fail(tax.error);
+ }
+
+ return Result.ok(Maybe.some(tax.data));
+ }
+}
diff --git a/modules/customer-invoices/src/api/application/proformas/services/index.ts b/modules/customer-invoices/src/api/application/proformas/services/index.ts
index 74c53181..fd2211f1 100644
--- a/modules/customer-invoices/src/api/application/proformas/services/index.ts
+++ b/modules/customer-invoices/src/api/application/proformas/services/index.ts
@@ -1,3 +1,5 @@
+export * from "./assemblers";
+export * from "./catalog-resolver";
export * from "./proforma-creator";
export * from "./proforma-document-generator.interface";
export * from "./proforma-document-metadata-factory";
@@ -6,4 +8,5 @@ export * from "./proforma-finder";
export * from "./proforma-issuer";
export * from "./proforma-number-generator.interface";
export * from "./proforma-public-services.interface";
+export * from "./proforma-status-charger";
export * from "./proforma-updater";
diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts
index 702a216e..aa423742 100644
--- a/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts
+++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts
@@ -1,42 +1,55 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
-import { type IProformaCreateProps, Proforma } from "../../../domain";
+import {
+ type IProformaCreateProps,
+ type IProformaItemCreateProps,
+ Proforma,
+} from "../../../domain";
+import type { ProformaCreateInputProps } from "../models";
import type { IProformaRepository } from "../repositories";
+import type { ProformaPaymentResolver } from "./catalog-resolver/proforma-payment-resolver";
+import type { ProformaTaxResolver } from "./catalog-resolver/proforma-tax-resolver";
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
export interface IProformaCreatorParams {
companyId: UniqueID;
id: UniqueID;
- props: Omit;
- transaction: unknown;
+ props: ProformaCreateInputProps;
+ transaction?: unknown;
}
export interface IProformaCreator {
create(params: IProformaCreatorParams): Promise>;
}
-type ProformaCreatorDeps = {
- numberService: IProformaNumberGenerator;
- repository: IProformaRepository;
-};
-
export class ProformaCreator implements IProformaCreator {
- private readonly numberService: IProformaNumberGenerator;
- private readonly repository: IProformaRepository;
-
- constructor(deps: ProformaCreatorDeps) {
- this.numberService = deps.numberService;
- this.repository = deps.repository;
- }
+ public constructor(
+ private readonly deps: {
+ repository: IProformaRepository;
+ numberService: IProformaNumberGenerator;
+ taxResolver: ProformaTaxResolver;
+ paymentResolver: ProformaPaymentResolver;
+ }
+ ) {}
async create(params: IProformaCreatorParams): Promise> {
const { companyId, id, props, transaction } = params;
+ const resolvedProps = await this.resolveCreateProps(props);
+
+ if (resolvedProps.isFailure) {
+ return Result.fail(resolvedProps.error);
+ }
+
// 1. Obtener siguiente número
const { series } = props;
- const numberResult = await this.numberService.getNextForCompany(companyId, series, transaction);
+ const numberResult = await this.deps.numberService.getNextForCompany(
+ companyId,
+ series,
+ transaction
+ );
if (numberResult.isFailure) {
return Result.fail(numberResult.error);
@@ -45,7 +58,7 @@ export class ProformaCreator implements IProformaCreator {
const invoiceNumber = numberResult.data;
// 2. Crear agregado
- const proformaResult = Proforma.create({ ...props, invoiceNumber, companyId }, id);
+ const proformaResult = Proforma.create({ ...resolvedProps.data, invoiceNumber, companyId }, id);
if (proformaResult.isFailure) {
return Result.fail(proformaResult.error);
@@ -54,7 +67,7 @@ export class ProformaCreator implements IProformaCreator {
const proforma = proformaResult.data;
// 3. Persistir
- const saveResult = await this.repository.create(proforma, transaction);
+ const saveResult = await this.deps.repository.create(proforma, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
@@ -62,4 +75,53 @@ export class ProformaCreator implements IProformaCreator {
return Result.ok(proforma);
}
+
+ private async resolveCreateProps(
+ props: ProformaCreateInputProps
+ ): Promise> {
+ // Tax Regime => comprobar que existe
+ const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
+ companyId: props.companyId,
+ code: props.taxRegimeCode,
+ });
+
+ if (taxRegimeResult.isFailure) {
+ return Result.fail(taxRegimeResult.error);
+ }
+
+ // Payment method => comprobar que existe
+ const paymentMethodResult = await this.deps.paymentResolver.ensurePaymentMethodById({
+ companyId: props.companyId,
+ id: props.paymentMethodId,
+ });
+
+ if (paymentMethodResult.isFailure) {
+ return Result.fail(paymentMethodResult.error);
+ }
+
+ // Items
+ const resolvedItems: IProformaItemCreateProps[] = [];
+
+ for (const item of props.items) {
+ const taxes = await this.deps.taxResolver.ensureItemTaxes({
+ companyId: props.companyId,
+ atDate: props.invoiceDate,
+ taxCodes: item.taxes,
+ });
+
+ if (taxes.isFailure) {
+ return Result.fail(taxes.error);
+ }
+
+ resolvedItems.push({
+ ...item,
+ taxes: taxes.data,
+ });
+ }
+
+ return Result.ok({
+ ...props,
+ items: resolvedItems,
+ });
+ }
}
diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-public-services.interface.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-public-services.interface.ts
index b7911bff..8595720a 100644
--- a/modules/customer-invoices/src/api/application/proformas/services/proforma-public-services.interface.ts
+++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-public-services.interface.ts
@@ -27,11 +27,6 @@ export interface IProformaPublicServices {
context: IProformaServicesContext
) => Promise>;
- getProformaByFactuGESId: (
- factugesId: string,
- context: IProformaServicesContext
- ) => Promise>;
-
getProformaSnapshotById: (
id: UniqueID,
context: IProformaServicesContext
diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-status-charger.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-status-charger.ts
new file mode 100644
index 00000000..6eb5d565
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-status-charger.ts
@@ -0,0 +1,66 @@
+import type { UniqueID } from "@repo/rdx-ddd";
+import { Result } from "@repo/rdx-utils";
+
+import type { Proforma } from "../../../domain";
+import { InvoiceStatus } from "../../../domain";
+import type { IProformaRepository } from "../repositories";
+
+export interface IProformaStatusChanger {
+ changeStatus(params: {
+ companyId: UniqueID;
+ id: UniqueID;
+ newStatus: string;
+ transaction?: unknown;
+ }): Promise>;
+}
+
+export class ProformaStatusChanger implements IProformaStatusChanger {
+ public constructor(
+ private readonly deps: {
+ repository: IProformaRepository;
+ }
+ ) {}
+
+ public async changeStatus(params: {
+ companyId: UniqueID;
+ id: UniqueID;
+ newStatus: string;
+ transaction?: unknown;
+ }): Promise> {
+ const statusResult = InvoiceStatus.create(params.newStatus);
+
+ if (statusResult.isFailure) {
+ return Result.fail(statusResult.error);
+ }
+
+ const proformaResult = await this.deps.repository.getByIdInCompany(
+ params.companyId,
+ params.id,
+ params.transaction
+ );
+
+ if (proformaResult.isFailure) {
+ return Result.fail(proformaResult.error);
+ }
+
+ const proforma = proformaResult.data;
+
+ const changeResult = proforma.changeStatus(statusResult.data);
+
+ if (changeResult.isFailure) {
+ return Result.fail(changeResult.error);
+ }
+
+ if (!changeResult.data) {
+ return Result.ok(proforma);
+ }
+
+ const updateResult = await this.deps.repository.update(proforma, params.transaction);
+
+ if (updateResult.isFailure) {
+ return Result.fail(updateResult.error);
+ }
+
+ return Result.ok(proforma);
+ }
+}
diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts
index d4de5bb7..c933c06d 100644
--- a/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts
+++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts
@@ -1,39 +1,41 @@
-import type { UniqueID } from "@repo/rdx-ddd";
+import type { UniqueID, UtcDate } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
-import type { Proforma, ProformaPatchProps } from "../../../domain";
+import type { Proforma, ProformaItemPatchProps, ProformaPatchProps } from "../../../domain";
+import type { ProformaPatchInputProps } from "../models";
import type { IProformaRepository } from "../repositories";
+import type { ProformaPaymentResolver } from "./catalog-resolver/proforma-payment-resolver";
+import type { ProformaTaxResolver } from "./catalog-resolver/proforma-tax-resolver";
+
export interface IProformaUpdater {
update(params: {
companyId: UniqueID;
id: UniqueID;
- patchProps: ProformaPatchProps;
+ patchProps: ProformaPatchInputProps;
transaction: unknown;
}): Promise>;
}
-type ProformaUpdaterDeps = {
- repository: IProformaRepository;
-};
-
export class ProformaUpdater implements IProformaUpdater {
- private readonly repository: IProformaRepository;
-
- constructor(deps: ProformaUpdaterDeps) {
- this.repository = deps.repository;
- }
+ public constructor(
+ private readonly deps: {
+ repository: IProformaRepository;
+ taxResolver: ProformaTaxResolver;
+ paymentResolver: ProformaPaymentResolver;
+ }
+ ) {}
async update(params: {
companyId: UniqueID;
id: UniqueID;
- patchProps: ProformaPatchProps;
+ patchProps: ProformaPatchInputProps;
transaction: unknown;
}): Promise> {
const { companyId, id, patchProps, transaction } = params;
// Recuperar agregado existente
- const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
+ const existingResult = await this.deps.repository.getByIdInCompany(companyId, id, transaction);
if (existingResult.isFailure) {
return Result.fail(existingResult.error);
@@ -41,15 +43,25 @@ export class ProformaUpdater implements IProformaUpdater {
const proforma = existingResult.data;
+ const resolvedPatch = await this.resolvePatchProps({
+ companyId,
+ currentInvoiceDate: proforma.invoiceDate,
+ patch: patchProps,
+ });
+
+ if (resolvedPatch.isFailure) {
+ return Result.fail(resolvedPatch.error);
+ }
+
// Aplicar cambios en el agregado
- const updateResult = proforma.update(patchProps);
+ const updateResult = proforma.update(resolvedPatch.data);
if (updateResult.isFailure) {
return Result.fail(updateResult.error);
}
// Persistir cambios
- const saveResult = await this.repository.update(proforma, transaction);
+ const saveResult = await this.deps.repository.update(proforma, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
@@ -57,4 +69,63 @@ export class ProformaUpdater implements IProformaUpdater {
return Result.ok(proforma);
}
+
+ private async resolvePatchProps(params: {
+ companyId: UniqueID;
+ currentInvoiceDate: UtcDate;
+ patch: ProformaPatchInputProps;
+ }): Promise> {
+ const { patch, companyId, currentInvoiceDate } = params;
+
+ if (patch.taxRegimeCode !== undefined) {
+ const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
+ companyId,
+ code: patch.taxRegimeCode,
+ });
+
+ if (taxRegimeResult.isFailure) {
+ return Result.fail(taxRegimeResult.error);
+ }
+ }
+
+ if (patch.paymentMethodId !== undefined) {
+ const paymentMethodResult = await this.deps.paymentResolver.ensurePaymentMethodById({
+ companyId,
+ id: patch.paymentMethodId,
+ });
+
+ if (paymentMethodResult.isFailure) {
+ return Result.fail(paymentMethodResult.error);
+ }
+ }
+
+ if (patch.items === undefined) {
+ return Result.ok(patch as ProformaPatchProps);
+ }
+
+ const effectiveInvoiceDate = patch.invoiceDate ?? currentInvoiceDate;
+ const resolvedItems: ProformaItemPatchProps[] = [];
+
+ for (const item of patch.items) {
+ const taxes = await this.deps.taxResolver.ensureItemTaxes({
+ companyId,
+ atDate: effectiveInvoiceDate,
+ taxCodes: item.taxes,
+ });
+
+ if (taxes.isFailure) {
+ return Result.fail(taxes.error);
+ }
+
+ resolvedItems.push({
+ ...item,
+ taxes: taxes.data,
+ });
+ }
+
+ return Result.ok({
+ ...patch,
+ items: resolvedItems,
+ });
+ }
}
diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts
index 1c50e780..11442c33 100644
--- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts
+++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts
@@ -2,7 +2,7 @@ import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToNullable } from "@repo/rdx-ddd";
import type { GetProformaByIdResponseDTO } from "../../../../../common";
-import type { Proforma } from "../../../../domain";
+import type { ProformaFullReadModel } from "../../models";
import type { IProformaItemsFullSnapshotBuilder } from "./proforma-items-full-snapshot-builder";
import type { IProformaRecipientFullSnapshotBuilder } from "./proforma-recipient-full-snapshot-builder";
@@ -11,17 +11,18 @@ import type { IProformaTaxesFullSnapshotBuilder } from "./proforma-taxes-full-sn
export type ProformaFullSnapshot = GetProformaByIdResponseDTO;
export interface IProformaFullSnapshotBuilder
- extends ISnapshotBuilder {}
+ extends ISnapshotBuilder {}
export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder {
constructor(
private readonly itemsBuilder: IProformaItemsFullSnapshotBuilder,
private readonly recipientBuilder: IProformaRecipientFullSnapshotBuilder,
private readonly taxesBuilder: IProformaTaxesFullSnapshotBuilder
- //private readonly paymentMethodBuilder: IProformaPaymentMethodFullSnapshotBuilder
) {}
- toOutput(proforma: Proforma): ProformaFullSnapshot {
+ toOutput(source: ProformaFullReadModel): ProformaFullSnapshot {
+ const { proforma } = source;
+
const calculationContext = {
languageCode: proforma.languageCode,
currencyCode: proforma.currencyCode,
@@ -31,12 +32,6 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
const items = this.itemsBuilder.toOutput(proforma.items, calculationContext);
const recipient = this.recipientBuilder.toOutput(proforma);
const taxes = this.taxesBuilder.toOutput(proforma.taxes());
- //const paymentMethod = this.paymentMethodBuilder.toOutput(proforma.paymentMethod);
-
- const paymentMethod = maybeToNullable(proforma.paymentMethodId, (value) => ({
- id: value.toString(),
- description: "",
- }));
const allTotals = proforma.totals();
@@ -63,7 +58,18 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
linked_invoice_id: maybeToNullable(proforma.linkedInvoiceId, (value) => value.toString()),
- payment_method: paymentMethod,
+ /*payment_method: {
+ id: source.paymentMethod.,
+ name: value.name,
+ description: maybeToNullable(value.description, (description) => description),
+ },
+
+ payment_term: null,
+
+ tax_regime: {
+ code: value.code,
+ description: maybeToNullable(value.description, (description) => description),
+ },*/
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
items_discount_amount: allTotals.itemsDiscountAmount.toObjectString(),
diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts
index 9a66c5ce..ce3ac72d 100644
--- a/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts
+++ b/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts
@@ -3,7 +3,7 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ChangeStatusProformaByIdRequestDTO } from "../../../../common";
-import type { IProformaFinder, IProformaUpdater } from "../services";
+import type { IProformaFullReadModelAssembler, IProformaStatusChanger } from "../services";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
type ChangeStatusProformaUseCaseInput = {
@@ -12,25 +12,15 @@ type ChangeStatusProformaUseCaseInput = {
dto: ChangeStatusProformaByIdRequestDTO;
};
-type ChangeStatusProformaUseCaseDeps = {
- finder: IProformaFinder;
- updater: IProformaUpdater;
- fullSnapshotBuilder: IProformaFullSnapshotBuilder;
- transactionManager: ITransactionManager;
-};
-
export class ChangeStatusProformaUseCase {
- private readonly finder: IProformaFinder;
- private readonly updater: IProformaUpdater;
- private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
- private readonly transactionManager: ITransactionManager;
-
- constructor(deps: ChangeStatusProformaUseCaseDeps) {
- this.finder = deps.finder;
- this.updater = deps.updater;
- this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
- this.transactionManager = deps.transactionManager;
- }
+ public constructor(
+ private readonly deps: {
+ statusChanger: IProformaStatusChanger;
+ fullReadModelAssembler: IProformaFullReadModelAssembler;
+ fullSnapshotBuilder: IProformaFullSnapshotBuilder;
+ transactionManager: ITransactionManager;
+ }
+ ) {}
public execute(params: ChangeStatusProformaUseCaseInput) {
const {
@@ -39,40 +29,40 @@ export class ChangeStatusProformaUseCase {
dto: { new_status },
} = params;
- const proformaIdOrError = UniqueID.create(proforma_id);
- if (proformaIdOrError.isFailure) {
- return Result.fail(proformaIdOrError.error);
+ const proformaIdResult = UniqueID.create(proforma_id);
+
+ if (proformaIdResult.isFailure) {
+ return Result.fail(proformaIdResult.error);
}
- const proformaId = proformaIdOrError.data;
+ const proformaId = proformaIdResult.data;
- return this.transactionManager.complete(async (transaction) => {
+ return this.deps.transactionManager.complete(async (transaction) => {
try {
- /** 1. Recuperamos la proforma */
- const proformaResult = await this.finder.findProformaById(
+ const changeResult = await this.deps.statusChanger.changeStatus({
companyId,
- proformaId,
- transaction
- );
- if (proformaResult.isFailure) {
- return Result.fail(proformaResult.error);
+ id: proformaId,
+ newStatus: new_status!,
+ transaction,
+ });
+
+ if (changeResult.isFailure) {
+ return Result.fail(changeResult.error);
}
- const proforma = proformaResult.data;
-
- /** 2. Hacer el cambio de estado */
- const transitionResult = await this.proformaDomainService.transition(proforma, new_status!);
- if (transitionResult.isFailure) return Result.fail(transitionResult.error);
-
- const updateResult = await this.updater.updateProformaStatusByIdInCompany(
+ const readModelResult = await this.deps.fullReadModelAssembler.assemble({
companyId,
- proformaId,
- transitionResult.data.status,
- transaction
- );
- if (updateResult.isFailure) return Result.fail(updateResult.error);
+ proforma: changeResult.data,
+ transaction,
+ });
- return Result.ok();
+ if (readModelResult.isFailure) {
+ return Result.fail(readModelResult.error);
+ }
+
+ const fullSnapshot = this.deps.fullSnapshotBuilder.toOutput(readModelResult.data);
+
+ return Result.ok(fullSnapshot);
} catch (error: unknown) {
return Result.fail(error as Error);
}
diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma.use-case.ts
new file mode 100644
index 00000000..8a4108e1
--- /dev/null
+++ b/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma.use-case.ts
@@ -0,0 +1,63 @@
+import type { ITransactionManager } from "@erp/core/api";
+import type { UniqueID } from "@repo/rdx-ddd";
+import { Result } from "@repo/rdx-utils";
+
+import type { CreateProformaRequestDTO } from "../../../../common";
+import type { ICreateProformaInputMapper } from "../mappers";
+import type { IProformaCreator, IProformaFullReadModelAssembler } from "../services";
+import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
+
+type CreateProformaUseCaseInput = {
+ companyId: UniqueID;
+ dto: CreateProformaRequestDTO;
+};
+
+export class CreateProformaUseCase {
+ public constructor(
+ private readonly deps: {
+ dtoMapper: ICreateProformaInputMapper;
+ creator: IProformaCreator;
+ fullReadModelAssembler: IProformaFullReadModelAssembler;
+ fullSnapshotBuilder: IProformaFullSnapshotBuilder;
+ transactionManager: ITransactionManager;
+ }
+ ) {}
+
+ public async execute(params: CreateProformaUseCaseInput) {
+ const { dto, companyId } = params;
+
+ // 1) Mapear DTO → props de dominio
+ const mappedPropsResult = this.deps.dtoMapper.map(dto, { companyId });
+ if (mappedPropsResult.isFailure) {
+ return Result.fail(mappedPropsResult.error);
+ }
+
+ const { props, id } = mappedPropsResult.data;
+
+ return this.deps.transactionManager.complete(async (transaction: unknown) => {
+ try {
+ const createResult = await this.deps.creator.create({ companyId, id, props, transaction });
+
+ if (createResult.isFailure) {
+ return Result.fail(createResult.error);
+ }
+
+ const readModelResult = await this.deps.fullReadModelAssembler.assemble({
+ companyId,
+ proforma: createResult.data,
+ transaction,
+ });
+
+ if (readModelResult.isFailure) {
+ return Result.fail(readModelResult.error);
+ }
+
+ const snapshot = this.deps.fullSnapshotBuilder.toOutput(readModelResult.data);
+
+ return Result.ok(snapshot);
+ } catch (error: unknown) {
+ return Result.fail(error as Error);
+ }
+ });
+ }
+}
diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts
deleted file mode 100644
index f24478f8..00000000
--- a/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import type { ITransactionManager } from "@erp/core/api";
-import type { UniqueID } from "@repo/rdx-ddd";
-import { Result } from "@repo/rdx-utils";
-
-import type { CreateProformaRequestDTO } from "../../../../../common";
-import type { ICreateProformaInputMapper } from "../../mappers";
-import type { IProformaCreator } from "../../services";
-import type { IProformaFullSnapshotBuilder } from "../../snapshot-builders";
-
-type CreateProformaUseCaseInput = {
- companyId: UniqueID;
- dto: CreateProformaRequestDTO;
-};
-
-type CreateProformaUseCaseDeps = {
- dtoMapper: ICreateProformaInputMapper;
- creator: IProformaCreator;
- fullSnapshotBuilder: IProformaFullSnapshotBuilder;
- transactionManager: ITransactionManager;
-};
-
-export class CreateProformaUseCase {
- private readonly dtoMapper: ICreateProformaInputMapper;
- private readonly creator: IProformaCreator;
- private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
- private readonly transactionManager: ITransactionManager;
-
- constructor(deps: CreateProformaUseCaseDeps) {
- this.dtoMapper = deps.dtoMapper;
- this.creator = deps.creator;
- this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
- this.transactionManager = deps.transactionManager;
- }
-
- public async execute(params: CreateProformaUseCaseInput) {
- const { dto, companyId } = params;
-
- // 1) Mapear DTO → props de dominio
- const mappedPropsResult = this.dtoMapper.map(dto, { companyId });
- if (mappedPropsResult.isFailure) {
- return Result.fail(mappedPropsResult.error);
- }
-
- const { props, id } = mappedPropsResult.data;
-
- return this.transactionManager.complete(async (transaction: unknown) => {
- try {
- const createResult = await this.creator.create({ companyId, id, props, transaction });
-
- if (createResult.isFailure) {
- return Result.fail(createResult.error);
- }
-
- const snapshot = this.fullSnapshotBuilder.toOutput(createResult.data);
-
- return Result.ok(snapshot);
- } catch (error: unknown) {
- return Result.fail(error as Error);
- }
- });
- }
-}
diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/index.ts
deleted file mode 100644
index d1d4cd6d..00000000
--- a/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./create-proforma.use-case";
diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/get-proforma-by-id.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/get-proforma-by-id.use-case.ts
index babd3659..f56f5a94 100644
--- a/modules/customer-invoices/src/api/application/proformas/use-cases/get-proforma-by-id.use-case.ts
+++ b/modules/customer-invoices/src/api/application/proformas/use-cases/get-proforma-by-id.use-case.ts
@@ -2,7 +2,7 @@ import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
-import type { IProformaFinder } from "../services";
+import type { IProformaFinder, IProformaFullReadModelAssembler } from "../services";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
type GetProformaUseCaseInput = {
@@ -11,34 +11,49 @@ type GetProformaUseCaseInput = {
};
export class GetProformaByIdUseCase {
- constructor(
- private readonly finder: IProformaFinder,
- private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder,
- private readonly transactionManager: ITransactionManager
+ public constructor(
+ private readonly deps: {
+ finder: IProformaFinder;
+ fullReadModelAssembler: IProformaFullReadModelAssembler;
+ fullSnapshotBuilder: IProformaFullSnapshotBuilder;
+ transactionManager: ITransactionManager;
+ }
) {}
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;
- return this.transactionManager.complete(async (transaction) => {
+ return this.deps.transactionManager.complete(async (transaction) => {
try {
- const proformaResult = await this.finder.findProformaById(
+ const proformaResult = await this.deps.finder.findProformaById(
companyId,
proformaId,
transaction
);
+
if (proformaResult.isFailure) {
return Result.fail(proformaResult.error);
}
- const fullSnapshot = this.fullSnapshotBuilder.toOutput(proformaResult.data);
+ const readModelResult = await this.deps.fullReadModelAssembler.assemble({
+ companyId,
+ proforma: proformaResult.data,
+ transaction,
+ });
+
+ if (readModelResult.isFailure) {
+ return Result.fail(readModelResult.error);
+ }
+
+ const fullSnapshot = this.deps.fullSnapshotBuilder.toOutput(readModelResult.data);
return Result.ok(fullSnapshot);
} catch (error: unknown) {
diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts
index 3f04906f..e23936a7 100644
--- a/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts
+++ b/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts
@@ -1,5 +1,5 @@
export * from "./change-status-proforma.use-case";
-export * from "./create-proforma";
+export * from "./create-proforma.use-case";
//export * from "./delete-proforma.use-case";
export * from "./get-proforma-by-id.use-case";
export * from "./issue-proforma.use-case";
diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts
index b2997aa3..10d24f0e 100644
--- a/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts
+++ b/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts
@@ -10,12 +10,6 @@ type IssueProformaUseCaseInput = {
proforma_id: string;
};
-type IssueProformaUseCaseDeps = {
- issuedInvoiceServices: IIssuedInvoicePublicServices;
- finder: IProformaFinder;
- issuer: IProformaIssuer;
- transactionManager: ITransactionManager;
-};
/**
* Caso de uso: Conversión de una issuedinvoice a factura definitiva.
*
@@ -26,17 +20,14 @@ type IssueProformaUseCaseDeps = {
* - Persiste ambas dentro de la misma transacción
*/
export class IssueProformaUseCase {
- private readonly issuedInvoiceServices: IIssuedInvoicePublicServices;
- private readonly finder: IProformaFinder;
- private readonly issuer: IProformaIssuer;
- private readonly transactionManager: ITransactionManager;
-
- constructor(deps: IssueProformaUseCaseDeps) {
- this.issuedInvoiceServices = deps.issuedInvoiceServices;
- this.finder = deps.finder;
- this.issuer = deps.issuer;
- this.transactionManager = deps.transactionManager;
- }
+ public constructor(
+ private readonly deps: {
+ issuedInvoiceServices: IIssuedInvoicePublicServices;
+ finder: IProformaFinder;
+ issuer: IProformaIssuer;
+ transactionManager: ITransactionManager;
+ }
+ ) {}
public execute(params: IssueProformaUseCaseInput) {
const { proforma_id, companyId } = params;
@@ -46,10 +37,10 @@ export class IssueProformaUseCase {
const proformaId = proformaIdOrError.data;
- return this.transactionManager.complete(async (transaction) => {
+ return this.deps.transactionManager.complete(async (transaction) => {
try {
// 1. Recuperamos la proforma
- const proformaResult = await this.finder.findProformaById(
+ const proformaResult = await this.deps.finder.findProformaById(
companyId,
proformaId,
transaction
@@ -60,7 +51,7 @@ export class IssueProformaUseCase {
// 2. Generamos la factura definitiva y la guardamos
const issuedInvoiceId = UniqueID.generateNewID();
- const createPropsOrError = await this.issuer.issueProforma({
+ const createPropsOrError = await this.deps.issuer.issueProforma({
companyId,
issuedInvoiceId,
proforma,
@@ -74,7 +65,7 @@ export class IssueProformaUseCase {
const createProps = createPropsOrError.data;
// Creamos y guardamos en persistencia la factura definitiva
- const invoiceResult = await this.issuedInvoiceServices.createIssuedInvoice(
+ const invoiceResult = await this.deps.issuedInvoiceServices.createIssuedInvoice(
issuedInvoiceId,
createProps,
{
diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma-by-id.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma-by-id.use-case.ts
index c11eec39..22bd537a 100644
--- a/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma-by-id.use-case.ts
+++ b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma-by-id.use-case.ts
@@ -3,9 +3,8 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { UpdateProformaByIdRequestDTO } from "../../../../common";
-import type { ProformaPatchProps } from "../../../domain";
import type { IUpdateProformaInputMapper } from "../mappers";
-import type { IProformaUpdater } from "../services";
+import type { IProformaFullReadModelAssembler, IProformaUpdater } from "../services";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
type UpdateProformaUseCaseInput = {
@@ -14,25 +13,16 @@ type UpdateProformaUseCaseInput = {
dto: UpdateProformaByIdRequestDTO;
};
-type UpdateProformaUseCaseDeps = {
- dtoMapper: IUpdateProformaInputMapper;
- updater: IProformaUpdater;
- fullSnapshotBuilder: IProformaFullSnapshotBuilder;
- transactionManager: ITransactionManager;
-};
-
export class UpdateProformaByIdUseCase {
- private readonly dtoMapper: IUpdateProformaInputMapper;
- private readonly updater: IProformaUpdater;
- private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
- private readonly transactionManager: ITransactionManager;
-
- constructor(deps: UpdateProformaUseCaseDeps) {
- this.dtoMapper = deps.dtoMapper;
- this.updater = deps.updater;
- this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
- this.transactionManager = deps.transactionManager;
- }
+ public constructor(
+ private readonly deps: {
+ dtoMapper: IUpdateProformaInputMapper;
+ updater: IProformaUpdater;
+ fullReadModelAssembler: IProformaFullReadModelAssembler;
+ fullSnapshotBuilder: IProformaFullSnapshotBuilder;
+ transactionManager: ITransactionManager;
+ }
+ ) {}
public execute(params: UpdateProformaUseCaseInput) {
const { companyId, proforma_id, dto } = params;
@@ -45,16 +35,16 @@ export class UpdateProformaByIdUseCase {
const proformaId = proformaIdOrError.data;
// Mapear DTO → props de dominio
- const patchPropsResult = this.dtoMapper.map(dto, { companyId });
+ const patchPropsResult = this.deps.dtoMapper.map(dto, { companyId });
if (patchPropsResult.isFailure) {
return patchPropsResult;
}
- const patchProps: ProformaPatchProps = patchPropsResult.data;
+ const patchProps = patchPropsResult.data;
- return this.transactionManager.complete(async (transaction) => {
+ return this.deps.transactionManager.complete(async (transaction) => {
try {
- const updateResult = await this.updater.update({
+ const updateResult = await this.deps.updater.update({
companyId,
id: proformaId,
patchProps,
@@ -65,57 +55,20 @@ export class UpdateProformaByIdUseCase {
return Result.fail(updateResult.error);
}
- return Result.ok(this.fullSnapshotBuilder.toOutput(updateResult.data));
- } catch (error: unknown) {
- return Result.fail(error as Error);
- }
- });
- }
-}
-
-/*
-
- const presenter = this.presenterRegistry.getPresenter({
- resource: "proforma",
- projection: "FULL",
- }) as ProformaFullPresenter;
-
- // Mapear DTO → props de dominio
- const patchPropsResult = mapDTOToUpdateProformaInvoicePatchProps(dto);
- if (patchPropsResult.isFailure) {
- return Result.fail(patchPropsResult.error);
- }
-
- const patchProps: ProformaPatchProps = patchPropsResult.data;
-
- return this.transactionManager.complete(async (transaction: unknown) => {
- try {
- const updatedInvoice = await this.service.patchProformaByIdInCompany(
+ const readModelResult = await this.deps.fullReadModelAssembler.assemble({
companyId,
- invoiceId,
- patchProps,
- transaction
- );
+ proforma: updateResult.data,
+ transaction,
+ });
- if (updatedInvoice.isFailure) {
- return Result.fail(updatedInvoice.error);
+ if (readModelResult.isFailure) {
+ return Result.fail(readModelResult.error);
}
- const invoiceOrError = await this.service.updateProformaInCompany(
- companyId,
- updatedInvoice.data,
- transaction
- );
- if (invoiceOrError.isFailure) return Result.fail(invoiceOrError.error);
-
- const invoice = invoiceOrError.data;
- const dto = presenter.toOutput(invoice);
- return Result.ok(dto);
+ return Result.ok(this.deps.fullSnapshotBuilder.toOutput(readModelResult.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}
-
-*/
diff --git a/modules/customer-invoices/src/api/domain/common/entities/index.ts b/modules/customer-invoices/src/api/domain/common/entities/index.ts
index 7556a841..66fa2ce8 100644
--- a/modules/customer-invoices/src/api/domain/common/entities/index.ts
+++ b/modules/customer-invoices/src/api/domain/common/entities/index.ts
@@ -1 +1,2 @@
-export * from "./invoice-payment-method";
+export * from "./invoice-payment-method.entity";
+export * from "./invoice-tax-regime.entity";
diff --git a/modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method/invoice-payment-method.ts b/modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method.entity.ts
similarity index 77%
rename from modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method/invoice-payment-method.ts
rename to modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method.entity.ts
index 5c6507ca..9f24f2b5 100644
--- a/modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method/invoice-payment-method.ts
+++ b/modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method.entity.ts
@@ -2,7 +2,7 @@ import { DomainEntity, type UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
export interface InvoicePaymentMethodProps {
- paymentDescription: string;
+ name: string;
}
export class InvoicePaymentMethod extends DomainEntity {
@@ -15,8 +15,8 @@ export class InvoicePaymentMethod extends DomainEntity {
+ private static readonly ERROR_CODE = "INVALID_INVOICE_TAX_REGIME";
+
+ private static readonly MIN_CODE = 1;
+ private static readonly MAX_CODE = 99;
+
+ protected static validate(props: InvoiceTaxRegimeProps) {
+ const schema = z.object({
+ code: z
+ .string()
+ .trim()
+ .regex(/^\d{2}$/, {
+ message: "Code must be a two-digit numeric string",
+ })
+ .refine(
+ (value) => {
+ const numericValue = Number(value);
+
+ return (
+ numericValue >= InvoiceTaxRegime.MIN_CODE && numericValue <= InvoiceTaxRegime.MAX_CODE
+ );
+ },
+ {
+ message: "Code must be between 01 and 99",
+ }
+ ),
+
+ description: z.string().trim().min(1, {
+ message: "Description is required",
+ }),
+ });
+
+ return schema.safeParse(props);
+ }
+
+ static create(props: InvoiceTaxRegimeProps, id?: UniqueID): Result {
+ const propsAreValid = InvoiceTaxRegime.validate(props);
+
+ if (!propsAreValid.success) {
+ const detail = propsAreValid.error.message;
+
+ return Result.fail(
+ new DomainValidationError(InvoiceTaxRegime.ERROR_CODE, "tax-regime", detail)
+ );
+ }
+
+ return Result.ok(
+ new InvoiceTaxRegime(
+ {
+ code: props.code.trim(),
+ description: props.description.trim(),
+ },
+ id
+ )
+ );
+ }
+
+ getProps(): InvoiceTaxRegimeProps {
+ return {
+ code: this.props.code,
+ description: this.props.description,
+ };
+ }
+
+ get code(): string {
+ return this.props.code;
+ }
+
+ get description(): string {
+ return this.props.description;
+ }
+
+ toObjectString() {
+ return {
+ id: String(this.id),
+ code: String(this.code),
+ description: String(this.description),
+ };
+ }
+}
diff --git a/modules/customer-invoices/src/api/domain/common/value-objects/index.ts b/modules/customer-invoices/src/api/domain/common/value-objects/index.ts
index 0a6646c4..d69995a8 100644
--- a/modules/customer-invoices/src/api/domain/common/value-objects/index.ts
+++ b/modules/customer-invoices/src/api/domain/common/value-objects/index.ts
@@ -1,7 +1,9 @@
+export * from "../entities/invoice-tax-regime.entity";
+
export * from "./invoice-address-type.vo";
export * from "./invoice-amount.vo";
export * from "./invoice-number.vo";
-export * from "./invoice-recipient";
+export * from "./invoice-recipient/invoice-recipient.vo";
export * from "./invoice-serie.vo";
export * from "./invoice-status.vo";
export * from "./item-amount.vo";
diff --git a/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts
index 40da6fba..2285db8c 100644
--- a/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts
+++ b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts
@@ -86,10 +86,18 @@ export class InvoiceStatus extends ValueObject {
return this.getProps();
}
- canTransitionTo(nextStatus: string): boolean {
+ public canTransitionTo(nextStatus: InvoiceStatus): boolean {
+ return INVOICE_TRANSITIONS[this.props.value].includes(nextStatus.toPrimitive());
+ }
+
+ public canTransitionToValue(nextStatus: string): boolean {
return INVOICE_TRANSITIONS[this.props.value].includes(nextStatus);
}
+ public isIssued(): boolean {
+ return this.props.value === INVOICE_STATUS.ISSUED;
+ }
+
toString() {
return String(this.props.value);
}
diff --git a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts
index 481f9110..34624e0d 100644
--- a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts
+++ b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts
@@ -12,6 +12,7 @@ import {
import { type Collection, type Maybe, Result } from "@repo/rdx-utils";
import {
+ INVOICE_STATUS,
InvoiceAmount,
type InvoiceNumber,
type InvoiceRecipient,
@@ -26,7 +27,7 @@ import {
type ProformaItemPatchProps,
ProformaItems,
} from "../entities";
-import { ProformaItemMismatch } from "../errors";
+import { InvalidProformaTransitionError, ProformaItemMismatch } from "../errors";
import type { IProformaTaxTotals, ProformaCalculationContext } from "../services";
import { ProformaItemTaxes } from "../value-objects";
@@ -300,6 +301,38 @@ export class Proforma extends AggregateRoot implements IP
return this.taxRegimeCode.isSome();
}
+ public changeStatus(nextStatus: InvoiceStatus): Result {
+ const currentStatus = this.status;
+
+ if (currentStatus.toPrimitive() === nextStatus.toPrimitive()) {
+ return Result.ok(false);
+ }
+
+ if (nextStatus.toPrimitive() === INVOICE_STATUS.ISSUED) {
+ return Result.fail(
+ new InvalidProformaTransitionError(
+ currentStatus.toPrimitive(),
+ nextStatus.toPrimitive(),
+ this.id.toString()
+ )
+ );
+ }
+
+ if (!currentStatus.canTransitionTo(nextStatus)) {
+ return Result.fail(
+ new InvalidProformaTransitionError(
+ currentStatus.toPrimitive(),
+ nextStatus.toPrimitive(),
+ this.id.toString()
+ )
+ );
+ }
+
+ this.props.status = nextStatus;
+
+ return Result.ok(true);
+ }
+
public issue(): Result {
// Antes de cambiar el estado de la proforma,
// comprobamos que se cumplen las condiciones
@@ -376,6 +409,16 @@ export class Proforma extends AggregateRoot implements IP
);
}
+ if (this.taxRegimeCode.isNone()) {
+ return Result.fail(
+ new DomainValidationError(
+ "MISSING_TAX_REGIME",
+ "taxRegime",
+ "Tax regime is required to issue the proforma"
+ )
+ );
+ }
+
/*if (this.operationDate.isSome() && this.operationDate.unwrap() > new Date()) {
return Result.fail(
new DomainValidationError(
diff --git a/modules/customer-invoices/src/api/index.ts b/modules/customer-invoices/src/api/index.ts
index ffeb92b1..ea98e5d1 100644
--- a/modules/customer-invoices/src/api/index.ts
+++ b/modules/customer-invoices/src/api/index.ts
@@ -15,7 +15,7 @@ export type { IProformaPublicServices } from "./application";
export const customerInvoicesAPIModule: IModuleServer = {
name: "customer-invoices",
version: "1.0.0",
- dependencies: ["customers"],
+ dependencies: ["catalogs", "customers"],
/**
* Fase de SETUP
diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts
index 0fad2aae..5c930240 100644
--- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts
+++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts
@@ -142,7 +142,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
const paymentVO = extractOrPushError(
InvoicePaymentMethod.create(
- { paymentDescription: String(raw.payment_method_description ?? "") },
+ { name: String(raw.payment_method_description ?? "") },
paymentId ?? undefined
),
"payment_method_description",
@@ -482,7 +482,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
payment_method_id: source.paymentMethod.toObjectString().id,
- payment_method_description: source.paymentMethod.toObjectString().payment_description,
+ payment_method_description: source.paymentMethod.toObjectString().name,
subtotal_amount_value: source.subtotalAmount.value,
subtotal_amount_scale: source.subtotalAmount.scale,
diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence-mappers.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence-mappers.di.ts
index befe225f..52f37f62 100644
--- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence-mappers.di.ts
+++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence-mappers.di.ts
@@ -1,34 +1,17 @@
-import type { ICatalogs } from "@erp/core/api";
-
-import { CreateProformaInputMapper } from "../../../application";
import { SequelizeProformaDomainMapper, SequelizeProformaSummaryMapper } from "../persistence";
export interface IProformaPersistenceMappers {
domainMapper: SequelizeProformaDomainMapper;
listMapper: SequelizeProformaSummaryMapper;
-
- createMapper: CreateProformaInputMapper;
}
-export const buildProformaPersistenceMappers = (
- catalogs: ICatalogs
-): IProformaPersistenceMappers => {
- const { taxCatalog, paymentCatalog } = catalogs;
-
+export const buildProformaPersistenceMappers = (): IProformaPersistenceMappers => {
// Mappers para el repositorio
- const domainMapper = new SequelizeProformaDomainMapper({
- taxCatalog,
- paymentCatalog,
- });
+ const domainMapper = new SequelizeProformaDomainMapper();
const listMapper = new SequelizeProformaSummaryMapper();
- // Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
- const createMapper = new CreateProformaInputMapper(catalogs);
-
return {
domainMapper,
listMapper,
-
- createMapper,
};
};
diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts
index 1cc67a37..e29953bb 100644
--- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts
+++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts
@@ -1,4 +1,4 @@
-import { type SetupParams, buildCatalogs } from "@erp/core/api";
+import type { SetupParams } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
@@ -6,12 +6,15 @@ import type { Transaction } from "sequelize";
import {
type IProformaCreatorParams,
type ProformaFullSnapshot,
+ buildProformaCatalogResolvers,
buildProformaCreator,
buildProformaFinder,
+ buildProformaReadModelAssemblers,
buildProformaSnapshotBuilders,
} from "../../../application";
import type { Proforma } from "../../../domain";
+import { resolveProformaCatalogsDeps } from "./proforma-catalog-deps.di";
import { buildProformaNumberGenerator } from "./proforma-number-generator.di";
import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
import { buildProformaRepository } from "./proforma-repositories.di";
@@ -28,11 +31,6 @@ export type ProformaPublicServices = {
context: ProformaServicesContext
) => Promise>;
- getProformaByFactuGESId: (
- factugesId: string,
- context: ProformaServicesContext
- ) => Promise>;
-
getProformaSnapshotById: (
id: UniqueID,
context: ProformaServicesContext
@@ -47,31 +45,56 @@ export type ProformaPublicServices = {
export function buildProformaPublicServices(
params: SetupParams,
- deps: ProformasInternalDeps
+ _deps: ProformasInternalDeps
): ProformaPublicServices {
const { database } = params;
- // Infrastructure
- const catalogs = buildCatalogs();
- const persistenceMappers = buildProformaPersistenceMappers(catalogs);
+ const catalogs = resolveProformaCatalogsDeps(params);
+
+ const persistenceMappers = buildProformaPersistenceMappers();
+
+ const repository = buildProformaRepository({
+ database,
+ mappers: persistenceMappers,
+ });
- const repository = buildProformaRepository({ database, mappers: persistenceMappers });
const numberService = buildProformaNumberGenerator();
- const snapshotsBuilder = buildProformaSnapshotBuilders();
-
- // Application helpers
- const creator = buildProformaCreator({ numberService, repository });
const finder = buildProformaFinder(repository);
+ const catalogResolvers = buildProformaCatalogResolvers({
+ taxDefinitionFinder: catalogs.taxDefinition.finder,
+ taxRegimeFinder: catalogs.taxRegime.finder,
+ paymentMethodFinder: catalogs.paymentMethod.finder,
+ });
+
+ const readModelAssemblers = buildProformaReadModelAssemblers({
+ paymentMethodFinder: catalogs.paymentMethod.finder,
+ taxRegimeFinder: catalogs.taxRegime.finder,
+ });
+
+ const snapshotBuilders = buildProformaSnapshotBuilders();
+
+ const creator = buildProformaCreator({
+ repository,
+ numberService,
+ taxResolver: catalogResolvers.taxResolver,
+ paymentResolver: catalogResolvers.paymentResolver,
+ });
+
return {
- createProforma: async (
+ async createProforma(
id: UniqueID,
props: IProformaCreatorParams["props"],
context: ProformaServicesContext
- ) => {
+ ): Promise> {
const { transaction, companyId } = context;
- const createResult = await creator.create({ companyId, id, props, transaction });
+ const createResult = await creator.create({
+ companyId,
+ id,
+ props,
+ transaction,
+ });
if (createResult.isFailure) {
return Result.fail(createResult.error);
@@ -80,10 +103,12 @@ export function buildProformaPublicServices(
return Result.ok(createResult.data);
},
- //internal.useCases.listProformas().execute(filters, context),
-
- getProformaById: async (id: UniqueID, context: ProformaServicesContext) => {
+ async getProformaById(
+ id: UniqueID,
+ context: ProformaServicesContext
+ ): Promise> {
const { transaction, companyId } = context;
+
const proformaResult = await finder.findProformaById(companyId, id, transaction);
if (proformaResult.isFailure) {
@@ -93,36 +118,31 @@ export function buildProformaPublicServices(
return Result.ok(proformaResult.data);
},
- getProformaByFactuGESId: async (factugesId: string, context: ProformaServicesContext) => {
+ async getProformaSnapshotById(
+ id: UniqueID,
+ context: ProformaServicesContext
+ ): Promise> {
const { transaction, companyId } = context;
- const proformaResult = await finder.findProformaByFactuGESId(
+
+ const proformaResult = await finder.findProformaById(companyId, id, transaction);
+
+ if (proformaResult.isFailure) {
+ return Result.fail(proformaResult.error);
+ }
+
+ const readModelResult = await readModelAssemblers.full.assemble({
companyId,
- factugesId,
- transaction
- );
+ proforma: proformaResult.data,
+ transaction,
+ });
- if (proformaResult.isFailure) {
- console.error("Error fetching proforma by FactuGES ID:", proformaResult.error);
- return Result.fail(proformaResult.error);
+ if (readModelResult.isFailure) {
+ return Result.fail(readModelResult.error);
}
- return Result.ok(proformaResult.data);
- },
-
- getProformaSnapshotById: async (id: UniqueID, context: ProformaServicesContext) => {
- const { transaction, companyId } = context;
- const proformaResult = await finder.findProformaById(companyId, id, transaction);
-
- if (proformaResult.isFailure) {
- return Result.fail(proformaResult.error);
- }
-
- const fullSnapshot = snapshotsBuilder.full.toOutput(proformaResult.data);
+ const fullSnapshot = snapshotBuilders.full.toOutput(readModelResult.data);
return Result.ok(fullSnapshot);
},
-
- //generateProformaReport: (id, options, context) => null,
- //internal.useCases.reportProforma().execute(id, options, context),
};
}
diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts
index b93aefe1..059f993f 100644
--- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts
+++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts
@@ -1,4 +1,3 @@
-import type { ICatalogPublicServices } from "@erp/catalogs/api";
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
import {
@@ -15,11 +14,14 @@ import {
buildGetProformaByIdUseCase,
buildIssueProformaUseCase,
buildListProformasUseCase,
+ buildProformaCatalogResolvers,
buildProformaCreator,
buildProformaFinder,
buildProformaInputMappers,
buildProformaIssuer,
+ buildProformaReadModelAssemblers,
buildProformaSnapshotBuilders,
+ buildProformaStatusChanger,
buildProformaToIssuedInvoicePropsConverter,
buildProformaUpdater,
buildReportProformaUseCase,
@@ -30,6 +32,7 @@ import { buildProformaDocumentService } from "./proforma-documents.di";
import { buildProformaNumberGenerator } from "./proforma-number-generator.di";
import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
import { buildProformaRepository } from "./proforma-repositories.di";
+import { resolveProformaCatalogsDeps } from "./proforrma-catalog-deps.di";
export type ProformasInternalDeps = {
useCases: {
@@ -51,33 +54,61 @@ export type ProformasInternalDeps = {
};
export function buildProformasDependencies(params: ModuleParams): ProformasInternalDeps {
- const { database, getService } = params;
- const catalogs = getService("catalogs");
+ const { database } = params;
+
+ const catalogs = resolveProformaCatalogsDeps(params);
- // Infrastructure
const transactionManager = buildTransactionManager(database);
- const persistenceMappers = buildProformaPersistenceMappers(catalogs);
+ const persistenceMappers = buildProformaPersistenceMappers();
+
+ const repository = buildProformaRepository({
+ database,
+ mappers: persistenceMappers,
+ });
+
+ const inputMappers = buildProformaInputMappers();
+ const snapshotBuilders = buildProformaSnapshotBuilders();
+
+ const catalogResolvers = buildProformaCatalogResolvers({
+ taxDefinitionFinder: catalogs.taxDefinition.finder,
+ taxRegimeFinder: catalogs.taxRegime.finder,
+ paymentMethodFinder: catalogs.paymentMethod.finder,
+ });
+
+ const readModelAssemblers = buildProformaReadModelAssemblers({
+ paymentMethodFinder: catalogs.paymentMethod.finder,
+ taxRegimeFinder: catalogs.taxRegime.finder,
+ });
- const repository = buildProformaRepository({ database, mappers: persistenceMappers });
const proformaNumberService = buildProformaNumberGenerator();
-
- // Application helpers
- const inputMappers = buildProformaInputMappers(catalogs);
const finder = buildProformaFinder(repository);
- const creator = buildProformaCreator({ numberService: proformaNumberService, repository });
- const proformaToIssuedInvoiceConverter = buildProformaToIssuedInvoicePropsConverter(catalogs);
+
+ const creator = buildProformaCreator({
+ taxResolver: catalogResolvers.taxResolver,
+ paymentResolver: catalogResolvers.paymentResolver,
+ numberService: proformaNumberService,
+ repository,
+ });
+
+ const updater = buildProformaUpdater({
+ repository,
+ taxResolver: catalogResolvers.taxResolver,
+ paymentResolver: catalogResolvers.paymentResolver,
+ });
+
+ const statusChanger = buildProformaStatusChanger({
+ repository,
+ });
+
+ const proformaToIssuedInvoiceConverter = buildProformaToIssuedInvoicePropsConverter();
const issuer = buildProformaIssuer({
proformaConverter: proformaToIssuedInvoiceConverter,
repository,
});
- const updater = buildProformaUpdater({ repository });
-
- const snapshotBuilders = buildProformaSnapshotBuilders();
const documentGeneratorPipeline = buildProformaDocumentService(params);
- // Internal use cases (factories)
return {
useCases: {
listProformas: () =>
@@ -90,23 +121,16 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
getProformaById: () =>
buildGetProformaByIdUseCase({
finder,
+ fullReadModelAssembler: readModelAssemblers.full,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
- reportProforma: () =>
- buildReportProformaUseCase({
- finder,
- fullSnapshotBuilder: snapshotBuilders.full,
- reportSnapshotBuilder: snapshotBuilders.report,
- documentService: documentGeneratorPipeline,
- transactionManager,
- }),
-
createProforma: () =>
buildCreateProformaUseCase({
creator,
dtoMapper: inputMappers.createInputMapper,
+ fullReadModelAssembler: readModelAssemblers.full,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
@@ -115,10 +139,21 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
buildUpdateProformaUseCase({
updater,
dtoMapper: inputMappers.updateInputMapper,
+ fullReadModelAssembler: readModelAssemblers.full,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
+ reportProforma: () =>
+ buildReportProformaUseCase({
+ finder,
+ fullReadModelAssembler: readModelAssemblers.full,
+ fullSnapshotBuilder: snapshotBuilders.full,
+ reportSnapshotBuilder: snapshotBuilders.report,
+ documentService: documentGeneratorPipeline,
+ transactionManager,
+ }),
+
issueProforma: (publicServices: { issuedInvoiceServices: IIssuedInvoicePublicServices }) =>
buildIssueProformaUseCase({
publicServices,
@@ -129,8 +164,8 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
changeStatusProforma: () =>
buildChangeStatusProformaUseCase({
- finder,
- updater,
+ statusChanger,
+ fullReadModelAssembler: readModelAssemblers.full,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforrma-catalog-deps.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforrma-catalog-deps.di.ts
new file mode 100644
index 00000000..6ed7825b
--- /dev/null
+++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforrma-catalog-deps.di.ts
@@ -0,0 +1,49 @@
+import type { CatalogsPublicServicesType } from "@erp/catalogs/api";
+import type { ModuleParams } from "@erp/core/api";
+
+type ProformaCatalogsDeps = {
+ taxDefinition: {
+ finder: CatalogsPublicServicesType["taxDefinitions"]["finder"];
+ };
+ taxRegime: {
+ finder: CatalogsPublicServicesType["taxRegimes"]["finder"];
+ };
+ paymentMethod: {
+ finder: CatalogsPublicServicesType["paymentMethods"]["finder"];
+ };
+};
+
+export function resolveProformaCatalogsDeps(params: ModuleParams): ProformaCatalogsDeps {
+ const taxDefinition =
+ params.getService("catalogs:taxDefinition");
+
+ if (!taxDefinition?.finder) {
+ throw new Error("Missing public service: catalogs:taxDefinition.finder");
+ }
+
+ const taxRegime =
+ params.getService("catalogs:taxRegime");
+
+ if (!taxRegime?.finder) {
+ throw new Error("Missing public service: catalogs:taxRegime.finder");
+ }
+
+ const paymentMethod =
+ params.getService("catalogs:paymentMethod");
+
+ if (!paymentMethod?.finder) {
+ throw new Error("Missing public service: catalogs:paymentMethod.finder");
+ }
+
+ return {
+ taxDefinition: {
+ finder: taxDefinition.finder,
+ },
+ taxRegime: {
+ finder: taxRegime.finder,
+ },
+ paymentMethod: {
+ finder: paymentMethod.finder,
+ },
+ };
+}
diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts
index 5e4e164d..d9a305d5 100644
--- a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts
+++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts
@@ -1,5 +1,4 @@
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
-import type { ICatalogPublicServices } from "@erp/catalogs/api";
import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express";
@@ -35,11 +34,9 @@ export const proformasRouter = (params: StartParams) => {
const deps = getInternal("customer-invoices", "proformas");
const issuedInvoicesServices = getService("self:issuedInvoices");
- const catalogServices = getService("self:catalogs");
const publicServices = {
issuedInvoiceServices: issuedInvoicesServices,
- catalogServices: catalogServices,
};
const router: Router = Router({ mergeParams: true });
diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts
index 29216252..3c9fe0a9 100644
--- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts
+++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts
@@ -1,4 +1,3 @@
-import type { IPaymentMethodPublicServices, ITaxRegimePublicServices } from "@erp/catalogs/api";
import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
CurrencyCode,
@@ -40,27 +39,12 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
private _recipientMapper: SequelizeProformaRecipientDomainMapper;
private _taxesMapper: SequelizeProformaTaxesDomainMapper;
- private _paymentMethodCatalog: IPaymentMethodPublicServices;
- private _taxRegimeCatalog: ITaxRegimePublicServices;
-
constructor(params: MapperParamsType) {
super();
- const { paymentCatalog, taxRegimeCatalog } = params as {
- paymentCatalog: IPaymentMethodPublicServices;
- taxRegimeCatalog: ITaxRegimePublicServices;
- };
-
- this._paymentMethodCatalog = paymentCatalog;
- this._taxRegimeCatalog = taxRegimeCatalog;
-
- if (!this._paymentMethodCatalog) {
- throw new Error('paymentCatalog not defined ("SequelizeProformaDomainMapper")');
- }
-
- this._itemsMapper = new SequelizeProformaItemDomainMapper(params);
+ this._itemsMapper = new SequelizeProformaItemDomainMapper();
this._recipientMapper = new SequelizeProformaRecipientDomainMapper();
- this._taxesMapper = new SequelizeProformaTaxesDomainMapper(params);
+ this._taxesMapper = new SequelizeProformaTaxesDomainMapper();
}
private _mapAttributesToDomain(raw: CustomerInvoiceModel, params?: MapperParamsType) {
@@ -277,7 +261,6 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
globalDiscountPercentage: attributes.globalDiscountPercentage!,
paymentMethodId: attributes.paymentMethodId!,
-
taxRegimeCode: attributes.taxRegimeCode!,
linkedInvoiceId: attributes.linkedInvoiceId!, // El id de la factura emitida (linked_invoice) se asigna al hacer issue() desde la proforma, no viene en el modelo de persistencia.
@@ -292,7 +275,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
}
}
- public mapToPersistence(
+ public async mapToPersistence(
source: Proforma,
params?: MapperParamsType
): Result {
@@ -333,47 +316,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
...params,
});
- // 4) Payment
- let payment: {
- id: string | null;
- description: string | null;
- } = { id: null, description: null };
-
- if (source.hasPaymentMethod) {
- const paymentId = source.paymentMethodId.unwrap();
- const paymentOrNot = this._paymentMethodCatalog.findById(paymentId.toString());
-
- if (paymentOrNot.isSome()) {
- const paymentItem = paymentOrNot.unwrap();
-
- payment = {
- id: paymentItem.id ?? null,
- description: paymentItem.description ?? null,
- };
- }
- }
-
- // 5) Tax regime
- let taxRegime: {
- code: string | null;
- description: string | null;
- } = { code: null, description: null };
-
- if (source.hasTaxRegime) {
- const taxRegimeCode = source.taxRegimeCode.unwrap();
- const taxRegimeOrNot = this._taxRegimeCatalog.findBy(taxRegimeCode.toString());
-
- if (taxRegimeOrNot.isSome()) {
- const taxRegimeItem = taxRegimeOrNot.unwrap();
-
- taxRegime = {
- code: taxRegimeItem.code ?? null,
- description: taxRegimeItem.description ?? null,
- };
- }
- }
-
- // 6) Si hubo errores de mapeo, devolvemos colección de validación
+ // 4) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
@@ -385,7 +328,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
const allAmounts = source.totals(); // Da los totales ya calculados
- const proformaValues: Partial = {
+ const proformaValues: CustomerInvoiceCreationAttributes = {
// Identificación
id: source.id.toPrimitive(),
company_id: source.companyId.toPrimitive(),
@@ -406,11 +349,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
description: maybeToNullable(source.description, (description) => description),
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
- payment_method_id: payment.id,
- payment_method_description: payment.description,
-
- tax_regime_code: taxRegime.code,
- tax_regime_description: taxRegime.description,
+ payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()),
+ tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value),
subtotal_amount_value: allAmounts.subtotalAmount.value,
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
@@ -453,7 +393,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
};
return Result.ok(
- proformaValues as CustomerInvoiceCreationAttributes
+ proformaValues satisfies CustomerInvoiceCreationAttributes
);
}
}
diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts
index f39feac3..5077951a 100644
--- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts
+++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts
@@ -1,4 +1,3 @@
-import type { JsonTaxCatalogProvider } from "@erp/core";
import {
DiscountPercentage,
type MapperParamsType,
@@ -36,21 +35,6 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
CustomerInvoiceItemCreationAttributes,
ProformaItem
> {
- private _taxCatalog!: JsonTaxCatalogProvider;
-
- constructor(params: MapperParamsType) {
- super();
- const { taxCatalog } = params as {
- taxCatalog: JsonTaxCatalogProvider;
- };
-
- if (!taxCatalog) {
- throw new Error('taxCatalog not defined ("ProformaItemDomainMapper")');
- }
-
- this._taxCatalog = taxCatalog;
- }
-
private mapAttributesToDomain(
raw: CustomerInvoiceItemModel,
params?: MapperParamsType
diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-taxes-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-taxes-domain.mapper.ts
index dcf7d17f..5980e6a4 100644
--- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-taxes-domain.mapper.ts
+++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-taxes-domain.mapper.ts
@@ -1,4 +1,3 @@
-import type { JsonTaxCatalogProvider } from "@erp/core";
import { type MapperParamsType, SequelizeDomainMapper, TaxPercentage } from "@erp/core/api";
import { UniqueID, type ValidationErrorDetail, maybeToNullable } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
@@ -26,21 +25,6 @@ export class SequelizeProformaTaxesDomainMapper extends SequelizeDomainMapper<
CustomerInvoiceTaxCreationAttributes,
IProformaTaxTotals
> {
- private taxCatalog!: JsonTaxCatalogProvider;
-
- constructor(params: MapperParamsType) {
- super();
- const { taxCatalog } = params as {
- taxCatalog: JsonTaxCatalogProvider;
- };
-
- if (!taxCatalog) {
- throw new Error('taxCatalog not defined ("SequelizeProformaTaxesDomainMapper")');
- }
-
- this.taxCatalog = taxCatalog;
- }
-
public mapToDomain(
source: CustomerInvoiceTaxModel,
params?: MapperParamsType
diff --git a/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts
index 2edf77f3..c202dda3 100644
--- a/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts
+++ b/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts
@@ -47,6 +47,7 @@ export const CreateProformaRequestSchema = z.object({
payment_method_id: z.uuid().nullable().optional(),
payment_term_id: z.uuid().nullable().optional(),
+ tax_regime_code: z.string().nullable().optional(),
items: z.array(CreateProformaItemRequestSchema),
});
diff --git a/modules/customer-invoices/src/common/dto/shared/payment-method-ref.dto.ts b/modules/customer-invoices/src/common/dto/shared/payment-method-ref.dto.ts
index 1ab009e9..94ad747e 100644
--- a/modules/customer-invoices/src/common/dto/shared/payment-method-ref.dto.ts
+++ b/modules/customer-invoices/src/common/dto/shared/payment-method-ref.dto.ts
@@ -2,6 +2,7 @@ import { z } from "zod/v4";
export const PaymentMethodRefSchema = z.object({
id: z.uuid(),
+ name: z.string(),
description: z.string(),
});
diff --git a/packages/rdx-ddd/src/helpers/extract-or-push-error.ts b/packages/rdx-ddd/src/helpers/extract-or-push-error.ts
index 807e6402..fd7071b4 100644
--- a/packages/rdx-ddd/src/helpers/extract-or-push-error.ts
+++ b/packages/rdx-ddd/src/helpers/extract-or-push-error.ts
@@ -1,7 +1,6 @@
import type { Result } from "@repo/rdx-utils";
import {
- DomainValidationError,
type ValidationErrorDetail,
isDomainValidationError,
isValidationErrorCollection,
@@ -16,53 +15,56 @@ import {
* @returns El valor extraído si el resultado es exitoso, o undefined si es un fallo.
* @template T - El tipo de dato esperado en el resultado exitoso.
* @throws {Error} Si el resultado es un fallo y no es una instancia de DomainValidationError.
- * @example
- * const result = Result.ok(42);
- * const value = extractOrPushError(result, 'some.path', []);
- * console.log(value); // 42
- * const errorResult = Result.fail(new Error('Something went wrong'));
- * const value = extractOrPushError(errorResult, 'some.path', []);
- * console.log(value); // undefined
- * // errors will contain [{ path: 'some.path', message: 'Something went wrong' }]
- *
- * @see Result
- * @see DomainValidationError
- * @see ValidationErrorDetail
*/
-export function extractOrPushError(
- result: Result,
+
+type ResultFn = Result | (() => Result);
+
+export function extractOrPushError(
+ result: ResultFn,
path: string,
errors: ValidationErrorDetail[]
): T | undefined {
- if (result.isFailure) {
- const error = result.error;
+ const actualResult = typeof result === "function" ? result() : result;
- if (isValidationErrorCollection(error)) {
- // Copiar todos los detalles, rellenando path si falta
+ if (!actualResult.isFailure) {
+ return actualResult.data;
+ }
- error.details?.forEach((detail) => {
+ const error = actualResult.error;
+
+ if (isValidationErrorCollection(error)) {
+ if (error.details?.length) {
+ error.details.forEach((detail) => {
errors.push({
...detail,
- path: path ?? detail.path,
+ path: detail.path ?? path,
});
});
- } else if (isDomainValidationError(error)) {
- errors.push({
- path,
- message: error.detail,
- value: (error as any).cause, // mantener la causa original
- });
} else {
- // Fallback genérico: Error desconocido tratado como validación simple
errors.push({
path,
- message: error.message ?? "Unknown error",
+ message: error.message ?? "Validation error",
});
}
return undefined;
}
- return result.data;
+ if (isDomainValidationError(error)) {
+ errors.push({
+ path,
+ message: error.detail,
+ value: error.cause,
+ });
+
+ return undefined;
+ }
+
+ errors.push({
+ path,
+ message: error.message ?? "Unknown error",
+ });
+
+ return undefined;
}
diff --git a/packages/rdx-ddd/src/value-objects/country-code.ts b/packages/rdx-ddd/src/value-objects/country-code.ts
new file mode 100644
index 00000000..3e0999ae
--- /dev/null
+++ b/packages/rdx-ddd/src/value-objects/country-code.ts
@@ -0,0 +1,47 @@
+import { Result } from "@repo/rdx-utils";
+import { z } from "zod/v4";
+
+import { translateZodValidationError } from "../helpers";
+
+import { ValueObject } from "./value-object";
+
+interface CountryCodeProps {
+ value: string;
+}
+
+export class CountryCode extends ValueObject {
+ protected static validate(value: string) {
+ const schema = z
+ .string()
+ .trim()
+ .uppercase()
+ .regex(/^[A-Z]{2}$/, {
+ message: "Country code must be 2 uppercase letters",
+ });
+
+ return schema.safeParse(value);
+ }
+
+ static create(code: string): Result {
+ const valueIsValid = CountryCode.validate(code);
+
+ if (!valueIsValid.success) {
+ return Result.fail(
+ translateZodValidationError("CountryCode creation failed", valueIsValid.error)
+ );
+ }
+ return Result.ok(new CountryCode({ value: valueIsValid.data }));
+ }
+
+ getProps(): string {
+ return this.props.value;
+ }
+
+ toPrimitive(): string {
+ return this.props.value;
+ }
+
+ toString() {
+ return String(this.props.value);
+ }
+}
diff --git a/packages/rdx-ddd/src/value-objects/country-region-code.ts b/packages/rdx-ddd/src/value-objects/country-region-code.ts
new file mode 100644
index 00000000..9a69531e
--- /dev/null
+++ b/packages/rdx-ddd/src/value-objects/country-region-code.ts
@@ -0,0 +1,47 @@
+import { Result } from "@repo/rdx-utils";
+import { z } from "zod/v4";
+
+import { translateZodValidationError } from "../helpers";
+
+import { ValueObject } from "./value-object";
+
+interface CountryRegionCodeProps {
+ value: string;
+}
+
+export class CountryRegionCode extends ValueObject {
+ protected static validate(value: string) {
+ const schema = z
+ .string()
+ .trim()
+ .uppercase()
+ .regex(/^[A-Z]{2}-[A-Z0-9-]+$/, {
+ message: "Region code must follow ISO-3166-2 pattern COUNTRY-REGION",
+ });
+
+ return schema.safeParse(value);
+ }
+
+ static create(code: string): Result {
+ const valueIsValid = CountryRegionCode.validate(code);
+
+ if (!valueIsValid.success) {
+ return Result.fail(
+ translateZodValidationError("CountryRegionCode creation failed", valueIsValid.error)
+ );
+ }
+ return Result.ok(new CountryRegionCode({ value: valueIsValid.data }));
+ }
+
+ getProps(): string {
+ return this.props.value;
+ }
+
+ toPrimitive(): string {
+ return this.props.value;
+ }
+
+ toString() {
+ return String(this.props.value);
+ }
+}
diff --git a/packages/rdx-ddd/src/value-objects/index.ts b/packages/rdx-ddd/src/value-objects/index.ts
index 132d8d1c..8e6520fc 100644
--- a/packages/rdx-ddd/src/value-objects/index.ts
+++ b/packages/rdx-ddd/src/value-objects/index.ts
@@ -1,5 +1,7 @@
export * from "./city";
export * from "./country";
+export * from "./country-code";
+export * from "./country-region-code";
export * from "./currency-code";
export * from "./email-address";
export * from "./language-code";
diff --git a/packages/rdx-ddd/src/value-objects/percentage.ts b/packages/rdx-ddd/src/value-objects/percentage.ts
index 7d083156..66c4e536 100644
--- a/packages/rdx-ddd/src/value-objects/percentage.ts
+++ b/packages/rdx-ddd/src/value-objects/percentage.ts
@@ -107,7 +107,10 @@ export class Percentage extends ValueObject {
}
toPrimitive() {
- return this.getProps();
+ return {
+ value: this.value,
+ scale: this.scale,
+ };
}
toNumber(): number {