2026-09-08 21:40:49 +00:00
# ARCHIVADO — Diseño backend consolidado anterior
> Estado: **HISTÓRICO — NO NORMATIVO**.
>
2026-09-10 09:49:52 +00:00
> Nota de obsolescencia fiscal: para el comportamiento CURRENT de Proforma, disponibilidad,
> snapshot de IssuedInvoice, grouping P10 y estado de DELETE de TaxDefinition, consultar
> [Proforma Tax Config](../../customer-invoices/proforma-tax-config.md). Este documento no se
> actualiza retrospectivamente.
>
2026-09-08 21:40:49 +00:00
> 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.
---
2026-08-26 11:03:39 +00:00
# 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:
```
< module > /
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< Input , OutputDTO >
```
donde:
```ts
OutputDTO = z.infer< typeof SomeSchema >
```
---
### 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:
`<Operation><Aggregate>RequestDTO`
Ejemplo:
* `CreateProformaRequestDTO`
**HTTP → Command**
Formato:
`<Operation><Aggregate>RequestMapper`
Ejemplo:
* `CreateProformaRequestMapper`
---
### B. Application
**Command**
Formato:
`<Operation><Aggregate>Command`
Ejemplo:
* `CreateProformaCommand`
**Command → Props**
Formato:
`<Operation><Aggregate>PropsMapper`
Ejemplo:
* `CreateProformaPropsMapper`
---
### C. Persistencia (Infrastructure / Sequelize)
Un único mapper bidireccional por agregado:
Formato:
`<Aggregate>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/< aggregate > /
```
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< Entity , Error >
```
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< ValueObject , Error >
```
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.