Appearance
Multi-Modo: Dual Database Pattern
QUÉ es Dual Database Pattern
Definición: Arquitectura con 2 bases de datos PostgreSQL físicamente separadas:
- Database oficial (
bautista) = Producción, datos reales - Database prueba (
bautista_pcon 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 oficialCasos de Uso Comunes
| Caso | Database | Propósito | Resultado |
|---|---|---|---|
| Facturación real | oficial (bautista) | Generar facturas de clientes | Afecta producción |
| Simulación de facturación | prueba (bautista_p) | Practicar sin afectar producción | NO afecta producción |
| Reporte de ventas oficial | oficial | Ver ventas reales | Datos de producción |
| Reporte de prueba | prueba | Ver ventas de simulación | Datos de testing |
| Reporte consolidado | oficial + prueba | Comparar oficial vs prueba | Mode 2 (ambas DBs) |
| Capacitación de usuarios | prueba | Entrenar con datos de prueba | NO afecta producción |
Parámetro 'prueba'
El parámetro prueba (boolean) determina en qué database se ejecutan las transacciones:
| Parámetro | Valor | Database | Alias principal apunta a | Uso |
|---|---|---|---|---|
prueba | false (default) | bautista | oficial | Producción |
prueba | true | bautista_p | prueba | Testing/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:#90EE90Configuración del Parámetro
Origen del parámetro prueba:
Query parameter HTTP (máxima prioridad)
GET /api/ventas/facturas?prueba=trueBody parameter JSON
json{ "prueba": true, "fecha": "2025-02-03", "cliente_id": 123 }Frontend toggle/switch
typescriptconst [modoPrueba, setModoPrueba] = useState(false); const handleCreateFactura = async (data: FacturaDTO) => { await api.post('/ventas/facturas', { ...data, prueba: modoPrueba // ← Incluir en request }); };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 Dato | Database Oficial | Database Prueba | Conexión a Usar |
|---|---|---|---|
| Maestros | ✅ SIEMPRE | ❌ NUNCA | oficial |
| Transaccionales | ✅ Producción | ✅ Testing | principal (dinámico) |
Tablas Maestras (SIEMPRE de oficial)
plan_cuentas- Plan de cuentas contablesconceptos_retencion- Conceptos fiscalestipos_comprobante- Tipos de documentosmonedas- Monedas del sistemacondiciones_iva- Condiciones fiscalesusuarios- Usuarios del sistemaconfiguracion- Configuración global
Tablas Transaccionales (según modo)
facturas- Facturas de ventarecibos- Recibos de cobroasientos- Asientos contablesmovimientos_stock- Movimientos de inventariomovimientos_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:#FFD700Detecció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:
| Mode | Label | Database(s) Consultadas | Indicador en PDF | Uso |
|---|---|---|---|---|
| 0 | Prueba | bautista_p (solo prueba) | [PRUEBA] | Ver datos de simulación |
| 1 | Oficial | bautista (solo oficial) | - (sin indicador) | Ver datos de producción |
| 2 | Consolidado | bautista + 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:#87CEEBImplementació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):
| Aspecto | Multi-Modo | Multi-Schema |
|---|---|---|
| Dimensión | Database (oficial vs prueba) | Schemas (1 vs N) |
| Propósito | Separación producción/testing | Búsqueda/consolidación cross-schema |
| Activación | prueba parameter | busqueda_en_todas_las_cajas=true |
| Alcance | 2 databases | N schemas en 1 database |
| Relación | Trabaja en nivel de DB | Trabaja en nivel de schema |
Matriz de Combinación (6 Combinaciones)
| # | Multi-Modo | Multi-Schema | Resultado | Ejemplo |
|---|---|---|---|---|
| 1 | Oficial | Single schema | Operación normal en producción | Crear factura en suc0001 (oficial) |
| 2 | Prueba | Single schema | Operación de testing | Crear factura en suc0001 (prueba) |
| 3 | Oficial | Multi-schema | Búsqueda cross-schema en producción | Buscar recibo en todas las cajas (oficial) |
| 4 | Prueba | Multi-schema | Búsqueda cross-schema en testing | Buscar recibo en todas las cajas (prueba) |
| 5 | Consolidado | Single schema | Reporte mode 2 en 1 schema | Libro diario de suc0001 (oficial + prueba) |
| 6 | Consolidado | Multi-schema | Reporte mode 2 multi-schema | Ventas 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 metadataEjemplo 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
oficialexplí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
docs/features/contabilidad/libro-diario-modos-process.md- Ejemplo de reporte con modosdocs/architecture/consolidacion-informes-multi-schema.md- Consolidación multi-schema + multi-modo
Código Fuente
server/connection/ConnectionManager.php- Aliasprincipal,resolveAlias(),isPruebaConnection()server/Middleware/ConnectionMiddleware.php- Lectura de parámetroprueba, setup de config globalinformes/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:#FFA07ASiguiente paso: Releer Database Architecture Index para entender las combinaciones de los 3 conceptos.
Referencias: