# ARCHIVADO — Diseño backend consolidado anterior > Estado: **HISTÓRICO — NO NORMATIVO**. > > Las reglas transversales vigentes están distribuidas por responsabilidad en [../backend-architecture.md](../backend-architecture.md), [../dependency-injection.md](../dependency-injection.md), [../http-request-flow.md](../http-request-flow.md), [../dto-guidelines.md](../dto-guidelines.md), [../module-boundaries.md](../module-boundaries.md) y [../testing-guidelines.md](../testing-guidelines.md). > > En particular, el flujo `RequestDTO → RequestMapper → Command → PropsMapper` que aparece más abajo no es una norma vigente: RequestMapper es opcional y se usa solo cuando existe adaptación HTTP propia. --- # DOCUMENTO DE DISEÑO — ERP MODULAR BACKEND (ESTADO CONSOLIDADO) ## 1. Contexto general ERP modular en monorepo con: * PNPM * Turborepo * Node.js ≥ 22 * TypeScript * ExpressJS * MariaDB * Sequelize v6 * Zod v4 Arquitectura basada en: * DDD pragmático * Clean Architecture estricta * Separación clara Domain / Application / Infrastructure * Composition root explícito por módulo * Sin contenedor global implícito * Sin DI mágico * Sin Service Locator * Sin dependencias cruzadas entre módulos Principios permanentes: * SOLID * KISS * DRY razonable (claridad > abstracción prematura) * Fail Fast * Idempotencia * Separación IO / lógica de negocio * Evitar sobreingeniería sistemáticamente --- # 2. Concepto de módulo Un módulo es una unidad funcional aislada que: * Tiene su propio dominio. * Tiene su propia capa Application. * Tiene su propia capa Infrastructure. * Expone servicios explícitamente a otros módulos. * No accede directamente a infraestructura interna de otros módulos. Cada módulo implementa el contrato `IModuleServer`. Estructura conceptual: ``` / api/ application/ infrastructure/ ``` No se comparte código cruzado entre módulos salvo a través de: * Core * Servicios expuestos explícitamente --- # 3. Rol del módulo Core `core` contiene: * Funcionalidades técnicas reutilizables. * Infraestructura transversal. * Interfaces compartidas. * Utilidades comunes. * Contratos base. Ubicación: ``` core/api/ application/ infra/ ``` Reglas: * Core no conoce módulos funcionales. * Core no depende de customer-invoices ni de ningún otro módulo de negocio. * Core solo define abstracciones y servicios reutilizables. Core es el único módulo permitido como dependencia técnica común. --- ## 4. Separación estricta de capas ### Domain Contiene: * Agregados * Entidades * Value Objects * Reglas de negocio * Invariantes No conoce: * HTTP * Sequelize * DTO * Infrastructure ### Application Contiene: * Use cases * Servicios de aplicación (`Creator`, `Updater`, `Issuer`, etc.) * Mappers DTO → Props * Interfaces de repositorios * Snapshot builders No conoce: * Express * Sequelize * Models ORM ### Infrastructure Contiene: * Controllers Express * DTO HTTP * Mappers Request DTO HTTP → Props * Implementaciones Sequelize * DomainMapper (persistencia ↔ dominio) * DI del módulo Nunca: * Application depende de Infrastructure * Domain depende de Application --- # 4.1 Estructura interna de un módulo funcional Ejemplo conceptual (customer-invoices): ``` customer-invoices/api/ application/ infrastructure/ ``` ### 4.1.1 Application Contiene: * Use cases * Servicios de lectura (Finder) * Servicios de escritura (WriteService) * Factories * Servicios de numeración * Snapshot builders * Application models * Interfaces de repositorio * Interfaces de servicios externos Application: * No conoce Express * No conoce Sequelize * No conoce filesystem * No conoce implementaciones concretas Solo depende de: * Dominio * Interfaces * Core --- ### 4.1.2 Infrastructure Contiene: * Implementaciones Sequelize * Implementaciones de repositorios * Mappers dominio ↔ ORM * Controllers Express * Rutas * Implementaciones de servicios externos * DI del módulo Infrastructure depende de: * Application * Dominio * Core Nunca al revés. --- ## 4.2 Snapshot builders (materialización de salida) Los snapshot builders pertenecen a la capa **Application** y son responsables de transformar modelos de aplicación o agregados en estructuras serializables para la capa HTTP. ### Rol en el flujo ``` UseCase ↓ SnapshotBuilder ↓ DTO (response) ↓ Controller (HTTP) ``` --- ### Principio clave > El contrato de salida está definido exclusivamente por los DTOs (schemas Zod). El snapshot builder solo los materializa. Esto refuerza la separación: * DTO → contrato * Builder → transformación --- ### Tipado obligatorio Todo snapshot builder debe tiparse contra el DTO inferido: ```ts ISnapshotBuilder ``` donde: ```ts OutputDTO = z.infer ``` --- ### Reglas obligatorias * No definir interfaces manuales duplicando DTOs. * No introducir tipos intermedios si no aportan valor real. * El builder debe ser determinista y puro. * El builder no puede depender de infraestructura. * El builder no puede contener lógica de negocio. --- ### Relación con Use Cases Los use cases: * ejecutan lógica * orquestan servicios * construyen snapshots mediante builders * devuelven DTOs (no entidades) ```ts const items = customers.map(customerSummaryBuilder.toOutput); return Result.ok({ page, per_page, ... items, }); ``` --- ### Relación con Domain El builder: * nunca modifica el dominio * nunca valida invariantes * nunca llama a `create()` ni `rehydrate()` Solo lee estado ya válido. --- ### Principio de consistencia > Si el DTO define `null`, el builder debe emitir `null`. > Si el DTO define enum, el builder debe emitir un valor válido del enum. Nunca: * `""` en lugar de `null` * strings genéricos en lugar de enums * boolean como string --- ### Objetivo arquitectónico Garantizar: * contratos HTTP estables * desacoplo dominio ↔ transporte * ausencia de duplicación de tipos * consistencia transversal en todos los módulos --- # Resultado Con estos cambios: * eliminas duplicación (`interface vs zod`) * alineas builders con DTOs * fuerzas consistencia en outputs (justo tu problema actual) * mantienes KISS (sin introducir capas nuevas) * refuerzas Clean Architecture sin añadir complejidad --- ## Siguiente paso lógico Te recomiendo ahora hacer un refactor pequeño pero crítico: 1. eliminar todos los `I*Snapshot` 2. extraer todos los `*SummarySchema` 3. tipar builders con `z.infer` Esto te va a eliminar el 80% de inconsistencias que estás viendo. --- ## 4.3. Composition Root por módulo Cada módulo: * Construye sus dependencias en `init()` * Registra modelos en `registerDependencies()` * Expone servicios explícitamente si quiere que otros módulos los usen No se modifica el contrato de módulos. No existe contenedor global compartido implícito. Cada módulo controla su propio wiring. --- ## 4.4. Registro de dependencias entre módulos Colaboración entre módulos se realiza mediante: ``` registerDependencies() ``` Regla: * Solo se exponen servicios útiles. * No se exponen repositorios. * No se exponen detalles de infraestructura. * Se exponen interfaces claras. Ejemplo conceptual: ``` return { models, services: { issuedInvoiceFinder, customerInvoiceWriteService } } ``` Otros módulos solo consumen lo que fue registrado. Nunca se importa directamente infraestructura de otro módulo. --- ## 5. Flujo semántico obligatorio Regla estructural fija: ``` RequestDTO (Infrastructure) ↓ PropsMapper (Application) ↓ Aggregate ``` Prohibido: * DTO → Aggregate * Saltar niveles Cada nivel tiene nombre distinto. No se mezclan responsabilidades. No hay ambigüedad. --- ## 6. Dominio de facturación (decisión consolidada) Se elimina el agregado híbrido `CustomerInvoice`. Se divide en: ### Proforma (agregado independiente) * Modelo dinámico * Calcula totales * Agrupa impuestos dinámicamente * Mutable (no contable) * No conoce Verifactu * No materializa impuestos ### IssuedInvoice (agregado independiente) * Snapshot materializado * Totales congelados * Impuestos materializados * Items materializados * No recalcula nada * Solo permite transición de estado contable * Almacena Verifactu Principio aplicado: Snapshot models must not recalculate. IssuedInvoice confía en el materializer. No valida cálculos internos. No recalcula dinámicamente. --- ## 7. Repositorios Se elimina `ICustomerInvoiceRepository`. Se crean: * `IProformaRepository` * `IIssuedInvoiceRepository` Cada repositorio gestiona un solo agregado. Actualmente se mantiene una sola tabla `customer_invoice`, pero: * El dominio no conoce `is_proforma`. * El filtrado vive exclusivamente en Infrastructure. Se ha decidido duplicar repositorios explícitamente. No usar clases base compartidas. No usar repositorios genéricos. --- ## 8. Inyección de dependencias * Constructor injection explícita. * Sin registry dinámico. * Sin service locator. * Si hay más de 2 dependencias → usar objeto `deps`. Ejemplo: ```ts constructor(private readonly deps: { mapper: IProformaDomainMapper; database: Database; }) {} ``` Wiring en DI del módulo. --- ## 9. Convención obligatoria de naming para mappers ### A. HTTP (Infrastructure) **Request DTO** Formato: `RequestDTO` Ejemplo: * `CreateProformaRequestDTO` **HTTP → Command** Formato: `RequestMapper` Ejemplo: * `CreateProformaRequestMapper` --- ### B. Application **Command** Formato: `Command` Ejemplo: * `CreateProformaCommand` **Command → Props** Formato: `PropsMapper` Ejemplo: * `CreateProformaPropsMapper` --- ### C. Persistencia (Infrastructure / Sequelize) Un único mapper bidireccional por agregado: Formato: `DomainMapper` Métodos obligatorios: * `toPersistence()` * `toDomain()` Ejemplo: * `ProformaDomainMapper` * `IssuedInvoiceDomainMapper` --- ### Reglas anti-ambigüedad Prohibido: * `CreateProformaMapper` * `DtoMapper` * `Transformer` * `Adapter` sin necesidad real El sufijo debe indicar claramente la dirección. --- ## 10. Servicios de aplicación Se adoptan servicios pequeños y explícitos: * `ProformaCreator` * `ProformaUpdater` * `ProformaDeleter` * `ProformaIssuer` Reglas: * Responsabilidad única. * No contener lógica HTTP. * No acceder directamente a Sequelize. * No mezclar múltiples operaciones en un único servicio. Se evita `CommandService`. --- ## 11. Casos de uso Los use cases: * Son ligeros. * Orquestan. * Gestionan transacción. * Delegan en servicios. * Construyen snapshot de salida. * Devuelven `Result`. No deben: * Mapear DTO. * Construir agregados. * Persistir directamente. * Ejecutar lógica de negocio. --- ## 12. Controllers Express Ubicación: ``` infrastructure/express// ``` Responsabilidad: * HTTP → RequestDTO * RequestDTO → Command (mediante RequestMapper) * Llamar UseCase * Traducir Result → HTTP Nunca: * Acceder a repositorios. * Construir agregados. * Ejecutar lógica de dominio. --- ## 13. Colaboración entre módulos Reglas: * Solo se exponen servicios explícitos. * No se exponen repositorios. * No se accede a infraestructura interna. * Core es la única dependencia técnica común. No introducir Event Bus global ni abstracciones prematuras. --- ## 14. Decisiones clave adoptadas * Separación real Proforma / IssuedInvoice. * Snapshot materializado para facturas emitidas. * Un repositorio por agregado. * Una sola tabla por ahora. * Duplicación preferida a abstracción prematura. * Inyección explícita de dependencias. * Mapper bidireccional único por agregado (persistencia). * Un mapper por operación (RequestMapper, PropsMapper). * Flujo obligatorio RequestDTO → Command → Props → Aggregate. --- # 15. Convenciones de naming adoptadas Interfaces: * Prefijo obligatorio `I` * `ICustomerInvoiceRepository` * `IIssuedInvoiceFinder` * `ICustomerInvoiceFactory` * `ICustomerInvoiceNumberService` Servicios: * `*Finder` → lectura * `*WriteService` → escritura * `*Factory` → construcción * `*NumberService` → numeración Se evita: * `CommandService` * Nombres ambiguos * ServiceLocator --- # 16. Separación lectura / escritura (pragmática) No se implementa CQRS formal completo. Pero sí se separa: * Finder (consultas) * WriteService (mutaciones) Motivo: * Claridad semántica * Mejor legibilidad * Mejor separación de responsabilidades Sin introducir sobreingeniería. --- # 17. Mappers Ubicación: ``` infrastructure/persistence/sequelize/mappers/ ``` Tipos: * domain/ * list/ Los mappers: * Solo los usa el repositorio. * No se exponen a Application. * No se usan fuera de Infrastructure. --- # 18. Reglas estrictas de dependencia Dominio: * No depende de nada externo. Application: * Depende solo de Dominio + interfaces + Core. Infrastructure: * Depende de Application + Dominio + Core. Nunca: * Application → Infrastructure * Dominio → Application * Módulo A → Infra de módulo B --- # 19. Desarrollo de nuevos módulos Para crear un nuevo módulo: 1. Crear estructura: ``` new-module/api/application new-module/api/infrastructure ``` 2. Definir: * Dominio * Interfaces en Application * Use cases * Servicios de lectura/escritura 3. Implementar Infra: * Repositorios * Controllers * DI 4. Registrar módulo en el loader global. 5. Exponer solo servicios necesarios mediante `registerDependencies`. No copiar lógica transversal: usar Core. --- # 20. Colaboración entre módulos Para que módulo A use lógica de módulo B: 1. B debe exponer servicio explícito. 2. A debe declarar dependencia. 3. El módulo loader debe garantizar orden de carga. 4. Se usa interfaz, no implementación concreta. Nunca: * Importar ficheros directamente. * Acceder a infraestructura interna. * Compartir repositorios. --- # 21. Evitar overengineering No introducir: * Event bus global si no es necesario. * Contenedores DI complejos. * Domain events si no hay necesidad real. * CQRS formal. * Versionado prematuro. * Abstracciones sin uso inmediato. Cada abstracción debe tener un caso real. --- # 22. Estilo de trabajo y preferencias Responder: * En castellano. * Código en inglés. * Comentarios en castellano. * Directo y técnico. * Sin introducciones innecesarias. * Sin emoticonos. * Señalar incoherencias. * Proponer refactors si mejoran claridad. * Priorizar arquitectura correcta. * Minimizar tokens sin perder precisión. * Revisar siempre riesgo de overengineering. * Mantener coherencia modular. * Diseñar nuevos módulos. * Garantizar separación correcta de capas. * Establecer contratos claros entre módulos. * Evitar dependencias cruzadas indebidas. * Evolucionar el sistema sin romper principios estructurales. Ante cualquier nueva propuesta: * Validar necesidad real. * Evaluar impacto estructural. * Evitar abstracción anticipada. --- ## 23. Convención de creación y rehidratación (obligatoria) ### 23.1 Principio general Se establecen dos operaciones semánticamente distintas en el dominio: ```text create() → construcción funcional (nacimiento en el dominio) rehydrate() → reconstrucción desde persistencia ``` Estas operaciones **no son intercambiables** y deben mantenerse separadas en todo el sistema. --- ## 23.2 Entidades y Aggregate Roots ### create() ```ts static create(props: CreateProps, id?: UniqueID): Result ``` Responsabilidades: * Crear una nueva instancia de dominio * Aplicar invariantes de negocio * Validar datos de entrada * Construir entidades internas * Registrar domain events (cuando se implementen) Características: * Devuelve `Result` * Puede fallar * Representa “nacimiento” del objeto en el dominio --- ### rehydrate() ```ts static rehydrate(props: InternalProps, id: UniqueID): Entity ``` Responsabilidades: * Reconstruir una instancia desde persistencia * Restaurar estado previamente válido Características: * No devuelve `Result` * No valida reglas de creación * No genera eventos * No ejecuta lógica adicional --- ### Reglas obligatorias * `create()` **NO se usa en infraestructura** * `rehydrate()` **NO se usa en casos de uso** * `create()` y `rehydrate()` **NO deben compartir lógica implícita** * El constructor debe ser `protected` --- ## 23.3 Value Objects ### create() ```ts static create(props: Props): Result ``` Responsabilidades: * Validar el valor * Normalizar si aplica * Construir una instancia inmutable Características: * Sin identidad * Sin eventos * Sin distinción entre creación y reconstrucción --- ### Reglas obligatorias * Los Value Objects **NO tienen `rehydrate()`** * Los Value Objects **siempre se reconstruyen mediante `create()`** * Los Value Objects **no dependen de persistencia** --- ## 23.4 Uso en el sistema ### Flujo de creación funcional ```text Controller ↓ UseCase ↓ Aggregate.create() ↓ Repository.save() ``` --- ### Flujo de reconstrucción ```text Repository ↓ DomainMapper ↓ Aggregate.rehydrate() ``` --- ## 23.5 Reglas de diseño adicionales * `update()` **nunca debe usar `create()` ni `rehydrate()`** * Las entidades **mutan su estado**, no se recrean * Los Value Objects **sí pueden recrearse libremente** * El dominio **no debe asumir origen de los datos** * Infraestructura es la única capa que usa `rehydrate()` --- ## 23.6 Naming adicional (opcional) Se permiten factories específicas en Value Objects cuando mejore la claridad: ```ts UtcDate.createFromISO(...) Money.fromDecimal(...) TaxCode.fromString(...) ``` Estas no sustituyen a `create()`, sino que complementan su uso. --- ## 23.7 Principio final > create() = nuevo estado válido en el dominio > rehydrate() = estado ya válido restaurado Esta distinción es obligatoria en todo el ERP.