Sistema de Rendiciones de Gastos
Documentación de negocio y de cómo están armados los flujos: carga del gasto, aprobación multinivel, firma hacia nómina, formularios custom por empresa (circuito Viáticos APSV), notificaciones, reportes Excel, multiempresa, import de personas e IA opcional.
1. Resumen ejecutivo
El sistema permite a empleados de una empresa (o de sus subempresas) cargar rendiciones de gastos con fecha, monto, observaciones y comprobantes; pasarlas por un workflow de aprobación configurable de N niveles; listar el proceso completo; exportar a Excel; e integrar el resultado con nómina u otros sistemas vía la misma API REST.
/api/v1/*. La lógica de estados, tenant y workflow vive
solo en el backend (*Service), nunca duplicada en la UI.
Capacidades Must
- Carga de rendición (fecha, observaciones, monto, adjuntos imagen/PDF).
- Workflow con N niveles configurables y avance secuencial.
- Listados con cabecera + adjuntos + historial por nivel.
- Exportación Excel del listado (mismo alcance de filtros).
- Multiempresa nativo + subempresas (árbol
parentId). - API-first / todo en services; mismo contrato para front e integradores.
- Importación de personas (API + front), scoped al tenant.
- IA externa opcional (análisis de comprobante → sugerencia o alta vía services).
- Seed demo reproducible (
database/seeds/), incl. empresa APSV. - Formularios custom por empresa (
standard|viaticos_apsv). - Circuito Viáticos APSV: borrador → aprobación → rendición → liquidación (estados 4–10).
- Notificaciones in-app + email SMTP opcional (
NotificationService). - Reportes Excel §11 (
ReportService), distintos del export de listado.
2. Negocio: qué problema resuelve
En operaciones (sucursales, fuerza de campo, admin), las personas gastan y necesitan rendir el gasto con evidencia. Sin un proceso claro aparecen demoras, rechazos opacos y datos que no llegan a nómina. Este producto estandariza:
- Quién pide (empleado, siempre dueño de su rendición).
- Quién decide (aprobadores por nivel, en orden).
- Qué se ve (estado + historial + adjuntos en un solo detalle).
- Cómo se cierra (firma / externalId → pendiente de pago → pagada).
- Cómo se aísla (cada dato lleva
companyId; no se cruza entre tenants).
KPIs de producto (referencia)
| KPI | Para qué sirve |
|---|---|
| Tiempo ciclo de aprobación | Desde create hasta último nivel OK |
| % rechazo por nivel | Calidad de comprobantes / reglas |
| Éxito de import personas | Onboarding multiempresa |
| Latencia p95 create/list | Salud API |
| Uso de export Excel | Adopción de control / auditoría |
3. Roles y capacidades
| Rol | Código | Puede | No puede |
|---|---|---|---|
| Administrador | admin |
Empresas/subempresas de su rama; import personas; plantillas workflow; ver/gestionar según alcance; aprobar como override; firmar | Actuar fuera de scopeCompanyIds |
| Empleado | employee |
Crear / editar / cancelar / firmar propias; listar propias; export propias; usar IA suggest | Aprobar niveles ajenos; ver rendiciones de otros (salvo admin) |
| Aprobador | approver |
Bandeja del nivel activo asignado a su userId; aprobar/rechazar con motivo | Decidir un nivel que no es el activo o no es suyo |
| Integración | API Key | Misma API con alcance de empresa (+ hijos si inheritChildren) |
Cruzar a otra raíz de holding |
authenticate + requireRoles
+ checks en services). El front solo oculta botones; no es la fuente de verdad.
Mapeo roles APSV (documento → sistema)
| Rol documento | Rol sistema | Usuario seed |
|---|---|---|
| Solicitante | employee | solicitante@apsv.demo |
| Jefe Directo (L1) | approver | jefe@apsv.demo |
| Gerente (L2) | approver | gerente@apsv.demo |
| Administración / Rendidor (L3) | admin | admin@apsv.demo |
4. Multiempresa y subempresas
Company (raíz / holding) parentId = NULL
├── Subcompany A parentId = raíz
│ └── Subcompany A.1 parentId = A
└── Subcompany B parentId = raíz
| Concepto | Regla |
|---|---|
companies.id | PK de empresa o subempresa |
companies.parentId | NULL = raíz; FK al padre; sin ciclos (validado en CompanyService) |
| Entidades operativas | Siempre con companyId (rendiciones, adjuntos, workflows, users, API keys, logs IA) |
| Visibilidad admin raíz | Puede ver/gestionar descendientes (scopeCompanyIds) |
| Visibilidad admin hoja | Solo su rama |
| API Key | Ligada a una empresa; opcionalmente hereda hijos |
API empresas
| Método | Ruta | Acción |
|---|---|---|
| GET | /api/v1/companies | Listar según alcance del token |
| GET | /api/v1/companies/:id | Detalle |
| GET | /api/v1/companies/:id/tree | Subárbol |
| POST | /api/v1/companies | Crear (admin; parentId opcional) |
| PATCH | /api/v1/companies/:id | Actualizar |
5. Máquina de estados
5.1 Estados de la rendición
| Id | Código API | Etiqueta UI | Significado |
|---|---|---|---|
| 0 | DISMISSED | Desestimada | Cancelada por el empleado o rechazada en algún nivel del workflow |
| 1 | PENDING | Pendiente | En proceso de aprobación (hay niveles pendientes) |
| 2 | PENDING_PAYMENT | Pendiente de pago | Todos los niveles OK (y/o firmada); lista para nómina/pago |
| 3 | PAID | Pagada | Cierre desde nómina / callback (estado final de pago) |
| 4 | DRAFT | Borrador | Solo formCode viaticos_apsv — sin workflow |
| 5 | SUBMITTED | Enviada | Transición breve opcional |
| 6 | APPROVED | Aprobada | Workflow completo APSV |
| 7 | PENDING_SETTLEMENT | Pendiente de rendición | Lista para planilla etapa 2 |
| 8 | SETTLED | Rendida | Viáticos/gastos cargados |
| 9 | OBSERVED | Observada | Admin pide corrección |
| 10 | LIQUIDATED | Liquidada | Admin marca liquidada |
Estados 0–3 son el circuito formCode=standard (sin regresión).
Estados 4–10 solo aplican a formCode=viaticos_apsv; las empresas standard
no los usan en el flujo diario.
Formularios custom: cada empresa declara su formulario en
company_form_configs (formCode + schemaJson + viaticoRulesJson)
vía GET/PUT /companies/:id/form-config. El front renderiza y valida según ese schema;
la lógica de transiciones vive en ReimbursementService según formCode.
Diagrama standard (0–3)
┌──────────────┐
create ─────────►│ PENDING │
└──────┬───────┘
reject/cancel │ último nivel APPROVED
│ │ (+ firma / externalId local)
▼ ▼
┌──────────┐ ┌──────────────────┐
│ DISMISSED│ │ PENDING_PAYMENT │──► PAID
└──────────┘ └──────────────────┘
▲
/signature
(niveles todos APPROVED)
Diagrama APSV (4–10)
DRAFT ──submit──► SUBMITTED (+ workflow N niveles)
│
reject ────┼──► DISMISSED
│
all approve ──► APPROVED ──► PENDING_SETTLEMENT
│
settlement ┼──► SETTLED
│ │
observe liquidate
▼ ▼
OBSERVED LIQUIDATED
│
re-settlement ──► SETTLED
En la respuesta JSON cada estado viene como objeto
{ id, name, label } (name en inglés, label en español)
para no acoplar la UI a códigos mágicos.
5.2 Estados de cada nivel (workflow_details)
| Id | Código | Etiqueta |
|---|---|---|
| 0 | REJECTED | Rechazado |
| 1 | PENDING | Pendiente |
| 3 | APPROVED | Aprobado |
El nivel activo es el de menor level que sigue en PENDING.
Solo ese nivel puede decidir. Tras aprobarlo, se desbloquea el siguiente.
6. Circuito Viáticos APSV
Empresa demo APSV (id=4) con formCode=viaticos_apsv.
Schema: database/seeds/forms/viaticos_apsv.schema.json.
Holding / Sur / Norte siguen en standard sin regresión.
IA off por defecto en APSV (aiScanEnabled=false).
6.1 Wizard UI en 2 etapas
- Solicitud / planilla de liquidación de viáticos — destino, motivo, fechas, agentes, vehículo, cuotas; se guarda como
DRAFTy se confirma conPOST .../submit. - Rendición de viáticos + gastos — tras aprobación completa (
PENDING_SETTLEMENT): líneas de viáticos/gastos, adjuntos, envío conPOST .../settlement.
6.2 Transiciones (services)
| Acción | Endpoint | Desde | Hacia | Quién |
|---|---|---|---|---|
| Crear borrador | POST /reimbursements | — | DRAFT | Solicitante |
| Editar borrador | PATCH /reimbursements/:id o update | DRAFT / OBSERVED* | mismo | Dueño |
| Confirmar | POST .../submit | DRAFT | SUBMITTED + workflow | Dueño |
| Aprobar/rechazar nivel | POST .../approve | SUBMITTED | siguiente / DISMISSED / APPROVED→PENDING_SETTLEMENT | Aprobador del nivel |
| Cargar rendición | POST .../settlement | PENDING_SETTLEMENT u OBSERVED | SETTLED | Solicitante / admin |
| Observar | POST .../observe | SETTLED | OBSERVED | Admin / Administración |
| Liquidar | POST .../liquidate | SETTLED | LIQUIDATED | Admin autorizado |
| Historial | GET .../events | — | — | Scope tenant |
| PDF planillas | GET .../pdf | — | — | Scope + ownership |
* En OBSERVED se reenvía settlement tras corrección; DRAFT es editable/cancelable sin workflow activo.
6.3 Cadena de aprobación seed
Solicitante → Jefe Directo (L1) → Gerente (L2) → Administración (L3) → PENDING_SETTLEMENT
6.4 Services de dominio APSV
| Service | Responsabilidad |
|---|---|
FormConfigService | CRUD / resolver company_form_configs por companyId |
ViaticoCalculationService | Cálculo de importes según viaticoRulesJson |
ReimbursementService | draft / submit / approve / settlement / observe / liquidate; branch por formCode |
NotificationService | In-app + SMTP; fire-and-forget; no aborta la TX de negocio |
ReportService | Reportes Excel §11 scoped por tenant |
PdfExportService | PDF planillas (liquidación / viáticos-gastos / comprobante standard) |
6.5 Invariantes APSV
- Empresas
standard: endpoints settlement/observe/liquidate rechazan siformCode ≠ viaticos_apsv. - DRAFT: sin workflow activo; editable/cancelable por el dueño.
- Submit crea workflow; rechazo de nivel → DISMISSED.
- Settlement solo desde PENDING_SETTLEMENT u OBSERVED.
- Liquidate solo desde SETTLED (rol autorizado).
- Notificaciones y reportes scoped por tenant; sin cruzar empresas.
- Tras mutaciones relevantes:
reimbursement_events+NotificationService.notify.
7. Flujo extremo a extremo — formCode standard
1. Admin configura empresa/subempresa + plantilla workflow (niveles + aprobadores)
2. Admin importa / da de alta personas (employee, approver) en la empresa destino
3. Empleado inicia sesión → Nueva rendición
· Fecha del gasto (obligatoria)
· Monto (obligatorio)
· Observaciones (obligatorias)
· ≥1 adjunto (imagen o documento allowlist)
Opcional: asistente IA (suggest) → confirma → create
4. Sistema crea reimbursement PENDING + adjuntos + workflow con N details PENDING
5. Aprobador L1 ve bandeja (inbox) → aprueba o rechaza (+ motive si rechazo)
6. Si rechazo → rendición DISMISSED; niveles pendientes restantes se cancelan
7. Si aprueba → desbloquea L2 … Ln
8. Último nivel aprobado → PENDING_PAYMENT (+ signature/externalId locales si aplica)
9. Empleado/admin puede firmar explícitamente (/signature) si aún PENDING con niveles OK
10. Nómina / integración marca PAID (callback) — no puede volver a PENDING
11. Auditoría / control: listado proceso completo + export Excel
8. Flujos por rol (UX)
Empleado
- Login
- Listado de mis rendiciones (filtros fecha/estado)
- Nueva (manual) o Asistida IA
- Detalle: adjuntos + niveles
- Editar / cancelar solo si PENDING y ningún nivel revisado
- Firmar cuando todos los niveles APPROVED
- Export Excel de su alcance
Aprobador
- Login
- Bandeja “Aprobar” (
inbox=1: solo nivel activo = yo) - Abrir detalle + comprobantes
- Aprobar o Rechazar (motive obligatorio en rechazo)
- La rendición avanza o queda DISMISSED
Administrador
- Árbol de empresas / crear subempresa
- Import personas (JSON, CSV/XLSX) o alta unitaria
- CRUD plantillas de workflow (niveles → aprobadorUserId)
- Form-config por empresa (
standard/viaticos_apsv) - Visibilidad de rendiciones de su rama; override de aprobación si aplica
- APSV: observar / liquidar; reportes §11; bandeja de notificaciones
Solicitante APSV
- Wizard etapa 1 → guardar DRAFT → confirmar (submit)
- Seguir aprobaciones Jefe → Gerente → Administración
- Wizard etapa 2 (PENDING_SETTLEMENT / OBSERVED) → settlement
- Descargar PDF de planillas; ver campana de notificaciones
Navegación del front: Rendiciones · Aprobar · Empresas · Personas · Plantillas · Reportes · Campana · Salir, con selector de empresa/rama. La UI se adapta al formCode de la empresa activa.
9. Workflow multinivel — cómo está armado
9.1 Piezas
| Pieza | Tabla / concepto | Rol |
|---|---|---|
| Plantilla | workflow_templates + workflow_template_levels |
Define, por empresa, cuántos niveles y qué aprobador va en cada uno. Debe haber una activa al crear. |
| Instancia | workflows |
1:1 con la rendición; copia el estado global del proceso |
| Detalle por nivel | workflow_details |
userId aprobador, level, statusId, motive, decidedAt |
9.2 Reglas de avance (implementadas en ReimbursementService)
- Al crear, se lee la plantilla activa de
companyIdy se materializan N details en PENDING. - Solo el nivel activo (menor
levelaún PENDING) puede decidir. - El decisor debe ser el
userIddel detail (o admin). - Aprobar → detail APPROVED +
decidedAt; si quedan pendientes, la rendición sigue PENDING. - Si era el último nivel en
standard→ rendición y workflow pasan aPENDING_PAYMENT; se seteansignature/externalIdlocales si faltaban. - Si era el último nivel en
viaticos_apsv→ APPROVED y luegoPENDING_SETTLEMENT(sin pasar por firma/nómina standard). - Rechazar → motive obligatorio; detail REJECTED; restantes PENDING → REJECTED; rendición DISMISSED.
- Cancelar (dueño/admin) solo si PENDING y todos los details aún PENDING → DISMISSED.
- Editar mismas precondiciones que cancelar; ownership (propia o admin).
- Firmar (
/signature): dueño o admin/API Key; todos los niveles APPROVED; pasa a PENDING_PAYMENT.
canEdit = status PENDING y todos los details PENDING.
activeLevel expone nivel, userId y detailId del siguiente decisor.
9.3 Diagrama secuencia — aprobación (standard)
Empleado API/Service Aprobador L1 Aprobador L2
│ POST /reimbursements │ │
│─────────────────────────► create + WF L1,L2 PENDING │
│ PENDING │ │
│ │ POST .../approve │
│ │◄────────────────────┤
│ L1 APPROVED │ │
│ L2 sigue PENDING │
│ │ │ POST approve
│ │ │◄──
│ PENDING_PAYMENT (último OK) │
│ (opcional) POST .../signature │
│─────────────────────────► PENDING_PAYMENT + externalId │
10. Adjuntos
- Al crear: al menos un archivo (imagen o documento).
- Tipos:
image/*(jpg, png, webp) y documentos allowlist (pdf, xlsx, doc/docx según middleware). - Metadatos: name, mimeType, size, path, kind (
image|document). - Storage:
{uploadDir}/{companyId}/reimbursements/{id}/— sin path traversal. - En update editable se pueden agregar más adjuntos.
- Descarga:
GET /reimbursements/:id/attachments/:attachmentIdcon scope tenant.
11. Importación de personas
Misma lógica para front e integradores (UserService). Persistencia solo vía services;
siempre con companyId destino dentro del alcance del token.
| Canal | Endpoint |
|---|---|
| Alta unitaria | POST /api/v1/users |
| Lote JSON | POST /api/v1/users/import |
| Archivo CSV/XLSX | POST /api/v1/users/import/file (multipart) |
Campos mínimos
email, firstName, lastName, documentId, docket (opc.), role(s), companyId, active.
Reglas de import
- Validación fila a fila; respuesta con
created,updated,skipped,errors[]. - Upsert opcional por email+companyId o documentId+companyId.
continueOnErrordefault true (no tumba el lote entero).- Roles y plantillas solo dentro del mismo tenant/rama.
- Rate limit y tamaño máx. de archivo.
CSV de prueba: database/seeds/people-import-sample.csv.
12. Servicio de IA externa (n8n OCR)
Adapter configurable: mock, n8n (N8nOcrAdapter) o
http genérico. Variables:
AI_PROVIDER, AI_API_URL (o N8N_BASE_URL +
N8N_WEBHOOK_PATH), N8N_USE_TEST, AI_ENABLED,
AI_TIMEOUT_MS, AI_MIN_CONFIDENCE.
Contrato: docs/ai-scanner-contract.md.
Postman: docs/postman/Rendiciones-OCR.postman_collection.json.
El workflow n8n usa visión (Mistral): POST multipart con campo
file → JSON con date, amount, merchant
(nullable), currency, confidence, partial,
retry_suggested. Si retry_suggested=true, la UI pide nueva foto
antes de guardar (no se crea la rendición).
Además del kill switch global (AI_ENABLED), cada empresa se habilita desde el
panel web: menú Scanner IA o Empresas → IA: encender
(companies.aiScanEnabled). Seed: Sucursal Sur = on.
| Modo | Comportamiento |
|---|---|
suggest (default / UI) |
Analiza → confirma usuario → POST /reimbursements (evita doble llamada a visión) |
auto_create |
Analiza → si confidence OK y no retry → crea + workflow; si retry_suggested → no crea |
enrich |
Sobre PENDING existente: completa campos faltantes (cuando esté expuesto) |
Schema n8n
{
"date": "YYYY-MM-DD",
"amount": 27.6,
"merchant": "Casa Pepe",
"currency": "ARS",
"confidence": 0.9,
"partial": false,
"retry_suggested": false
}
- Persistencia solo vía ReimbursementService.
- Traza en
ai_analysis_logs(sin secretos). - Endpoints:
POST /ai/analyze,POST /ai/analyze-and-create.
13. Listados, Excel y reportes §11
El listado (GET /reimbursements) incluye usuario, adjuntos y workflow/details
(proceso completo). Filtros: fecha desde/hasta, statusId, companyId, paginación;
empleados ven solo propias; inbox=1 filtra bandeja del aprobador.
13.1 Export del listado (proceso completo)
Export (GET /reimbursements/export): mismos filtros; genera .xlsx
(ExcelExportService) con columnas del proceso:
- id, fecha gasto, empleado, empresa, monto, observaciones, status
- adjuntos (nombres / cantidad / tipos)
- por cada nivel: nivel, aprobador, estado, motivo, fecha decisión
- firma (sí/no), externalId, reviewer nómina, reviewerMotive, createdAt, updatedAt
13.2 Reportes Excel §11 (ReportService)
Distintos del export de listado. Catálogo: GET /reports.
Descarga: GET /reports/:code/export (roles admin/approver; scoped por tenant).
| Código | Nombre | Grupo |
|---|---|---|
solicitudes_todas | Solicitudes realizadas | solicitudes |
solicitudes_aprobadas | Solicitudes aprobadas | solicitudes |
solicitudes_rechazadas | Solicitudes rechazadas | solicitudes |
solicitudes_pendientes | Solicitudes pendientes | solicitudes |
rendiciones_pendientes | Rendiciones pendientes | rendiciones |
rendiciones_liquidadas | Rendiciones liquidadas | rendiciones |
gastos_periodo | Gastos por período | gastos |
gastos_agente | Gastos por agente | gastos |
gastos_destino | Gastos por destino | gastos |
gastos_area | Gastos por área | gastos |
13.3 PDF por rendición
GET /reimbursements/:id/pdf — APSV: planilla de liquidación + viáticos/gastos + imágenes adjuntas;
standard: comprobante + adjuntos (PdfExportService).
14. Notificaciones (§10)
Canales: bandeja in-app (notifications) + email SMTP opcional.
Disparo desde services tras mutaciones (submit, approve/reject, pendiente de rendición, observada, liquidada, etc.).
| Endpoint | Acción |
|---|---|
GET /notifications | Bandeja del usuario autenticado |
POST /notifications/:id/read | Marcar una leída |
POST /notifications/read-all | Marcar todas leídas |
| Variable | Uso |
|---|---|
NOTIFY_EMAIL_ENABLED | default false — activa envío SMTP |
SMTP_HOST / SMTP_PORT / SMTP_SECURE | Servidor |
SMTP_USER / SMTP_PASS | Credenciales (solo env) |
SMTP_FROM | Remitente |
emailStatus y no se revierte la operación de negocio.
Healthcheck de la app es independiente de SMTP.
15. Arquitectura técnica
[ Front propio (public/) ] ──┐
[ ERP / portal / app ] ──────┼──► /api/v1/* ──► Controllers ──► Services ──► Sequelize ──► MSSQL
[ AI Provider externo ] ◄────┘ │
integrations/ai (adapter n8n/mock/…)
integrations/paysheet (stub/opcional)
NotificationService → SMTP (opcional)
| Capa | Responsabilidad |
|---|---|
| routes | HTTP, multer, roles |
| controllers | Finos: parse req → service → response |
| services | Negocio, transacciones, tenant, workflow, APSV, reportes, notif |
| models | Sequelize / MSSQL |
| middlewares | auth JWT/API Key, upload, errors |
| integrations | IA, PaySheet, SMTP — sin bypass de dominio |
Services de dominio (además de Reimbursement / Company / User)
FormConfigService·ViaticoCalculationServiceNotificationService·ReportService·PdfExportServiceAiAnalysisService(adapter) · ExcelExportService
Estructura de carpetas
src/{config,models,migrations,seeders,routes,controllers,services,middlewares,integrations,utils}
public/ → front SPA ligera (sin Vite/Webpack)
landing/ → página comercial
docs/ → wiki, OpenAPI, DBML, pipeline, ENTREGA
database/seeds/ → dataset demo (+ forms/viaticos_apsv.schema.json)
uploads/ → adjuntos runtime
tests/ → unitarios críticos
16. Superficie API /api/v1
Contrato vivo OpenAPI 1.2.0 (~74 operaciones, cobertura completa de
src/routes/v1.js):
Swagger UI ·
/api/openapi.json ·
fuente docs/api/openapi.json.
Guía humana detallada (estados, flujos, tablas, ejemplos):
docs/api/API.md. Colección Postman: docs/postman/.
Authorization: Bearer) o API Key (X-API-Key).
Envelope: { success, message, data, meta? }. Health: GET /health (fuera de v1).
Auth
| Método | Ruta | Notas |
|---|---|---|
| POST | /auth/login | email/password → JWT + user (sin auth previa) |
Empresas y form-config
| Método | Ruta | Acción |
|---|---|---|
| GET | /companies | Listar según scope (?tree=1) |
| GET/POST/PATCH/DELETE | /companies · /:id | CRUD / soft delete |
| GET | /companies/:id/tree | Subárbol |
| GET/PUT | /companies/:id/form-config | Leer / upsert formCode + schema + reglas |
Rendiciones (standard + APSV)
| Método | Ruta | Acción |
|---|---|---|
| GET | /reimbursements | Listar + proceso (+ inbox) |
| GET | /reimbursements/:id | Detalle completo |
| GET | /reimbursements/export | Excel/CSV listado |
| GET | /reimbursements/:id/pdf | PDF planillas / comprobante |
| GET | /reimbursements/:id/events | Historial de eventos |
| POST | /reimbursements | Crear (standard→PENDING; APSV→DRAFT) |
| POST | /reimbursements/:id/update | Editar si canEdit / DRAFT |
| POST | /reimbursements/:id/cancel | Cancelar → DISMISSED |
| DELETE | /reimbursements/:id | Cancelar o borrar físico si DISMISSED |
| POST | /reimbursements/:id/approve | Decidir nivel activo |
| POST | /reimbursements/:id/signature | Firmar → PENDING_PAYMENT (standard) |
| POST | /reimbursements/:id/pay | Marcar PAID + comprobantes |
| POST | /reimbursements/:id/submit | APSV: DRAFT → SUBMITTED + workflow |
| POST | /reimbursements/:id/settlement | APSV: etapa 2 → SETTLED |
| POST | /reimbursements/:id/observe | APSV: → OBSERVED |
| POST | /reimbursements/:id/liquidate | APSV: → LIQUIDATED |
| GET | /reimbursements/:id/attachments/:attachmentId | Descarga |
Circuito Gastos (missions / person-settlements)
| Método | Ruta | Acción |
|---|---|---|
| GET/POST | /expense-types | Tipos de gasto |
| GET/PUT | /expense-types/monthly-rates | Tarifas del mes |
| GET/POST | /missions | Solicitudes de gasto |
| POST | /missions/:id/submit | Enviar a aprobación |
| GET | /person-settlements | Liquidaciones por persona |
| POST | /person-settlements/:id/approve | Decidir nivel |
| PATCH | /person-settlements/:id/quantities | Cantidades / importes |
| POST | /person-settlements/:id/liquidate | Liquidar |
Notificaciones, reportes, users, AI, workflows
- Notifications: list / read / read-all
- Reports:
GET /reports,GET /reports/:code/export, person-month - Users: list / create / import / import/file · menú por usuario
- Workflow templates + person-workflows (override por persona)
- AI:
POST /ai/analyze,POST /ai/analyze-and-create - Health:
GET /health
17. Modelo de datos (DER)
Fuente DBML: docs/schema.dbml (visualizar en dbdiagram.io).
Entidades centrales:
companies— árbol multiempresa (aiScanEnabled, etc.)company_form_configs— formCode, schemaJson, viaticoRulesJsonusers— personas/roles por companyId (area, jobTitle)api_keys— integración server-to-serverworkflow_templates/workflow_template_levelsreimbursements— cabecera (+ campos APSV: destination, motive, payloadJson, agentsJson, …)reimbursement_attachments(expenseLineId opcional)reimbursement_expense_lines/reimbursement_viatico_linesreimbursement_events— historial de accionesnotifications— bandeja in-appworkflows/workflow_detailsai_analysis_logs
18. Autenticación e integración
| Mecanismo | Cómo |
|---|---|
| JWT usuario | Authorization: Bearer <token> — claims: sub, email, role, companyId, scopeCompanyIds |
| API Key | Header X-API-Key — resuelve companyId + scope (hijos si inherit) |
Para embeber en otro sistema: apuntar el front con window.API_BASE_URL,
configurar CORS_ORIGINS, usar la misma OpenAPI. Seed demo incluye
demo-api-key-holding-001 (solo no-prod).
19. Reglas invariantes (checklist de negocio)
- Empleado crea/edita/cancela/firma solo rendiciones propias.
- Aprobadores actúan solo en su nivel asignado y cuando es el nivel activo.
- No editar ni cancelar si algún nivel ya fue revisado (standard PENDING).
- Firma solo con niveles completos APPROVED (standard; sin re-firmar si ya informada).
- Callback nómina no puede devolver a PENDING.
- Toda query operativa filtra por companyId / scopeCompanyIds.
- Create / update / cancel / approve / signature / submit / settlement en transacciones.
- IA nunca escribe directo en MSSQL ni saltea workflow.
- Import personas no sale del árbol permitido del token/API Key.
parentIdno puede formar ciclos.- Create standard exige plantilla de workflow activa; APSV exige plantilla al submit.
- Settlement / observe / liquidate solo con
formCode=viaticos_apsvy estados válidos. - Fallo de email no revierte la mutación de negocio.
20. Front propio
App estática en public/ (HTML/CSS/JS). No usa Vite ni Webpack (estándar del proyecto).
Consume únicamente HTTP/JSON (+ multipart). Pantallas alineadas a los flujos de §8:
listado con badges de estado, alta standard, wizard APSV (2 etapas), bandeja de aprobación,
admin de empresas/form-config, import de personas, plantillas, asistente IA (si
aiScanEnabled), campana de notificaciones, reportes §11, descarga PDF.
Landing comercial separada: landing/index.html (ruta típica /landing/).
21. Set de datos de prueba
Nunca usar en producción. Password demo documentada: Demo123!
| Usuario | Rol | Empresa |
|---|---|---|
| admin@holding.demo | admin raíz | Holding (id=1) · standard |
| admin@sur.demo | admin | Sucursal Sur (id=2) · standard |
| empleado@sur.demo | employee | Sur |
| aprobador1@sur.demo | approver L1 | Sur |
| aprobador2@sur.demo | approver L2 | Sur |
| empleado@norte.demo / admin@norte.demo | … | Norte (id=3) · standard |
| solicitante@apsv.demo | employee | APSV (id=4) · viaticos_apsv |
| jefe@apsv.demo | approver L1 | APSV |
| gerente@apsv.demo | approver L2 | APSV |
| admin@apsv.demo | admin / L3 | APSV |
Escenarios standard: PENDING sin revisar · PENDING con L1 OK · DISMISSED · aislamiento Norte/Sur · CSV import.
APSV: casos en DRAFT, SUBMITTED, PENDING_SETTLEMENT, SETTLED, OBSERVED, LIQUIDATED; plantilla 3 niveles;
schema en forms/viaticos_apsv.schema.json. Sur tiene aiScanEnabled=true; APSV off.
npx sequelize-cli db:migrate
npm run seed:demo
# fixtures adjuntos: npm run db:fixtures && npm run db:attachments
Detalle: database/seeds/README.md.
22. Seguridad (mínimo)
- Helmet, CORS, rate limiting, sanitización.
- Secretos solo en variables de entorno (JWT, DB, AI, SMTP); consultas parametrizadas (Sequelize).
- bcrypt + JWT con expiración.
- Uploads: allowlist MIME, tamaño máximo, path bajo companyId.
- IDOR prevenido por scopeCompanyIds + ownership en services (también notificaciones y reportes).
- IA: rate/timeout; sin PII innecesaria en logs; flag por empresa
aiScanEnabled. - Rate limit en import masivo y exports de reportes.
23. Operación y despliegue
cp .env.example .env # DB_* → SQL Server externo; SMTP_* / AI_* según entorno
npm install
npx sequelize-cli db:migrate
npm run seed:demo
npm run dev # o: docker compose up --build
- App:
http://localhost:3000/ - Swagger:
/api-docs - Health:
GET /health(independiente de SMTP) - Wiki: este archivo (
docs/wiki.html) - Índice entrega:
docs/ENTREGA.md - Docker: backend-only; MSSQL externo vía
DB_*(verDOCKER.md) - Contrato OCR n8n:
docs/ai-scanner-contract.md· Postman OCR endocs/postman/
24. Troubleshooting
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| Login 401 | Sin seed / password distinta / DB | Verificar migrate + seed; password Demo123! |
| Create 400 “No hay plantilla…” | Empresa sin workflow template activo | Crear/activar plantilla en esa companyId |
| Create 400 sin adjunto | Falta files[] (standard) | Enviar multipart con al menos un archivo; APSV DRAFT puede relajar campos |
| Approve 403 | No es el aprobador del nivel activo | Usar el userId del activeLevel o admin |
| Edit/cancel 400 | Ya hubo una revisión de nivel | Esperado: canEdit=false |
| submit / settlement 400 | formCode ≠ viaticos_apsv o estado inválido | Verificar form-config empresa APSV y estado actual |
| IA 503 / 403 | AI off o aiScanEnabled=false |
AI_PROVIDER=mock o habilitar Sur / PATCH company |
| Email no llega | NOTIFY_EMAIL_ENABLED=false o SMTP mal |
Revisar env; la operación de negocio igual debe quedar OK |
| Datos de otra sucursal | Scope incorrecto | Revisar companyId del token y parentId |