Appearance
Decisiones para el refinamiento
Documento migrado desde
franmc01/ninaku-core-api. Las decisiones de negocio, dominio y base de datos siguen vigentes; las referencias a ASP.NET Core / .NET son históricas: el backend actual es NestJS y su arquitectura se define en ARCHITECTURE.md y NESTJS_STACK_GUIDE.md.
Estado: decisiones vigentes. La revisión 5 implementa la fundación multi-business; las separaciones físicas de verticales y el backend continúan pendientes. Fecha de revisión: 2026-09-16.
D01. Plataforma y stack
Monolito modular en NestJS 12 / TypeScript sobre Node.js 24 con PostgreSQL. Organización, identidad, acceso y capacidades constituyen la base. Catálogo, inventario, ventas y pagos son módulos de negocio reutilizables, no requisitos funcionales de cualquier tenant.
La instalación limpia de Ninaku Core requiere PostgreSQL 18. La imagen inicial de PostgreSQL 17 es histórica. El workflow ejecuta los SQL en PostgreSQL 18 nativo y registra la versión menor efectiva en el log; las pruebas históricas no sustituyen esa ejecución. No seleccionar ORM, broker, proveedor de nube o framework de CQRS en este PR.
D02. Una definición canónica por schema
Cada schema tiene un archivo fuente propietario. Tablas, restricciones, funciones y políticas deben quedar trazables a ese propietario. Infraestructura como extensiones y orquestación de instalación es una excepción técnica identificada, no un nuevo dominio.
El baseline todavía incumple la concentración por archivo en 0050, 0170, 0300 y 0400. Se conserva íntegro en esta entrega. No se eliminarán FK para simular independencia.
Procedimiento para el próximo cambio: mover primero definiciones sin dependencias posteriores, ubicar puentes en un propietario de integración explícito y calcular el orden real. Si una regla restante requiere una fase tardía, definir su fuente en el archivo del propietario y una instalación por fases con llamadas explícitas o artefactos derivados reproducibles. No introducir un runner complejo antes de probar un caso real. Nunca otorgar acceso runtime a una instalación parcial sin seguridad finalizada. Un instalador final solo orquesta; no mantiene otra copia manual de reglas.
D03. Independencia tiene tres niveles
- Comercial: habilitar capacidades por organización.
- Aplicación: módulos colaboran mediante contratos públicos y conservan sus escrituras.
- Instalación: un módulo no exige físicamente tablas de verticales ausentes.
El baseline implementa estructura para capacidades, pero sus FK aún fuerzan dependencias amplias. Una columna nullable o una capacidad desactivada no elimina una dependencia DDL. El objetivo inmediato es demostrar core e inventario sin cocina; no prometer todas las combinaciones de 38 schemas.
D04. Organización, propiedad y colaboración
Organization es el límite tenant. Business Unit modela una unidad de negocio administrativa/operativa y Vertical declara capacidades funcionales; no son el mismo concepto. Legal Entity identifica la entidad legal; Brand, la identidad comercial; Site, el lugar; Outlet, la unidad operativa. Las asignaciones efectivas Outlet→Business Unit y Outlet→Legal Entity conservan vigencia. Sales congela ambas dimensiones y Payments congela Legal Entity.
Un tenant puede combinar verticales. Dos organizaciones independientes no comparten acceso por pertenecer a Ninaku ni por tener el mismo propietario humano. Relaciones B2B requieren aceptación, alcance, revocación y mensajes autorizados. No exponer IDs, datos personales ni costos internos fuera del contrato.
D05. Inventario
El historial de cantidad y valoración conserva la autoridad. Los saldos son reconstruibles; el saldo usado para admitir una salida debe ser coherente dentro de la transacción o derivarse bajo el bloqueo adecuado. Una proyección asíncrona sirve para reportes, no para garantizar capacidad.
Conservar unidades/incrementos explícitos, precisión exacta, historial de primer uso, lotes, tránsito, conteos y diferencias. Separar transferencia en una entidad legal de operaciones entre entidades. Registrar correcciones por movimientos/contrapartidas según el contrato, sin reescribir significado histórico.
D06. Offline por operación
El cliente registra comandos y resultados locales durables. Sync no duplica reglas del dominio. El servidor aplica el mismo contrato usado online y distingue hechos ya ocurridos de intenciones todavía rechazables. Una orden offline_replay debe referenciar un comando Sync tipado del módulo Sales, con dispositivo, Outlet y tiempo efectivo coherentes; una marca booleana aislada no basta.
La desconexión impide garantizar disponibilidad global si varios dispositivos gastan la misma capacidad sin coordinación. Elegir reconciliación, asignación previa exclusiva u obligación de estar online por operación. La revocación de permisos no puede ser instantánea en un dispositivo aislado: definir autorización offline limitada, vigencia y tratamiento de hechos registrados durante la desconexión. No confiar solo en la hora del dispositivo.
D07. SQL y el caso de uso
SQL canónico conserva integridad, RLS y mecanismos de concurrencia. El caso de uso en Application coordina autorización, transacción y consecuencias. Los resolvers ya presentes en SQL se conservan hasta decidir explícitamente si se trasladan; no duplicar su autoridad en Application.
El ORM futuro no genera un modelo alternativo ni ejecuta DDL al arrancar. Todo adaptador participante en una operación atómica comparte conexión/transacción y contexto tenant. Persistencia de contexto con parámetros y alcance transaccional; separar autoridad de onboarding de la operación ordinaria.
D08. Validación vinculada a la fuente
Evidencia = commit + archivos/checksums + entorno + escenario + resultado. Una cuenta histórica de pruebas no valida este repositorio. Distinguir revisión estática, instalación, comportamiento, carga, recuperación y producto completo.
La revisión 5 añade pruebas nativas adversariales para la fundación multi-business, ownership de Payments, bridges Cash/Treasury, replay offline y provisión concurrente. Los escenarios no cubiertos en VALIDATION.md continúan pendientes y no se consideran certificados por analogía.
D09. Clean Architecture por módulo
Una aplicación desplegable con límites de módulo y dependencias dirigidas hacia Domain/Application. Contratos entre módulos en proceso; no acceso a repositorios, SQL, clientes PostgreSQL ni infraestructura interna de otro módulo. El host compone y las transacciones coordinadas conservan una única unidad de trabajo cuando la regla exige atomicidad. La especificación vigente está en MODULE_CONTRACTS.md; API_AND_MODULE_CONTRACTS.md permanece como evidencia histórica migrada.
D10. API por caso de uso y presupuesto de pantalla
Máximo dos solicitudes de datos para carga inicial normal con sesión válida; contenido acotado, paginación y consultas adicionales bajo demanda. Compositores de lectura evitan que el frontend coordine veinte APIs. Escrituras por acciones de negocio, idempotentes donde producen efectos, errores Problem Details y contratos OpenAPI explícitos. No confundir pocas llamadas HTTP con pocas consultas SQL.
D11. Moneda e idioma independientes
Conservar moneda de documento/pago y moneda funcional por ledger, con snapshots de conversión y reglas de redondeo explícitas. Locale de interfaz, contenido traducido y documentos tienen resoluciones distintas. No inferir moneda o zona horaria del idioma. El análisis actual registra un riesgo concreto de redondeo FX pendiente de resolver con pruebas de negocio.
D12. Destino de telemetría
Grafana Cloud es el destino elegido para trazas y métricas OTLP, mediante el gateway de la región prod-sa-east-1. Staging ya lo configura con OTEL_EXPORTER_OTLP_ENDPOINT (URL base que termina en /otlp; el SDK añade la ruta de cada señal) y OTEL_EXPORTER_OTLP_HEADERS. La cabecera debe ser el par completo Authorization=Basic <base64>, nunca solo el valor base64: el parser exige la forma nombre=valor por cada entrada separada por comas y hace fallar el arranque nombrando OTEL_EXPORTER_OTLP_HEADERS si falta el =, el nombre o el valor, en vez de dejar la exportación sin autenticar con un 401 silencioso. Muestreo y retención siguen sin decidir.
D13. Compatibilidad de contrato OpenAPI
Un cambio incompatible en el OpenAPI publicado falla el CI salvo que el componente major de info.version haya subido respecto al baseline. Subir el major es la forma explícita y autodocumentada de aceptar una ruptura de contrato: no depende de una excepción manual ni de que alguien recuerde justificarla, y deja constancia del cambio en el propio documento.
D14. Severidad de logs para 404
Un 404 por ruta no reconocida se registra en info; un 404 por ruta reconocida pero recurso no encontrado permanece en warn. warn se reserva para lo que una persona puede necesitar atender: si el rastreo automático de rutas inexistentes también fuera warn, el ruido de escáneres y bots ahogaría la señal real.
D15. Checks requeridos en main y staging
build-test, sql-regressions, railway-and-migrator, pre-foundation-integrity y openapi-compatibility son checks requeridos en los rulesets de main y staging, no solo workflows que se ejecutan. Un job que corre sin ser requerido no bloquea un merge roto; no es un gate.
D16. Flujo de entrega
El trabajo entra por pull requests apiladas hacia staging, cada una acotada a 400 líneas cambiadas. staging se fusiona a main con merge commit, nunca squash, para conservar el historial de cada pull request. Con el CI de main en verde, staging avanza en fast-forward hasta el commit de fusión de main, de modo que ambas ramas quedan en el mismo punto y la siguiente tanda de trabajo parte de una base idéntica.
D17. Resolución de la versión de servicio
La versión de servicio expuesta en cada registro de log y span se resuelve como SERVICE_VERSION explícita; si falta, RAILWAY_GIT_COMMIT_SHA truncada a 12 caracteres; si tampoco existe, unknown. El orden asegura que un span pueda rastrearse hasta el build que lo produjo sin depender de que alguien la declare manualmente en cada entorno.
D18. Contratos públicos entre módulos
Cada módulo de negocio posee una superficie pública explícita bajo contracts/. Otro módulo puede consumir esa superficie, pero no puede importar repositorios, SQL, infraestructura, handlers internos ni tipos del driver PostgreSQL del módulo proveedor. Contract significa lo que el módulo ofrece; Port significa lo que el módulo necesita y permanece interno salvo decisión explícita.
La colaboración se divide en tres modos y no se unifica en un bus genérico: contrato síncrono en proceso cuando el resultado es necesario para completar el caso de uso, read contract para composiciones de lectura, y evento durable mediante outbox/inbox para consecuencias desacoplables o recuperables. No hay HTTP interno entre módulos del mismo monolito.
Cuando una invariancia exige atomicidad entre varios propietarios, los contratos participantes pueden compartir una sesión/transacción Foundation opaca y tenant-safe; esa superficie no expone pg.PoolClient. Cada módulo conserva sus propias escrituras e invariantes. Los efectos externos nunca se consideran atómicos con PostgreSQL y usan orquestación recuperable.
Los contratos usan lenguaje de negocio y tipos command/query/result explícitos. No exponen entidades ORM, filas crudas, query builders, SDKs externos ni operaciones CRUD universales. Los verticales consumen exactamente los mismos contratos y no son un atajo para escribir tablas de módulos compartidos.
Cuando existan los primeros módulos reales, CI debe bloquear imports cross-module hacia domain/, application/, infrastructure/ o presentation/ de otro módulo y permitir únicamente su superficie pública documentada. La convención completa y criterios de aceptación viven en MODULE_CONTRACTS.md.
D19. Alcance del CI por lo que cambia
En las pull requests, cada workflow se ejecuta solo si la petición toca los archivos de los que depende. En los push a main y staging no hay filtro alguno: las ramas de registro se verifican siempre completas.
La decisión se tomó con medición, no por intuición. En una jornada de 29 commits el repositorio consumió unos 119 minutos de ejecución, de los cuales 79 correspondieron a database e infrastructure; en esos mismos 29 commits no hubo un solo cambio bajo database/ ni bajo .railway/. Dos tercios del gasto se fueron en verificar entradas que nadie había tocado.
Los filtros se derivan de lo que cada workflow ejecuta realmente, no de lo que su nombre sugiere. Todos los scripts db:* viven bajo database/ y iac:typecheck bajo .railway/, de modo que sus filtros son directos. Hay dos dependencias que no son evidentes y que, omitidas, producirían verdes falsos: app ejecuta db:prepare antes de integración y e2e, así que depende de database/, y su suite unitaria cubre los specs de tooling/; infrastructure construye la imagen del migrador desde database/Dockerfile, así que también depende de database/.
repository queda sin filtrar a propósito. Es el más barato de los cuatro y es el portón estructural: openapi:compat necesita ver todo cambio para detectar deriva del contrato, y un filtro lo volvería ciego justo cuando importa.
Cada workflow declara además un grupo de concurrencia que cancela ejecuciones superadas, pero solo en pull requests. Un push a main o staging nunca se cancela.
Trampa a recordar si vuelven los rulesets: un workflow con filtro de rutas que además sea check requerido nunca reporta en las pull requests que omite, y esa pull request no puede fusionarse jamás. Hoy los rulesets están inertes porque la organización está en plan free con el repositorio privado, así que la combinación no se da. Cuando vuelvan, hay que sacar esos checks de la lista de requeridos o agregar un job complementario con el mismo nombre y el filtro inverso que simplemente tenga éxito.