Skip to content

Multi-Modo: Dual Database Pattern

QUÉ es Dual Database Pattern

Definición: Arquitectura con 2 bases de datos PostgreSQL físicamente separadas:

  1. Database oficial (bautista) = Producción, datos reales
  2. Database prueba (bautista_p con sufijo _p) = Testing, simulaciones, capacitación

Principio fundamental: Las transacciones se ejecutan en UNA de las dos databases según el parámetro prueba. Los datos maestros SIEMPRE se leen de la database oficial (sin duplicación).

Analogía: Imagina dos edificios idénticos:

  • Edificio OFICIAL: Operaciones reales con consecuencias
  • Edificio PRUEBA: Simulaciones sin afectar el oficial

Puedes elegir en qué edificio trabajar con un interruptor (prueba=true/false), pero los manuales de procedimiento (maestros) están solo en el oficial y se consultan desde ambos edificios.

CUÁNDO usar:

  • 🧪 Simular operaciones sin afectar producción
  • 📚 Capacitar usuarios con datos de prueba
  • 🔬 Testing de funcionalidades nuevas
  • 📊 Comparar resultados oficial vs prueba en reportes

Problema que Resuelve

Escenario Real

Sin multi-modo:

Usuario: "Quiero simular una facturación para practicar"
Sistema: Genera factura #12345
Resultado: ❌ Factura REAL creada en producción
         ❌ Afecta stock, contabilidad, reportes oficiales
         ❌ Hay que revertir manualmente (riesgo de inconsistencia)

Con multi-modo:

Usuario: "Quiero simular una facturación" (toggle prueba=true)
Sistema: Genera factura #12345 en database bautista_p
Resultado: ✅ Factura de PRUEBA creada
         ✅ Stock/contabilidad/reportes de producción NO afectados
         ✅ Datos de prueba completamente aislados
         ✅ Se puede resetear DB prueba sin afectar oficial

Casos de Uso Comunes

CasoDatabasePropósitoResultado
Facturación realoficial (bautista)Generar facturas de clientesAfecta producción
Simulación de facturaciónprueba (bautista_p)Practicar sin afectar producciónNO afecta producción
Reporte de ventas oficialoficialVer ventas realesDatos de producción
Reporte de pruebapruebaVer ventas de simulaciónDatos de testing
Reporte consolidadooficial + pruebaComparar oficial vs pruebaMode 2 (ambas DBs)
Capacitación de usuariospruebaEntrenar con datos de pruebaNO afecta producción

Parámetro 'prueba'

El parámetro prueba (boolean) determina en qué database se ejecutan las transacciones:

ParámetroValorDatabaseAlias principal apunta aUso
pruebafalse (default)bautistaoficialProducción
pruebatruebautista_ppruebaTesting/Simulación

Flujo de Request con Parámetro prueba

mermaid
sequenceDiagram
    participant F as Frontend (Toggle prueba)
    participant CM as ConnectionMiddleware
    participant ConnMgr as ConnectionManager
    participant PGO as PostgreSQL (bautista)
    participant PGP as PostgreSQL (bautista_p)

    F->>CM: POST /api/ventas/facturas<br/>prueba=true<br/>X-Schema: suc0001caja001

    CM->>CM: Leer parámetro prueba = true

    CM->>ConnMgr: setupConnection('principal', schema='suc0001caja001', prueba=true)

    ConnMgr->>ConnMgr: Resolver alias 'principal':<br/>prueba=true → 'principal' = 'prueba'

    ConnMgr->>ConnMgr: Obtener credentials de DB prueba:<br/>host, port, dbname=bautista_p

    ConnMgr->>PGP: Conectar a bautista_p<br/>SET search_path = suc0001caja001, suc0001, public

    PGP-->>ConnMgr: Connection ready (DB prueba)

    Note over ConnMgr,PGO: Maestros SIEMPRE de oficial
    ConnMgr->>PGO: getConnection('oficial')<br/>SELECT * FROM plan_cuentas

    PGO-->>ConnMgr: Plan de cuentas (maestros)

    Note over ConnMgr,PGP: Transacciones en prueba
    ConnMgr->>PGP: getConnection('principal')<br/>INSERT INTO facturas (...)

    PGP-->>F: Factura creada en DB prueba

    style PGP fill:#FFD700
    style PGO fill:#90EE90

Configuración del Parámetro

Origen del parámetro prueba:

  1. Query parameter HTTP (máxima prioridad)

    GET /api/ventas/facturas?prueba=true
  2. Body parameter JSON

    json
    {
      "prueba": true,
      "fecha": "2025-02-03",
      "cliente_id": 123
    }
  3. Frontend toggle/switch

    typescript
    const [modoPrueba, setModoPrueba] = useState(false);
    
    const handleCreateFactura = async (data: FacturaDTO) => {
      await api.post('/ventas/facturas', {
        ...data,
        prueba: modoPrueba // ← Incluir en request
      });
    };
  4. Default: false (siempre producción por seguridad)

ConnectionManager Alias Pattern

Concepto: 'principal' es Alias Dinámico

Problema: Servicios no deberían conocer si están en modo oficial o prueba.

Solución: Usar nombre lógico principal que se resuelve dinámicamente según parámetro prueba:

php
// ConnectionManager::resolveAlias()
public static function resolveAlias(string $alias): string
{
    if ($alias === 'principal') {
        // Leer configuración global del request
        $prueba = self::$globalConfig['prueba'] ?? false;

        return $prueba ? 'prueba' : 'oficial';
    }

    return $alias; // Otros aliases sin resolución
}

Configuración en ConnectionMiddleware

php
// ConnectionMiddleware::__invoke()
public function __invoke(Request $request, RequestHandler $handler): Response
{
    // 1. Leer parámetro prueba del request
    $params = $request->getQueryParams();
    $body = $request->getParsedBody();

    $prueba = $params['prueba'] ?? $body['prueba'] ?? false;
    $prueba = filter_var($prueba, FILTER_VALIDATE_BOOLEAN);

    // 2. Configurar globalmente para resolver 'principal'
    ConnectionManager::setGlobalConfig(['prueba' => $prueba]);

    // 3. Continuar request
    return $handler->handle($request);
}

Uso en Servicios (Transparente)

php
namespace App\service\Ventas;

use App\connection\ConnectionManager;

class FacturaService
{
    public function createFactura(array $data): array
    {
        // NO necesitamos saber si estamos en modo oficial o prueba
        // 'principal' se resuelve automáticamente

        $conn = ConnectionManager::getConnection('principal');
        // Si prueba=false → conexión a bautista (oficial)
        // Si prueba=true → conexión a bautista_p (prueba)

        $sql = "INSERT INTO facturas (fecha, cliente_id, total)
                VALUES (:fecha, :cliente_id, :total)";

        $conn->executeStatement($sql, [
            'fecha' => $data['fecha'],
            'cliente_id' => $data['cliente_id'],
            'total' => $data['total'],
        ]);

        return ['id' => $conn->lastInsertId()];
    }
}

Beneficios:

  • ✅ Servicios agnósticos de modo (código más simple)
  • ✅ Cambio de modo sin modificar servicios
  • ✅ Testing más fácil (solo cambiar parámetro prueba)
  • ✅ Seguridad: Default siempre oficial (no accidental a prueba)

Datos Maestros SIEMPRE de Oficial

Regla Crítica

Principio: Los datos maestros (configuración, plan de cuentas, conceptos fiscales, etc.) NUNCA se duplican en database prueba. SIEMPRE se leen de database oficial.

Razón: Evitar inconsistencias y mantener única fuente de verdad.

Segregación de Datos

Tipo de DatoDatabase OficialDatabase PruebaConexión a Usar
Maestros✅ SIEMPRE❌ NUNCAoficial
Transaccionales✅ Producción✅ Testingprincipal (dinámico)

Tablas Maestras (SIEMPRE de oficial)

  • plan_cuentas - Plan de cuentas contables
  • conceptos_retencion - Conceptos fiscales
  • tipos_comprobante - Tipos de documentos
  • monedas - Monedas del sistema
  • condiciones_iva - Condiciones fiscales
  • usuarios - Usuarios del sistema
  • configuracion - Configuración global

Tablas Transaccionales (según modo)

  • facturas - Facturas de venta
  • recibos - Recibos de cobro
  • asientos - Asientos contables
  • movimientos_stock - Movimientos de inventario
  • movimientos_caja - Movimientos de caja

Pattern en Servicios

php
namespace App\service\Contabilidad;

use App\connection\ConnectionManager;

class AsientoService
{
    public function createAsiento(array $data): array
    {
        // 1. Maestros SIEMPRE de oficial
        $connOficial = ConnectionManager::getConnection('oficial');

        $planCuentas = $connOficial->fetchAllAssociative("
            SELECT * FROM plan_cuentas WHERE activo = true
        ");

        // Validar que las cuentas existan en plan maestro
        foreach ($data['items'] as $item) {
            $cuentaExiste = array_filter($planCuentas, fn($c) => $c['id'] === $item['cuenta_id']);
            if (empty($cuentaExiste)) {
                throw new \InvalidArgumentException("Cuenta {$item['cuenta_id']} no existe en plan maestro");
            }
        }

        // 2. Transacciones en 'principal' (oficial o prueba según parámetro)
        $connPrincipal = ConnectionManager::getConnection('principal');

        // Si prueba=false → inserta en bautista.asientos
        // Si prueba=true → inserta en bautista_p.asientos
        $sql = "INSERT INTO asientos (fecha, concepto) VALUES (:fecha, :concepto)";
        $connPrincipal->executeStatement($sql, [
            'fecha' => $data['fecha'],
            'concepto' => $data['concepto']
        ]);

        return ['id' => $connPrincipal->lastInsertId()];
    }
}

Diagrama de flujo:

mermaid
graph TD
    SVC[Service: createAsiento]

    SVC -->|getConnection('oficial')| MAESTROS[Leer plan_cuentas<br/>SIEMPRE de bautista]

    MAESTROS --> VALIDAR[Validar cuentas existen<br/>en plan maestro]

    VALIDAR -->|getConnection('principal')| TRANS{prueba=?}

    TRANS -->|false| OFICIAL[INSERT en bautista.asientos<br/>PRODUCCIÓN]
    TRANS -->|true| PRUEBA[INSERT en bautista_p.asientos<br/>TESTING]

    style MAESTROS fill:#90EE90
    style OFICIAL fill:#90EE90
    style PRUEBA fill:#FFD700

Detección de Modo: isPruebaConnection()

Propósito

Determinar en qué database se está trabajando para lógica condicional (ej: logging diferente, validaciones extras en producción).

Lógica Conceptual

php
// ConnectionManager::isPruebaConnection()
public static function isPruebaConnection(string $connectionName = 'principal'): bool
{
    // 1. Resolver alias (si es 'principal', resolver a 'oficial' o 'prueba')
    $resolvedName = self::resolveAlias($connectionName);

    // 2. Verificar si es conexión de prueba
    return $resolvedName === 'prueba';
}

Uso en Código

php
namespace App\service\Ventas;

use App\connection\ConnectionManager;

class FacturaService
{
    public function createFactura(array $data): array
    {
        $conn = ConnectionManager::getConnection('principal');

        // Lógica condicional según modo
        if (ConnectionManager::isPruebaConnection('principal')) {
            // Modo prueba: Validaciones más laxas, logging diferente
            $this->logger->info("Creando factura en modo PRUEBA", $data);
        } else {
            // Modo oficial: Validaciones estrictas, auditoría completa
            $this->logger->warning("Creando factura en modo OFICIAL", $data);
            $this->validateProduction($data); // Validaciones extras
        }

        $sql = "INSERT INTO facturas (...) VALUES (...)";
        $conn->executeStatement($sql, $data);

        return ['id' => $conn->lastInsertId()];
    }
}

Casos de uso:

  • ✅ Logging diferenciado (INFO en prueba, WARNING en oficial)
  • ✅ Validaciones más estrictas en producción
  • ✅ Auditoría solo en oficial
  • ✅ Notificaciones solo en oficial (no enviar emails en prueba)

Modos de Consolidación en Reportes

3 Modos de Generación

Los reportes (informes) pueden generar datos de:

ModeLabelDatabase(s) ConsultadasIndicador en PDFUso
0Pruebabautista_p (solo prueba)[PRUEBA]Ver datos de simulación
1Oficialbautista (solo oficial)- (sin indicador)Ver datos de producción
2Consolidadobautista + bautista_p (ambas)[CONSOLIDADO]Comparar oficial vs prueba

Flujo de Generación de Reporte

mermaid
graph TD
    USER[Usuario: Generar reporte]

    USER -->|mode=?| SELECTOR{Selector de Modo}

    SELECTOR -->|mode=0| MODE0[Mode 0: PRUEBA]
    SELECTOR -->|mode=1| MODE1[Mode 1: OFICIAL]
    SELECTOR -->|mode=2| MODE2[Mode 2: CONSOLIDADO]

    MODE0 -->|Consultar| DBP[(bautista_p)]
    DBP --> DATAP[Datos de prueba]
    DATAP --> PDF0[PDF con indicador [PRUEBA]]

    MODE1 -->|Consultar| DBO[(bautista)]
    DBO --> DATAO[Datos oficiales]
    DATAO --> PDF1[PDF sin indicador]

    MODE2 -->|Consultar| DBP2[(bautista_p)]
    MODE2 -->|Consultar| DBO2[(bautista)]
    DBP2 --> DATAP2[Datos de prueba]
    DBO2 --> DATAO2[Datos oficiales]
    DATAP2 --> MERGE[Merge con indicadores]
    DATAO2 --> MERGE
    MERGE --> PDF2[PDF con indicador [CONSOLIDADO]]

    style MODE0 fill:#FFD700
    style MODE1 fill:#90EE90
    style MODE2 fill:#87CEEB

Implementación Conceptual

php
// informes/index.php
$mode = $requestData['mode'] ?? 1; // Default: oficial

switch ($mode) {
    case 0: // Prueba
        $connPrueba = createConnection($credentials['prueba']);
        $data = fetchData($connPrueba, $filters);
        $pdf = generatePDF($data, '[PRUEBA]');
        break;

    case 1: // Oficial
        $connOficial = createConnection($credentials['oficial']);
        $data = fetchData($connOficial, $filters);
        $pdf = generatePDF($data); // Sin indicador
        break;

    case 2: // Consolidado
        $connOficial = createConnection($credentials['oficial']);
        $connPrueba = createConnection($credentials['prueba']);

        $dataOficial = fetchData($connOficial, $filters);
        $dataPrueba = fetchData($connPrueba, $filters);

        // Merge con indicadores
        $dataMerged = [
            ['titulo' => 'DATOS OFICIALES', 'rows' => $dataOficial],
            ['titulo' => 'DATOS PRUEBA', 'rows' => $dataPrueba],
        ];

        $pdf = generatePDF($dataMerged, '[CONSOLIDADO]');
        break;
}

return $pdf;

Ejemplo de Reporte Consolidado

Libro Diario - Mode 2 (Consolidado):

┌────────────────────────────────────────────────┐
│  LIBRO DIARIO [CONSOLIDADO]                   │
├────────────────────────────────────────────────┤
│  DATOS OFICIALES                               │
├────────────────────────────────────────────────┤
│  Fecha       │ Asiento │ Cuenta │ Debe  │ Haber│
│  2025-02-01  │ 1       │ 101    │ 1000  │ 0    │
│  2025-02-01  │ 1       │ 201    │ 0     │ 1000 │
│  2025-02-02  │ 2       │ 102    │ 2000  │ 0    │
│  2025-02-02  │ 2       │ 202    │ 0     │ 2000 │
├────────────────────────────────────────────────┤
│  DATOS PRUEBA                                  │
├────────────────────────────────────────────────┤
│  Fecha       │ Asiento │ Cuenta │ Debe  │ Haber│
│  2025-02-03  │ 1       │ 101    │ 500   │ 0    │
│  2025-02-03  │ 1       │ 201    │ 0     │ 500  │
└────────────────────────────────────────────────┘

Casos de uso de Mode 2:

  • 📊 Comparar resultados de simulaciones vs producción
  • 🔍 Verificar diferencias antes de aplicar cambios a producción
  • 📈 Análisis de impacto (¿qué pasaría si...?)

Multi-Modo vs Multi-Schema

CRÍTICO: Dimensiones Independientes

Multi-modo y multi-schema son ortogonales (independientes):

AspectoMulti-ModoMulti-Schema
DimensiónDatabase (oficial vs prueba)Schemas (1 vs N)
PropósitoSeparación producción/testingBúsqueda/consolidación cross-schema
Activaciónprueba parameterbusqueda_en_todas_las_cajas=true
Alcance2 databasesN schemas en 1 database
RelaciónTrabaja en nivel de DBTrabaja en nivel de schema

Matriz de Combinación (6 Combinaciones)

#Multi-ModoMulti-SchemaResultadoEjemplo
1OficialSingle schemaOperación normal en producciónCrear factura en suc0001 (oficial)
2PruebaSingle schemaOperación de testingCrear factura en suc0001 (prueba)
3OficialMulti-schemaBúsqueda cross-schema en producciónBuscar recibo en todas las cajas (oficial)
4PruebaMulti-schemaBúsqueda cross-schema en testingBuscar recibo en todas las cajas (prueba)
5ConsolidadoSingle schemaReporte mode 2 en 1 schemaLibro diario de suc0001 (oficial + prueba)
6ConsolidadoMulti-schemaReporte mode 2 multi-schemaVentas de todas las cajas (oficial + prueba)

Ejemplo de Combinación #4: Multi-Schema en Prueba

Request:
  POST /api/ctacte/recibos/search
  Body: {
    "id": 12345,
    "prueba": true,                        ← Multi-Modo: prueba
    "busqueda_en_todas_las_cajas": true    ← Multi-Schema: activo
  }

Resultado:
  1. Multi-Modo: Database = bautista_p
  2. Multi-Schema: Buscar en N schemas
  3. Schemas consultados:
     - bautista_p.suc0001caja001.recibos
     - bautista_p.suc0001caja002.recibos
     - bautista_p.suc0001caja003.recibos
  4. Retorna: Recibo encontrado en bautista_p con _schema metadata

Ejemplo de Combinación #6: Reporte Consolidado Multi-Schema

Request:
  POST /informes/generate
  Body: {
    "code": 150,  // Reporte de ventas
    "mode": 2,    // Consolidado (oficial + prueba)
    "multi_schema": true,  // Todas las cajas
    "fecha_inicio": "2025-02-01",
    "fecha_fin": "2025-02-28"
  }

Resultado:
  1. Multi-Modo: Mode 2 → Consultar bautista + bautista_p
  2. Multi-Schema: Consultar N cajas en cada DB

  Queries ejecutadas:
    OFICIAL (bautista):
      - suc0001caja001.facturas
      - suc0001caja002.facturas
      - suc0001caja003.facturas

    PRUEBA (bautista_p):
      - suc0001caja001.facturas
      - suc0001caja002.facturas
      - suc0001caja003.facturas

  PDF generado:
    ┌──────────────────────────────┐
    │ VENTAS [CONSOLIDADO]         │
    ├──────────────────────────────┤
    │ OFICIAL                      │
    │   Caja 001: $10,000          │
    │   Caja 002: $15,000          │
    │   Caja 003: $12,000          │
    │   SUBTOTAL: $37,000          │
    ├──────────────────────────────┤
    │ PRUEBA                       │
    │   Caja 001: $500             │
    │   Caja 002: $800             │
    │   Caja 003: $300             │
    │   SUBTOTAL: $1,600           │
    └──────────────────────────────┘

Reglas Arquitecturales

RA-MM-001: Separación de Transaccionales

Descripción: Los datos transaccionales DEBEN estar separados entre database oficial y database prueba. Los maestros DEBEN estar solo en database oficial.

Implicación:

  • ✅ Facturas, recibos, asientos → duplicados en oficial + prueba
  • ✅ Plan de cuentas, conceptos → solo en oficial
  • ❌ NO duplicar maestros en prueba

RA-MM-002: Alias 'principal' Dinámico

Descripción: El alias principal DEBE resolverse dinámicamente según parámetro prueba. Los servicios DEBEN usar principal en lugar de oficial o prueba directamente.

Implicación:

  • ✅ Servicios: ConnectionManager::getConnection('principal')
  • ❌ Servicios: ConnectionManager::getConnection('prueba') (no hardcodear)
  • ⚠️ Excepción: Maestros siempre usan oficial explícitamente

RA-MM-003: Indicador de Modo en Reportes

Descripción: Los reportes DEBEN indicar visualmente el modo en el PDF generado. Mode 0 → [PRUEBA], Mode 2 → [CONSOLIDADO], Mode 1 → sin indicador.

Implicación:

  • ✅ PDF en modo prueba tiene marca de agua o header [PRUEBA]
  • ✅ PDF consolidado tiene indicador [CONSOLIDADO]
  • ❌ NO generar PDFs de prueba sin indicador (riesgo de confusión)

RA-MM-004: Detección de Modo Disponible

Descripción: Los servicios DEBEN poder detectar el modo actual mediante isPruebaConnection() para lógica condicional (logging, validaciones, notificaciones).

Implicación:

  • ✅ Usar ConnectionManager::isPruebaConnection('principal')
  • ✅ Validaciones más estrictas en oficial
  • ❌ NO enviar notificaciones en modo prueba

RA-MM-005: Independencia de Multi-Schema

Descripción: Multi-modo y multi-schema SON independientes. Multi-modo determina la database, multi-schema determina los schemas consultados.

Implicación:

  • ✅ Pueden combinarse libremente (6 combinaciones)
  • ✅ Multi-schema funciona IGUAL en oficial y prueba
  • ❌ NO asumir que multi-schema implica un modo específico

Referencias a Implementación

Documentación Técnica

Código Fuente

  • server/connection/ConnectionManager.php - Alias principal, resolveAlias(), isPruebaConnection()
  • server/Middleware/ConnectionMiddleware.php - Lectura de parámetro prueba, setup de config global
  • informes/index.php - Lógica de modes 0/1/2 en reportes

Skills de Claude Code

  • .claude/skills/bautista-record-modes/ - Implementar dual database pattern

Ejemplos de Uso

Ejemplo 1: Frontend con Toggle de Modo

typescript
// ts/ventas/components/FacturacionForm.tsx
import { useState } from 'react';
import { api } from '../../api/api';

export function FacturacionForm() {
  const [modoPrueba, setModoPrueba] = useState(false);
  const [factura, setFactura] = useState<FacturaDTO>({
    fecha: '2025-02-03',
    cliente_id: 123,
    total: 1000,
  });

  const handleSubmit = async () => {
    // Incluir parámetro prueba en request
    const response = await api.post('/ventas/facturas', {
      ...factura,
      prueba: modoPrueba, // ← Determina database (oficial o prueba)
    });

    if (modoPrueba) {
      alert(`Factura de PRUEBA creada: #${response.data.id}`);
    } else {
      alert(`Factura OFICIAL creada: #${response.data.id}`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Toggle de modo */}
      <label>
        <input
          type="checkbox"
          checked={modoPrueba}
          onChange={e => setModoPrueba(e.target.checked)}
        />
        Modo Prueba (simulación)
      </label>

      {/* Indicador visual */}
      {modoPrueba && (
        <div className="alert alert-warning">
          ⚠️ Estás en modo PRUEBA. Los datos NO afectarán producción.
        </div>
      )}

      {/* Resto del formulario */}
      <input
        type="date"
        value={factura.fecha}
        onChange={e => setFactura({...factura, fecha: e.target.value})}
      />

      <button type="submit">
        {modoPrueba ? 'Simular Factura' : 'Crear Factura'}
      </button>
    </form>
  );
}

Ejemplo 2: Reporte con Selector de Modo

typescript
// ts/contabilidad/components/LibroDiarioReport.tsx
import { useState } from 'react';
import { api } from '../../api/api';

type ReportMode = 0 | 1 | 2;

export function LibroDiarioReport() {
  const [mode, setMode] = useState<ReportMode>(1); // Default: oficial
  const [filters, setFilters] = useState({
    fecha_inicio: '2025-02-01',
    fecha_fin: '2025-02-28',
  });

  const handleGenerate = async () => {
    // Llamar a servicio de informes con mode
    const response = await api.post('/informes/generate', {
      code: 500, // Código de reporte libro diario
      mode,      // 0=Prueba, 1=Oficial, 2=Consolidado
      ...filters,
    }, {
      responseType: 'blob', // PDF
    });

    // Descargar PDF
    const url = window.URL.createObjectURL(new Blob([response.data]));
    const link = document.createElement('a');
    link.href = url;
    link.setAttribute('download', `libro_diario_mode${mode}.pdf`);
    document.body.appendChild(link);
    link.click();
  };

  return (
    <div>
      <h2>Libro Diario</h2>

      {/* Selector de modo */}
      <label>
        <input
          type="radio"
          value={0}
          checked={mode === 0}
          onChange={() => setMode(0)}
        />
        Modo Prueba
      </label>

      <label>
        <input
          type="radio"
          value={1}
          checked={mode === 1}
          onChange={() => setMode(1)}
        />
        Modo Oficial
      </label>

      <label>
        <input
          type="radio"
          value={2}
          checked={mode === 2}
          onChange={() => setMode(2)}
        />
        Modo Consolidado (Oficial + Prueba)
      </label>

      {/* Indicador visual */}
      {mode === 0 && <div className="badge badge-warning">[PRUEBA]</div>}
      {mode === 2 && <div className="badge badge-info">[CONSOLIDADO]</div>}

      {/* Filtros */}
      <input
        type="date"
        value={filters.fecha_inicio}
        onChange={e => setFilters({...filters, fecha_inicio: e.target.value})}
      />
      <input
        type="date"
        value={filters.fecha_fin}
        onChange={e => setFilters({...filters, fecha_fin: e.target.value})}
      />

      <button onClick={handleGenerate}>
        Generar PDF
      </button>
    </div>
  );
}

Ejemplo 3: Service con Maestros de Oficial

php
namespace App\service\Contabilidad;

use App\connection\ConnectionManager;

class AsientoService
{
    public function createAsiento(array $data): array
    {
        // 1. MAESTROS: SIEMPRE de oficial (incluso en modo prueba)
        $connOficial = ConnectionManager::getConnection('oficial');

        $planCuentas = $connOficial->fetchAllAssociative("
            SELECT id, codigo, nombre FROM plan_cuentas WHERE activo = true
        ");

        // 2. Validar que las cuentas existan
        foreach ($data['items'] as $item) {
            $found = array_filter($planCuentas, fn($c) => $c['id'] === $item['cuenta_id']);
            if (empty($found)) {
                throw new \InvalidArgumentException(
                    "Cuenta {$item['cuenta_id']} no existe en plan maestro"
                );
            }
        }

        // 3. TRANSACCIONES: en 'principal' (dinámico según prueba parameter)
        $connPrincipal = ConnectionManager::getConnection('principal');

        // Lógica condicional según modo
        if (ConnectionManager::isPruebaConnection('principal')) {
            // Modo prueba: logging diferente
            $this->logger->info("Creando asiento en modo PRUEBA", $data);
        } else {
            // Modo oficial: auditoría completa
            $this->logger->warning("Creando asiento en modo OFICIAL", $data);
            $this->auditLogger->logAsientoCreation($data);
        }

        // Insertar asiento (en database correspondiente)
        $sql = "INSERT INTO asientos (fecha, concepto, debe, haber)
                VALUES (:fecha, :concepto, :debe, :haber)";

        $connPrincipal->executeStatement($sql, [
            'fecha' => $data['fecha'],
            'concepto' => $data['concepto'],
            'debe' => $data['debe'],
            'haber' => $data['haber'],
        ]);

        $asientoId = $connPrincipal->lastInsertId();

        // Retornar con indicador de modo
        return [
            'id' => $asientoId,
            'mode' => ConnectionManager::isPruebaConnection('principal') ? 'prueba' : 'oficial',
        ];
    }
}

Diagrama Completo de Multi-Modo

mermaid
graph TB
    subgraph "Frontend"
        TOGGLE[Toggle prueba<br/>true/false]
        MODE_SELECTOR[Mode Selector<br/>0/1/2]
    end

    subgraph "Middleware"
        CM[ConnectionMiddleware<br/>Lee parámetro prueba]
    end

    subgraph "ConnectionManager"
        RESOLVE[Resolve Alias<br/>principal → oficial o prueba]
        DETECT[isPruebaConnection<br/>Detección de modo]
    end

    subgraph "Databases"
        DBO[(bautista<br/>OFICIAL)]
        DBP[(bautista_p<br/>PRUEBA)]

        subgraph "Maestros (solo oficial)"
            MAESTROS[plan_cuentas<br/>conceptos_retencion<br/>usuarios]
        end

        subgraph "Transaccionales (duplicados)"
            TRANS_O[facturas<br/>recibos<br/>asientos]
            TRANS_P[facturas<br/>recibos<br/>asientos]
        end
    end

    TOGGLE -->|prueba=true/false| CM

    CM -->|setGlobalConfig| RESOLVE

    RESOLVE -->|prueba=false| ALIAS_O[principal → oficial]
    RESOLVE -->|prueba=true| ALIAS_P[principal → prueba]

    ALIAS_O -->|getConnection| DBO
    ALIAS_P -->|getConnection| DBP

    DBO -->|Contiene| MAESTROS
    DBO -->|Contiene| TRANS_O

    DBP -->|Contiene| TRANS_P
    DBP -.->|Lee maestros de| MAESTROS

    DETECT -->|isPruebaConnection| ALIAS_O
    DETECT -->|isPruebaConnection| ALIAS_P

    MODE_SELECTOR -->|mode=0| DBP
    MODE_SELECTOR -->|mode=1| DBO
    MODE_SELECTOR -->|mode=2| CONSOLIDADO[Consultar ambas]

    CONSOLIDADO --> DBO
    CONSOLIDADO --> DBP

    style DBO fill:#90EE90
    style DBP fill:#FFD700
    style MAESTROS fill:#87CEEB
    style CONSOLIDADO fill:#FFA07A

Siguiente paso: Releer Database Architecture Index para entender las combinaciones de los 3 conceptos.

Referencias: