Skip to content

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 alias principal, 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 requestAlias principal apunta aDatabase destino de iteban
false (default)oficialbautista.{schema}
truepruebabautista_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ámetroTipoDescripción
cursorstring (opcional)Cursor opaco devuelto por la respuesta anterior. Ausente = primera página.
pageSizeint (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:

FuenteMétodos HTTPPrecedencia
Body JSON / form dataPOST, PATCH, PUTAlta
Query stringGET, DELETE, cualquier métodoBaja (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ónAlias usadoPropó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:

CampoReglaComportamiento
pruebanullable|booleanSi ausente → default false (modo oficial)

MovimientoConciliadoController

  • getPaginated() lee cursor y pageSize del query string y llama a MovimientoConciliadoService::getAllConsolidated(). Ya no extrae prueba para seleccionar DB — el servicio consolida siempre ambas databases. El endpoint legacy (con prueba + OFFSET) se mantiene marcado como @deprecated hasta confirmar estabilidad en producción.
  • partialUpdate() extrae _origen del body del request y llama a MovimientoConciliadoService::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:

VistaComponente ReactEstado
Vista de carga de movimientos de cajaModeChangermodo (boolean)

Nota: MovimientosConciliadosView.tsx ya no incluye ModeChanger ni estado modo. 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=true cuando 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.

ValorComportamientoIndicador en PDF
0Solo movimientos no oficiales (bautista_p)[PRUEBA]
1Solo movimientos oficiales (bautista)sin indicador
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

TablaDatabase (modo oficial)Database (modo prueba)
itebanbautista.{schema}bautista_p.{schema}
rectrabautista.{schema}bautista_p.{schema}
movimibautista.{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 pagoCrea itebanCrea 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/:

ArchivoPropósito
20240913184602_new_table_iteban.phpCREATE TABLE iteban en bautista_p
{timestamp}_delete_foreign_key_iteban_to_movimi.phpDROP 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

Documentación relacionada en Tesorería

Código fuente

  • bautista-backend/service/Tesoreria/MovimientoBancarioService.php
  • bautista-backend/Middleware/ConnectionMiddleware.php
  • bautista-backend/validator/CreateMovimientoCajaValidator.php
  • bautista-backend/controller/modulo-tesoreria/MovimientoConciliadoController.php
  • bautista-app/ts/tesoreria/views/MovimientosConciliadosView.tsx
  • informes/libro_bancos.php

Historial de cambios

FechaVersiónDescripción
2026-03-111.0Documento 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-161.1Vista 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.