Files
whatsapp/migrations/etl/run_etl.php
Lizandro GuarnizoandClaude Sonnet 4.6 5fedd233f4 feat(lis): migración completa Firebird→MySQL + tracking de muestras en turnero
- Migraciones LIS 01-08: schema completo del nuevo LIS (secciones, protocolos,
  ítems de resultado, perfiles, tarifas, empresas, histórico transaccional)
- ETL Firebird→MySQL: script CLI con conversión WIN1252→UTF-8, batches de 500,
  resolución de FKs y deduplicación de pacientes
- turnero_muestras: tracking pendiente/recibida/rechazada por tipo de tubo
- lugar.php: widget de recepción de muestras (solo tipo=muestras)
- update_muestra_estado.php: API para marcar estado de muestra
- create_solicitud.php: auto-crea muestras al guardar solicitud
- get_consentimientos.php: incluye muestras[] en el response
- 6 vistas SQL: v_muestras_hoy, v_recepcion_completa, v_examen_precio, etc.
- numero_orden en encabezado del formulario firmado (D-/F- color diferenciado)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 21:44:22 -05:00

716 lines
33 KiB
PHP

#!/usr/bin/env php
<?php
/**
* ETL Firebird 2.5 → MySQL (utf8mb4)
* Sistema: DBLAB_XIMENA_FB25 → nuevo sistema WhatsApp-Lab
*
* Uso:
* php run_etl.php [--dry-run] [--skip-historico] [--only=PASO]
*
* Pasos disponibles (--only):
* secciones | especialidades | tipos_muestra | protocolos | items |
* perfiles | exam_tipos | tarifas | empresas |
* medicos | pacientes | recepciones | relaciones | pagos
*
* Requerimientos:
* - PHP 8.0+ con extensión PDO_Firebird (php-firebird) instalada
* - Archivo config.local.php con credenciales reales
* - Las migraciones LIS 01-07 ya ejecutadas en MySQL
*/
declare(strict_types=1);
set_time_limit(0);
ini_set('memory_limit', '512M');
require_once __DIR__ . '/helpers.php';
// ── Configuración ────────────────────────────────────────────────────────────
$cfgFile = file_exists(__DIR__ . '/config.local.php')
? __DIR__ . '/config.local.php'
: __DIR__ . '/config.php';
$cfg = require $cfgFile;
// Argumentos CLI
$args = array_slice($argv ?? [], 1);
$dryRun = in_array('--dry-run', $args, true) || $cfg['options']['dry_run'];
$skipHist = in_array('--skip-historico', $args, true) || $cfg['options']['skip_historico'];
$onlyPaso = null;
foreach ($args as $arg) {
if (str_starts_with($arg, '--only=')) {
$onlyPaso = strtolower(substr($arg, 7));
}
}
$logFp = fopen($cfg['options']['log_file'], 'w');
etlLog('=== ETL Firebird → MySQL ===', $logFp);
etlLog("DRY_RUN: " . ($dryRun ? 'SÍ' : 'NO'), $logFp);
if ($onlyPaso) etlLog("Solo paso: $onlyPaso", $logFp);
// ── Conexiones ───────────────────────────────────────────────────────────────
try {
$fb = new PDO(
$cfg['firebird']['dsn'],
$cfg['firebird']['user'],
$cfg['firebird']['password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
etlLog('Conexión Firebird OK', $logFp);
} catch (\Throwable $e) {
etlLog('ERROR conectando Firebird: ' . $e->getMessage(), $logFp);
exit(1);
}
try {
$my = new PDO(
"mysql:host={$cfg['mysql']['host']};port={$cfg['mysql']['port']};dbname={$cfg['mysql']['dbname']};charset=utf8mb4",
$cfg['mysql']['user'],
$cfg['mysql']['password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => true]
);
$my->exec("SET NAMES utf8mb4");
$my->exec("SET foreign_key_checks = 0");
etlLog('Conexión MySQL OK', $logFp);
} catch (\Throwable $e) {
etlLog('ERROR conectando MySQL: ' . $e->getMessage(), $logFp);
exit(1);
}
$charset = $cfg['firebird']['charset'];
// ── Función auxiliar: ¿ejecutar este paso? ──────────────────────────────────
function shouldRun(string $paso, ?string $only): bool {
return $only === null || $only === $paso;
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 1 — SECCION → lab_secciones
// ════════════════════════════════════════════════════════════════════════════
if (shouldRun('secciones', $onlyPaso)) {
etlLog('--- SECCION → lab_secciones ---', $logFp);
$total = fbCount($fb, 'SECCION');
etlLog(" Origen: $total registros", $logFp);
$rows = [];
foreach ($fb->query('SELECT CODSECCION, NOMBSECCION FROM SECCION') as $r) {
$rows[] = [
'codigo' => cleanStr($r['CODSECCION'], $charset),
'nombre' => cleanStr($r['NOMBSECCION'], $charset),
];
}
if (!$dryRun) {
$n = batchInsert($my, 'lab_secciones', $rows, true);
etlLog(" Insertados: $n", $logFp);
} else {
etlLog(" [DRY] Se insertarían " . count($rows), $logFp);
}
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 2 — ESPECIALIDAD → lab_especialidades
// ════════════════════════════════════════════════════════════════════════════
if (shouldRun('especialidades', $onlyPaso)) {
etlLog('--- ESPECIALIDAD → lab_especialidades ---', $logFp);
$rows = [];
foreach ($fb->query('SELECT CODESPECIA, NOMBESPECIA FROM ESPECIALIDAD') as $r) {
$rows[] = [
'codigo' => cleanStr($r['CODESPECIA'], $charset),
'nombre' => cleanStr($r['NOMBESPECIA'], $charset),
];
}
if (!$dryRun) {
$n = batchInsert($my, 'lab_especialidades', $rows, true);
etlLog(" Insertados: $n / " . count($rows), $logFp);
}
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 3 — EXAMEN.TIPOMUESTRA → lab_tipos_muestra
// ════════════════════════════════════════════════════════════════════════════
if (shouldRun('tipos_muestra', $onlyPaso)) {
etlLog('--- EXAMEN.TIPOMUESTRA → lab_tipos_muestra ---', $logFp);
$rows = [];
$seen = [];
foreach ($fb->query('SELECT DISTINCT TIPOMUESTRA FROM EXAMEN WHERE TIPOMUESTRA IS NOT NULL') as $r) {
$cod = cleanStr($r['TIPOMUESTRA'], $charset);
if (!$cod || isset($seen[$cod])) continue;
$seen[$cod] = true;
$rows[] = [
'codigo' => $cod,
'nombre' => ucwords(strtolower($cod)), // nombre provisional, editar luego
];
}
etlLog(" Tipos únicos encontrados: " . count($rows), $logFp);
if (!$dryRun) {
$n = batchInsert($my, 'lab_tipos_muestra', $rows, true);
etlLog(" Insertados: $n", $logFp);
}
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 4 — TARIFAID → lab_tarifas_id
// ════════════════════════════════════════════════════════════════════════════
if (shouldRun('tarifas', $onlyPaso)) {
etlLog('--- TARIFAID → lab_tarifas_id ---', $logFp);
$rows = [];
foreach ($fb->query('SELECT IDTARIFA, NOMTARIFA, IDTARIFABASE, PORCENTAJE FROM TARIFAID') as $r) {
$rows[] = [
'id' => (int)$r['IDTARIFA'],
'nombre' => cleanStr($r['NOMTARIFA'], $charset),
'tarifa_origen' => $r['IDTARIFABASE'] ? (int)$r['IDTARIFABASE'] : null,
'porcentaje' => (float)($r['PORCENTAJE'] ?? 0),
];
}
if (!$dryRun) {
$n = batchInsert($my, 'lab_tarifas_id', $rows, true);
etlLog(" Insertados: $n / " . count($rows), $logFp);
}
// ── TARIFA → lab_tarifas (104k registros, se hace en batches) ────────
etlLog('--- TARIFA → lab_tarifas ---', $logFp);
$total = fbCount($fb, 'TARIFA');
etlLog(" Origen: $total registros", $logFp);
$stmt = $fb->query('SELECT CODIGO, IDTARIFA, VALOR, RECARGO_URG, RECARGO_FES, RECARGO_ESP FROM TARIFA');
$batch = [];
$count = 0;
while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
$batch[] = [
'cod_examen_legacy' => cleanStr($r['CODIGO'], $charset),
'exam_tipo_id' => null, // se resuelve en paso 6 (exam_tipos)
'tarifa_id' => (int)$r['IDTARIFA'],
'valor' => (float)($r['VALOR'] ?? 0),
'recargo_urg' => (float)($r['RECARGO_URG'] ?? 0),
'recargo_fes' => (float)($r['RECARGO_FES'] ?? 0),
'recargo_esp' => (float)($r['RECARGO_ESP'] ?? 0),
];
if (count($batch) >= 500) {
if (!$dryRun) $count += batchInsert($my, 'lab_tarifas', $batch, true);
$batch = [];
}
}
if ($batch && !$dryRun) $count += batchInsert($my, 'lab_tarifas', $batch, true);
etlLog(" Insertados: $count", $logFp);
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 5 — PROTOCOLO + ITEM → lab_protocolos + lab_items_resultado
// ════════════════════════════════════════════════════════════════════════════
if (shouldRun('protocolos', $onlyPaso)) {
etlLog('--- PROTOCOLO → lab_protocolos ---', $logFp);
$rows = [];
foreach ($fb->query('SELECT CODPROTOCOLO, NOMPROTOCOLO, CODSECCION, IDPLANILLA, ONLYITEMS FROM PROTOCOLO') as $r) {
$rows[] = [
'codigo' => cleanStr($r['CODPROTOCOLO'], $charset),
'nombre' => cleanStr($r['NOMPROTOCOLO'], $charset),
'cod_seccion' => cleanStr($r['CODSECCION'], $charset),
'id_planilla' => cleanStr($r['IDPLANILLA'], $charset),
'only_show_items' => $r['ONLYITEMS'] ? 1 : 0,
];
}
if (!$dryRun) {
$n = batchInsert($my, 'lab_protocolos', $rows, true);
etlLog(" Protocolos insertados: $n / " . count($rows), $logFp);
}
}
if (shouldRun('items', $onlyPaso)) {
etlLog('--- ITEM → lab_items_resultado ---', $logFp);
$total = fbCount($fb, 'ITEM');
etlLog(" Origen: $total registros", $logFp);
$stmt = $fb->query('SELECT CODPROTOCOLO,NOMITEM,TIPOSEXO,TIPO,MEDIDA,ABREVITEM,VMINREF,VMAXREF,ORDEN,FORMULA,CUPS FROM ITEM ORDER BY CODPROTOCOLO, ORDEN');
$batch = [];
$count = 0;
while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
$batch[] = [
'cod_protocolo' => cleanStr($r['CODPROTOCOLO'], $charset),
'nombre' => cleanStr($r['NOMITEM'], $charset),
'tipo_sexo' => cleanStr($r['TIPOSEXO'], $charset),
'tipo' => cleanStr($r['TIPO'], $charset),
'medida' => cleanStr($r['MEDIDA'], $charset),
'abreviatura' => cleanStr($r['ABREVITEM'], $charset),
'vmin_ref' => is_numeric($r['VMINREF']) ? (float)$r['VMINREF'] : null,
'vmax_ref' => is_numeric($r['VMAXREF']) ? (float)$r['VMAXREF'] : null,
'orden' => (int)($r['ORDEN'] ?? 0),
'formula' => cleanStr($r['FORMULA'], $charset),
'cups_detalle' => cleanStr($r['CUPS'], $charset),
];
if (count($batch) >= 500) {
if (!$dryRun) $count += batchInsert($my, 'lab_items_resultado', $batch, true);
$batch = [];
}
}
if ($batch && !$dryRun) $count += batchInsert($my, 'lab_items_resultado', $batch, true);
etlLog(" Insertados: $count", $logFp);
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 6 — PERFIL + PERFIL_EXA → lab_perfiles + lab_perfil_examenes
// ════════════════════════════════════════════════════════════════════════════
if (shouldRun('perfiles', $onlyPaso)) {
etlLog('--- PERFIL → lab_perfiles ---', $logFp);
$rows = [];
foreach ($fb->query('SELECT CODPERFIL, NOMPERFIL FROM PERFIL') as $r) {
$rows[] = [
'nombre' => cleanStr($r['NOMPERFIL'], $charset),
];
}
// Firebird usa VARCHAR código; MySQL usa INT AUTO_INCREMENT
// Guardamos el código viejo → id nuevo en memoria para PERFIL_EXA
if (!$dryRun) {
// Insertar uno a uno para mapear código → id nuevo
$mapaPerfiles = [];
foreach ($fb->query('SELECT CODPERFIL, NOMPERFIL FROM PERFIL') as $r) {
$stmt2 = $my->prepare('INSERT IGNORE INTO lab_perfiles (nombre) VALUES (?)');
$stmt2->execute([cleanStr($r['NOMPERFIL'], $charset)]);
$newId = (int)$my->lastInsertId();
if ($newId) $mapaPerfiles[cleanStr($r['CODPERFIL'], $charset)] = $newId;
}
etlLog(" Perfiles insertados: " . count($mapaPerfiles), $logFp);
// PERFIL_EXA — mapear cod_examen → exam_tipo_id
etlLog('--- PERFIL_EXA → lab_perfil_examenes ---', $logFp);
$examMap = [];
foreach ($my->query('SELECT id, codigo_legacy FROM exam_tipos WHERE codigo_legacy IS NOT NULL') as $r) {
$examMap[$r['codigo_legacy']] = (int)$r['id'];
}
$peBatch = [];
foreach ($fb->query('SELECT CODPERFIL, CODEXAMEN FROM PERFIL_EXA') as $r) {
$pId = $mapaPerfiles[cleanStr($r['CODPERFIL'], $charset)] ?? null;
$eId = $examMap[cleanStr($r['CODEXAMEN'], $charset)] ?? null;
if ($pId && $eId) {
$peBatch[] = ['perfil_id' => $pId, 'exam_tipo_id' => $eId];
}
}
$n = batchInsert($my, 'lab_perfil_examenes', $peBatch, true);
etlLog(" Relaciones perfil-examen: $n", $logFp);
}
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 7 — EXAMEN → exam_tipos (ampliar con campos legacy)
// ════════════════════════════════════════════════════════════════════════════
if (shouldRun('exam_tipos', $onlyPaso)) {
etlLog('--- EXAMEN → exam_tipos ---', $logFp);
$total = fbCount($fb, 'EXAMEN');
etlLog(" Origen: $total registros", $logFp);
$insertados = 0;
$actualizados = 0;
$stmt = $fb->query(
'SELECT CODIGO, NOMEXAMEN, CODPROT, TIPOMUESTRA, NIVEL, CUPS,
ABREVEXAMEN, SEREMITE, SERECIBE, CODSECCION
FROM EXAMEN'
);
$checkStmt = $my->prepare('SELECT id FROM exam_tipos WHERE codigo_legacy = ?');
$updStmt = $my->prepare(
'UPDATE exam_tipos SET
cod_protocolo = ?, tipo_muestra = ?, nivel = ?, cups = ?,
abreviatura = ?, seremite = ?, serecibe = ?
WHERE codigo_legacy = ?'
);
$insStmt = $my->prepare(
'INSERT IGNORE INTO exam_tipos
(codigo, nombre, categoria, codigo_legacy, cod_protocolo,
tipo_muestra, nivel, cups, abreviatura, seremite, serecibe)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
);
while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
$codLeg = cleanStr($r['CODIGO'], $charset);
$nombre = cleanStr($r['NOMEXAMEN'], $charset);
$proto = cleanStr($r['CODPROT'], $charset);
$tMuest = cleanStr($r['TIPOMUESTRA'], $charset);
$nivel = $r['NIVEL'] ? (int)$r['NIVEL'] : null;
$cups = cleanStr($r['CUPS'], $charset);
$abrev = cleanStr($r['ABREVEXAMEN'], $charset);
$serem = $r['SEREMITE'] ? 1 : 0;
$serec = cleanStr($r['SERECIBE'], $charset);
$seccion = cleanStr($r['CODSECCION'], $charset);
if (!$codLeg || !$nombre) continue;
$checkStmt->execute([$codLeg]);
$existing = $checkStmt->fetchColumn();
if ($dryRun) continue;
if ($existing) {
$updStmt->execute([$proto, $tMuest, $nivel, $cups, $abrev, $serem, $serec, $codLeg]);
$actualizados++;
} else {
// Usar el código Firebird como código del nuevo sistema (si no hay conflicto)
$insStmt->execute([$codLeg, $nombre, $seccion, $codLeg, $proto, $tMuest, $nivel, $cups, $abrev, $serem, $serec]);
$insertados++;
}
}
etlLog(" Nuevos: $insertados | Actualizados: $actualizados", $logFp);
// Resolver exam_tipo_id en lab_tarifas (ahora que los exámenes ya están)
if (!$dryRun) {
etlLog(' Resolviendo exam_tipo_id en lab_tarifas...', $logFp);
$updated = $my->exec(
'UPDATE lab_tarifas t
JOIN exam_tipos e ON e.codigo_legacy = t.cod_examen_legacy
SET t.exam_tipo_id = e.id
WHERE t.exam_tipo_id IS NULL'
);
etlLog(" lab_tarifas actualizadas: $updated filas", $logFp);
}
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 8 — EMPRESA + EMPRESA_SUB + EXAMEN_EMP
// ════════════════════════════════════════════════════════════════════════════
if (shouldRun('empresas', $onlyPaso)) {
etlLog('--- EMPRESA → lab_empresas ---', $logFp);
$rows = [];
foreach ($fb->query(
'SELECT NIT,NOMBRE,RAZSOCIAL,IDTARIFA,DESCUENTO,CODEEPS,
TIPOUSUARIO,TIPOUSUARIOSISPRO,CODCONTRATO,CODTERCERO,
CENTROCOSTO,REQAUTORIZA,ACTIVA
FROM EMPRESA'
) as $r) {
$rows[] = [
'nit' => cleanStr($r['NIT'], $charset),
'nombre' => cleanStr($r['NOMBRE'], $charset),
'razon_social' => cleanStr($r['RAZSOCIAL'], $charset),
'tarifa_id' => $r['IDTARIFA'] ? (int)$r['IDTARIFA'] : null,
'descuento_pct' => (float)($r['DESCUENTO'] ?? 0),
'codigo_eps' => cleanStr($r['CODEEPS'], $charset),
'tipo_usuario' => cleanStr($r['TIPOUSUARIO'], $charset),
'tipo_usuario_sispro' => cleanStr($r['TIPOUSUARIOSISPRO'],$charset),
'cod_contrato' => cleanStr($r['CODCONTRATO'], $charset),
'cod_tercero' => cleanStr($r['CODTERCERO'], $charset),
'centro_costo' => cleanStr($r['CENTROCOSTO'], $charset),
'req_autoriza' => $r['REQAUTORIZA'] ? 1 : 0,
'activa' => $r['ACTIVA'] ? 1 : 0,
];
}
if (!$dryRun) {
$n = batchInsert($my, 'lab_empresas', $rows, true);
etlLog(" Empresas: $n / " . count($rows), $logFp);
}
etlLog('--- EMPRESA_SUB → lab_empresa_subgrupos ---', $logFp);
$rows = [];
foreach ($fb->query('SELECT NIT,SUBGRUPO,IDTARIFA,REF_SUBGRUPO,CODCONTRATO FROM EMPRESA_SUB') as $r) {
$rows[] = [
'nit_empresa' => cleanStr($r['NIT'], $charset),
'subgrupo' => cleanStr($r['SUBGRUPO'], $charset),
'tarifa_id' => $r['IDTARIFA'] ? (int)$r['IDTARIFA'] : null,
'ref_subgrupo' => cleanStr($r['REF_SUBGRUPO'],$charset),
'cod_contrato' => cleanStr($r['CODCONTRATO'], $charset),
];
}
if (!$dryRun) {
$n = batchInsert($my, 'lab_empresa_subgrupos', $rows, true);
etlLog(" Subgrupos: $n", $logFp);
}
etlLog('--- EXAMEN_EMP → lab_examenes_empresa ---', $logFp);
$rows = [];
foreach ($fb->query('SELECT NIT,CODIGO,CODIGOEMP FROM EXAMEN_EMP') as $r) {
$rows[] = [
'nit_empresa' => cleanStr($r['NIT'], $charset),
'cod_examen_legacy' => cleanStr($r['CODIGO'], $charset),
'exam_tipo_id' => null,
'codigo_empresa' => cleanStr($r['CODIGOEMP'],$charset),
];
}
if (!$dryRun) {
$n = batchInsert($my, 'lab_examenes_empresa', $rows, true);
// Resolver exam_tipo_id
$my->exec(
'UPDATE lab_examenes_empresa ee
JOIN exam_tipos e ON e.codigo_legacy = ee.cod_examen_legacy
SET ee.exam_tipo_id = e.id
WHERE ee.exam_tipo_id IS NULL'
);
etlLog(" Examenes-empresa: $n", $logFp);
}
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 9 — MEDICO → medicos
// ════════════════════════════════════════════════════════════════════════════
if (shouldRun('medicos', $onlyPaso)) {
etlLog('--- MEDICO → medicos ---', $logFp);
$rows = [];
foreach ($fb->query(
'SELECT CODMEDICO,NOMBRES,APELLIDOS,CODESPECIA,TELEFONO1,EMAIL,DOCIDMEDICO,ACTIVO
FROM MEDICO'
) as $r) {
$rows[] = [
'codigo' => cleanStr($r['CODMEDICO'], $charset),
'nombres' => cleanStr($r['NOMBRES'], $charset),
'apellidos' => cleanStr($r['APELLIDOS'], $charset),
'cod_especialidad'=> cleanStr($r['CODESPECIA'], $charset),
'telefonos' => cleanStr($r['TELEFONO1'], $charset),
'email' => cleanStr($r['EMAIL'], $charset),
'docidmedico' => cleanStr($r['DOCIDMEDICO'], $charset),
'activo' => $r['ACTIVO'] ? 1 : 0,
];
}
if (!$dryRun) {
$n = batchInsert($my, 'medicos', $rows, true);
etlLog(" Médicos: $n / " . count($rows), $logFp);
}
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 10 — PACIENTE → lab_pacientes
// ════════════════════════════════════════════════════════════════════════════
if (shouldRun('pacientes', $onlyPaso)) {
etlLog('--- PACIENTE → lab_pacientes ---', $logFp);
$total = fbCount($fb, 'PACIENTE');
etlLog(" Origen: $total registros", $logFp);
$stmt = $fb->query(
'SELECT CODPAC,DOCIDENT,TIPOIDENT,NOMBRES,APELLIDOS,
TELEFONO1,EMAIL,FECHANAC,SEXO,CIUDAD,
OCUPACION,CODETNIA,TIPORES
FROM PACIENTE
ORDER BY CODPAC'
);
$checkStmt = $my->prepare('SELECT id FROM lab_pacientes WHERE numero_documento = ?');
$insStmt = $my->prepare(
'INSERT IGNORE INTO lab_pacientes
(numero_documento, tipo_documento, nombre_completo, telefono,
email, fecha_nacimiento, genero, ciudad,
ocupacion, codetnia, tipores, codigo_legacy, es_historico)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,1)'
);
$updLegacy = $my->prepare(
'UPDATE lab_pacientes SET codigo_legacy = ? WHERE numero_documento = ? AND codigo_legacy IS NULL'
);
$nuevos = $coincidentes = 0;
while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
$doc = cleanStr($r['DOCIDENT'], $charset);
if (!$doc) continue;
$nombres = cleanStr($r['NOMBRES'], $charset);
$apellidos = cleanStr($r['APELLIDOS'], $charset);
$nombre = trim("$nombres $apellidos");
if ($dryRun) continue;
$checkStmt->execute([$doc]);
$existeId = $checkStmt->fetchColumn();
if ($existeId) {
// Ya existe → solo actualizar codigo_legacy si falta
$updLegacy->execute([cleanStr($r['CODPAC'], $charset), $doc]);
$coincidentes++;
} else {
$tipoDoc = match(strtoupper(cleanStr($r['TIPOIDENT'], $charset) ?? '')) {
'CC' => 'CC',
'CE' => 'CE',
'TI' => 'TI',
'PA' => 'PA',
'NIT' => 'NIT',
'RC' => 'RC',
'MS' => 'MS',
default => 'CC',
};
$genero = match(strtoupper(cleanStr($r['SEXO'], $charset) ?? '')) {
'M' => 'M', 'F' => 'F', default => null
};
$insStmt->execute([
$doc,
$tipoDoc,
$nombre ?: 'Sin nombre',
cleanStr($r['TELEFONO1'], $charset),
cleanStr($r['EMAIL'], $charset),
fbDate($r['FECHANAC']),
$genero,
cleanStr($r['CIUDAD'], $charset),
cleanStr($r['OCUPACION'], $charset),
cleanStr($r['CODETNIA'], $charset),
cleanStr($r['TIPORES'], $charset),
cleanStr($r['CODPAC'], $charset),
]);
$nuevos++;
}
}
etlLog(" Nuevos: $nuevos | Coincidentes (codigo_legacy actualizado): $coincidentes", $logFp);
}
// ════════════════════════════════════════════════════════════════════════════
// PASO 11-13 — Histórico transaccional (RECEPCION / RELACION / PAGOS)
// ════════════════════════════════════════════════════════════════════════════
if (!$skipHist) {
// ── 11. RECEPCION → lab_recepciones ──────────────────────────────────
if (shouldRun('recepciones', $onlyPaso)) {
etlLog('--- RECEPCION → lab_recepciones ---', $logFp);
$total = fbCount($fb, 'RECEPCION');
etlLog(" Origen: $total registros", $logFp);
// Mapa paciente legacy → id nuevo
$pacMap = [];
foreach ($my->query('SELECT id, codigo_legacy FROM lab_pacientes WHERE codigo_legacy IS NOT NULL') as $r) {
$pacMap[$r['codigo_legacy']] = (int)$r['id'];
}
// Mapa médico legacy → id nuevo
$medMap = [];
foreach ($my->query('SELECT id, codigo FROM medicos WHERE codigo IS NOT NULL') as $r) {
$medMap[$r['codigo']] = (int)$r['id'];
}
$stmt = $fb->query(
'SELECT IDRECEPCION,CODPAC,CODMEDICO,NIT,SUBGRUPO,
FECHA,HORAINICIO,PREFIJO,NUMFACTURA,
VALORTOTAL,VALORDESC,DIAGPPAL,TIPOUSUARIO,AUTORIZACION,USUARIO
FROM RECEPCION
ORDER BY IDRECEPCION'
);
$batch = [];
$count = 0;
while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
$codPac = cleanStr($r['CODPAC'], $charset);
$codMed = cleanStr($r['CODMEDICO'],$charset);
$batch[] = [
'id' => (int)$r['IDRECEPCION'],
'cod_paciente_legacy' => $codPac,
'paciente_id' => $pacMap[$codPac] ?? null,
'cod_medico_legacy' => $codMed,
'medico_id' => $medMap[$codMed] ?? null,
'nit_empresa' => cleanStr($r['NIT'], $charset),
'subgrupo' => cleanStr($r['SUBGRUPO'], $charset),
'fecha_recepcion' => fbDate($r['FECHA']),
'hora_inicio' => cleanStr($r['HORAINICIO'], $charset),
'prefijo' => cleanStr($r['PREFIJO'], $charset),
'num_factura' => $r['NUMFACTURA'] ? (int)$r['NUMFACTURA'] : null,
'valor_total' => (float)($r['VALORTOTAL'] ?? 0),
'valor_desc' => (float)($r['VALORDESC'] ?? 0),
'diag_ppal' => cleanStr($r['DIAGPPAL'], $charset),
'tipo_usuario' => cleanStr($r['TIPOUSUARIO'], $charset),
'autorizacion' => cleanStr($r['AUTORIZACION'], $charset),
'usuario' => cleanStr($r['USUARIO'], $charset),
];
if (count($batch) >= 500) {
if (!$dryRun) $count += batchInsert($my, 'lab_recepciones', $batch, true);
$batch = [];
if ($count % 5000 === 0) etlLog(" ... $count procesadas", $logFp);
}
}
if ($batch && !$dryRun) $count += batchInsert($my, 'lab_recepciones', $batch, true);
etlLog(" Insertadas: $count", $logFp);
}
// ── 12. RELACION → lab_relaciones ─────────────────────────────────────
if (shouldRun('relaciones', $onlyPaso)) {
etlLog('--- RELACION → lab_relaciones ---', $logFp);
$total = fbCount($fb, 'RELACION');
etlLog(" Origen: $total registros", $logFp);
// Mapa cod_examen_legacy → exam_tipo_id
$examMap = [];
foreach ($my->query('SELECT id, codigo_legacy FROM exam_tipos WHERE codigo_legacy IS NOT NULL') as $r) {
$examMap[$r['codigo_legacy']] = (int)$r['id'];
}
$stmt = $fb->query(
'SELECT IDRECEPCION,CODIGO,PRECIO,FECHAREPORT,REPORTADO,REPORPOR,VALIDADO,USRVALIDA,FECHAVALIDA
FROM RELACION'
);
$batch = [];
$count = 0;
while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
$codExam = cleanStr($r['CODIGO'], $charset);
$batch[] = [
'recepcion_id' => (int)$r['IDRECEPCION'],
'cod_examen_legacy' => $codExam,
'exam_tipo_id' => $examMap[$codExam] ?? null,
'precio' => (float)($r['PRECIO'] ?? 0),
'fecha_reportado' => fbDate($r['FECHAREPORT']),
'reportado' => $r['REPORTADO'] ? 1 : 0,
'reportado_por' => cleanStr($r['REPORPOR'], $charset),
'validado' => $r['VALIDADO'] ? 1 : 0,
'usuario_valida' => cleanStr($r['USRVALIDA'], $charset),
'fecha_valida' => fbDate($r['FECHAVALIDA']),
];
if (count($batch) >= 500) {
if (!$dryRun) $count += batchInsert($my, 'lab_relaciones', $batch, true);
$batch = [];
if ($count % 10000 === 0) etlLog(" ... $count", $logFp);
}
}
if ($batch && !$dryRun) $count += batchInsert($my, 'lab_relaciones', $batch, true);
etlLog(" Insertadas: $count", $logFp);
}
// ── 13. PAGOS + PAGOS_DET ─────────────────────────────────────────────
if (shouldRun('pagos', $onlyPaso)) {
etlLog('--- PAGOS → lab_pagos ---', $logFp);
$rows = [];
foreach ($fb->query('SELECT NUMCAJA,IDRECEPCION,VALOR,FECHAPAGO,USUARIO FROM PAGOS') as $r) {
$rows[] = [
'numcaja_legacy' => (int)$r['NUMCAJA'],
'recepcion_id' => (int)$r['IDRECEPCION'],
'valor' => (float)($r['VALOR'] ?? 0),
'fecha' => fbDate($r['FECHAPAGO']),
'usuario' => cleanStr($r['USUARIO'], $charset),
];
}
if (!$dryRun) {
$n = batchInsert($my, 'lab_pagos', $rows, true);
etlLog(" Pagos: $n / " . count($rows), $logFp);
}
etlLog('--- PAGOS_DET → lab_pagos_det ---', $logFp);
// Mapa numcaja → id nuevo
$pagoMap = [];
if (!$dryRun) {
foreach ($my->query('SELECT id, numcaja_legacy FROM lab_pagos') as $r) {
$pagoMap[$r['numcaja_legacy']] = (int)$r['id'];
}
}
$rows = [];
foreach ($fb->query('SELECT NUMCAJA,TIPOPAGO,VALOR,NUMDOC FROM PAGOS_DET') as $r) {
$pagoId = $pagoMap[(int)$r['NUMCAJA']] ?? null;
if (!$pagoId) continue;
$rows[] = [
'pago_id' => $pagoId,
'tipo_pago' => cleanStr($r['TIPOPAGO'], $charset),
'valor' => (float)($r['VALOR'] ?? 0),
'num_doc' => cleanStr($r['NUMDOC'], $charset),
];
}
if (!$dryRun) {
$n = batchInsert($my, 'lab_pagos_det', $rows, true);
etlLog(" Detalles de pago: $n", $logFp);
}
}
} else {
etlLog('--- Histórico omitido (--skip-historico) ---', $logFp);
}
// ── Restaurar FK checks ──────────────────────────────────────────────────────
if (!$dryRun) {
$my->exec("SET foreign_key_checks = 1");
}
etlLog('=== ETL COMPLETADO ===', $logFp);
fclose($logFp);
echo "Log guardado en: {$cfg['options']['log_file']}\n";