Compare commits

..

No commits in common. "34520a18ba1f5474a3949fff2843cbc0d8ccd6fe" and "f1f329fa0d6ec89c99b09633940551b38b8360a7" have entirely different histories.

194 changed files with 813 additions and 3401 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/factuges-server", "name": "@erp/factuges-server",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "tsup src/index.ts --config tsup.config.ts", "build": "tsup src/index.ts --config tsup.config.ts",
@ -45,7 +45,6 @@
"@erp/document-series": "workspace:*", "@erp/document-series": "workspace:*",
"@erp/identity": "workspace:*", "@erp/identity": "workspace:*",
"@erp/factuges": "workspace:*", "@erp/factuges": "workspace:*",
"@erp/suppliers": "workspace:*",
"@repo/rdx-logger": "workspace:*", "@repo/rdx-logger": "workspace:*",
"@repo/rdx-utils": "workspace:*", "@repo/rdx-utils": "workspace:*",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",

View File

@ -5,7 +5,8 @@ import customersAPIModule from "@erp/customers/api";
import documentSeriesAPIModule from "@erp/document-series/api"; import documentSeriesAPIModule from "@erp/document-series/api";
import factuGESAPIModule from "@erp/factuges/api"; import factuGESAPIModule from "@erp/factuges/api";
import identityAPIModule from "@erp/identity/api"; import identityAPIModule from "@erp/identity/api";
import suppliersAPIModule from "@erp/suppliers/api";
//import suppliersAPIModule from "@erp/suppliers/api";
import { registerModule } from "./lib"; import { registerModule } from "./lib";
@ -22,5 +23,5 @@ export const registerModules = () => {
registerModule(documentSeriesAPIModule); registerModule(documentSeriesAPIModule);
registerModule(customerInvoicesAPIModule); registerModule(customerInvoicesAPIModule);
registerModule(factuGESAPIModule); registerModule(factuGESAPIModule);
registerModule(suppliersAPIModule); //registerModule(suppliersAPIModule);
}; };

View File

@ -1,7 +1,7 @@
{ {
"name": "@erp/factuges-web", "name": "@erp/factuges-web",
"private": true, "private": true,
"version": "0.10.5", "version": "0.10.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit",
@ -36,7 +36,6 @@
"@erp/customer-invoices": "workspace:*", "@erp/customer-invoices": "workspace:*",
"@erp/customers": "workspace:*", "@erp/customers": "workspace:*",
"@erp/identity": "workspace:*", "@erp/identity": "workspace:*",
"@erp/suppliers": "workspace:*",
"@repo/rdx-utils": "workspace:*", "@repo/rdx-utils": "workspace:*",
"@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/geist": "^5.2.8",
"@fontsource-variable/geist-mono": "^5.2.7", "@fontsource-variable/geist-mono": "^5.2.7",

View File

@ -4,7 +4,6 @@ import CoreModuleManifest, { type IModuleClient } from "@erp/core/client";
import { CustomerInvoicesModuleManifest } from "@erp/customer-invoices/client"; import { CustomerInvoicesModuleManifest } from "@erp/customer-invoices/client";
import { CustomersModuleManifest } from "@erp/customers/client"; import { CustomersModuleManifest } from "@erp/customers/client";
import { IdentityModuleManifest } from "@erp/identity/client"; import { IdentityModuleManifest } from "@erp/identity/client";
import { SuppliersModuleManifest } from "@erp/suppliers/client";
export const modules: IModuleClient[] = [ export const modules: IModuleClient[] = [
IdentityModuleManifest, IdentityModuleManifest,
@ -12,6 +11,5 @@ export const modules: IModuleClient[] = [
CompaniesModuleManifest, CompaniesModuleManifest,
CatalogsModuleManifest, CatalogsModuleManifest,
CustomersModuleManifest, CustomersModuleManifest,
SuppliersModuleManifest,
CustomerInvoicesModuleManifest, CustomerInvoicesModuleManifest,
]; ];

View File

@ -1,174 +0,0 @@
# Frontend de suppliers: diseño e implementación UI/UX
## 1. Objetivo
El frontend de `suppliers` debe ofrecer listado, detalle, creación y edición con el mismo lenguaje visual y los mismos patrones de navegación que `customers`, respetando el contrato funcional más pequeño de Supplier.
Este documento es la fuente normativa para la implementación. La paridad buscada es de arquitectura y experiencia, no de campos ni capacidades de negocio.
## 2. Estado actual
Antes de esta implementación, Supplier solo disponía de entidades frontend, adapters, APIs y hooks de TanStack Query. La sidebar enlazaba a `/suppliers`, pero no existían manifest, rutas, páginas, formularios ni traducciones del módulo.
El API client tenía una incompatibilidad activa: la actualización delegaba en `IDataSource.updateOne()`, que usa PUT, aunque Supplier solo expone PATCH.
## 3. Customer como referencia
Se adoptan de Customer:
- manifest y rutas lazy por módulo;
- páginas de listado con `RdxPageContainer`, `RdxPageHeader`, búsqueda con debounce y `DataTable`;
- editores fullscreen con `RdxEditorPageLayout`, header persistente, protección de cambios sin guardar y secciones Rdx;
- `PageErrorState`, skeletons, alertas y toasts del sistema;
- React Hook Form con validación Zod;
- queries y mutations encapsuladas en hooks y adapters de transporte.
No se copia la fiscalidad, la eliminación, el panel lateral ni las acciones exclusivas de Customer.
## 4. Diferencias funcionales Customer/Supplier
| Área | Customer | Supplier |
| --- | --- | --- |
| Lifecycle | Puede tener capacidades propias | Status solo informativo |
| Eliminación | Existe contrato Customer | No existe DELETE Supplier |
| Fiscalidad | Incluye configuración fiscal | Fuera de alcance |
| Pagos | Puede incluir términos y métodos | Fuera de alcance |
| Dirección | Contrato agrupado en áreas modernas | DTO Supplier plano |
| Actualización | Contrato propio | PATCH parcial, sin `null` para borrar |
## 5. Arquitectura frontend Supplier
```txt
Page
-> controller hook
-> query/mutation hook
-> API function
-> DTO adapter
-> frontend entity
```
Los DTO snake_case quedan limitados a `common`, API y adapters. Páginas, formularios y entidades usan camelCase.
## 6. Rutas
| Ruta | Pantalla | Layout |
| --- | --- | --- |
| `/suppliers` | Listado | `app-sidebar` |
| `/suppliers/create` | Creación | `app-fullscreen` |
| `/suppliers/:id` | Detalle | `app-fullscreen` |
| `/suppliers/:id/edit` | Edición | `app-fullscreen` |
Se adopta `edit`, que es la convención vigente de Customer. No se crean rutas DELETE, activate o deactivate.
## 7. Listado
El listado usa header, descripción, acción principal, `SimpleSearchInput`, tabla server-side y paginación. Las columnas son:
- identidad: nombre, nombre comercial, TIN y tipo empresa/persona;
- referencia;
- contacto principal;
- status no interactivo;
- acciones Ver y Editar.
Solo `name`, `reference` y `email_primary` ofrecen sorting, porque pertenecen a la whitelist real. La búsqueda usa el query param `q`, debounce de 300 ms y reinicia la página.
## 8. Detail
El detalle presenta un header con nombre, contexto secundario y badge de status. Se agrupa en Identificación, Contacto, Dirección y Preferencias. Los valores ausentes muestran `-` para mantener una cuadrícula estable. Las únicas acciones son Volver y Editar.
## 9. Create
Create reutiliza el editor común Supplier y genera el UUID en el cliente. El payload conserva el DTO plano del backend y omite campos opcionales vacíos. Tras éxito actualiza caché, muestra feedback y navega al detalle.
## 10. Update
Update carga el detalle, adapta la entidad al formulario y construye el PATCH desde `dirtyFields`. Solo se envían campos modificados con valor utilizable. Un campo vacío se omite porque el contrato vigente no admite `null => borrar`; la UI no inventa esa semántica.
La petición se realiza explícitamente con método PATCH, nunca con `IDataSource.updateOne()`.
## 11. Formularios y validación
Un único `SupplierFormSchema` en camelCase valida create y update. Se reutilizan schemas semánticos de Core para TIN, país, código postal, idioma, moneda, teléfono y web. Las secciones son Identificación, Dirección, Contacto y Preferencias.
`isCompany` es boolean y usa el selector visual empresa/persona. `status` no forma parte del formulario.
## 12. API / React Query
Las keys separan listado y detalle. Create establece el detalle e invalida listados. Update establece el detalle, parchea filas presentes e invalida listados. El cambio de empresa reutiliza el reset global de queries y el interceptor común que añade `X-Company-Id`.
## 13. Estados loading/error/empty
- List usa `SkeletonDataTable` en carga inicial.
- Detail y Update usan skeleton de editor.
- Errores bloqueantes usan `PageErrorState` con reintento y regreso.
- Errores de mutación usan `ErrorAlert` y toast.
- La tabla conserva el empty state común; una búsqueda sin resultados mantiene visible el buscador.
## 14. Responsive
Header y acciones apilan en móvil. Formularios pasan de dos columnas a una. Las tarjetas de detalle usan una columna en móvil y dos en escritorio. La tabla conserva scroll horizontal y paginación del componente compartido.
## 15. Accesibilidad
Todos los inputs tienen labels asociados mediante los componentes Rdx. Los botones incluyen nombre accesible, los iconos decorativos usan `aria-hidden`, las filas de acciones son navegables y los errores se asocian mediante React Hook Form. El foco se mueve al primer campo inválido.
## 16. i18n
Todos los textos visibles pertenecen al namespace `Suppliers`. Se mantienen recursos completos en `es` y `en`; no se insertan textos de interfaz específicos en componentes.
## 17. Componentes reutilizados
- `RdxPageContainer`, `RdxPageStack`, `RdxPageHeader`, `RdxPageActions`;
- `RdxEditorPageLayout`, `RdxEditorSection`, `RdxFormGrid`;
- `DataTable`, `SkeletonDataTable`, `DataTableColumnHeader`, `RdxRowActions`;
- `TextField`, `SelectField`, `PageFormHeader`, `FormActionsBar`;
- `SimpleSearchInput`, `PageErrorState`, `ErrorAlert`;
- `UnsavedChangesProvider` y `useReturnToNavigation`.
## 18. Componentes Supplier específicos
- `SuppliersGrid` y sus columnas, porque expresan identidad y acciones Supplier;
- `SupplierStatusBadge`, limitado al status del dominio;
- `SupplierEditorForm` y sus secciones, porque el DTO plano y la ausencia de fiscalidad son específicos;
- páginas y controllers de List, Detail, Create y Update.
No se extraen componentes desde Customer: la similitud visual no justifica acoplar ambos dominios.
## 19. Decisiones descartadas
Quedan expresamente fuera:
- DELETE;
- activate/deactivate y cualquier lifecycle HTTP;
- `paymentTermId`;
- `paymentMethodId`;
- fiscalidad y defaults fiscales;
- integración con `supplier-invoices`;
- edición de status;
- refactor del DTO plano de dirección;
- semántica `null => borrar` no soportada.
## 20. Plan de implementación
1. Corregir API PATCH, keys, adapters y estrategia de caché.
2. Registrar manifest y rutas Supplier.
3. Implementar listado y navegación.
4. Implementar detalle.
5. Implementar formulario compartido y Create.
6. Implementar Update con `dirtyFields`.
7. Añadir recursos `es` y `en`.
8. Revisar responsive y accesibilidad.
9. Añadir tests de mappers, PATCH parcial y rutas.
10. Ejecutar tests, typecheck, lint, build dirigido y checklist manual.
### Checklist manual
- Abrir listado, buscar por nombre, TIN y referencia.
- Ordenar nombre, referencia y email.
- Abrir detalle y navegar a editar.
- Guardar un único campo y confirmar que el PATCH es parcial.
- Guardar `isCompany = false`.
- Crear Supplier y validar errores de formulario/API.
- Confirmar actualización de detail y list.
- Cambiar de empresa y comprobar aislamiento.
- Revisar listado, detalle y formularios en desktop, tablet y móvil.

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/auth", "name": "@erp/auth",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,7 +1,7 @@
{ {
"name": "@erp/catalogs", "name": "@erp/catalogs",
"description": "Catalogs module", "description": "Catalogs module",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,7 +1,7 @@
{ {
"name": "@erp/companies", "name": "@erp/companies",
"description": "Companies module", "description": "Companies module",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/core", "name": "@erp/core",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/customer-invoices", "name": "@erp/customer-invoices",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -83,7 +83,6 @@ test("resolvePaymentTerm normaliza percentage.value cuando llega numérico del c
assert.equal(currencyCodeResult.isSuccess, true); assert.equal(currencyCodeResult.isSuccess, true);
assert.equal(discountResult.isSuccess, true); assert.equal(discountResult.isSuccess, true);
// biome-ignore lint/complexity/useLiteralKeys: <Solucion rápida para test, no es necesario refactorizar>
const result = converter["resolvePaymentTerm"]({ const result = converter["resolvePaymentTerm"]({
proforma: { proforma: {
companyId, companyId,

View File

@ -1,7 +1,7 @@
{ {
"name": "@erp/customers", "name": "@erp/customers",
"description": "Customers", "description": "Customers",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,7 +1,7 @@
{ {
"name": "@erp/document-series", "name": "@erp/document-series",
"description": "Document series module", "description": "Document series module",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/factuges", "name": "@erp/factuges",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,7 +1,7 @@
{ {
"name": "@erp/identity", "name": "@erp/identity",
"description": "Identity module", "description": "Identity module",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,7 +1,7 @@
{ {
"name": "@erp/supplier-invoices", "name": "@erp/supplier-invoices",
"description": "Supplier invoices", "description": "Supplier invoices",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,7 +1,7 @@
{ {
"name": "@erp/suppliers", "name": "@erp/suppliers",
"description": "Suppliers", "description": "Suppliers",
"version": "0.10.5", "version": "0.10.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,3 +1,5 @@
import type { ICatalogs } from "@erp/core/api";
import { import {
CreateSupplierInputMapper, CreateSupplierInputMapper,
type ICreateSupplierInputMapper, type ICreateSupplierInputMapper,
@ -10,9 +12,11 @@ export interface ISupplierInputMappers {
updateInputMapper: IUpdateSupplierInputMapper; updateInputMapper: IUpdateSupplierInputMapper;
} }
export const buildSupplierInputMappers = (): ISupplierInputMappers => { export const buildSupplierInputMappers = (catalogs: ICatalogs): ISupplierInputMappers => {
const { taxCatalog } = catalogs;
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado // Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado
const createInputMapper = new CreateSupplierInputMapper(); const createInputMapper = new CreateSupplierInputMapper({ taxCatalog });
const updateInputMapper = new UpdateSupplierInputMapper(); const updateInputMapper = new UpdateSupplierInputMapper();
return { return {

View File

@ -48,7 +48,6 @@ export function buildCreateSupplierUseCase(deps: {
} }
export function buildUpdateSupplierUseCase(deps: { export function buildUpdateSupplierUseCase(deps: {
finder: ISupplierFinder;
updater: ISupplierUpdater; updater: ISupplierUpdater;
dtoMapper: IUpdateSupplierInputMapper; dtoMapper: IUpdateSupplierInputMapper;
fullSnapshotBuilder: ISupplierFullSnapshotBuilder; fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
@ -56,9 +55,41 @@ export function buildUpdateSupplierUseCase(deps: {
}) { }) {
return new UpdateSupplierUseCase({ return new UpdateSupplierUseCase({
dtoMapper: deps.dtoMapper, dtoMapper: deps.dtoMapper,
finder: deps.finder,
updater: deps.updater, updater: deps.updater,
fullSnapshotBuilder: deps.fullSnapshotBuilder, fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager, transactionManager: deps.transactionManager,
}); });
} }
/*export function buildReportSupplierUseCase(deps: {
finder: ISupplierFinder;
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
reportSnapshotBuilder: ISupplierReportSnapshotBuilder;
documentService: SupplierDocumentGeneratorService;
transactionManager: ITransactionManager;
}) {
return new ReportSupplierUseCase(
deps.finder,
deps.fullSnapshotBuilder,
deps.reportSnapshotBuilder,
deps.documentService,
deps.transactionManager
);
}*/
/*
export function buildDeleteSupplierUseCase(deps: { finder: ISupplierFinder }) {
return new DeleteSupplierUseCase(deps.finder);
}
export function buildIssueSupplierUseCase(deps: { finder: ISupplierFinder }) {
return new IssueSupplierUseCase(deps.finder);
}
export function buildChangeStatusSupplierUseCase(deps: {
finder: ISupplierFinder;
transactionManager: ITransactionManager;
}) {
return new ChangeStatusSupplierUseCase(deps.finder, deps.transactionManager);
}*/

View File

@ -1,3 +1,4 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import { import {
City, City,
Country, Country,
@ -21,142 +22,148 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { CreateSupplierRequestDTO } from "../../../common";
import { type ISupplierCreateProps, SupplierStatus } from "../../domain"; import { type ISupplierCreateProps, SupplierStatus } from "../../domain";
import type { CreateSupplierCommand } from "../use-cases/create-supplier.command";
export interface ICreateSupplierInputMapper { export interface ICreateSupplierInputMapper {
map( map(
command: CreateSupplierCommand, dto: CreateSupplierRequestDTO,
params: { companyId: UniqueID } params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: ISupplierCreateProps }>; ): Result<{ id: UniqueID; props: ISupplierCreateProps }>;
} }
export class CreateSupplierInputMapper implements ICreateSupplierInputMapper { export class CreateSupplierInputMapper implements ICreateSupplierInputMapper {
private readonly taxCatalog: JsonTaxCatalogProvider;
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
this.taxCatalog = params.taxCatalog;
}
public map( public map(
command: CreateSupplierCommand, dto: CreateSupplierRequestDTO,
params: { companyId: UniqueID } params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: ISupplierCreateProps }> { ): Result<{ id: UniqueID; props: ISupplierCreateProps }> {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const { companyId } = params; const { companyId } = params;
const supplierId = extractOrPushError(UniqueID.create(command.supplierId), "id", errors); const supplierId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const status = SupplierStatus.active(); const status = SupplierStatus.active();
const isCompany = command.isCompany; const isCompany = dto.is_company === "true";
const reference = extractOrPushError( const reference = extractOrPushError(
maybeFromNullableResult(command.reference, (value) => Name.create(value)), maybeFromNullableResult(dto.reference, (value) => Name.create(value)),
"reference", "reference",
errors errors
); );
const name = extractOrPushError(Name.create(command.name), "name", errors); const name = extractOrPushError(Name.create(dto.name), "name", errors);
const tradeName = extractOrPushError( const tradeName = extractOrPushError(
maybeFromNullableResult(command.tradeName, (value) => Name.create(value)), maybeFromNullableResult(dto.trade_name, (value) => Name.create(value)),
"trade_name", "trade_name",
errors errors
); );
const tinNumber = extractOrPushError( const tinNumber = extractOrPushError(
maybeFromNullableResult(command.tin, (value) => TINNumber.create(value)), maybeFromNullableResult(dto.tin, (value) => TINNumber.create(value)),
"tin", "tin",
errors errors
); );
const street = extractOrPushError( const street = extractOrPushError(
maybeFromNullableResult(command.street, (value) => Street.create(value)), maybeFromNullableResult(dto.street, (value) => Street.create(value)),
"street", "street",
errors errors
); );
const street2 = extractOrPushError( const street2 = extractOrPushError(
maybeFromNullableResult(command.street2, (value) => Street.create(value)), maybeFromNullableResult(dto.street2, (value) => Street.create(value)),
"street2", "street2",
errors errors
); );
const city = extractOrPushError( const city = extractOrPushError(
maybeFromNullableResult(command.city, (value) => City.create(value)), maybeFromNullableResult(dto.city, (value) => City.create(value)),
"city", "city",
errors errors
); );
const province = extractOrPushError( const province = extractOrPushError(
maybeFromNullableResult(command.province, (value) => Province.create(value)), maybeFromNullableResult(dto.province, (value) => Province.create(value)),
"province", "province",
errors errors
); );
const postalCode = extractOrPushError( const postalCode = extractOrPushError(
maybeFromNullableResult(command.postalCode, (value) => PostalCode.create(value)), maybeFromNullableResult(dto.postal_code, (value) => PostalCode.create(value)),
"postal_code", "postal_code",
errors errors
); );
const country = extractOrPushError( const country = extractOrPushError(
maybeFromNullableResult(command.country, (value) => Country.create(value)), maybeFromNullableResult(dto.country, (value) => Country.create(value)),
"country", "country",
errors errors
); );
const primaryEmailAddress = extractOrPushError( const primaryEmailAddress = extractOrPushError(
maybeFromNullableResult(command.emailPrimary, (value) => EmailAddress.create(value)), maybeFromNullableResult(dto.email_primary, (value) => EmailAddress.create(value)),
"email_primary", "email_primary",
errors errors
); );
const secondaryEmailAddress = extractOrPushError( const secondaryEmailAddress = extractOrPushError(
maybeFromNullableResult(command.emailSecondary, (value) => EmailAddress.create(value)), maybeFromNullableResult(dto.email_secondary, (value) => EmailAddress.create(value)),
"email_secondary", "email_secondary",
errors errors
); );
const primaryPhoneNumber = extractOrPushError( const primaryPhoneNumber = extractOrPushError(
maybeFromNullableResult(command.phonePrimary, (value) => PhoneNumber.create(value)), maybeFromNullableResult(dto.phone_primary, (value) => PhoneNumber.create(value)),
"phone_primary", "phone_primary",
errors errors
); );
const secondaryPhoneNumber = extractOrPushError( const secondaryPhoneNumber = extractOrPushError(
maybeFromNullableResult(command.phoneSecondary, (value) => PhoneNumber.create(value)), maybeFromNullableResult(dto.phone_secondary, (value) => PhoneNumber.create(value)),
"phone_secondary", "phone_secondary",
errors errors
); );
const primaryMobileNumber = extractOrPushError( const primaryMobileNumber = extractOrPushError(
maybeFromNullableResult(command.mobilePrimary, (value) => PhoneNumber.create(value)), maybeFromNullableResult(dto.mobile_primary, (value) => PhoneNumber.create(value)),
"mobile_primary", "mobile_primary",
errors errors
); );
const secondaryMobileNumber = extractOrPushError( const secondaryMobileNumber = extractOrPushError(
maybeFromNullableResult(command.mobileSecondary, (value) => PhoneNumber.create(value)), maybeFromNullableResult(dto.mobile_secondary, (value) => PhoneNumber.create(value)),
"mobile_secondary", "mobile_secondary",
errors errors
); );
const faxNumber = extractOrPushError( const faxNumber = extractOrPushError(
maybeFromNullableResult(command.fax, (value) => PhoneNumber.create(value)), maybeFromNullableResult(dto.fax, (value) => PhoneNumber.create(value)),
"fax", "fax",
errors errors
); );
const website = extractOrPushError( const website = extractOrPushError(
maybeFromNullableResult(command.website, (value) => URLAddress.create(value)), maybeFromNullableResult(dto.website, (value) => URLAddress.create(value)),
"website", "website",
errors errors
); );
const languageCode = extractOrPushError( const languageCode = extractOrPushError(
LanguageCode.create(command.languageCode), LanguageCode.create(dto.language_code),
"language_code", "language_code",
errors errors
); );
const currencyCode = extractOrPushError( const currencyCode = extractOrPushError(
CurrencyCode.create(command.currencyCode), CurrencyCode.create(dto.currency_code),
"currency_code", "currency_code",
errors errors
); );

View File

@ -21,15 +21,17 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils"; import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
import type { UpdateSupplierByIdRequestDTO } from "../../../common";
import type { SupplierPatchProps } from "../../domain"; import type { SupplierPatchProps } from "../../domain";
import type { UpdateSupplierPatchInput } from "../use-cases/update-supplier.command";
/** /**
* UpdateSupplierInputMapper * UpdateSupplierInputMapper
* Convierte el DTO a las props validadas (SupplierProps). * Convierte el DTO a las props validadas (SupplierProps).
* No construye directamente el agregado. * No construye directamente el agregado.
* Los campos omitidos no se modifican. El DTO HTTP actual todavía no expresa * Tri-estado:
* borrado explícito mediante null; esa semántica se incorporará al modernizar el contrato. * - campo omitido → no se cambia
* - campo con valor null/"" → se quita el valor -> set(None()),
* - campo con valor no-vacío → se pone el nuevo valor -> set(Some(VO)).
* *
* @param dto - DTO con los datos a cambiar en el cliente * @param dto - DTO con los datos a cambiar en el cliente
* @returns Cambios en las propiedades del cliente * @returns Cambios en las propiedades del cliente
@ -38,13 +40,13 @@ import type { UpdateSupplierPatchInput } from "../use-cases/update-supplier.comm
export interface IUpdateSupplierInputMapper { export interface IUpdateSupplierInputMapper {
map( map(
dto: UpdateSupplierPatchInput, dto: UpdateSupplierByIdRequestDTO,
params: { companyId: UniqueID } params: { companyId: UniqueID }
): Result<SupplierPatchProps>; ): Result<SupplierPatchProps>;
} }
export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper { export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
public map(dto: UpdateSupplierPatchInput, _params: { companyId: UniqueID }) { public map(dto: UpdateSupplierByIdRequestDTO, params: { companyId: UniqueID }) {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const supplierPatchProps: SupplierPatchProps = {}; const supplierPatchProps: SupplierPatchProps = {};
@ -57,8 +59,16 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
); );
}); });
toPatchField(dto.isCompany).ifSet((isCompany) => { toPatchField(dto.is_company).ifSet((is_company) => {
supplierPatchProps.isCompany = isCompany; if (isNullishOrEmpty(is_company)) {
errors.push({ path: "is_company", message: "is_company cannot be empty" });
return;
}
supplierPatchProps.isCompany = extractOrPushError(
Result.ok(Boolean(is_company!)),
"is_company",
errors
);
}); });
toPatchField(dto.name).ifSet((name) => { toPatchField(dto.name).ifSet((name) => {
@ -69,9 +79,9 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
supplierPatchProps.name = extractOrPushError(Name.create(name!), "name", errors); supplierPatchProps.name = extractOrPushError(Name.create(name!), "name", errors);
}); });
toPatchField(dto.tradeName).ifSet((tradeName) => { toPatchField(dto.trade_name).ifSet((trade_name) => {
supplierPatchProps.tradeName = extractOrPushError( supplierPatchProps.tradeName = extractOrPushError(
maybeFromNullableResult(tradeName, (value) => Name.create(value)), maybeFromNullableResult(trade_name, (value) => Name.create(value)),
"trade_name", "trade_name",
errors errors
); );
@ -85,49 +95,49 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
); );
}); });
toPatchField(dto.emailPrimary).ifSet((emailPrimary) => { toPatchField(dto.email_primary).ifSet((email_primary) => {
supplierPatchProps.emailPrimary = extractOrPushError( supplierPatchProps.emailPrimary = extractOrPushError(
maybeFromNullableResult(emailPrimary, (value) => EmailAddress.create(value)), maybeFromNullableResult(email_primary, (value) => EmailAddress.create(value)),
"email_primary", "email_primary",
errors errors
); );
}); });
toPatchField(dto.emailSecondary).ifSet((emailSecondary) => { toPatchField(dto.email_secondary).ifSet((email_secondary) => {
supplierPatchProps.emailSecondary = extractOrPushError( supplierPatchProps.emailSecondary = extractOrPushError(
maybeFromNullableResult(emailSecondary, (value) => EmailAddress.create(value)), maybeFromNullableResult(email_secondary, (value) => EmailAddress.create(value)),
"email_secondary", "email_secondary",
errors errors
); );
}); });
toPatchField(dto.mobilePrimary).ifSet((mobilePrimary) => { toPatchField(dto.mobile_primary).ifSet((mobile_primary) => {
supplierPatchProps.mobilePrimary = extractOrPushError( supplierPatchProps.mobilePrimary = extractOrPushError(
maybeFromNullableResult(mobilePrimary, (value) => PhoneNumber.create(value)), maybeFromNullableResult(mobile_primary, (value) => PhoneNumber.create(value)),
"mobile_primary", "mobile_primary",
errors errors
); );
}); });
toPatchField(dto.mobileSecondary).ifSet((mobileSecondary) => { toPatchField(dto.mobile_secondary).ifSet((mobile_secondary) => {
supplierPatchProps.mobileSecondary = extractOrPushError( supplierPatchProps.mobilePrimary = extractOrPushError(
maybeFromNullableResult(mobileSecondary, (value) => PhoneNumber.create(value)), maybeFromNullableResult(mobile_secondary, (value) => PhoneNumber.create(value)),
"mobile_secondary", "mobile_secondary",
errors errors
); );
}); });
toPatchField(dto.phonePrimary).ifSet((phonePrimary) => { toPatchField(dto.phone_primary).ifSet((phone_primary) => {
supplierPatchProps.phonePrimary = extractOrPushError( supplierPatchProps.phonePrimary = extractOrPushError(
maybeFromNullableResult(phonePrimary, (value) => PhoneNumber.create(value)), maybeFromNullableResult(phone_primary, (value) => PhoneNumber.create(value)),
"phone_primary", "phone_primary",
errors errors
); );
}); });
toPatchField(dto.phoneSecondary).ifSet((phoneSecondary) => { toPatchField(dto.phone_secondary).ifSet((phone_secondary) => {
supplierPatchProps.phoneSecondary = extractOrPushError( supplierPatchProps.phoneSecondary = extractOrPushError(
maybeFromNullableResult(phoneSecondary, (value) => PhoneNumber.create(value)), maybeFromNullableResult(phone_secondary, (value) => PhoneNumber.create(value)),
"phone_secondary", "phone_secondary",
errors errors
); );
@ -149,7 +159,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
); );
}); });
toPatchField(dto.languageCode).ifSet((languageCode) => { toPatchField(dto.language_code).ifSet((languageCode) => {
if (isNullishOrEmpty(languageCode)) { if (isNullishOrEmpty(languageCode)) {
errors.push({ path: "language_code", message: "Language code cannot be empty" }); errors.push({ path: "language_code", message: "Language code cannot be empty" });
return; return;
@ -162,7 +172,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
); );
}); });
toPatchField(dto.currencyCode).ifSet((currencyCode) => { toPatchField(dto.currency_code).ifSet((currencyCode) => {
if (isNullishOrEmpty(currencyCode)) { if (isNullishOrEmpty(currencyCode)) {
errors.push({ path: "currency_code", message: "Currency code cannot be empty" }); errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
return; return;
@ -194,7 +204,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
} }
public mapPostalAddress( public mapPostalAddress(
dto: UpdateSupplierPatchInput, dto: UpdateSupplierByIdRequestDTO,
errors: ValidationErrorDetail[] errors: ValidationErrorDetail[]
): PostalAddressPatchProps | undefined { ): PostalAddressPatchProps | undefined {
const postalAddressPatchProps: PostalAddressPatchProps = {}; const postalAddressPatchProps: PostalAddressPatchProps = {};
@ -231,7 +241,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
); );
}); });
toPatchField(dto.postalCode).ifSet((postalCode) => { toPatchField(dto.postal_code).ifSet((postalCode) => {
postalAddressPatchProps.postalCode = extractOrPushError( postalAddressPatchProps.postalCode = extractOrPushError(
maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)), maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)),
"postal_code", "postal_code",

View File

@ -69,4 +69,14 @@ export interface ISupplierRepository {
transaction: unknown transaction: unknown
): Promise<Result<Collection<SupplierSummary>, Error>>; ): Promise<Result<Collection<SupplierSummary>, Error>>;
/**
* Elimina un Supplier por su ID, dentro de una empresa.
* Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía.
*
*/
deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: unknown
): Promise<Result<boolean, Error>>;
} }

View File

@ -1,3 +1,4 @@
export * from "./supplier-creator.service"; export * from "./supplier-creator.service";
export * from "./supplier-finder.service"; export * from "./supplier-finder.service";
export * from "./supplier-public-services.interface";
export * from "./supplier-updater.service"; export * from "./supplier-updater.service";

View File

@ -0,0 +1,22 @@
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
import type { ISupplierCreateProps, Supplier } from "../../domain";
export interface ISupplierServicesContext {
transaction: unknown;
companyId: UniqueID;
}
export interface ISupplierPublicServices {
findSupplierByTIN: (
tin: TINNumber,
context: ISupplierServicesContext
) => Promise<Result<Supplier, Error>>;
createSupplier: (
id: UniqueID,
props: ISupplierCreateProps,
context: ISupplierServicesContext
) => Promise<Result<Supplier, Error>>;
}

View File

@ -1,3 +1,4 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Supplier, SupplierPatchProps } from "../../domain"; import type { Supplier, SupplierPatchProps } from "../../domain";
@ -5,7 +6,8 @@ import type { ISupplierRepository } from "../repositories";
export interface ISupplierUpdater { export interface ISupplierUpdater {
update(params: { update(params: {
supplier: Supplier; companyId: UniqueID;
id: UniqueID;
props: SupplierPatchProps; props: SupplierPatchProps;
transaction: unknown; transaction: unknown;
}): Promise<Result<Supplier, Error>>; }): Promise<Result<Supplier, Error>>;
@ -23,11 +25,21 @@ export class SupplierUpdater implements ISupplierUpdater {
} }
async update(params: { async update(params: {
supplier: Supplier; companyId: UniqueID;
id: UniqueID;
props: SupplierPatchProps; props: SupplierPatchProps;
transaction: unknown; transaction: unknown;
}): Promise<Result<Supplier, Error>> { }): Promise<Result<Supplier, Error>> {
const { supplier, props, transaction } = params; const { companyId, id, props, transaction } = params;
// Recuperar agregado existente
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existingResult.isFailure) {
return Result.fail(existingResult.error);
}
const supplier = existingResult.data;
// Aplicar cambios en el agregado // Aplicar cambios en el agregado
const updateResult = supplier.update(props); const updateResult = supplier.update(props);

View File

@ -0,0 +1,2 @@
export * from "./supplier-snapshot.interface";
export * from "./supplier-snapshot-builder";

View File

@ -2,13 +2,14 @@ import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyString } from "@repo/rdx-ddd"; import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { Supplier } from "../../../domain"; import type { Supplier } from "../../../domain";
import type { SupplierDetailDTO } from "../../../../common";
import type { ISupplierFullSnapshot } from "./supplier-snapshot.interface";
export interface ISupplierFullSnapshotBuilder export interface ISupplierFullSnapshotBuilder
extends ISnapshotBuilder<Supplier, SupplierDetailDTO> {} extends ISnapshotBuilder<Supplier, ISupplierFullSnapshot> {}
export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder { export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder {
toOutput(supplier: Supplier): SupplierDetailDTO { toOutput(supplier: Supplier): ISupplierFullSnapshot {
const address = supplier.address.toPrimitive(); const address = supplier.address.toPrimitive();
return { return {
@ -17,7 +18,7 @@ export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder
status: supplier.isActive ? "active" : "inactive", status: supplier.isActive ? "active" : "inactive",
reference: maybeToEmptyString(supplier.reference, (value) => value.toPrimitive()), reference: maybeToEmptyString(supplier.reference, (value) => value.toPrimitive()),
is_company: supplier.isCompany, is_company: String(supplier.isCompany),
name: supplier.name.toPrimitive(), name: supplier.name.toPrimitive(),
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toPrimitive()), trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toPrimitive()),
tin: maybeToEmptyString(supplier.tin, (value) => value.toPrimitive()), tin: maybeToEmptyString(supplier.tin, (value) => value.toPrimitive()),

View File

@ -0,0 +1,35 @@
export interface ISupplierFullSnapshot {
id: string;
company_id: string;
status: string;
reference: string;
is_company: string;
name: string;
trade_name: string;
tin: string;
street: string;
street2: string;
city: string;
province: string;
postal_code: string;
country: string;
email_primary: string;
email_secondary: string;
phone_primary: string;
phone_secondary: string;
mobile_primary: string;
mobile_secondary: string;
fax: string;
website: string;
language_code: string;
currency_code: string;
metadata?: Record<string, string>;
}

View File

@ -0,0 +1,2 @@
export * from "./supplier-summary-snapshot.interface";
export * from "./supplier-summary-snapshot-builder";

View File

@ -2,13 +2,14 @@ import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyString } from "@repo/rdx-ddd"; import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { SupplierSummary } from "../../models"; import type { SupplierSummary } from "../../models";
import type { SupplierSummaryDTO } from "../../../../common";
import type { ISupplierSummarySnapshot } from "./supplier-summary-snapshot.interface";
export interface ISupplierSummarySnapshotBuilder export interface ISupplierSummarySnapshotBuilder
extends ISnapshotBuilder<SupplierSummary, SupplierSummaryDTO> {} extends ISnapshotBuilder<SupplierSummary, ISupplierSummarySnapshot> {}
export class SupplierSummarySnapshotBuilder implements ISupplierSummarySnapshotBuilder { export class SupplierSummarySnapshotBuilder implements ISupplierSummarySnapshotBuilder {
toOutput(supplier: SupplierSummary): SupplierSummaryDTO { toOutput(supplier: SupplierSummary): ISupplierSummarySnapshot {
const { address } = supplier; const { address } = supplier;
return { return {
@ -17,7 +18,7 @@ export class SupplierSummarySnapshotBuilder implements ISupplierSummarySnapshotB
status: supplier.isActive ? "active" : "inactive", status: supplier.isActive ? "active" : "inactive",
reference: maybeToEmptyString(supplier.reference, (value) => value.toString()), reference: maybeToEmptyString(supplier.reference, (value) => value.toString()),
is_company: supplier.isCompany, is_company: String(supplier.isCompany),
name: supplier.name.toString(), name: supplier.name.toString(),
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toString()), trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toString()),
tin: maybeToEmptyString(supplier.tin, (value) => value.toString()), tin: maybeToEmptyString(supplier.tin, (value) => value.toString()),

View File

@ -0,0 +1,35 @@
export type ISupplierSummarySnapshot = {
id: string;
company_id: string;
status: string;
reference: string;
is_company: string;
name: string;
trade_name: string;
tin: string;
street: string;
street2: string;
city: string;
postal_code: string;
province: string;
country: string;
email_primary: string;
email_secondary: string;
phone_primary: string;
phone_secondary: string;
mobile_primary: string;
mobile_secondary: string;
fax: string;
website: string;
language_code: string;
currency_code: string;
metadata?: Record<string, string>;
};

View File

@ -2,14 +2,16 @@ import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { ICreateSupplierInputMapper } from "../mappers"; import type { CreateSupplierRequestDTO } from "../../../common";
import type { ISupplierCreator } from "../services"; import type { ISupplierCreator } from "../services";
import type { ISupplierFullSnapshotBuilder } from "../snapshot-builders"; import type {
import type { CreateSupplierCommand } from "./create-supplier.command"; ICreateSupplierInputMapper,
ISupplierFullSnapshotBuilder,
} from "../snapshot-builders";
type CreateSupplierUseCaseInput = { type CreateSupplierUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
command: CreateSupplierCommand; dto: CreateSupplierRequestDTO;
}; };
type CreateSupplierUseCaseDeps = { type CreateSupplierUseCaseDeps = {
@ -33,9 +35,9 @@ export class CreateSupplierUseCase {
} }
public execute(params: CreateSupplierUseCaseInput) { public execute(params: CreateSupplierUseCaseInput) {
const { command, companyId } = params; const { dto, companyId } = params;
const mappedPropsResult = this.dtoMapper.map(command, { companyId }); const mappedPropsResult = this.dtoMapper.map(dto, { companyId });
if (mappedPropsResult.isFailure) { if (mappedPropsResult.isFailure) {
return mappedPropsResult; return mappedPropsResult;
} }

View File

@ -0,0 +1,6 @@
export * from './activate-supplier.use-case';
export * from './create-supplier.use-case';
export * from './deactivate-supplier.use-case';
export * from './get-supplier.use-case';
export * from './list-suppliers.use-case';
export * from './update-supplier.use-case';

View File

@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail
MODULE_ROOT="./"
DIRS=(
"$MODULE_ROOT"
"$MODULE_ROOT/application"
"$MODULE_ROOT/application/di"
"$MODULE_ROOT/application/dtos"
"$MODULE_ROOT/application/mappers"
"$MODULE_ROOT/application/repositories"
"$MODULE_ROOT/application/services"
"$MODULE_ROOT/application/use-cases"
"$MODULE_ROOT/domain"
"$MODULE_ROOT/domain/aggregates"
"$MODULE_ROOT/domain/collections"
"$MODULE_ROOT/domain/errors"
"$MODULE_ROOT/domain/value-objects"
"$MODULE_ROOT/infrastructure"
"$MODULE_ROOT/infrastructure/di"
"$MODULE_ROOT/infrastructure/express"
"$MODULE_ROOT/infrastructure/express/controllers"
"$MODULE_ROOT/infrastructure/persistence"
"$MODULE_ROOT/infrastructure/persistence/sequelize"
"$MODULE_ROOT/infrastructure/persistence/sequelize/mappers"
"$MODULE_ROOT/infrastructure/persistence/sequelize/models"
"$MODULE_ROOT/infrastructure/persistence/sequelize/repositories"
)
FILES=(
"$MODULE_ROOT/application/repositories/supplier-repository.interface.ts"
"$MODULE_ROOT/application/services/supplier-creator.service.ts"
"$MODULE_ROOT/application/services/supplier-updater.service.ts"
"$MODULE_ROOT/application/services/supplier-finder.service.ts"
"$MODULE_ROOT/application/use-cases/create-supplier.use-case.ts"
"$MODULE_ROOT/application/use-cases/update-supplier.use-case.ts"
"$MODULE_ROOT/application/use-cases/get-supplier.use-case.ts"
"$MODULE_ROOT/application/use-cases/list-suppliers.use-case.ts"
"$MODULE_ROOT/application/use-cases/activate-supplier.use-case.ts"
"$MODULE_ROOT/application/use-cases/deactivate-supplier.use-case.ts"
"$MODULE_ROOT/domain/aggregates/supplier.aggregate.ts"
"$MODULE_ROOT/domain/collections/supplier-taxes.collection.ts"
"$MODULE_ROOT/domain/errors/supplier-domain.errors.ts"
"$MODULE_ROOT/domain/value-objects/supplier-status.value-object.ts"
"$MODULE_ROOT/infrastructure/express/supplier.routes.ts"
"$MODULE_ROOT/infrastructure/persistence/sequelize/models/supplier-sequelize.model.ts"
"$MODULE_ROOT/infrastructure/persistence/sequelize/mappers/sequelize-supplier-domain.mapper.ts"
"$MODULE_ROOT/infrastructure/persistence/sequelize/mappers/supplier-summary.mapper.ts"
"$MODULE_ROOT/infrastructure/persistence/sequelize/repositories/sequelize-supplier.repository.ts"
)
mkdir -p "${DIRS[@]}"
touch "${FILES[@]}"
create_index() {
local dir="$1"
local index_file="$dir/index.ts"
: > "$index_file"
mapfile -t ts_files < <(
find "$dir" -maxdepth 1 -type f -name "*.ts" ! -name "index.ts" \
| sort
)
mapfile -t subdirs < <(
find "$dir" -maxdepth 1 -mindepth 1 -type d \
| sort
)
for file in "${ts_files[@]}"; do
local base
base="$(basename "$file" .ts)"
echo "export * from './$base';" >> "$index_file"
done
for subdir in "${subdirs[@]}"; do
local name
name="$(basename "$subdir")"
echo "export * from './$name';" >> "$index_file"
done
}
for dir in "${DIRS[@]}"; do
create_index "$dir"
done
echo "Módulo supplier creado correctamente en $MODULE_ROOT"

View File

@ -5,9 +5,9 @@ import {
type LanguageCode, type LanguageCode,
type Name, type Name,
type PhoneNumber, type PhoneNumber,
type PostalAddress,
type PostalAddressPatchProps, type PostalAddressPatchProps,
type PostalAddressProps, type PostalAddressProps,
PostalAddress,
type TINNumber, type TINNumber,
type URLAddress, type URLAddress,
type UniqueID, type UniqueID,
@ -87,9 +87,8 @@ export interface ISupplier {
readonly currencyCode: CurrencyCode; readonly currencyCode: CurrencyCode;
} }
export type SupplierInternalProps = Omit<ISupplierCreateProps, "address" | "status"> & { type SupplierInternalProps = Omit<ISupplierCreateProps, "address"> & {
address: PostalAddress; readonly address: PostalAddress;
status: SupplierStatus;
}; };
/** /**
@ -115,7 +114,6 @@ export class Supplier extends AggregateRoot<SupplierInternalProps> implements IS
{ {
...internalProps, ...internalProps,
address: postalAddressResult.data, address: postalAddressResult.data,
status: props.status ?? SupplierStatus.active(),
}, },
id id
); );
@ -131,7 +129,7 @@ export class Supplier extends AggregateRoot<SupplierInternalProps> implements IS
return Result.ok(contact); return Result.ok(contact);
} }
private static validateCreateProps(_props: ISupplierCreateProps): Result<void, Error> { private static validateCreateProps(props: ISupplierCreateProps): Result<void, Error> {
return Result.ok(); return Result.ok();
} }
@ -146,27 +144,18 @@ export class Supplier extends AggregateRoot<SupplierInternalProps> implements IS
public update(partialSupplier: SupplierPatchProps): Result<Supplier, Error> { public update(partialSupplier: SupplierPatchProps): Result<Supplier, Error> {
const { address: partialAddress, ...rest } = partialSupplier; const { address: partialAddress, ...rest } = partialSupplier;
const nextProps: SupplierInternalProps = {
...this.props, Object.assign(this.props, rest);
...rest,
};
if (partialAddress) { if (partialAddress) {
const addressResult = PostalAddress.create({ const addressResult = this.address.update(partialAddress);
...this.address.getProps(),
...partialAddress,
});
if (addressResult.isFailure) { if (addressResult.isFailure) {
return Result.fail(addressResult.error); return Result.fail(addressResult.error);
} }
nextProps.address = addressResult.data;
} }
Object.assign(this.props, nextProps); return Result.ok();
return Result.ok(this);
} }
public activate(): Result<Supplier, Error> { public activate(): Result<Supplier, Error> {

View File

@ -1,7 +1,10 @@
import type { IModuleServer } from "@erp/core/api"; import type { IModuleServer } from "@erp/core/api";
import type { ISupplierPublicServices } from "./application";
import { models, suppliersRouter } from "./infrastructure"; import { models, suppliersRouter } from "./infrastructure";
import { buildSuppliersDependencies } from "./infrastructure/di"; import { buildSupplierPublicServices, buildSuppliersDependencies } from "./infrastructure/di";
export * from "./infrastructure/persistence/sequelize";
export const suppliersAPIModule: IModuleServer = { export const suppliersAPIModule: IModuleServer = {
name: "suppliers", name: "suppliers",
@ -16,11 +19,17 @@ export const suppliersAPIModule: IModuleServer = {
* - NO conecta infraestructura * - NO conecta infraestructura
*/ */
async setup(params) { async setup(params) {
const { logger } = params; const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
// El grafo se construye una sola vez y permanece privado al módulo. // 1) Dominio interno
const internal = buildSuppliersDependencies(params); const internal = buildSuppliersDependencies(params);
// 2) Servicios públicos (Application Services)
const suppliersServices: ISupplierPublicServices = buildSupplierPublicServices(
params,
internal
);
logger.info("🚀 Suppliers module dependencies registered", { logger.info("🚀 Suppliers module dependencies registered", {
label: this.name, label: this.name,
}); });
@ -29,6 +38,11 @@ export const suppliersAPIModule: IModuleServer = {
// Modelos Sequelize del módulo // Modelos Sequelize del módulo
models, models,
// Servicios expuestos a otros módulos
services: {
general: suppliersServices, // 'suppliers:general'
},
// Implementación privada del módulo // Implementación privada del módulo
internal, internal,
}; };
@ -42,7 +56,7 @@ export const suppliersAPIModule: IModuleServer = {
* - NO construye dominio * - NO construye dominio
*/ */
async start(params) { async start(params) {
const { logger } = params; const { app, baseRoutePath, logger, getInternal } = params;
// Registro de rutas HTTP // Registro de rutas HTTP
suppliersRouter(params); suppliersRouter(params);

View File

@ -0,0 +1,2 @@
export * from "./supplier-public-services";
export * from "./suppliers.di";

View File

@ -1,3 +1,5 @@
import type { ICatalogs } from "@erp/core/api";
import { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../persistence"; import { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../persistence";
export interface ISupplierPersistenceMappers { export interface ISupplierPersistenceMappers {
@ -7,9 +9,13 @@ export interface ISupplierPersistenceMappers {
//createMapper: CreateSupplierInputMapper; //createMapper: CreateSupplierInputMapper;
} }
export const buildSupplierPersistenceMappers = (): ISupplierPersistenceMappers => { export const buildSupplierPersistenceMappers = (
catalogs: ICatalogs
): ISupplierPersistenceMappers => {
const { taxCatalog } = catalogs;
// Mappers para el repositorio // Mappers para el repositorio
const domainMapper = new SequelizeSupplierDomainMapper(); const domainMapper = new SequelizeSupplierDomainMapper({ taxCatalog });
const summaryMapper = new SequelizeSupplierSummaryMapper(); const summaryMapper = new SequelizeSupplierSummaryMapper();
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado // Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado

View File

@ -0,0 +1,65 @@
import { type SetupParams, buildCatalogs } from "@erp/core/api";
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import {
type ISupplierPublicServices,
type ISupplierServicesContext,
buildSupplierCreator,
buildSupplierFinder,
} from "../../application";
import type { ISupplierCreateProps } from "../../domain";
import { buildSupplierPersistenceMappers } from "./supplier-persistence-mappers.di";
import { buildSupplierRepository } from "./supplier-repositories.di";
import type { SuppliersInternalDeps } from "./suppliers.di";
export function buildSupplierPublicServices(
params: SetupParams,
deps: SuppliersInternalDeps
): ISupplierPublicServices {
const { database } = params;
const catalogs = buildCatalogs();
// Infrastructure
const persistenceMappers = buildSupplierPersistenceMappers(catalogs);
const repository = buildSupplierRepository({ database, mappers: persistenceMappers });
const finder = buildSupplierFinder({ repository });
const creator = buildSupplierCreator({ repository });
return {
findSupplierByTIN: async (tin: TINNumber, context: ISupplierServicesContext) => {
const { companyId, transaction } = context;
const supplierResult = await finder.findSupplierByTIN(companyId, tin, transaction);
if (supplierResult.isFailure) {
return Result.fail(supplierResult.error);
}
return Result.ok(supplierResult.data);
},
createSupplier: async (
id: UniqueID,
props: ISupplierCreateProps,
context: ISupplierServicesContext
) => {
const { companyId, transaction } = context;
const supplierResult = await creator.create({
companyId,
id,
props,
transaction,
});
if (supplierResult.isFailure) {
return Result.fail(supplierResult.error);
}
return Result.ok(supplierResult.data);
},
};
}

View File

@ -1,19 +1,17 @@
import { type ModuleParams, buildTransactionManager } from "@erp/core/api"; import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
import { import {
type CreateSupplierUseCase, type CreateSupplierUseCase,
type GetSupplierByIdUseCase, type GetSupplierByIdUseCase,
type ListSuppliersUseCase, type ListSuppliersUseCase,
type UpdateSupplierUseCase,
buildCreateSupplierUseCase, buildCreateSupplierUseCase,
buildGetSupplierByIdUseCase, buildGetSupplierByIdUseCase,
buildListSuppliersUseCase, buildListSuppliersUseCase,
buildSupplierUpdater,
buildUpdateSupplierUseCase,
buildSupplierCreator, buildSupplierCreator,
buildSupplierFinder, buildSupplierFinder,
buildSupplierInputMappers, buildSupplierInputMappers,
buildSupplierSnapshotBuilders, buildSupplierSnapshotBuilders,
buildSupplierUpdater,
} from "../../application"; } from "../../application";
import { buildSupplierPersistenceMappers } from "./supplier-persistence-mappers.di"; import { buildSupplierPersistenceMappers } from "./supplier-persistence-mappers.di";
@ -24,7 +22,12 @@ export type SuppliersInternalDeps = {
listSuppliers: () => ListSuppliersUseCase; listSuppliers: () => ListSuppliersUseCase;
getSupplierById: () => GetSupplierByIdUseCase; getSupplierById: () => GetSupplierByIdUseCase;
createSupplier: () => CreateSupplierUseCase; createSupplier: () => CreateSupplierUseCase;
updateSupplier: () => UpdateSupplierUseCase; //updateSupplier: () => UpdateSupplierUseCase;
/*
deleteSupplier: () => DeleteSupplierUseCase;
changeStatusSupplier: () => ChangeStatusSupplierUseCase;*/
}; };
}; };
@ -33,17 +36,20 @@ export function buildSuppliersDependencies(params: ModuleParams): SuppliersInter
// Infrastructure // Infrastructure
const transactionManager = buildTransactionManager(database); const transactionManager = buildTransactionManager(database);
const persistenceMappers = buildSupplierPersistenceMappers(); const catalogs = buildCatalogs();
const persistenceMappers = buildSupplierPersistenceMappers(catalogs);
const repository = buildSupplierRepository({ database, mappers: persistenceMappers }); const repository = buildSupplierRepository({ database, mappers: persistenceMappers });
//const numberService = buildSupplierNumberGenerator();
// Application helpers // Application helpers
const inputMappers = buildSupplierInputMappers(); const inputMappers = buildSupplierInputMappers(catalogs);
const finder = buildSupplierFinder({ repository }); const finder = buildSupplierFinder({ repository });
const creator = buildSupplierCreator({ repository }); const creator = buildSupplierCreator({ repository });
const updater = buildSupplierUpdater({ repository }); const updater = buildSupplierUpdater({ repository });
const snapshotBuilders = buildSupplierSnapshotBuilders(); const snapshotBuilders = buildSupplierSnapshotBuilders();
//const documentGeneratorPipeline = buildSupplierDocumentService(params);
// Internal use cases (factories) // Internal use cases (factories)
return { return {
@ -70,14 +76,13 @@ export function buildSuppliersDependencies(params: ModuleParams): SuppliersInter
transactionManager, transactionManager,
}), }),
updateSupplier: () => /*updateSupplier: () =>
buildUpdateSupplierUseCase({ buildUpdateSupplierUseCase({
finder,
updater, updater,
dtoMapper: inputMappers.updateInputMapper, dtoMapper: inputMappers.updateInputMapper,
fullSnapshotBuilder: snapshotBuilders.full, fullSnapshotBuilder: snapshotBuilders.full,
transactionManager, transactionManager,
}), }),*/
}, },
}; };
} }

View File

@ -8,13 +8,9 @@ import {
import type { CreateSupplierRequestDTO } from "../../../../common/dto"; import type { CreateSupplierRequestDTO } from "../../../../common/dto";
import type { CreateSupplierUseCase } from "../../../application"; import type { CreateSupplierUseCase } from "../../../application";
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper"; import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
import type { CreateSupplierRequestMapper } from "../mappers/create-supplier-request.mapper";
export class CreateSupplierController extends ExpressController { export class CreateSupplierController extends ExpressController {
public constructor( public constructor(private readonly useCase: CreateSupplierUseCase) {
private readonly useCase: CreateSupplierUseCase,
private readonly requestMapper: CreateSupplierRequestMapper
) {
super(); super();
this.errorMapper = suppliersApiErrorMapper; this.errorMapper = suppliersApiErrorMapper;
@ -32,9 +28,8 @@ export class CreateSupplierController extends ExpressController {
return this.forbiddenError("Tenant ID not found"); return this.forbiddenError("Tenant ID not found");
} }
const dto = this.req.body as CreateSupplierRequestDTO; const dto = this.req.body as CreateSupplierRequestDTO;
const command = this.requestMapper.map(dto);
const result = await this.useCase.execute({ command, companyId }); const result = await this.useCase.execute({ dto, companyId });
return result.match( return result.match(
(data) => this.created(data), (data) => this.created(data),

View File

@ -0,0 +1,38 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { DeleteSupplierUseCase } from "../../../application";
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
export class DeleteSupplierController extends ExpressController {
public constructor(private readonly useCase: DeleteSupplierUseCase) {
super();
this.errorMapper = suppliersApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
async executeImpl(): Promise<any> {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { supplier_id } = this.req.params;
const result = await this.useCase.execute({ supplier_id, companyId });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -5,11 +5,11 @@ import {
requireCompanyContextGuard, requireCompanyContextGuard,
} from "@erp/core/api"; } from "@erp/core/api";
import type { GetSupplierByIdUseCase } from "../../../application"; import type { GetSupplierUseCase } from "../../../application";
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper"; import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
export class GetSupplierController extends ExpressController { export class GetSupplierController extends ExpressController {
public constructor(private readonly useCase: GetSupplierByIdUseCase) { public constructor(private readonly useCase: GetSupplierUseCase) {
super(); super();
this.errorMapper = suppliersApiErrorMapper; this.errorMapper = suppliersApiErrorMapper;

View File

@ -1,4 +1,5 @@
export * from "./create-supplier.controller"; export * from "./create-supplier.controller";
export * from "./delete-supplier.controller";
export * from "./get-supplier.controller"; export * from "./get-supplier.controller";
export * from "./list-suppliers.controller"; export * from "./list-suppliers.controller";
export * from "./update-supplier.controller"; export * from "./update-supplier.controller";

View File

@ -8,13 +8,9 @@ import {
import type { UpdateSupplierByIdRequestDTO } from "../../../../common/dto"; import type { UpdateSupplierByIdRequestDTO } from "../../../../common/dto";
import type { UpdateSupplierUseCase } from "../../../application"; import type { UpdateSupplierUseCase } from "../../../application";
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper"; import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
import type { UpdateSupplierByIdRequestMapper } from "../mappers/update-supplier-by-id-request.mapper";
export class UpdateSupplierController extends ExpressController { export class UpdateSupplierController extends ExpressController {
public constructor( public constructor(private readonly useCase: UpdateSupplierUseCase) {
private readonly useCase: UpdateSupplierUseCase,
private readonly requestMapper: UpdateSupplierByIdRequestMapper
) {
super(); super();
this.errorMapper = suppliersApiErrorMapper; this.errorMapper = suppliersApiErrorMapper;
@ -33,9 +29,8 @@ export class UpdateSupplierController extends ExpressController {
} }
const { supplier_id } = this.req.params; const { supplier_id } = this.req.params;
const dto = this.req.body as UpdateSupplierByIdRequestDTO; const dto = this.req.body as UpdateSupplierByIdRequestDTO;
const command = this.requestMapper.map({ supplierId: supplier_id, dto });
const result = await this.useCase.execute({ companyId, command }); const result = await this.useCase.execute({ supplier_id, companyId, dto });
return result.match( return result.match(
(data) => this.ok(data), (data) => this.ok(data),

View File

@ -17,8 +17,6 @@ import {
ListSuppliersController, ListSuppliersController,
UpdateSupplierController, UpdateSupplierController,
} from "./controllers"; } from "./controllers";
import { CreateSupplierRequestMapper } from "./mappers/create-supplier-request.mapper";
import { UpdateSupplierByIdRequestMapper } from "./mappers/update-supplier-by-id-request.mapper";
export const suppliersRouter = (params: StartParams) => { export const suppliersRouter = (params: StartParams) => {
const { app, config, getInternal } = params; const { app, config, getInternal } = params;
@ -80,22 +78,35 @@ export const suppliersRouter = (params: StartParams) => {
validateRequest(CreateSupplierRequestSchema, "body"), validateRequest(CreateSupplierRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.createSupplier(); const useCase = deps.useCases.createSupplier();
const controller = new CreateSupplierController(useCase, new CreateSupplierRequestMapper()); const controller = new CreateSupplierController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
router.patch( router.put(
"/:supplier_id", "/:supplier_id",
//checkTabContext,
validateRequest(UpdateSupplierByIdParamsRequestSchema, "params"), validateRequest(UpdateSupplierByIdParamsRequestSchema, "params"),
validateRequest(UpdateSupplierByIdRequestSchema, "body"), validateRequest(UpdateSupplierByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const controller = new UpdateSupplierController( const useCase = deps.useCases.updateSupplier();
deps.useCases.updateSupplier(), const controller = new UpdateSupplierController(useCase);
new UpdateSupplierByIdRequestMapper()
);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
/*router.delete(
"/:supplier_id",
//checkTabContext,
validateRequest(DeleteSupplierByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.delete();
const controller = new DeleteSupplierController(useCase);
return controller.execute(req, res, next);
}
);*/
app.use(`${config.server.apiBasePath}/suppliers`, router); app.use(`${config.server.apiBasePath}/suppliers`, router);
}; };

View File

@ -1,4 +1,5 @@
import { SequelizeDomainMapper } from "@erp/core/api"; import type { TaxCatalogProvider } from "@erp/core";
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import { import {
City, City,
Country, Country,
@ -22,25 +23,22 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import { type ISupplierCreateProps, Supplier, SupplierStatus } from "../../../../../domain";
type SupplierInternalProps,
Supplier,
SupplierStatus,
SUPPLIER_STATUS,
} from "../../../../../domain";
import type { SupplierCreationAttributes, SupplierModel } from "../../../sequelize"; import type { SupplierCreationAttributes, SupplierModel } from "../../../sequelize";
const PERSISTED_STATUS_TO_DOMAIN: Readonly<Record<string, SUPPLIER_STATUS>> = {
active: SUPPLIER_STATUS.ACTIVE,
inactive: SUPPLIER_STATUS.INACTIVE,
} as const;
export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper< export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
SupplierModel, SupplierModel,
SupplierCreationAttributes, SupplierCreationAttributes,
Supplier Supplier
> { > {
public mapToDomain(source: SupplierModel): Result<Supplier, Error> { private readonly taxCatalog: TaxCatalogProvider;
constructor(params: { taxCatalog: TaxCatalogProvider }) {
super();
this.taxCatalog = params.taxCatalog;
}
public mapToDomain(source: SupplierModel, params?: MapperParamsType): Result<Supplier, Error> {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
@ -52,11 +50,7 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
); );
const isCompany = source.is_company; const isCompany = source.is_company;
const status = extractOrPushError( const status = extractOrPushError(SupplierStatus.create(source.status), "status", errors);
SupplierStatus.create(PERSISTED_STATUS_TO_DOMAIN[source.status] ?? source.status),
"status",
errors
);
const reference = extractOrPushError( const reference = extractOrPushError(
maybeFromNullableResult(source.reference, (value) => Name.create(value)), maybeFromNullableResult(source.reference, (value) => Name.create(value)),
"reference", "reference",
@ -194,7 +188,7 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
return Result.fail(new ValidationErrorCollection("Supplier props mapping failed", errors)); return Result.fail(new ValidationErrorCollection("Supplier props mapping failed", errors));
} }
const supplierProps: SupplierInternalProps = { const supplierProps: ISupplierCreateProps = {
companyId: companyId!, companyId: companyId!,
status: status!, status: status!,
reference: reference!, reference: reference!,
@ -219,13 +213,16 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
currencyCode: currencyCode!, currencyCode: currencyCode!,
}; };
return Result.ok(Supplier.rehydrate(supplierProps, supplierId!)); return Supplier.create(supplierProps, supplierId);
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(err as Error); return Result.fail(err as Error);
} }
} }
public mapToPersistence(source: Supplier): Result<SupplierCreationAttributes, Error> { public mapToPersistence(
source: Supplier,
params?: MapperParamsType
): Result<SupplierCreationAttributes, Error> {
const supplierValues: Partial<SupplierCreationAttributes> = { const supplierValues: Partial<SupplierCreationAttributes> = {
id: source.id.toPrimitive(), id: source.id.toPrimitive(),
company_id: source.companyId.toPrimitive(), company_id: source.companyId.toPrimitive(),

View File

@ -1,4 +1,4 @@
import { SequelizeQueryMapper } from "@erp/core/api"; import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
import { import {
City, City,
Country, Country,
@ -22,19 +22,17 @@ import {
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { SupplierSummary } from "../../../../../application"; import type { SupplierSummary } from "../../../../../application";
import { SupplierStatus, SUPPLIER_STATUS } from "../../../../../domain"; import { SupplierStatus } from "../../../../../domain";
import type { SupplierModel } from "../../models"; import type { SupplierModel } from "../../models";
const PERSISTED_STATUS_TO_DOMAIN: Readonly<Record<string, SUPPLIER_STATUS>> = {
active: SUPPLIER_STATUS.ACTIVE,
inactive: SUPPLIER_STATUS.INACTIVE,
} as const;
export class SequelizeSupplierSummaryMapper extends SequelizeQueryMapper< export class SequelizeSupplierSummaryMapper extends SequelizeQueryMapper<
SupplierModel, SupplierModel,
SupplierSummary SupplierSummary
> { > {
public mapToReadModel(raw: SupplierModel): Result<SupplierSummary, Error> { public mapToReadModel(
raw: SupplierModel,
params?: MapperParamsType
): Result<SupplierSummary, Error> {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
// 1) Valores escalares (atributos generales) // 1) Valores escalares (atributos generales)
@ -42,11 +40,7 @@ export class SequelizeSupplierSummaryMapper extends SequelizeQueryMapper<
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
const isCompany = raw.is_company; const isCompany = raw.is_company;
const status = extractOrPushError( const status = extractOrPushError(SupplierStatus.create(raw.status), "status", errors);
SupplierStatus.create(PERSISTED_STATUS_TO_DOMAIN[raw.status] ?? raw.status),
"status",
errors
);
const reference = extractOrPushError( const reference = extractOrPushError(
maybeFromNullableResult(raw.reference, (value) => Name.create(value)), maybeFromNullableResult(raw.reference, (value) => Name.create(value)),
"reference", "reference",

View File

@ -217,13 +217,12 @@ export default (database: Sequelize) => {
{ name: "idx_tin", fields: ["tin"] }, // <- para servicios externos { name: "idx_tin", fields: ["tin"] }, // <- para servicios externos
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get { name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
// Quiksearch => "CriteriaToSequelizeConverter" construye la sentencia SQL de búsqueda FULLTEXT // Para búsquedas simples => se hace con el "CriteriaToSequelizeConverter"
// a partir de este índice. Las columnas deben coincidir con los campos de búsqueda rápida definidos en el repositorio. /*{
{ name: "ft_supplier",
name: "idx_suppliers_fulltext_search",
type: "FULLTEXT", type: "FULLTEXT",
fields: ["name", "trade_name", "reference", "tin", "email_primary", "mobile_primary"], fields: ["name", "trade_name", "reference", "tin", "email_primary", "mobile_primary"],
}, },*/
], ],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope whereMergeStrategy: "and", // <- cómo tratar el merge de un scope

View File

@ -11,7 +11,7 @@ import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction }
import type { ISupplierRepository, SupplierSummary } from "../../../../application"; import type { ISupplierRepository, SupplierSummary } from "../../../../application";
import type { Supplier } from "../../../../domain"; import type { Supplier } from "../../../../domain";
import type { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../mappers"; import type { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../../mappers";
import { SupplierModel } from "../models"; import { SupplierModel } from "../models";
export class SupplierRepository export class SupplierRepository
@ -66,13 +66,9 @@ export class SupplierRepository
try { try {
const dtoResult = this.domainMapper.mapToPersistence(supplier); const dtoResult = this.domainMapper.mapToPersistence(supplier);
if (dtoResult.isFailure) { const { id, ...updatePayload } = dtoResult.data;
return Result.fail(dtoResult.error);
}
const { id, company_id, ...updatePayload } = dtoResult.data;
const [affected] = await SupplierModel.update(updatePayload, { const [affected] = await SupplierModel.update(updatePayload, {
where: { id, company_id /*, version */ }, where: { id /*, version */ },
//fields: Object.keys(updatePayload), //fields: Object.keys(updatePayload),
transaction, transaction,
individualHooks: true, individualHooks: true,
@ -306,4 +302,33 @@ export class SupplierRepository
} }
} }
/**
*
* Elimina o marca como eliminado un proveedor.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el proveedor.
* @param id - UUID del proveedor a eliminar.
* @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error>
*/
async deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<boolean, Error>> {
try {
const deleted = await SupplierModel.destroy({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
if (deleted === 0) {
return Result.fail(new EntityNotFoundError("Supplier", "id", id.toString()));
}
return Result.ok(true);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
} }

View File

@ -4,7 +4,7 @@ export const CreateSupplierRequestSchema = z.object({
id: z.string().nonempty(), id: z.string().nonempty(),
reference: z.string().optional(), reference: z.string().optional(),
is_company: z.boolean(), is_company: z.string().toLowerCase().default("true"),
name: z.string(), name: z.string(),
trade_name: z.string().optional(), trade_name: z.string().optional(),
tin: z.string().optional(), tin: z.string().optional(),

View File

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

View File

@ -7,7 +7,7 @@ import { z } from "zod/v4";
*/ */
export const GetSupplierByIdRequestSchema = z.object({ export const GetSupplierByIdRequestSchema = z.object({
supplier_id: z.uuid(), supplier_id: z.string(),
}); });
export type GetSupplierByIdRequestDTO = z.infer<typeof GetSupplierByIdRequestSchema>; export type GetSupplierByIdRequestDTO = z.infer<typeof GetSupplierByIdRequestSchema>;

View File

@ -1,4 +1,5 @@
export * from "./create-supplier.request.dto"; export * from "./create-supplier.request.dto";
export * from "./delete-supplier-by-id.request.dto";
export * from "./get-supplier-by-id.request.dto"; export * from "./get-supplier-by-id.request.dto";
export * from "./supplier-list.request.dto"; export * from "./supplier-list.request.dto";
export * from "./update-supplier-by-id.request.dto"; export * from "./update-supplier-by-id.request.dto";

View File

@ -1,13 +1,13 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
export const UpdateSupplierByIdParamsRequestSchema = z.object({ export const UpdateSupplierByIdParamsRequestSchema = z.object({
supplier_id: z.uuid(), supplier_id: z.string(),
}); });
export const UpdateSupplierByIdRequestSchema = z.object({ export const UpdateSupplierByIdRequestSchema = z.object({
reference: z.string().optional(), reference: z.string().optional(),
is_company: z.boolean().optional(), is_company: z.string().optional(),
name: z.string().optional(), name: z.string().optional(),
trade_name: z.string().optional(), trade_name: z.string().optional(),
tin: z.string().optional(), tin: z.string().optional(),

View File

@ -7,12 +7,12 @@ import {
} from "@erp/core"; } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
export const SupplierDetailSchema = z.object({ export const GetSupplierByIdResponseSchema = z.object({
id: z.uuid(), id: z.uuid(),
company_id: z.uuid(), company_id: z.uuid(),
reference: z.string(), reference: z.string(),
is_company: z.boolean(), is_company: z.string(),
name: z.string(), name: z.string(),
trade_name: z.string(), trade_name: z.string(),
tin: z.string(), tin: z.string(),
@ -21,8 +21,8 @@ export const SupplierDetailSchema = z.object({
street2: z.string(), street2: z.string(),
city: z.string(), city: z.string(),
province: z.string(), province: z.string(),
postal_code: z.union([PostalCodeSchema, z.literal("")]), postal_code: PostalCodeSchema,
country: z.union([CountryCodeSchema, z.literal("")]), country: CountryCodeSchema,
email_primary: z.string(), email_primary: z.string(),
email_secondary: z.string(), email_secondary: z.string(),
@ -34,6 +34,9 @@ export const SupplierDetailSchema = z.object({
fax: z.string(), fax: z.string(),
website: z.string(), website: z.string(),
legal_record: z.string(),
default_taxes: TaxCombinationCodeSchema,
status: z.string(), status: z.string(),
language_code: LanguageCodeSchema, language_code: LanguageCodeSchema,
currency_code: CurrencyCodeSchema, currency_code: CurrencyCodeSchema,
@ -41,7 +44,4 @@ export const SupplierDetailSchema = z.object({
metadata: MetadataSchema.optional(), metadata: MetadataSchema.optional(),
}); });
export type SupplierDetailDTO = z.infer<typeof SupplierDetailSchema>; export type GetSupplierByIdResponseDTO = z.infer<typeof GetSupplierByIdResponseSchema>;
export const GetSupplierByIdResponseSchema = SupplierDetailSchema;
export type GetSupplierByIdResponseDTO = SupplierDetailDTO;

Some files were not shown because too many files have changed in this diff Show More