""" Servicio de sincronización de pacientes RIPS → WhatsApp Lab. Mapea campos Firebird al formato esperado por /api/lab/ingest_paciente.php. """ import re import json import asyncio import httpx from datetime import datetime from typing import Optional from app.database import get_connection def guardar_sync_log( resultado: dict, user_id, origen: str = "manual", modo: str = "insertar", ) -> None: detalle_raw = resultado.get("detalle", []) errores_det = None errores = [d for d in detalle_raw if not d.get("ok")] if errores: errores_det = json.dumps( [{"doc": e["doc"], "nombre": e["nombre"], "msg": e["message"]} for e in errores], ensure_ascii=False, ) detalle_json = None if detalle_raw: detalle_json = json.dumps( [{ "doc": d["doc"], "nombre": d["nombre"], "action": d.get("action", ""), "examenes_cnt": d.get("examenes_guardados", 0), "diagnostico_cod": d.get("diagnostico_cod", ""), "diagnostico_nombre": d.get("diagnostico_nombre", ""), "nit_empresa": d.get("nit_empresa", ""), "nom_empresa": d.get("nom_empresa", ""), "valor_total": d.get("valor_total"), "examenes_det": d.get("examenes_det", []), } for d in detalle_raw], ensure_ascii=False, ) conn = get_connection() conn.execute( """INSERT INTO sync_wa_log (user_id, origen, modo, total, created, skipped, updated, errores, errores_det, detalle_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( user_id, origen, modo, resultado.get("total", 0), resultado.get("created", 0), resultado.get("skipped", 0), resultado.get("updated", 0), resultado.get("errores", 0), errores_det, detalle_json, ), ) conn.commit() conn.close() _TIPO_DOC_MAP = { "CC": "CC", "TI": "TI", "RC": "RC", "CE": "CE", "PA": "PA", "NIT": "NIT", "MS": "MS", "AS": "CC", "SI": "CC", "CN": "CC", "DE": "CE", "CD": "PA", "PE": "CE", "PT": "PA", } _SEXO_MAP = {"M": "M", "F": "F", "H": "M"} def _fmt_fecha_iso(val) -> Optional[str]: if not val: return None if hasattr(val, "strftime"): return val.strftime("%Y-%m-%d") s = str(val)[:10] if re.match(r"\d{4}-\d{2}-\d{2}", s): return s # dd/mm/yyyy parts = s.split("/") if len(parts) == 3: return f"{parts[2]}-{parts[1]}-{parts[0]}" return None def mapear_paciente(row: dict) -> dict: """Convierte una fila de Firebird PACIENTE al body de ingest_paciente.php.""" nombres = (row.get("NOMBRES") or "").strip() apellidos = (row.get("APELLIDOS") or "").strip() nombre_completo = f"{nombres} {apellidos}".strip().upper() tipo_raw = str(row.get("TIPOIDENT") or "CC").strip() tipo_doc = _TIPO_DOC_MAP.get(tipo_raw, "CC") sexo_raw = str(row.get("SEXO") or "M").strip().upper() genero = _SEXO_MAP.get(sexo_raw, "M") email = (row.get("EMAIL") or "").strip().lower() if not email or "@sinregistro" in email: email = "" return { "nombre_completo": nombre_completo, "numero_documento": str(row.get("DOCIDENT") or "").strip(), "tipo_documento": tipo_doc, "telefono": str(row.get("TELEFONOS") or "").strip(), "email": email, "fecha_nacimiento": _fmt_fecha_iso(row.get("F_NACIMIENTO")), "genero": genero, "direccion": (row.get("DIRECCION") or "").strip(), "ciudad": (row.get("NOM_CIUDAD") or "").strip(), "origen": "lab", } async def sync_paciente( row: dict, url: str, api_key: str, client: Optional[httpx.AsyncClient] = None, modo: str = "upsert", examenes: Optional[list] = None, ) -> dict: """Envía un paciente al endpoint de WhatsApp. Retorna {ok, action, message}.""" payload = mapear_paciente(row) payload["modo"] = modo if examenes: payload["examenes"] = examenes headers = { "Content-Type": "application/json", "X-Lab-Key": api_key, } try: if client: resp = await client.post(url, json=payload, headers=headers) else: async with httpx.AsyncClient(timeout=15) as c: resp = await c.post(url, json=payload, headers=headers) try: data = resp.json() except Exception as json_err: raw = resp.text[:300].strip() return { "ok": False, "action": "error", "message": f"HTTP {resp.status_code} — respuesta no JSON: {raw!r}", "doc": payload["numero_documento"], "nombre": payload["nombre_completo"], } return { "ok": data.get("ok", False), "action": data.get("action", ""), "message": data.get("message") or data.get("error", ""), "doc": payload["numero_documento"], "nombre": payload["nombre_completo"], } except Exception as e: return { "ok": False, "action": "error", "message": str(e), "doc": payload.get("numero_documento", ""), "nombre": payload.get("nombre_completo", ""), } async def sync_todos(rows: list, url: str, api_key: str, timeout: int = 30, modo: str = "upsert", examenes_map: Optional[dict] = None, concurrencia: int = 10) -> dict: """Envía una lista de filas de pacientes en paralelo. Retorna resumen {total, created, skipped, updated, errores}.""" resultado = {"total": len(rows), "created": 0, "skipped": 0, "updated": 0, "errores": 0, "detalle": []} sem = asyncio.Semaphore(concurrencia) async def _enviar(row: dict, client: httpx.AsyncClient) -> dict: async with sem: doc = str(row.get("DOCIDENT") or "").strip() examenes = examenes_map.get(doc) if examenes_map else None return await sync_paciente(row, url, api_key, client, modo, examenes) async with httpx.AsyncClient(timeout=timeout) as client: tasks = [_enviar(row, client) for row in rows] resultados = await asyncio.gather(*tasks, return_exceptions=True) for r in resultados: if isinstance(r, Exception): resultado["errores"] += 1 resultado["detalle"].append({"ok": False, "action": "error", "message": str(r), "doc": "", "nombre": ""}) continue if r["ok"]: action = r["action"] if action == "created": resultado["created"] += 1 elif action == "skipped": resultado["skipped"] += 1 else: resultado["updated"] += 1 else: resultado["errores"] += 1 resultado["detalle"].append(r) return resultado