Ciclo de Gasto (Compromiso → Causado → Pagado)
El Ciclo de Gasto es la state machine canónica que ACE utiliza para trazar todo movimiento de dinero hacia un proveedor desde el momento que se firma una orden de compra hasta el cierre del asiento contable.
Reemplaza la dispersión previa de cuatro estados-máquina paralelos
(budget_commitments.status, vendor_invoices.status, vendor_payments.status,
journal_entries.status) con una única fuente de verdad y un event log
inmutable (append-only) para auditoría completa.
Las cinco etapas
CANCELLED (terminal, desde cualquier no-terminal)
↑
COMMITTED ──→ ACCRUED ──→ AUTHORIZED_TO_PAY ──→ PAID ──→ CLOSED (terminal)
(skip si flag=FALSE)
| Etapa | Trigger | Asiento contable | Norma |
|---|---|---|---|
| COMMITTED (Comprometido) | Aprobación de PO o contrato | Ninguno — sólo control presupuestal | Control interno |
| ACCRUED (Causado) | Factura del proveedor recibida y aprobada + recepción confirmada | D: Gasto/Activo H: Cuentas por Pagar | VEN-NIIF Sec 2.36 (devengado) |
| AUTHORIZED_TO_PAY | Tesorería aprueba el pago | Ninguno — control de tesorería | Control de tesorería |
| PAID (Pagado) | Egreso bancario ejecutado | D: Cuentas por Pagar H: Banco | Pagos efectivos |
| CLOSED (Cerrado) | Asientos posteados + reconciliados con extracto | Ninguno | Cierre contable |
El estado AUTHORIZED_TO_PAY es configurable
Cada ciclo tiene un flag requires_payment_authorization (default TRUE).
Si está en FALSE, la transición ACCRUED → PAID es directa (apropiado para pagos
de baja materialidad o flujos de petty cash). Si está en TRUE, requiere paso
expreso por tesorería.
Marco legal
ACE es empresa mixta de hidrocarburos bajo la Ley Orgánica de Hidrocarburos 2026 (G.O. 6.978). No es ente descentralizado del sector público, por lo que ONAPRE / LOAFSP no aplican directamente. Adoptamos el ciclo de cuatro etapas como mejor práctica industrial alineada al reporting upstream PDVSA, no por imposición legal.
La obligación contable VE-NIIF Sec 2.36 (registro por devengado, no por caja) sí es vinculante — y es por eso que el estado ACCRUED es el hito explícito que dispara el asiento contable.
Trazabilidad y bitácora de eventos
Cada transición genera una entrada inmutable en budget_commitment_events:
| Campo | Descripción |
|---|---|
from_status | Estado origen (NULL si es evento inicial / backfill) |
to_status | Estado destino |
actor_id | Usuario que disparó la transición (NULL si automática) |
payload | Refs a entidades (factura, pago, JE) en JSONB |
amount_delta | Diferencial monetario aplicado |
notes | Notas humanas (razón de cancelación, observaciones) |
created_at | Timestamp inmutable |
La tabla NO admite UPDATE ni DELETE desde la aplicación — es append-only por diseño, ideal para auditoría CGR / Contraloría si se solicita.
Hooks automáticos
Para minimizar fricción operacional, dos hooks transicionan el ciclo automáticamente desde flujos existentes (tolerantes a errores — no bloquean la operación si no encuentran el commitment):
- VendorInvoice aprobada →
COMMITTED → ACCRUED(víaonVendorInvoiceApproved) - VendorPayment completado →
[ACCRUED|AUTHORIZED_TO_PAY] → PAID(víaonVendorPaymentCompleted)
Las transiciones manuales en UI (botones de la tarjeta de Acciones) son para:
- Saltos no automatizables (CLOSED requiere validación manual con extracto)
- Casos donde no hay PO vinculada (compras spot, payroll)
- Cancelación con razón obligatoria
UI
Listado
Disponible en /budget/expenditure-cycles. Filtros por estado, fecha y
búsqueda libre. Cada fila muestra documento, descripción, proveedor, proyecto,
montos comprometido/pagado, estado actual con chip de color y fecha.

Detalle
/budget/expenditure-cycles/:id muestra:
- Resumen — datos del ciclo
- Acciones — botones de transición disponibles según estado actual + flags
- Progreso — Stepper visual de las 5 etapas
- Bitácora — eventos cronológicos con actor y notas

Tras transicionar a ACCRUED, el botón cambia a "Autorizar pago (tesorería)" y el evento se registra en la bitácora:

Bitácora completa multi-transición
Tras avanzar por COMMITTED → ACCRUED → AUTHORIZED_TO_PAY, la bitácora muestra los 3 eventos cronológicos:

Cancelación con razón obligatoria
Cualquier ciclo en estado no-terminal puede cancelarse, pero el modal exige
una razón de texto libre. La razón queda registrada en cancellation_reason y
también en notes del evento — para trazabilidad explícita.

Permisos
| Permiso | Para |
|---|---|
budget:cycle:read | Ver ciclos y eventos |
budget:cycle:transition | Avanzar (causar, pagar, cerrar) |
budget:cycle:authorize-pay | Autorizar pago (rol tesorería) |
budget:cycle:cancel | Cancelar ciclo con razón |
Asignados por defecto a: Director Técnico, Gerente de Contratación, Gerente de
Logística, Estimador de Costos, Gerente de Finanzas, Contador, Administrador
de Sistema, y solo lectura para gerencias generales (Yacimiento, Operaciones,
Planificación). Super Admin con *:* cubre todos.
Endpoints
GET /api/budget/expenditure-cycles — list paginado con filtros
GET /api/budget/expenditure-cycles/:id — detalle con todas las relaciones
GET /api/budget/expenditure-cycles/:id/events — sólo eventos (orden cronológico)
POST /api/budget/expenditure-cycles/:id/transition — { toStatus, payload?, notes? }
POST /api/budget/expenditure-cycles/:id/cancel — { reason }
Estados legacy (pre-sub4)
Los enum values PARTIALLY_INVOICED, FULLY_INVOICED, EXECUTED quedan en
la tabla por retrocompatibilidad con datos históricos. La state machine los
normaliza a COMMITTED al evaluar transiciones, así que ciclos viejos
pueden continuar el flujo sin migración manual.