FactuGES_ERP/docs/architecture/DOCUMENTO-DE-DISEÑO-—-ERP-MODULAR-BACKEND-(ESTADO-CONSOLIDADO).md
david e0ba5ef229 feat: add proforma duplicator service and related tests
- Implemented ProformaDuplicator service for duplicating proforma documents.
- Added tests for duplicating proformas, including scenarios for successful duplication, audit failure propagation, and payment term adaptations.
- Created ProformaStatusChanger service to handle status changes of proformas with corresponding tests.
- Developed ProformaArchiver service for unarchiving proformas and added tests to verify functionality.
- Introduced a test fixture for creating proforma instances for testing purposes.
2026-08-26 13:03:39 +02:00

17 KiB

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:

ISnapshotBuilder<Input, OutputDTO>

donde:

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)
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:

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:

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()

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()

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()

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

Controller
  ↓
UseCase
  ↓
Aggregate.create()
  ↓
Repository.save()

Flujo de reconstrucción

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:

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.