Appearance
Movimientos Bancarios — Modo Prueba y Dual-Database
Módulo: Tesorería Tipo: Process Estado: ✅ Implementado Fecha: 2026-03-11
📋 Contexto arquitectural: Multi-Modo: Dual Database Pattern
Este documento describe el comportamiento específico de los movimientos bancarios (
iteban) frente al patrón dual-database de Sistema Bautista. Para entender el modelo general de databases oficial/prueba, el aliasprincipal, y los modos de consolidación en reportes, consulte el documento arquitectural.
Descripción
Los movimientos bancarios de tipo transferencia generan un registro en la tabla iteban. Esta tabla es transaccional: debe existir en la database oficial (bautista) cuando se opera en modo oficial, y en la database de prueba (bautista_p) cuando se opera en modo prueba.
Bug corregido
MovimientoBancarioService hardcodeaba $manager->get('oficial') para el modelo de escritura, en lugar de usar $manager->get('principal'). Esto causaba que todas las escrituras a iteban fueran siempre a bautista, incluso cuando el request llevaba prueba=true. El fix consiste en cambiar 'oficial' por 'principal' en la asignación del model.
php
// ANTES (bug): siempre escribía en bautista
$this->model = new MovimientoBancario($manager->get('oficial'));
// DESPUÉS (correcto): respeta el modo activo del request
$this->model = new MovimientoBancario($manager->get('principal'));Comportamiento
Escritura de movimientos bancarios
MovimientoBancarioService usa $manager->get('principal') para el repositorio de iteban. El alias principal se resuelve dinámicamente según el parámetro prueba del request:
prueba en request | Alias principal apunta a | Database destino de iteban |
|---|---|---|
false (default) | oficial | bautista.{schema} |
true | prueba | bautista_p.{schema} |
Para operaciones de metadatos (MultiSchemaService, consultas a information_schema), MovimientoBancarioService sigue usando 'oficial' explícitamente, ya que los esquemas maestros solo existen en la database oficial.
Flujo corregido — Recibo con transferencia en modo prueba
Request: POST /tesoreria/recibos { prueba: true, medio_pago: "transferencia", ... }
1. ConnectionMiddleware
├── Lee prueba=true del body
└── ConnectionManager::setGlobalConfig(['prueba' => true])
└── Alias 'principal' → resuelve a 'prueba' (bautista_p)
2. AbstractReciboController
└── Llama a MovimientoCajaService::create(data)
3. MovimientoCajaService
├── Crea registro en movimi → bautista_p.{schema}.movimi ✅
└── Si medio_pago es transferencia → llama MovimientoBancarioService::create(data)
4. MovimientoBancarioService
├── $manager->get('principal') → conexión a bautista_p
├── Crea registro en iteban → bautista_p.{schema}.iteban ✅
└── Retorna id_iteban
5. MovimientoCajaService
└── Guarda id_iteban en rectra → bautista_p.{schema}.rectra ✅Consulta de conciliación (modo consolidado)
El endpoint GET /tesoreria/mov-conciliados siempre opera en modo consolidado: consulta ambas databases independientemente y combina los resultados. El parámetro prueba ya no es válido y es ignorado silenciosamente.
GET /tesoreria/mov-conciliados?id_banco=5&cursor=<opaco>&pageSize=20
1. MovimientoConciliadoController::getPaginated()
├── Lee cursor (opcional) y pageSize del query string
└── Llama a MovimientoConciliadoService::getAllConsolidated()
2. MovimientoConciliadoService::getAllConsolidated()
├── Abre dos instancias independientes:
│ ├── model_oficial = new MovimientoCaja($manager->get('oficial'))
│ └── model_prueba = new MovimientoCaja($manager->get('prueba'))
├── Si cursor presente: decodifica base64 → JSON → {fecvto, id, origen}
│ └── Retorna 422 si el cursor es malformado
├── Ejecuta query en cada DB con condición keyset (si hay cursor):
│ └── (r.fecvto < :c_fecvto OR (r.fecvto = :c_fecvto AND r.id < :c_id))
├── Cada fila de oficial incluye '_origen' = 'oficial'
├── Cada fila de prueba incluye '_origen' = 'prueba'
├── Merge PHP de ambos arrays ordenando por (fecvto DESC, id DESC)
├── Slice de los primeros pageSize registros
├── Genera nextCursor = base64(json({fecvto, id, origen} del último registro))
│ └── null si no hay más registros
└── Ejecuta dos COUNT independientes → totalRowCount = COUNT(oficial) + COUNT(prueba)
3. Response
├── data: [...] (filas con _origen por registro)
└── meta: { cursor, totalRowCount, hasMore, pageSize }Paginación por cursor keyset
| Parámetro | Tipo | Descripción |
|---|---|---|
cursor | string (opcional) | Cursor opaco devuelto por la respuesta anterior. Ausente = primera página. |
pageSize | int (opcional) | Registros por página. Default: 20. Máximo: 20. |
El cursor codifica { fecvto, id_iteban, origen } del último registro de la página actual en base64/JSON. El cliente no debe parsear ni construir el cursor — solo reenviarlo tal como se recibió.
Prerequisito de índice: La paginación keyset requiere el índice idx_iteban_fecvto_id ON iteban(fecvto DESC, id) en ambas databases (tenancy/ y tenancy_p/). Sin este índice la consulta es O(n) en tablas grandes.
Backend
ConnectionMiddleware — campo prueba
El middleware lee el parámetro prueba de dos fuentes, con el siguiente orden de precedencia:
| Fuente | Métodos HTTP | Precedencia |
|---|---|---|
| Body JSON / form data | POST, PATCH, PUT | Alta |
| Query string | GET, DELETE, cualquier método | Baja (fallback si no está en body) |
php
// ConnectionMiddleware — lógica de lectura
$params = $request->getQueryParams(); // query string
$body = $request->getParsedBody(); // body
// Body tiene precedencia sobre query params
$prueba = $body['prueba'] ?? $params['prueba'] ?? false;
$prueba = filter_var($prueba, FILTER_VALIDATE_BOOLEAN);
ConnectionManager::setGlobalConfig(['prueba' => $prueba]);Esta extensión a query params fue necesaria para que los endpoints GET (como /mov-conciliados) puedan operar en modo prueba sin cambiar a POST.
MovimientoBancarioService
| Conexión | Alias usado | Propósito |
|---|---|---|
| Model de escritura | 'principal' | INSERT/UPDATE en iteban (bautista o bautista_p según modo) |
| MultiSchemaService | 'oficial' | Consultas a information_schema (solo existe en oficial) |
CreateMovimientoCajaValidator
Agrega el campo prueba como opcional:
| Campo | Regla | Comportamiento |
|---|---|---|
prueba | nullable|boolean | Si ausente → default false (modo oficial) |
MovimientoConciliadoController
getPaginated()leecursorypageSizedel query string y llama aMovimientoConciliadoService::getAllConsolidated(). Ya no extraepruebapara seleccionar DB — el servicio consolida siempre ambas databases. El endpoint legacy (conprueba+ OFFSET) se mantiene marcado como@deprecatedhasta confirmar estabilidad en producción.partialUpdate()extrae_origendel body del request y llama aMovimientoConciliadoService::partialUpdateConsolidado($id, $data, $origen). Valida que_origen ∈ ['oficial', 'prueba']; retorna 422 si ausente o inválido.
Frontend
Vistas con ModeChanger (Tesorería)
Las siguientes vistas incluyen el componente ModeChanger y mantienen estado modo (boolean) que se envía como prueba en cada request:
| Vista | Componente React | Estado |
|---|---|---|
| Vista de carga de movimientos de caja | ModeChanger | modo (boolean) |
Nota:
MovimientosConciliadosView.tsxya no incluyeModeChangerni estadomodo. La vista siempre opera en modo consolidado — ver sección "Consulta de conciliación" más abajo.
Guards eliminados
Dos guards en el frontend legacy fueron removidos porque impedían operar en modo prueba:
list-valores.js
- Guard que bloqueaba la visualización de transferencias cuando el modo era prueba o consolidado.
- Eliminado: las transferencias deben ser visibles en cualquier modo.
form-movimientos.js
- Guard que ocultaba cuentas con
relaciona_bancos=truecuando el modo era prueba. - Eliminado: las cuentas bancarias deben estar disponibles en modo prueba para poder registrar transferencias.
Informes
Libro de Bancos — siempre mode=2
El informe libro_bancos.php opera siempre en modo consolidado (mode=2). El parámetro mode ya no es configurable por el usuario — el JS legacy (libro-bancos.js) lo hardcodea a 2 y la vista PHP no renderiza ningún selector de modo.
| Valor | Comportamiento | Indicador en PDF |
|---|---|---|
0 | bautista_p) | [PRUEBA] |
1 | bautista) | |
2 (único activo) | Consolidado: ambas databases, columna origen visible | [CONSOLIDADO] |
Corrección W-04: saldoAnterior consolidado
Anteriormente en mode=2, el bloque $acumularSaldoAnterior() solo ejecutaba la query contra bautista, omitiendo bautista_p. Esto causaba que el saldo anterior reportado fuera incorrecto (solo oficial). La corrección asegura que $acumularSaldoAnterior($connPrueba) se ejecute siempre cuando mode=2, sumando el saldo anterior de bautista_p al de bautista en el mismo array $saldoAnteriorPorCuenta[].
Estrategia PHP para mode=2 (consolidado)
El informe ejecuta dos queries independientes (una por database) y combina los resultados en PHP:
1. Query a bautista.{schema}.iteban → $rowsOficial (con origen='oficial')
2. Query a bautista_p.{schema}.iteban → $rowsPrueba (con origen='prueba')
3. array_merge($rowsOficial, $rowsPrueba) → $rowsMerged
4. usort($rowsMerged, fn por fecha) → ordenamiento cronológico unificado
5. Renderizar PDF con columna "Origen" y disclaimer [CONSOLIDADO]Esta estrategia es PHP-side (no SQL JOIN entre databases) porque PostgreSQL no puede hacer JOINs cross-database directamente.
Tablas afectadas
| Tabla | Database (modo oficial) | Database (modo prueba) |
|---|---|---|
iteban | bautista.{schema} | bautista_p.{schema} |
rectra | bautista.{schema} | bautista_p.{schema} |
movimi | bautista.{schema} | bautista_p.{schema} |
Medios de pago y creación de iteban
Solo las transferencias bancarias crean un registro en iteban. Los demás medios de pago no generan movimiento bancario:
| Medio de pago | Crea iteban | Crea movimi |
|---|---|---|
| Transferencia | ✅ Sí | ✅ Sí |
| Efectivo | ❌ No | ✅ Sí |
| Cheque de tercero | ❌ No | ✅ Sí |
| Cheque propio | ❌ No | ✅ Sí |
| Otros | ❌ No | ✅ Sí |
Invariante de integridad
rectra.id_iteban y iteban.id DEBEN estar siempre en la misma database. La FK nunca cruza databases:
✅ CORRECTO:
rectra (bautista_p.suc0001) → id_iteban=42 → iteban (bautista_p.suc0001).id=42
❌ INCORRECTO (antes del fix):
rectra (bautista_p.suc0001) → id_iteban=42 → iteban (bautista.suc0001).id=42
(FK rota: rectra en prueba apuntaba a iteban en oficial)Esta invariante es la razón por la que MovimientoBancarioService debe usar 'principal' y no 'oficial'.
Migraciones
Para que bautista_p tenga la tabla iteban, se requieren dos wrappers en el directorio tenancy_p/:
| Archivo | Propósito |
|---|---|
20240913184602_new_table_iteban.php | CREATE TABLE iteban en bautista_p |
{timestamp}_delete_foreign_key_iteban_to_movimi.php | DROP FK iteban → movimi en bautista_p (la FK no existe en prueba) |
Los wrappers en tenancy_p/ ejecutan las mismas migraciones de estructura que tenancy/, pero contra la database bautista_p.
Rollback
Para revertir el fix y restaurar el comportamiento anterior (escritura siempre en bautista):
php
// En MovimientoBancarioService — cambiar 'principal' de vuelta a 'oficial'
$this->model = new MovimientoBancario($manager->get('oficial'));Nota: Revertir esto rompe la invariante de integridad en modo prueba. Solo hacerlo si se entiende el impacto.
Referencias
Arquitectura
- Multi-Modo: Dual Database Pattern — Patrón general, alias
principal, modos de consolidación - Multi-Tenant (Schema-Based Tenancy) — Schemas por sucursal/caja
Documentación relacionada en Tesorería
- mov-conciliados-paginacion-view.md — Vista de movimientos conciliados con paginación
- movimiento-caja-multi-schema-process.md — Búsqueda multi-schema de movimientos de caja
Código fuente
bautista-backend/service/Tesoreria/MovimientoBancarioService.phpbautista-backend/Middleware/ConnectionMiddleware.phpbautista-backend/validator/CreateMovimientoCajaValidator.phpbautista-backend/controller/modulo-tesoreria/MovimientoConciliadoController.phpbautista-app/ts/tesoreria/views/MovimientosConciliadosView.tsxinformes/libro_bancos.php
Historial de cambios
| Fecha | Versión | Descripción |
|---|---|---|
| 2026-03-11 | 1.0 | Documento inicial: corrección del bug 'oficial' → 'principal' en MovimientoBancarioService, extensión de prueba a query params en ConnectionMiddleware, guards eliminados en frontend legacy, soporte mode=0/1/2 en Libro de Bancos, migraciones tenancy_p/. |
| 2026-03-16 | 1.1 | Vista consolidada: eliminar MovimientosConciliadosView.tsx de tabla ModeChanger; reemplazar flujo prueba=true/false por consolidación con cursor keyset; actualizar MovimientoConciliadoController; Libro de Bancos hardcodeado a mode=2; corrección W-04 (saldoAnterior suma ambas DBs); agregar subsección paginación por cursor keyset. |