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ón | Contradice lo implementado |
|---|---|---|
| C1 | Guatemala 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 |
| C2 | Se 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 borrar | Sí — ADR-005 (doble usuario con reautenticación in-band) |
| C3 | El 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 → fondeo | Sí — hoy lo tipea el operador |
| C4 | El 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 PDV | Sí — hoy hay 2 capas |
| C5 | Al pagar se valida solo el saldo de la caja operativa. Se saca la validación contra el prefondeo | Sí — INSUFFICIENT_PREFUNDING |
| C6 | El prefondeo país admite saldo negativo (sobregiro); no bloquea la operación | — |
| C7 | Fondear una caja por encima del prefondeo del país dispara una alerta, no un bloqueo ("una alerta más que un validador", Diego) | — |
| C8 | Los motivos de fondeo/ajuste pasan a ser una lista desplegable fija, no texto libre ni combobox con "agregar" | Sí — hoy es combobox libre |
| C9 | El motivo se muestra en la descripción del movimiento y hay filtro por motivo en el listado y en el export | — |
| C10 | Mover 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: 20Pago de 5 USD de principal + 2 USD de comisión desde la caja 1:
| Capa | Antes | Después | Qué descuenta |
|---|---|---|---|
| Caja 1 | 20 | 15 | solo el principal (en moneda local o USD, según C1) |
| Prefondeo País | 100 | 95 | solo el principal |
| Prefondeo Holding | 200 | 193 | principal + 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.
¿El Prefondeo Holding es un pool único global o sigue siendo por país?✅ RESUELTO (2026-08-06, Carlos): global. El ledger conservacountry_iden 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 deWHERE, no de esquema. Ver ADR-007.- 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.Blindadolo mencionó Diego primero como tipo de ajuste y después quedó como fondeo (un blindado solo suma). Se implementa así — si tesorería quiereBlindadotambié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()— sacarsupervisor_id_tokendel request, borrarresolveSupervisor()y la dependencia deFirebaseAuthdel constructor; sacaropening_balancedel request y derivarlo delclosing_balancede la últimacash_sessioncerrada de esa caja (0si es la primera apertura). Ajustar la metadata deCASH_SESSION_OPENED(sin supervisor).- Migración:
cash_sessions.opened_by_supervisor_id→nullable()(hoy esNOT 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 elCORREGIDO al implementar (2026-08-06): el cierre deja de generarRETIRO_CIERREy la apertura siguiente recrea elDEPOSITO_APERTURA.RETIRO_CIERREy 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 porclosed_atcon 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
consten el modelo), validaciónin:enfondeo()yajuste()ymotivos()devolviendo el catálogo fijo en vez deSELECT DISTINCT motivo. MotivoOtrohabilita un campomotivo_detallede texto libre (nullable,max:255) — nueva columna encash_movements. - Filtro
?motivo=y?type=enmovimientos()y enreporte(). - Datos existentes: los motivos de texto libre ya cargados en staging quedan fuera del catálogo. Se mapean con un
UPDATEpuntual en la migración (los que coincidan) y el resto pasa aOtrocon el texto original enmotivo_detalle— nunca se pierde el dato.
Frontend (CajaView.vue)
- Sacar el step
supervisordel modal de apertura, los campos de email/password y el import degetSecondaryIdToken. Verificar sigetSecondaryIdTokenqueda huérfano enservices/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:
NSelectcerrado (sintag), con campo de descripción condicional aOtro. - 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, defaultfalse= paga en USD). Convive conlocal_currencyy 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 conNO_EXCHANGE_RATE422, que con esta config sería un bloqueo falso).- Exponer la moneda efectiva en
/meo en el payload de país que ya consume el frontend.
Frontend
- Toggle en ABM Países (
admin/PaisesView.vue), junto amodulo_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-azureya reservó el 006 (migración a Azure) sin mergear amain.
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 tocarPrefundingService,DashboardController,ReportesControllery 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, sinfee) de las transaccionesPAIDde ese país. Puede dar negativo (C6) — no se clampea ni se bloquea. PrefundingService::balance()(Holding) sigue descontandoamount + 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_PREFUNDINGpara países conmodulo_cajaactivo: 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()responde200con un flagwarningcuando el monto excede el saldo del prefondeo país, en vez de422. 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
200y 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.vueactual → 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_paisenRoleSeeder+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:
- 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.
- La migración de motivos existentes en staging (E4b.1).
- 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-cajafeature/e4b-2-moneda-pago-paisfeature/e4b-3-prefondeo-tres-capas

