""" 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", []), }