Files
rips_manager/app/services/scheduler.py
T
Lizandro GuarnizoandClaude Sonnet 4.6 408325192a fix: use FECHA_RECEPCION for exam query in scheduler, not HORAINICIORECEPCION
HORAINICIORECEPCION is a TIME field that can be NULL or outside the
2-minute window even when FECHA_RECEPCION matches, causing patients
to sync without their exams. Using the same FECHA_RECEPCION filter
for both queries ensures consistency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 09:32:42 -05:00

199 lines
8.0 KiB
Python

"""
Scheduler: sync automático de pacientes con recepción reciente → WhatsApp Lab.
Corre cada minuto y envía los pacientes con FECHA_RECEPCION en los últimos ventana_min minutos.
"""
from datetime import datetime, timedelta
from app.database import get_connection
from app.services.firebird_service import get_firebird_from_config
from app.services.whatsapp_sync import sync_todos, guardar_sync_log
# Filtra directamente en Firebird por FECHA_RECEPCION para no depender de HORAINICIORECEPCION
_SQL_RECIENTES = """
SELECT DISTINCT
p.CODIGO,
p.TIPOIDENT,
p.DOCIDENT,
p.NOMBRES,
p.APELLIDOS,
p.DIRECCION,
p.CIUDAD AS COD_CIUDAD,
c.NOMBRE AS NOM_CIUDAD,
p.TELEFONOS,
p.EMAIL,
p.F_NACIMIENTO,
p.SEXO,
p.TIPORES,
p.CODETNIA,
r.HORAINICIORECEPCION
FROM PACIENTE p
LEFT JOIN CIUDAD c ON c.CODIGO = p.CIUDAD
JOIN RECEPCION r ON r.COD_PACIENTE = p.CODIGO
WHERE r.FECHA_RECEPCION >= DATEADD(MINUTE, ?, CURRENT_TIMESTAMP)
"""
_SQL_EXAMENES_VENTANA = """
SELECT
TRIM(p.DOCIDENT) AS DOCIDENT,
r.IDRECEPCION,
r.HORAINICIORECEPCION,
TRIM(rel.COD_EXAMEN) AS COD_EXAMEN,
TRIM(ex.NOMBRE) AS NOM_EXAMEN,
COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
COALESCE(NULLIF(rel.PRECIO, 0), t.VALOR, 0) AS PRECIO,
TRIM(r.DIAG_PPAL) AS DIAG_PPAL,
TRIM(d.CONCEPTO) AS DIAG_CONCEPTO,
TRIM(m.DOCIDMEDICO) AS DOCIDMEDICO,
TRIM(r.NIT_EMPRESA) AS NIT_EMPRESA,
TRIM(e.NOMBRE) AS NOM_EMPRESA,
r.VALORTOTAL
FROM RECEPCION r
JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN)
LEFT JOIN DIAGNOSTICO d ON TRIM(d.COD_DIAG) = TRIM(r.DIAG_PPAL)
LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO
JOIN PACIENTE p ON p.CODIGO = r.COD_PACIENTE
LEFT JOIN EMPRESA e ON TRIM(e.NIT) = TRIM(r.NIT_EMPRESA)
LEFT JOIN EMPRESA_SUB es ON TRIM(es.NIT_EMP) = TRIM(r.NIT_EMPRESA)
AND TRIM(es.SUBGRUPO) = TRIM(r.SUBGRUPO)
LEFT JOIN TARIFA t ON TRIM(t.COD_EXAMEN) = TRIM(rel.COD_EXAMEN)
AND t.TARIFA = COALESCE(es.TARIFA, e.TARIFA)
WHERE r.FECHA_RECEPCION >= DATEADD(MINUTE, ?, CURRENT_TIMESTAMP)
ORDER BY r.IDRECEPCION
"""
def _parse_hora(val) -> datetime | None:
if not val:
return None
# Si ya es datetime (fdb devuelve objetos datetime)
if isinstance(val, datetime):
ahora = datetime.now()
return ahora.replace(hour=val.hour, minute=val.minute, second=val.second, microsecond=0)
s = str(val)
try:
# "2026-07-16T15:20:12" o "2026-07-16 15:20:12"
if len(s) > 10 and (s[10] == "T" or s[10] == " "):
dt = datetime.fromisoformat(s.replace(" ", "T"))
ahora = datetime.now()
return ahora.replace(hour=dt.hour, minute=dt.minute, second=dt.second, microsecond=0)
# "15:20:12"
partes = s.split(":")
ahora = datetime.now()
return ahora.replace(hour=int(partes[0]), minute=int(partes[1]),
second=int(partes[2].split(".")[0]), microsecond=0)
except Exception:
return None
def _fmt_hora_str(val) -> str:
"""Extrae HH:MM:SS de un datetime o string timestamp de Firebird."""
if not val:
return ""
if isinstance(val, datetime):
return val.strftime("%H:%M:%S")
s = str(val)
# "2026-07-16T15:20:12" o "2026-07-16 15:20:12"
if len(s) > 10 and (s[10] == "T" or s[10] == " "):
return s[11:19]
return s[:8]
def _safe_num(val):
"""Convierte Decimal/int/float a float, o None."""
if val is None:
return None
try:
return float(val)
except Exception:
return None
async def sync_recientes(ventana_min: int = 2) -> dict:
"""Job que corre cada minuto: sincroniza pacientes con recepción en los últimos `ventana_min` min."""
conn = get_connection()
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
conn.close()
wa_url = configs.get("whatsapp_url", "").rstrip("/")
wa_key = configs.get("whatsapp_api_key", "")
if not wa_url or not wa_key:
return {"ok": False, "error": "whatsapp_url o whatsapp_api_key no configurados"}
fb, ok, msg = get_firebird_from_config(configs)
if not ok:
return {"ok": False, "error": f"Firebird: {msg}"}
ok_pac, err_pac, rows_pac = fb.execute_query(_SQL_RECIENTES, (-ventana_min,))
ok_ex, _, rows_ex = fb.execute_query(_SQL_EXAMENES_VENTANA, (-ventana_min,))
fb.disconnect()
if not ok_pac:
return {"ok": False, "error": f"Query pacientes: {err_pac}"}
recientes = rows_pac or []
total_hoy = len(recientes)
if not recientes:
return {"ok": True, "total": 0, "created": 0, "updated": 0,
"skipped": 0, "errores": 0, "total_hoy": 0,
"ventana_min": ventana_min, "mensaje": "Sin recepciones en la ventana de tiempo"}
examenes_map: dict[str, list] = {}
if ok_ex and rows_ex:
for ex in rows_ex:
doc = str(ex.get("DOCIDENT") or "").strip()
if not doc:
continue
examenes_map.setdefault(doc, []).append({
"cod_examen": (ex.get("COD_EXAMEN") or "").strip(),
"nombre": (ex.get("NOM_EXAMEN") or "").strip(),
"cups": (ex.get("CUPS") or "").strip(),
"precio": _safe_num(ex.get("PRECIO")),
"recepcion_id": ex.get("IDRECEPCION"),
"hora": _fmt_hora_str(ex.get("HORAINICIORECEPCION")),
"diagnostico_cod": (ex.get("DIAG_PPAL") or "").strip(),
"diagnostico_nombre": (ex.get("DIAG_CONCEPTO") or "").strip(),
"medico_docidmedico": (ex.get("DOCIDMEDICO") or "").strip(),
"nit_empresa": (ex.get("NIT_EMPRESA") or "").strip(),
"nom_empresa": (ex.get("NOM_EMPRESA") or "").strip(),
"valor_total": _safe_num(ex.get("VALORTOTAL")),
})
ingest_url = f"{wa_url}/api/lab/ingest_paciente.php"
timeout = int(configs.get("api_timeout", 30))
resultado = await sync_todos(recientes, ingest_url, wa_key, timeout,
modo="upsert", examenes_map=examenes_map)
# Enriquecer detalle con exámenes, diagnóstico, empresa y valor para el log
for entry in resultado.get("detalle", []):
doc = entry.get("doc", "")
exams = examenes_map.get(doc, [])
if exams:
first = exams[0]
entry["diagnostico_cod"] = first.get("diagnostico_cod", "")
entry["diagnostico_nombre"] = first.get("diagnostico_nombre", "")
entry["nit_empresa"] = first.get("nit_empresa", "")
entry["nom_empresa"] = first.get("nom_empresa", "")
entry["valor_total"] = first.get("valor_total")
entry["examenes_det"] = [
{"cups": e.get("cups", ""), "nombre": e.get("nombre", ""), "precio": e.get("precio")}
for e in exams
]
if resultado["total"] > 0:
guardar_sync_log(resultado, None, origen="scheduler", modo="upsert")
return {
"ok": True,
"total": resultado["total"],
"created": resultado["created"],
"updated": resultado["updated"],
"skipped": resultado["skipped"],
"errores": resultado["errores"],
"total_hoy": total_hoy,
"ventana_min": ventana_min,
"detalle": resultado.get("detalle", []),
}