Tracking de muestras para el Data Lab: el viaje de vuelta del sync
La ida ya existía: ingest_paciente.php recibe el payload con los exámenes y los guarda (y responde con examenes_guardados como acuse); el ERP genera las muestras solo, a partir del tipo de muestra de su catálogo — por eso el payload NO necesita campos nuevos. Lo que no existía era la vuelta: que el sistema del laboratorio pueda saber qué pasó con cada muestra sin que nadie mire el ERP. get_tracking_muestras.php lo resuelve consultando por rango de fechas (tope 31 días) o por documento, con la misma autenticación X-Lab-Key del ingest. Devuelve, por turno: fecha, código, paciente, exámenes en CÓDIGO LEGACY (el que el Data Lab entiende) y cada muestra con tipo, estado, motivo de rechazo, hora y quién la recibió. Solo lectura. scripts/test_tracking_muestras.php: 11 pruebas ejecutando el endpoint real contra la base real con el caso E018 del 27/08 — sus 10 muestras, el coprológico rechazado con su motivo, el filtro por documento, la clave, y la cordura de parámetros. Pendiente humano (no de código): crear MYBPC3 y SONDA en el módulo Exámenes; son los únicos 2 códigos de 265 vistos en RIPS esta semana sin mapeo, y no los invento porque llevan precio y tipo de muestra clínicos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
398ea98b0b
commit
0bcb518619
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
/**
|
||||
* GET /api/lab/get_tracking_muestras.php
|
||||
* Tracking de muestras para el Data Lab / RIPS Manager (server-a-server).
|
||||
*
|
||||
* La sincronización de entrada ya existe: ingest_paciente.php recibe los
|
||||
* exámenes y el ERP genera y marca las muestras. Este endpoint es la vuelta:
|
||||
* el sistema del laboratorio consulta qué pasó con cada muestra — recibida,
|
||||
* rechazada y por qué, pendiente — sin depender de que nadie mire el ERP.
|
||||
*
|
||||
* Autenticación: header X-Lab-Key: <LAB_SYNC_KEY> (igual que el ingest).
|
||||
*
|
||||
* Parámetros:
|
||||
* desde Y-m-d opcional (por defecto: hoy)
|
||||
* hasta Y-m-d opcional (por defecto: igual a desde)
|
||||
* documento string opcional — solo ese paciente
|
||||
*
|
||||
* Respuesta:
|
||||
* { ok, desde, hasta, total, turnos: [ { fecha, turno, estado_turno,
|
||||
* numero_documento, paciente, examenes: [cod_legacy…],
|
||||
* muestras: [ { tipo, nombre_tipo, estado, motivo_rechazo,
|
||||
* recibida_at, recibida_por } ] } ] }
|
||||
*
|
||||
* Solo lectura: no crea, no modifica, no borra.
|
||||
*/
|
||||
|
||||
ob_start();
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function salir(array $r, int $code = 200): void {
|
||||
ob_clean();
|
||||
http_response_code($code);
|
||||
echo json_encode($r, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
salir(['ok' => false, 'error' => 'Método no permitido'], 405);
|
||||
}
|
||||
|
||||
// ── Autenticación por API key, idéntica al ingest ────────────────────────
|
||||
$keyHeader = $_SERVER['HTTP_X_LAB_KEY'] ?? $_SERVER['HTTP_X_LAB_SYNC_KEY'] ?? '';
|
||||
if (!defined('LAB_SYNC_KEY') || !$keyHeader || !hash_equals(LAB_SYNC_KEY, $keyHeader)) {
|
||||
salir(['ok' => false, 'error' => 'No autorizado'], 401);
|
||||
}
|
||||
|
||||
// ── Parámetros ───────────────────────────────────────────────────────────
|
||||
$hoy = date('Y-m-d');
|
||||
$desde = $_GET['desde'] ?? $hoy;
|
||||
$hasta = $_GET['hasta'] ?? $desde;
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $desde)) $desde = $hoy;
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $hasta)) $hasta = $desde;
|
||||
if ($desde > $hasta) [$desde, $hasta] = [$hasta, $desde];
|
||||
// Tope de 31 días por consulta: es un endpoint de sincronización, no de archivo
|
||||
if ((strtotime($hasta) - strtotime($desde)) > 31 * 86400) {
|
||||
salir(['ok' => false, 'error' => 'Rango máximo: 31 días'], 400);
|
||||
}
|
||||
$documento = trim($_GET['documento'] ?? '');
|
||||
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
|
||||
// ── Turnos del rango con paciente y solicitud ────────────────────────────
|
||||
$where = "DATE(t.creado_at) BETWEEN :desde AND :hasta AND ts.id IS NOT NULL";
|
||||
$binds = ['desde' => $desde, 'hasta' => $hasta];
|
||||
if ($documento !== '') {
|
||||
$where .= " AND p.numero_documento = :doc";
|
||||
$binds['doc'] = $documento;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT t.id AS turno_id, t.codigo AS turno, t.estado AS estado_turno,
|
||||
DATE(t.creado_at) AS fecha,
|
||||
ts.id AS solicitud_id,
|
||||
p.numero_documento, p.nombre_completo AS paciente
|
||||
FROM turnero_turnos t
|
||||
JOIN turnero_solicitudes ts ON ts.turno_id = t.id
|
||||
LEFT JOIN lab_pacientes p ON p.id = ts.paciente_id
|
||||
WHERE {$where}
|
||||
ORDER BY t.creado_at"
|
||||
);
|
||||
$stmt->execute($binds);
|
||||
$turnos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$turnos) {
|
||||
salir(['ok' => true, 'desde' => $desde, 'hasta' => $hasta, 'total' => 0, 'turnos' => []]);
|
||||
}
|
||||
|
||||
$solIds = array_column($turnos, 'solicitud_id');
|
||||
$ph = implode(',', array_fill(0, count($solIds), '?'));
|
||||
|
||||
// ── Muestras de esas solicitudes, con nombre del tipo y quién recibió ────
|
||||
$stmtM = $pdo->prepare(
|
||||
"SELECT m.solicitud_id, m.tipo_muestra AS tipo, m.estado, m.motivo_rechazo,
|
||||
m.recibida_at, u.full_name AS recibida_por,
|
||||
tm.nombre AS nombre_tipo
|
||||
FROM turnero_muestras m
|
||||
LEFT JOIN admin_users u ON u.id = m.recibida_por
|
||||
LEFT JOIN lab_tipos_muestra tm
|
||||
ON tm.codigo COLLATE utf8mb4_unicode_ci = m.tipo_muestra COLLATE utf8mb4_unicode_ci
|
||||
WHERE m.solicitud_id IN ({$ph})"
|
||||
);
|
||||
$stmtM->execute($solIds);
|
||||
$muestrasPorSol = [];
|
||||
foreach ($stmtM->fetchAll(PDO::FETCH_ASSOC) as $m) {
|
||||
$sid = (int) $m['solicitud_id'];
|
||||
unset($m['solicitud_id']);
|
||||
$muestrasPorSol[$sid][] = $m;
|
||||
}
|
||||
|
||||
// ── Exámenes (códigos legacy, que son los que el Data Lab entiende) ──────
|
||||
$stmtE = $pdo->prepare(
|
||||
"SELECT i.solicitud_id, COALESCE(NULLIF(et.codigo_legacy,''), et.codigo) AS cod
|
||||
FROM turnero_examen_items i
|
||||
JOIN exam_tipos et ON et.id = i.exam_tipo_id
|
||||
WHERE i.solicitud_id IN ({$ph})"
|
||||
);
|
||||
$stmtE->execute($solIds);
|
||||
$examenesPorSol = [];
|
||||
foreach ($stmtE->fetchAll(PDO::FETCH_ASSOC) as $e) {
|
||||
$examenesPorSol[(int) $e['solicitud_id']][] = $e['cod'];
|
||||
}
|
||||
|
||||
$salida = [];
|
||||
foreach ($turnos as $t) {
|
||||
$sid = (int) $t['solicitud_id'];
|
||||
$salida[] = [
|
||||
'fecha' => $t['fecha'],
|
||||
'turno' => $t['turno'],
|
||||
'estado_turno' => $t['estado_turno'],
|
||||
'numero_documento' => $t['numero_documento'],
|
||||
'paciente' => $t['paciente'],
|
||||
'examenes' => $examenesPorSol[$sid] ?? [],
|
||||
'muestras' => $muestrasPorSol[$sid] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
salir(['ok' => true, 'desde' => $desde, 'hasta' => $hasta,
|
||||
'total' => count($salida), 'turnos' => $salida]);
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* scripts/test_tracking_muestras.php — php scripts/test_tracking_muestras.php
|
||||
*
|
||||
* Prueba el endpoint de tracking de muestras ejecutándolo de verdad con la
|
||||
* clave real contra la base real. Solo lectura: el endpoint no escribe nada.
|
||||
* Caso de referencia: el turno E018 del 27/08, cuyas muestras conocemos
|
||||
* (10 tipos, coprológico rechazado con motivo «la trae mañana»).
|
||||
*/
|
||||
$fallos = 0;
|
||||
function ok(string $d, bool $c): void {
|
||||
global $fallos;
|
||||
echo ($c ? ' ok ' : ' FALLA ') . $d . "\n";
|
||||
if (!$c) $fallos++;
|
||||
}
|
||||
|
||||
function llamar(array $get, ?string $clave): ?array {
|
||||
$code = '$_SERVER["REQUEST_METHOD"]="GET";'
|
||||
. '$_GET=json_decode(getenv("T_GET"),true);'
|
||||
. 'if(getenv("T_KEY")!==false && getenv("T_KEY")!=="")'
|
||||
. ' $_SERVER["HTTP_X_LAB_KEY"]=getenv("T_KEY");'
|
||||
. 'require ' . var_export(dirname(__DIR__) . '/api/lab/get_tracking_muestras.php', true) . ';';
|
||||
$cmd = sprintf('T_GET=%s T_KEY=%s php -r %s 2>/dev/null',
|
||||
escapeshellarg(json_encode($get)),
|
||||
escapeshellarg($clave ?? ''),
|
||||
escapeshellarg($code));
|
||||
return json_decode(shell_exec($cmd) ?: 'null', true);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
$clave = LAB_SYNC_KEY;
|
||||
|
||||
// ── 1. Sin clave: afuera ────────────────────────────────────────────────
|
||||
$r = llamar(['desde' => '2026-08-27'], null);
|
||||
ok('sin la clave responde no-autorizado', is_array($r) && ($r['ok'] ?? true) === false);
|
||||
|
||||
// ── 2. Con clave, el día del E018 conocido ──────────────────────────────
|
||||
$r = llamar(['desde' => '2026-08-27'], $clave);
|
||||
ok('con la clave responde ok', is_array($r) && ($r['ok'] ?? false) === true);
|
||||
ok('trae turnos de ese día', ($r['total'] ?? 0) > 0);
|
||||
|
||||
$e018 = null;
|
||||
foreach ($r['turnos'] ?? [] as $t) if ($t['turno'] === 'E018') { $e018 = $t; break; }
|
||||
ok('el E018 está en la respuesta', $e018 !== null);
|
||||
if ($e018) {
|
||||
ok('con sus 10 muestras', count($e018['muestras']) === 10);
|
||||
ok('y sus exámenes en código legacy (49)', count($e018['examenes']) === 49);
|
||||
$mf = null;
|
||||
foreach ($e018['muestras'] as $m) if ($m['tipo'] === 'MF') { $mf = $m; break; }
|
||||
ok('el coprológico aparece RECHAZADO con su motivo',
|
||||
$mf && $mf['estado'] === 'rechazada' && $mf['motivo_rechazo'] === 'la trae mañana');
|
||||
$ss = null;
|
||||
foreach ($e018['muestras'] as $m) if ($m['tipo'] === 'SS') { $ss = $m; break; }
|
||||
ok('el suero aparece recibido, con quién y cuándo',
|
||||
$ss && $ss['estado'] === 'recibida' && !empty($ss['recibida_por']) && !empty($ss['recibida_at']));
|
||||
}
|
||||
|
||||
// ── 3. Filtro por documento ─────────────────────────────────────────────
|
||||
if ($e018) {
|
||||
$r = llamar(['desde' => '2026-08-27', 'documento' => $e018['numero_documento']], $clave);
|
||||
$codigos = array_column($r['turnos'] ?? [], 'turno');
|
||||
ok('filtrando por documento solo vienen los turnos de ese paciente',
|
||||
in_array('E018', $codigos, true)
|
||||
&& count($codigos) === count(array_filter($r['turnos'], fn($t) => $t['numero_documento'] === $e018['numero_documento'])));
|
||||
}
|
||||
|
||||
// ── 4. Cordura de parámetros ────────────────────────────────────────────
|
||||
$r = llamar(['desde' => '2026-01-01', 'hasta' => '2026-12-31'], $clave);
|
||||
ok('un rango de un año se rechaza (tope 31 días)', ($r['ok'] ?? true) === false);
|
||||
$r = llamar(['desde' => 'basura'], $clave);
|
||||
ok('una fecha inválida cae al día de hoy sin romperse', ($r['ok'] ?? false) === true);
|
||||
|
||||
echo "\n" . ($fallos === 0 ? "Todo correcto.\n" : "$fallos pruebas fallaron.\n");
|
||||
exit($fallos === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user