- whatsapp_sync: send patients concurrently (asyncio.gather + semaphore=10) instead of sequentially — eliminates long waits with many patients - envios_erp: auto-refresh historial every 60s when scheduler tab is active - main: wrap scheduler job to catch exceptions and log them to sync_wa_log so failures are visible in historial Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
212 lines
7.1 KiB
Python
212 lines
7.1 KiB
Python
"""
|
|
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
|