From 9651f14302adab88b4d530396f3b5633317f7927 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:48:50 -0500 Subject: [PATCH] feat(lab): endpoint server-to-server ingest_paciente para sync desde RIPS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agrega /api/lab/ingest_paciente.php con autenticación por X-Lab-Key (LAB_SYNC_KEY en config). Hace UPSERT por numero_documento sin requerir sesión de admin — permite que RIPS Manager sincronice pacientes directo a lab_pacientes. Co-Authored-By: Claude Sonnet 4.6 --- api/lab/ingest_paciente.php | 160 ++++++++++++++++++++++++++++++++++++ config/config.php | 4 + 2 files changed, 164 insertions(+) create mode 100644 api/lab/ingest_paciente.php diff --git a/api/lab/ingest_paciente.php b/api/lab/ingest_paciente.php new file mode 100644 index 0000000..17b74d6 --- /dev/null +++ b/api/lab/ingest_paciente.php @@ -0,0 +1,160 @@ + (sin sesión de usuario). + * + * Body JSON (todos opcionales excepto nombre_completo en creación): + * { numero_documento, tipo_documento, nombre_completo, telefono, email, + * fecha_nacimiento, genero, direccion, ciudad, eps, origen } + * + * Respuesta: + * { ok, action: "created"|"updated"|"skipped", id, message } + */ + +ob_start(); +require_once __DIR__ . '/../../config/config.php'; +require_once __DIR__ . '/../../classes/lab/Paciente.php'; + +error_reporting(E_ERROR | E_PARSE); +ini_set('display_errors', '0'); +ini_set('html_errors', '0'); + +register_shutdown_function(function () { + $err = error_get_last(); + if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) { + ob_clean(); + http_response_code(500); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['ok' => false, 'error' => 'Error interno: ' . $err['message']]); + } +}); + +header('Content-Type: application/json; charset=utf-8'); + +if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + http_response_code(204); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + echo json_encode(['ok' => false, 'error' => 'Método no permitido']); + exit; +} + +// ── Autenticación por API key ──────────────────────────────────────────────── +$keyHeader = $_SERVER['HTTP_X_LAB_KEY'] + ?? $_SERVER['HTTP_X_LAB_SYNC_KEY'] + ?? ''; + +if (!$keyHeader || !hash_equals(LAB_SYNC_KEY, $keyHeader)) { + http_response_code(401); + ob_clean(); + echo json_encode(['ok' => false, 'error' => 'API key inválida']); + exit; +} + +// ── Leer body ──────────────────────────────────────────────────────────────── +$raw = file_get_contents('php://input'); +$data = json_decode($raw, true); + +if (!is_array($data)) { + http_response_code(400); + ob_clean(); + echo json_encode(['ok' => false, 'error' => 'Body JSON inválido']); + exit; +} + +// ── Normalizar campos ──────────────────────────────────────────────────────── +$campos = []; + +if (!empty($data['nombre_completo'])) { + $campos['nombre_completo'] = mb_strtoupper(trim($data['nombre_completo'])); +} +if (!empty($data['numero_documento'])) { + $campos['numero_documento'] = trim($data['numero_documento']); +} +if (!empty($data['tipo_documento'])) { + $tiposValidos = ['CC', 'CE', 'TI', 'PA', 'NIT', 'RC', 'MS']; + $t = strtoupper(trim($data['tipo_documento'])); + $campos['tipo_documento'] = in_array($t, $tiposValidos) ? $t : 'CC'; +} +if (!empty($data['telefono'])) { + $tel = preg_replace('/[^0-9+]/', '', $data['telefono']); + if (strlen($tel) >= 6) { + $campos['telefono'] = $tel; + } +} +if (!empty($data['email']) && filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { + $e = strtolower(trim($data['email'])); + // Ignorar emails de relleno generados por RIPS + if (!str_contains($e, '@sinregistro.co') && !str_contains($e, 'sinregistro')) { + $campos['email'] = $e; + } +} +if (!empty($data['fecha_nacimiento'])) { + $fn = trim($data['fecha_nacimiento']); + // Acepta dd/mm/yyyy o yyyy-mm-dd + if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $fn, $m)) { + $campos['fecha_nacimiento'] = "{$m[3]}-{$m[2]}-{$m[1]}"; + } elseif (preg_match('/^\d{4}-\d{2}-\d{2}$/', $fn)) { + $campos['fecha_nacimiento'] = $fn; + } +} +if (!empty($data['genero'])) { + $g = strtoupper(trim($data['genero'])); + if (in_array($g, ['M', 'F', 'O'])) { + $campos['genero'] = $g; + } +} +if (!empty($data['direccion'])) { + $campos['direccion'] = trim($data['direccion']); +} +if (!empty($data['ciudad'])) { + $campos['ciudad'] = trim($data['ciudad']); +} +if (!empty($data['eps'])) { + $campos['eps'] = trim($data['eps']); +} + +// ── UPSERT ─────────────────────────────────────────────────────────────────── +try { + $pac = new Paciente(); + + $existente = null; + if (!empty($campos['numero_documento'])) { + $existente = $pac->porDocumento($campos['numero_documento']); + } + + ob_clean(); + + if ($existente) { + $pac->actualizar($existente['id'], $campos, null); + echo json_encode([ + 'ok' => true, + 'action' => 'updated', + 'id' => $existente['id'], + 'message' => 'Paciente actualizado', + ]); + } elseif (!empty($campos['nombre_completo'])) { + $id = $pac->crear($campos, null); + echo json_encode([ + 'ok' => true, + 'action' => 'created', + 'id' => $id, + 'message' => 'Paciente creado', + ]); + } else { + http_response_code(422); + echo json_encode([ + 'ok' => false, + 'action' => 'skipped', + 'error' => 'Sin número de documento ni nombre: registro omitido', + ]); + } +} catch (Exception $e) { + ob_clean(); + http_response_code(500); + echo json_encode(['ok' => false, 'error' => $e->getMessage()]); +} diff --git a/config/config.php b/config/config.php index 9cac115..ce1b00d 100644 --- a/config/config.php +++ b/config/config.php @@ -402,6 +402,10 @@ if (!defined('BASE_URL')) { if (!defined('MIGRATION_TOKEN')) { define('MIGRATION_TOKEN', 'lab2026migrate'); } +// API key server-to-server para ingesta de pacientes desde RIPS Manager +if (!defined('LAB_SYNC_KEY')) { + define('LAB_SYNC_KEY', getenv('LAB_SYNC_KEY') ?: 'rips-lab-sync-2026'); +} define('TIMEZONE', 'America/Bogota'); // Información del desarrollador