Preguntas Abiertas — Módulo Caja
Análisis Funcional: Módulo Caja — parte 7 de 7. ← UX de múltiples cajas y fondeo · Índice
Lo que todavía falta confirmar con el cliente, más lo que se implementó por analogía o por criterio del equipo y nunca se validó. Mismo criterio que las Preguntas Abiertas del análisis original: numeradas, con estado y fecha, y sin redondear como cerrado nada que no lo esté.
Estados: ✅ RESUELTO (y en main) · ⚠️ ABIERTA · 📌 PENDIENTE NO BLOQUEANTE
6.1 Abiertas
Alcance del Prefondeo Holding: ¿pool único global o por país?— ✅ RESUELTO (2026-08-06): global. Enmain. La transcripción de la reunión admitía las dos lecturas y nadie las había reconciliado: Carlos habló de un fondeo único para todos los países, y Diego ejemplificó con 200 USD repartidos entre Ecuador y Guatemala —que se lee igual de bien como "200 globales" que como "200 de esta holding para estos dos países"—. Eran modelos distintos: uno tiene un saldo, el otro tiene N. Resolución (Carlos, 2026-08-06): el ledger conservacountry_iden cada asiento y en cada pago, pero el saldo que se muestra es la suma global de todos los países —PrefundingService::globalBalance(). Así el reporte desagregado por país y PDV que se le informa a Soterex sigue funcionando. Ver ADR-007 §1.Catálogo exacto de motivos: ¿
Blindadova también como motivo de ajuste? — ⚠️ ABIERTA (2026-08-05). No bloqueante. Diego lo mencionó primero como tipo de ajuste — pidió "un ajuste que sea netamente identificable de este tipo de transacción de blindados", para distinguirlo de un ajuste por faltante — y al enumerar los tipos de ajuste dijo "faltante de arqueo, el blindado y sobrante de arqueo". Después, cuando Carlos aclaró que "esto es solo fondeo, todo aumenta; después están los ajustes, que pueden ser positivo o negativo",Blindadoquedó del lado de fondeo y Diego cerró con "en el de fondeo está bien: fondeo inicial, blindado, transferencia financiero, y otro". Implementado así (CashMovement::MOTIVOS_FONDEO/MOTIVOS_AJUSTE,backend/app/Models/CashMovement.php:30-50, ya enmain): fondeo →Fondeo inicial,Blindado,Transferencia financiero,Otro; ajuste →Faltante de arqueo,Sobrante de arqueo,Otro. Qué hace falta: confirmar si tesorería también quiereBlindadodisponible en ajustes. Si la respuesta es sí, es agregar una constante — por eso no bloquea. El catálogo se decidió cerrado justamente para que nadie escriba "blindado" y "transferencia de dinero" como conceptos distintos, así que agregar motivos siempre va a ser una decisión de tesorería, no del operador.¿Qué rol carga el Prefondeo País?— ✅ RESUELTO (2026-08-06): Supervisor y Admin. Enmain. Teresa dejó explícito que la carga es manual, por tesorería, pero "tesorería" no es un rol del sistema: los sembrados son Backoffice, Supervisor y Admin (RoleSeeder). Resolución (Carlos): no se crea un rol nuevo —prefondeo_paisqueda con L·E para Supervisor y todo para Admin (RoleSeeder.php:72-94,95-113), mismo criterio que el prefondeo que ya existe.Un mismo usuario con dos cajas abiertas a la vez— ✅ RESUELTO (2026-08-06): se bloquea la segunda apertura. Enmain. El diseño de múltiples cajas asumía "una caja abierta por persona a la vez; cambio de turno = cerrar y volver a abrir", pero nada lo impedía: el índice único parcialcash_sessions_one_open_per_boxgarantiza una sola sesión abierta por caja, no por usuario. Con dos cajas abiertas a su nombre,applyCashBoxDiscount()resolvía la caja con unfirst()sinorderBy: de cuál salía la plata lo decidía el orden de la base. Resolución (Carlos), implementada:USER_ALREADY_HAS_OPEN_CASH_BOX(409) más el índice únicocash_sessions_one_open_per_user(backend/database/migrations/2026_08_06_130000_e4b_una_sola_caja_abierta_por_usuario.php), yapplyCashBoxDiscount()ahora ordena explícitamente (orderByDesc('opened_at'),TransactionController.php:328) como defensa en profundidad para lo que hubiera quedado abierto de a dos antes de esta regla.Alcance del permiso de caja: ¿por país o por PDV asignado?— ✅ RESUELTO (2026-08-06): por PDV. Era un bug, ya corregido y enmain.CashBoxController::disponibles()listaba las cajas acotadas por PDV (PermissionService::allowedStationIds), pero todas las demás operaciones sobre una caja puntual —apertura, cierre, fondeo, ajuste, movimientos, reporte— pasaban porauthorizeStationScope(), que a pesar del nombre validaba solo a nivel país. Un Backoffice acotado a la sucursal A no veía en pantalla las cajas de la sucursal B, pero podía operarlas por API conociendo el id. Resolución:authorizeStationScope()(CashBoxController.php:621-638) ahora chequea tambiénallowedStationIds, mismo criterio queTransactionController::authorizeScope. Las asignaciones sin PDVs (todos los del país, ej. Admin) siguen sin restricción. Sigue siendo un caso a revisar en E6.La caja que paga puede ser de un PDV distinto al que se le atribuye la transacción — 📌 PENDIENTE NO BLOQUEANTE (2026-08-04). Parcialmente resuelto. Desde múltiples cajas, "a qué PDV se atribuye la transacción" y "qué caja física paga" son dos cosas que se resuelven por separado: la primera por el PDV activo del usuario, la segunda por la caja que ese usuario abrió. El caso multipaís —pagar una transacción de un país con la caja de otro— ya se cerró:
CASH_BOX_COUNTRY_MISMATCHlo bloquea desde el 2026-08-05 (TransactionController::applyCashBoxDiscount(),:342-355). Lo que sigue sin resolver es el cruce dentro de un mismo país: un operador con acceso a dos sucursales del mismo país podría tener abierta la caja de una y pagar una transacción atribuida a la otra — quedaría un reporte por PDV que no coincide con el movimiento de caja. Está documentado como decisión consciente en el diseño de E4-multi §2.3 ("no pedido, no se agrega esa validación cruzada"). Qué hace falta: ver si en el uso real aparece; si aparece, es un chequeo chico.Las reglas del
AJUSTEnunca se confirmaron con el cliente — 📌 PENDIENTE NO BLOQUEANTE (2026-08-05). La operación de ajuste no está en las decisiones del plan v2 — la agregó el equipo por analogía al implementar E4, y el cliente la vio funcionando recién en la reunión del 2026-08-05 (donde la usó sin objetarla, y de hecho pidió motivos para ella). Quedan sin confirmar tres criterios que se decidieron solos:- Se gatea con
fondeo_caja:write(Supervisor/Admin), no con un permiso propio. - Un ajuste que dejaría la caja en negativo se rechaza (
ADJUSTMENT_WOULD_GO_NEGATIVE). - Requiere sesión de caja abierta — y con el saldo arrastrándose entre sesiones (ya en
main), un blindado o faltante detectado fuera de horario sigue sin poder registrarse hasta la próxima apertura (ver 06.1 #11 más abajo). Qué hace falta: validarlas con tesorería.
- Se gatea con
¿La validación de saldo se saca también en los países sin módulo Caja?— ✅ RESUELTO (2026-08-06): se mantiene. Enmain.ADR-007 §2 conserva una validación de saldo para los países sin Caja (Ecuador, con EPOS, en producción), ahora contra el Prefondeo País. Era una asunción del equipo, no un pedido. Resolución (Carlos): se mantiene. Implementado enTransactionController::pay()(:232-245): decisión consciente y no como omisión.Los comprobantes membretados de apertura y cierre están decididos pero no existen — 📌 PENDIENTE NO BLOQUEANTE (2026-08-05). Llega con E5. Las decisiones #14 y #15 del plan v2 definen un comprobante membretado de apertura y otro de cierre, en formato térmico de 58 mm, accesibles y reimprimibles desde la lista de movimientos. Las columnas están (
cash_sessions.opening_receipt_pathyclosing_receipt_path) pero nadie las escribe: la generación de PDFs quedó fuera del alcance de E4 y llega con E5, con el mismo motor que la papeleta. El mensaje falso ya se corrigió. La versión anterior de este documento señalaba que, al cerrar una caja, la app decía "El comprobante de cierre queda disponible en Movimientos" sin que existiera ningún comprobante. Ese texto se sacó al reescribir el modal de cierre para E4b.1: hoy la notificación de éxito es simplementeCaja cerrada(CajaView.vue:206), sin mencionar ningún comprobante. Sigue pendiente lo de fondo: los comprobantes en sí no existen, y eso sigue siendo trabajo de E5.¿Qué pasa con el ledger de una caja si el país cambia de moneda de pago?— ✅ RESUELTO (2026-08-06): la moneda se guarda en cada movimiento. Enmain.cash_movements.amount_localguardaba un número sin moneda: se infería del país. Si un país que venía operando en moneda local pasaba a pagar en USD —o al revés— los movimientos viejos y los nuevos quedaban en el mismo ledger, en monedas distintas, y el saldo derivado sumaba peras con manzanas. No era hipotético:pago_en_moneda_locales editable desde ABM Países. Resolución (Carlos), implementada:cash_movements.currency(backend/database/migrations/2026_08_06_110000_e4b2_moneda_de_pago_por_pais.php) registra en qué moneda se cargó cada movimiento. El histórico nunca se reinterpreta —CashBoxService::balance()filtra por moneda vigente (:31-40) y expone lo que quedó en una moneda anterior víaresidualBalances()(:50-62).¿Se puede fondear o ajustar una caja cerrada? — 📌 PENDIENTE NO BLOQUEANTE (2026-08-05). Hoy no: fondeo y ajuste exigen una sesión abierta (
CASH_SESSION_CLOSED, 422). El escenario que describió Diego encaja igual —"la caja el día uno tiene saldo cero; viene el blindado, llegan los 20; voy a esta pantallita, fondeo inicial, ingresan los 20"—: se abre la caja y después se fondea. Pero si un blindado llega fuera del horario de operación, la plata no se puede registrar hasta la próxima apertura. Con el saldo ya arrastrándose entre sesiones (E4b.1, enmain), este desfasaje pesa un poco más que antes de resolverlo, porque ahora "esperar a la apertura siguiente" es la única vía. Qué hace falta: confirmar que ese desfasaje no molesta en la operación real.
6.2 Cerradas en la revisión del 2026-08-05 — ya en main
Estaban abiertas antes de la reunión y quedaron definidas ahí. A diferencia de cuando se escribió la primera versión de este documento, las cuatro ya están implementadas y corriendo en main — mergearon el 2026-08-06, más de un mes antes de esta revisión.
¿El saldo inicial de apertura lo tipea el operador o se hereda? — ✅ RESUELTO (2026-08-05) e implementado. Se hereda del cierre anterior de esa misma caja (cero en la primera apertura), y no es editable ni se ofrece como sugerencia. Diego: "que lo tome automáticamente, no debería ser como sugerencia".
CashBoxService::inheritedOpeningBalance()(backend/app/Services/CashBoxService.php:79-82). Ver Modelo de saldos §1.8.¿La apertura y el cierre necesitan autorización de un Supervisor? — ✅ RESUELTO (2026-08-05) e implementado: no. Se eliminó la reautenticación in-band que había definido ADR-005.
opened_by_supervisor_idquedónullabley dejó de escribirse (backend/database/migrations/2026_08_06_100000_e4b1_caja_sin_supervisor_y_motivos_cerrados.php:24), y el códigoSUPERVISOR_INVALIDya no existe. Carlos: "sacamos los validadores para abrir caja y cerrar caja por parte de los supervisores".Fondear una caja por encima del prefondeo del país: ¿se bloquea? — ✅ RESUELTO (2026-08-05) e implementado: no se bloquea, se avisa. Diego lo pidió "como una alerta más que un validador".
CashBoxController::fondeo()responde 201, crea el movimiento igual, y agrega unwarningcon códigoOVER_COUNTRY_PREFUNDINGcuando el disponible para distribuir del país queda negativo (overDistributionWarning(),:406-425). Ver 5.4.¿En qué moneda se paga? — ✅ RESUELTO (2026-08-05) e implementado: configurable por país. Guatemala paga en dólares porque no tiene autorización para cambio de divisa (Teresa):
countries.pago_en_moneda_local(defaultfalse,backend/database/migrations/2026_08_06_110000_e4b2_moneda_de_pago_por_pais.php). Con la bandera apagada,NO_EXCHANGE_RATEno puede dispararse (TransactionController::applyCashBoxDiscount(),:357-371). El módulo de tipo de cambio no se tocó: queda para cuando llegue la autorización o aparezca la casa de cambio tercerizada que Teresa mencionó como segunda instancia.
Resumen — qué bloquea de verdad
- No queda nada bloqueando ni pendiente de definición cerrada. Las cinco preguntas que quedaban abiertas al 2026-08-05 se cerraron el 2026-08-06: #1 (Holding global con desglose por país), #3 (Prefondeo País lo cargan Supervisor y Admin), #4 (se bloquea la segunda apertura del mismo usuario), #5 (el permiso de caja acota por PDV — era un bug, ya corregido) y #8 (los países sin Caja mantienen validación contra el Prefondeo País). Las cuatro decisiones de la reunión del cliente (#12-#15) están implementadas y en
maindesde esa misma fecha. - #5 igual conserva su lugar en la auditoría de seguridad de E6: el fix cierra el agujero concreto, pero el alcance de permisos sobre plata merece una pasada completa.
- #6 pasó de "abierta" a "parcialmente resuelta": el cruce multipaís se cerró con
CASH_BOX_COUNTRY_MISMATCH; el cruce PDV↔caja dentro de un mismo país sigue sin validarse, a propósito. - El resto (#2, #7, #9, #11) son pendientes menores que no frenan nada — #9 en particular ya no incluye un mensaje falso en la app, solo falta implementar el comprobante en sí (E5).
Análisis Funcional: Módulo Caja — parte 7 de 7. ← UX de múltiples cajas y fondeo · Índice

