Skip to content

E4b — Ajustes al Módulo Caja tras la revisión con el cliente (2026-08-05)

Fuente: reunión "Revisión desarrollo Caja Soterex" (41 min) — Teresa Ortiz (CIS-EC), Diego Sánchez (CIS-EC, tesorería) y Carlos San Martín (CIS-AR). Carlos hizo el walkthrough del módulo Caja ya implementado (E4 + E4-multi, mergeado a main) y el cliente definió los cambios de abajo.

Qué es esto: una entrega correctiva sobre E4, no una entrega nueva del plan v2. Se inserta entre E4 y E5 — E5 (wizard de pago) depende de la moneda de pago y de los saldos, así que conviene cerrar E4b antes de arrancar E5.

Estado del que se parte: main @ 84cc403, con E1–E4 mergeados.


1. Lo que decidió el cliente

#DecisiónContradice lo implementado
C1Guatemala paga en dólares: no tiene autorización para cambio de divisa. El TC queda desarrollado pero la moneda de pago pasa a ser configurable por país (moneda local / solo dólar)Sí — hoy el pago exige TC vigente o falla NO_EXCHANGE_RATE
C2Se saca la autorización de Supervisor para abrir y cerrar caja. Queda bajo responsabilidad del cajero; todo movimiento ya está auditado y no se puede borrarSí — ADR-005 (doble usuario con reautenticación in-band)
C3El saldo inicial de apertura es automático = saldo de cierre anterior de esa caja. No editable, ni siquiera como sugerencia. Diferencia real de plata → ajuste; plata nueva que entra → fondeoSí — hoy lo tipea el operador
C4El prefondeo se parte en 3 capas: Prefondeo Holding (existente, principal + comisión), Prefondeo País (nuevo) (solo principales, sin comisión) y Caja operativa por PDVSí — hoy hay 2 capas
C5Al pagar se valida solo el saldo de la caja operativa. Se saca la validación contra el prefondeoSí — INSUFFICIENT_PREFUNDING
C6El prefondeo país admite saldo negativo (sobregiro); no bloquea la operación
C7Fondear una caja por encima del prefondeo del país dispara una alerta, no un bloqueo ("una alerta más que un validador", Diego)
C8Los motivos de fondeo/ajuste pasan a ser una lista desplegable fija, no texto libre ni combobox con "agregar"Sí — hoy es combobox libre
C9El motivo se muestra en la descripción del movimiento y hay filtro por motivo en el listado y en el export
C10Mover plata del país a una caja no descuenta el prefondeo país (es distribución banco→caja, no gasto). El prefondeo país solo baja con pagos

El ejemplo canónico (Diego, validado por Carlos en la reunión)

Holding 200 USD
   └── prefondeo país Guatemala: 100        (holding sigue en 200 hasta que haya un pago)
         ├── caja 1: 20                      (país sigue en 100 — C10)
         └── caja 2: 20

Pago de 5 USD de principal + 2 USD de comisión desde la caja 1:

CapaAntesDespuésQué descuenta
Caja 12015solo el principal (en moneda local o USD, según C1)
Prefondeo País10095solo el principal
Prefondeo Holding200193principal + comisión

El Holding arranca en 200, no en 195. En la reunión Diego hizo la cuenta hablada en dos pasos (200 − 5 = 195, después − 2 = 193) y el 195 es el intermedio, no un saldo inicial. Corregido el 2026-08-06 para que no termine en un test.

Las comisiones se liquidan a fin de mes entre Holding y Soterex — por eso el país nunca las ve.


2. Puntos a confirmar antes de tocar el esquema

Ya no queda nada bloqueando. El punto #1 era el único que frenaba a E4b.3 y se resolvió el 2026-08-06.

  1. ¿El Prefondeo Holding es un pool único global o sigue siendo por país?✅ RESUELTO (2026-08-06, Carlos): global. El ledger conserva country_id en cada asiento y en cada pago, pero el saldo que se muestra es la suma de todos los países. Así el reporte por país que Carlos mostró en la reunión ("desde Ciudad de Guatemala Central se pagaron 2230 USD, de eso 78 USD de comisión") sigue funcionando y, si más adelante se define que es por país, es un cambio de WHERE, no de esquema. Ver ADR-007.
  2. Catálogo exacto de motivos (C8). De la reunión salen: fondeo → Fondeo inicial, Blindado, Transferencia financiero, Otro; ajuste → Faltante de arqueo, Sobrante de arqueo, Otro. Blindado lo mencionó Diego primero como tipo de ajuste y después quedó como fondeo (un blindado solo suma). Se implementa así — si tesorería quiere Blindado también en ajustes, es agregar una constante.

3. Las tres sub-entregas

E4b.1 — Ajustes de operatoria de caja (sin cambio del modelo de saldos)

Cubre C2, C3, C8, C9. Es el bloque de menor riesgo y el más visible para el cliente: se puede mergear y mostrar sin esperar a la re-arquitectura del prefondeo.

Backend

  • CashBoxController::apertura() — sacar supervisor_id_token del request, borrar resolveSupervisor() y la dependencia de FirebaseAuth del constructor; sacar opening_balance del request y derivarlo del closing_balance de la última cash_session cerrada de esa caja (0 si es la primera apertura). Ajustar la metadata de CASH_SESSION_OPENED (sin supervisor).
  • Migración: cash_sessions.opened_by_supervisor_idnullable() (hoy es NOT NULL). No se borra la columna: las sesiones históricas de staging tienen el dato y el ledger es inmutable.
  • Continuidad del saldo entre sesiones — el punto delicado. Se mantiene el RETIRO_CIERRE y la apertura siguiente recrea el DEPOSITO_APERTURA. CORREGIDO al implementar (2026-08-06): el cierre deja de generar RETIRO_CIERRE y la apertura no crea depósito — la plata se queda en la caja. El plan original mostraba cero entre el cierre y la apertura siguiente con la plata en el PDV, y además dejaba el saldo heredado ambiguo (ordenar por closed_at con timestamps de precisión 0: dos cierres en el mismo segundo podían heredar el saldo equivocado). Ver ADR-007 §5.
  • Catálogo de motivos como constantes tipadas en el backend (enum PHP o const en el modelo), validación in: en fondeo() y ajuste() y motivos() devolviendo el catálogo fijo en vez de SELECT DISTINCT motivo. Motivo Otro habilita un campo motivo_detalle de texto libre (nullable, max:255) — nueva columna en cash_movements.
  • Filtro ?motivo= y ?type= en movimientos() y en reporte().
  • Datos existentes: los motivos de texto libre ya cargados en staging quedan fuera del catálogo. Se mapean con un UPDATE puntual en la migración (los que coincidan) y el resto pasa a Otro con el texto original en motivo_detalle — nunca se pierde el dato.

Frontend (CajaView.vue)

  • Sacar el step supervisor del modal de apertura, los campos de email/password y el import de getSecondaryIdToken. Verificar si getSecondaryIdToken queda huérfano en services/firebase.ts — si no lo usa nadie más, se borra.
  • Modal de apertura: el monto pasa a ser un dato de solo lectura ("Saldo inicial: X, tomado del cierre anterior") con la leyenda de que las diferencias se resuelven por ajuste.
  • Motivos: NSelect cerrado (sin tag), con campo de descripción condicional a Otro.
  • Listado de movimientos: mostrar el motivo bajo el tipo y agregar el filtro por motivo/tipo, que se propaga al export.

Tests: CashBoxApiTest (apertura sin supervisor; apertura hereda el cierre anterior; apertura inicial en 0; motivo fuera del catálogo → 422; Otro sin detalle → 422), CashBoxServiceTest, CajaView.spec.ts.


E4b.2 — Moneda de pago por país (C1)

Backend

  • Migración: countries.pago_en_moneda_local (boolean, default false = paga en USD). Convive con local_currency y con el módulo de TC, que no se toca — queda listo para cuando Guatemala tenga la autorización o aparezca la casa de cambio tercerizada que mencionó Teresa.
  • TransactionController::applyCashBoxDiscount() — si el país paga en USD: amount_local = amount, exchange_rate = null, y no se exige TC vigente (hoy corta con NO_EXCHANGE_RATE 422, que con esta config sería un bloqueo falso).
  • Exponer la moneda efectiva en /me o en el payload de país que ya consume el frontend.

Frontend

  • Toggle en ABM Países (admin/PaisesView.vue), junto a modulo_caja / local_currency.
  • Caja, movimientos, reporte de cajas y export: rotular los montos con la moneda efectiva y ocultar la columna "TC aplicado" cuando el país opera en USD (hoy imprime , que confunde).

Tests: PagoApiTest / TransactionApiTest — pago en país USD sin ningún TC cargado descuenta la caja por el monto nominal y no falla.


E4b.3 — Re-arquitectura del prefondeo en 3 capas (C4, C5, C6, C7, C10)

La entrega grande. Las decisiones estructurales ya están escritas en ADR-007 (2026-08-05), que supersede parcialmente a ADR-005 (doble saldo → tres capas, y baja del doble usuario de C2).

Numeración: es ADR-007 y no 006 porque feature/migracion-azure ya reservó el 006 (migración a Azure) sin mergear a main.

Modelo

  • prefunding_entries (existente) pasa a ser el ledger Holding. Se renombra el concepto en código/UI, no la tabla — renombrar la tabla obliga a tocar PrefundingService, DashboardController, ReportesController y los tests sin ganar nada.
  • Nueva country_prefunding_entries: country_id, amount, motivo (opcional), created_by, timestamps. Carga manual por tesorería (Teresa lo dejó explícito).
  • Nuevo CountryPrefundingService::balance() = Σ asientos del país − Σ amount (solo principal, sin fee) de las transacciones PAID de ese país. Puede dar negativo (C6) — no se clampea ni se bloquea.
  • PrefundingService::balance() (Holding) sigue descontando amount + fee, con el alcance global del punto 2.1 de arriba.
  • El fondeo de una caja no escribe nada en el ledger del país (C10). Es lo que ya hace el código hoy — vale confirmarlo con un test de regresión para que no se rompa por accidente.

Pago (TransactionController::pay())

  • Se elimina el corte por INSUFFICIENT_PREFUNDING para países con modulo_caja activo: la única validación es la de la caja operativa (C5).
  • Asunción explícita: para países sin módulo Caja (Ecuador, con EPOS, hoy en producción) se mantiene la validación de saldo, pero contra el prefondeo país (solo principales). Sacarla también ahí dejaría a Ecuador sin ninguna validación de saldo, y eso no es lo que se discutió — la conversación entera fue sobre Guatemala con caja. Si el cliente quiere sacarla en todos lados, es borrar una rama.

Alerta de fondeo (C7)

  • CashBoxController::fondeo() responde 200 con un flag warning cuando el monto excede el saldo del prefondeo país, en vez de 422. No bloquea.
  • El aviso es posterior, no una confirmación previa. (Corregido el 2026-08-06: este plan decía "el frontend lo muestra como confirmación — ¿Confirmar igual?", que contradice al backend que el propio plan especifica: si la respuesta es 200 y el movimiento ya se creó, preguntar si confirmás es preguntar por algo que ya pasó. Manda ADR-007 §3, que define la alerta como posterior.)
  • El diseño exacto del aviso —banner inline persistente en la tarjeta de saldo, sin auto-dismiss, y cómo se distingue visualmente de un error que sí bloquea— está en §5.4 del análisis funcional.

Pantallas y permisos

  • PrefondeoView.vue actual → Prefondeo Holding (rótulos, saldo global, desglose por país).
  • Nueva pantalla Prefondeo País: saldo del país, listado de asientos, alta manual. Saldo negativo en rojo con leyenda, sin bloqueos.
  • Módulo de permiso nuevo prefondeo_pais en RoleSeeder + RolePermission, con su fila en el ABM de Roles y Permisos. Tesorería/Supervisor escriben; Admin todo.
  • Reportes: el desglose por país/PDV con principal y comisión que Carlos mostró en la reunión es lo que se le reporta a Soterex — vive en la capa Holding, no en la del país.

Tests: CountryPrefundingApiTest (nuevo), PrefundingApiTest (holding con comisión), TransactionApiTest (pago sin saldo de país no se bloquea con caja activa; el país queda negativo), CashBoxApiTest (fondeo por encima del país devuelve warning y crea el movimiento).


4. Orden, dependencias y riesgo

E4b.1 (operatoria) ──┐
E4b.2 (moneda) ──────┴─→ E4b.3 (3 capas) ─→ E5 (wizard) ─→ E6 ─→ E7
  • E4b.1 y E4b.2 son independientes entre sí y de E4b.3 — pueden ir en branches paralelos y mergearse apenas pasen review.
  • E4b.3 depende de resolver el punto 2.1 (alcance del prefondeo holding). Todo lo demás se puede arrancar hoy.
  • E5 arranca cuando E4b.2 esté mergeada (el wizard muestra monto en moneda local y TC — con pago en USD ese step cambia) y, para el validador de saldo del paso 4, cuando E4b.3 defina qué se valida.

Riesgo alto — plata real, mirar con lupa en el review:

  1. La continuidad del saldo entre cierre y apertura (E4b.1). Un error acá inventa o borra plata en el ledger. Test explícito de tres sesiones encadenadas con fondeos y pagos en el medio.
  2. La migración de motivos existentes en staging (E4b.1).
  3. Sacar INSUFFICIENT_PREFUNDING (E4b.3) sin dejar a Ecuador sin ninguna validación.

Deuda que E4b abre a propósito: ADR-005 queda parcialmente superseded (doble usuario y doble saldo). Se corrige en el ADR-007 de E4b.3 y se refleja en E7 (documentación total), que ya tiene en su alcance "la decisión explícita de no implementar aprobación por supervisor" — ahora con más motivo.


5. Fuera de alcance de E4b (queda anotado, no se pierde)

  • API real de Soterex — Teresa: "solamente falta la reunión con la gente de Soterex por tema la API". Sigue siendo M5, y sigue bloqueado por el punto #2b del Análisis Funcional.
  • Mail de Maggie con la documentación requerida para el pago — es el insumo de E5 (carta firmada, scan de identificación, foto). Carlos quedó en revisarlo; si falta algo, Teresa lo recaba.
  • Casa de cambio tercerizada (Teresa, "segunda instancia"): CIS conseguiría una casa de cambio que haga la conversión y CIS cobra comisión. No se diseña ahora — el flag de C1 deja la puerta abierta.

6. Convenciones de esta entrega

Sin novedad respecto del resto del proyecto: branch por sub-entrega → implementación → tests → code-reviewer sobre el diff → corregir → commit → push → PR. El merge a main sigue necesitando ok explícito en el chat, igual que todo lo demás.

Branches propuestos:

  • feature/e4b-1-operatoria-caja
  • feature/e4b-2-moneda-pago-pais
  • feature/e4b-3-prefondeo-tres-capas

Documentación viva — se actualiza junto con el código, no es un anexo aparte.