import json as json_lib import httpx from datetime import datetime from fastapi import APIRouter, Request, Form, Depends from fastapi.responses import JSONResponse from app.database import get_connection from app.auth import get_current_user from app.services.firebird_service import get_firebird_from_config from app.services.json_generator import generar_rda_paciente from app.services.api_client import get_tns_token, TNS_BASE from app.routes.contratos import load_contrato_map, load_excluded_set, load_sin_contrato_set from app.utils.activity import log_activity, get_ip router = APIRouter(prefix="/transaccion", tags=["transaccion"]) def _cfg_and_fb(): conn = get_connection() cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()} conn.close() return cfg def _query_rows(cfg, query_text, factura, fecha_inicio, fecha_fin): import re as _re fb, ok, msg = get_firebird_from_config(cfg) if not ok: return None, msg, None nums = _re.findall(r'\d+', factura or "") num_val = int(nums[-1]) if nums else 0 prefix = _re.sub(r'[\d\s]', '', factura or "").strip().upper() # Solo agregar los parámetros que la query realmente usa params = {} if ":fecha_ini" in query_text: params["fecha_ini"] = f"{fecha_inicio} 00:00:00" if ":fecha_fin" in query_text: params["fecha_fin"] = f"{fecha_fin} 23:59:59" if ":num_factura" in query_text: params["num_factura"] = num_val if ":prefijo" in query_text: params["prefijo"] = prefix if ":ps_numero" in query_text: params["ps_numero"] = num_val ok2, err, rows = fb.execute_query(query_text, params) fb.disconnect() if not ok2: return None, err, None # Filtro Python por prefijo (PREFIJO o PS_PREFIJO) if prefix and rows: if "PS_PREFIJO" in rows[0]: rows = [r for r in rows if str(r.get("PS_PREFIJO") or "").strip().upper() == prefix] elif "PREFIJO" in rows[0]: rows = [r for r in rows if str(r.get("PREFIJO") or "").strip().upper() == prefix] return rows, None, fb def _is_preserv(rows: list) -> bool: return bool(rows) and "ID_PS" in rows[0] def _agrupar(rows: list) -> dict: """Agrupa por ID_PS (pre-servicios) o IDRECEPCION (regulares).""" from collections import defaultdict grupos = defaultdict(list) key_field = "ID_PS" if _is_preserv(rows) else "IDRECEPCION" for row in rows: grupos[row.get(key_field)].append(dict(row)) return dict(grupos) def _build_rda(grupo_rows: list, cfg: dict, contrato_map: dict, sin_contrato_set_val: set) -> tuple: """Devuelve (rda_json, numero_override, prefijo_override, factura_display).""" prof_def = cfg.get("profesional_default", "") esp_def = cfg.get("especialidad_default", "") remis_def = cfg.get("remisionante_default", "00") prefijo_def = cfg.get("prefijo_tns_default", "00") if _is_preserv(grupo_rows): ps_prefijo = str(grupo_rows[0].get("PS_PREFIJO") or "SC").strip() ps_numero = str(grupo_rows[0].get("PS_NUMERO") or "").strip() prefijo_override = "00" if ps_prefijo == "RCXC" else ps_prefijo rda = generar_rda_paciente( grupo_rows, prof_def, esp_def, remis_def, prefijo_def, numero_override=ps_numero, contrato_map=contrato_map, prefijo_override=prefijo_override, sin_contrato_set=sin_contrato_set_val, ) factura_display = f"{ps_prefijo}-{ps_numero.zfill(5)}" return rda, ps_numero, prefijo_override, factura_display else: num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip() rda = generar_rda_paciente( grupo_rows, prof_def, esp_def, remis_def, prefijo_def, numero_override=num_fac, contrato_map=contrato_map, sin_contrato_set=sin_contrato_set_val, ) return rda, num_fac, "", str(grupo_rows[0].get("NUM_FACTURA", "")) def _is_sent(idrecepcion: int) -> dict: """Returns the latest envio record for this idrecepcion, or None.""" conn = get_connection() row = conn.execute( "SELECT status, mensaje_tns, created_at FROM envios WHERE idrecepcion=? AND tipo='transaccion' ORDER BY id DESC LIMIT 1", (idrecepcion,) ).fetchone() conn.close() if row: return {"status": row["status"], "mensaje": row["mensaje_tns"], "at": row["created_at"]} return None @router.get("") async def transaccion_page(request: Request, user: dict = Depends(get_current_user)): conn = get_connection() queries = conn.execute( "SELECT * FROM queries WHERE query_type = 'transaccion' ORDER BY name" ).fetchall() configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} conn.close() return request.app.state.templates.TemplateResponse("transaccion.html", { "request": request, "user": user, "queries": queries, "configs": configs, }) @router.post("/preview") async def preview_transaccion( request: Request, user: dict = Depends(get_current_user), query_id: int = Form(...), factura: str = Form(""), fecha_inicio: str = Form(...), fecha_fin: str = Form(...), contrato: str = Form(""), ): conn = get_connection() q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()} conn.close() if not q: return JSONResponse({"success": False, "message": "Consulta no encontrada"}) rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin) if rows is None: return JSONResponse({"success": False, "message": err}) if not rows: return JSONResponse({"success": False, "message": "Sin datos"}) excluded = load_excluded_set() contrato_map = load_contrato_map() sin_contrato_set_val = load_sin_contrato_set() grupos_all = _agrupar(rows) if contrato: grupos = {k: v for k, v in grupos_all.items() if str(v[0].get("CODCONTRATO") or "").strip() == contrato.strip()} else: grupos = {k: v for k, v in grupos_all.items() if str(v[0].get("CODCONTRATO") or "").strip() not in excluded} items = [] for key, grupo_rows in grupos.items(): enviado = _is_sent(key) cod_c = str(grupo_rows[0].get("CODCONTRATO") or "").strip() is_excluded = cod_c in excluded rda, _, _, factura_display = _build_rda(grupo_rows, cfg, contrato_map, sin_contrato_set_val) items.append({ "idrecepcion": key, "factura": factura_display, "paciente": grupo_rows[0].get("COD_PACIENTE", ""), "contrato": grupo_rows[0].get("CODCONTRATO", ""), "examenes": [r.get("COD_EXAMEN", "") for r in grupo_rows], "valor": float(grupo_rows[0].get("VALORTOTAL") or 0), "enviado": enviado, "excluido": is_excluded, "json": rda, }) pendientes = sum(1 for i in items if not i["enviado"] and not i["excluido"]) enviados_ok = sum(1 for i in items if i["enviado"] and i["enviado"]["status"] == "success") enviados_err = sum(1 for i in items if i["enviado"] and i["enviado"]["status"] == "error") excluidos = sum(1 for i in items if i["excluido"]) return JSONResponse({ "success": True, "total": len(items), "pendientes": pendientes, "enviados_ok": enviados_ok, "enviados_err": enviados_err, "excluidos": excluidos, "items": items, }) @router.post("/send-one") async def send_one( request: Request, user: dict = Depends(get_current_user), idrecepcion: int = Form(...), query_id: int = Form(...), factura: str = Form(""), fecha_inicio: str = Form(...), fecha_fin: str = Form(...), contrato: str = Form(""), ): conn = get_connection() q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()} conn.close() rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin) if rows is None: return JSONResponse({"success": False, "message": err}) grupos = _agrupar(rows) grupo_rows = grupos.get(idrecepcion) if not grupo_rows: return JSONResponse({"success": False, "message": f"Registro {idrecepcion} no encontrado"}) cod_c = str(grupo_rows[0].get("CODCONTRATO") or "").strip() if cod_c in load_excluded_set(): return JSONResponse({"success": False, "message": f"Contrato {cod_c} está excluido del envío TNS"}) token, token_err = await get_tns_token( cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "") ) if not token: return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"}) headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"} api_sucursal = cfg.get("api_sucursal", "") or "00" endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}" rda_json, numero_override_one, _, factura_display = _build_rda( grupo_rows, cfg, load_contrato_map(), load_sin_contrato_set() ) raw_resp = "" ok_rda = False msg_tns = "" try: async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client: r = await client.post(endpoint, json=rda_json, headers=headers) raw_resp = r.text try: data = r.json() ok_rda = bool(data.get("status") or (data.get("data") or {}).get("success", False)) msg_tns = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300]) except Exception: ok_rda = r.status_code < 300 msg_tns = raw_resp[:300] except Exception as ex: msg_tns = str(ex) conn = get_connection() conn.execute(""" INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato, fecha_inicio, fecha_fin, pacientes_count, servicios_count, status, json_enviado, respuesta_api, mensaje_tns, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, ( user["user_id"], "transaccion", factura_display, idrecepcion, str(grupo_rows[0].get("CODCONTRATO", "")), fecha_inicio, fecha_fin, 1, len(grupo_rows), "success" if ok_rda else "error", json_lib.dumps(rda_json, ensure_ascii=False)[:10000], raw_resp[:2000], msg_tns, datetime.now().isoformat(), )) conn.commit() conn.close() log_activity(user["user_id"], user["username"], "rda_enviado", f"Factura {factura_display} | Contrato {grupo_rows[0].get('CODCONTRATO','')} | {'OK' if ok_rda else 'ERROR: '+msg_tns[:80]}", get_ip(request)) return JSONResponse({"success": ok_rda, "message": msg_tns, "raw_tns": raw_resp, "idrecepcion": idrecepcion}) @router.post("/send") async def send_transaccion( request: Request, user: dict = Depends(get_current_user), query_id: int = Form(...), factura: str = Form(""), fecha_inicio: str = Form(...), fecha_fin: str = Form(...), solo_pendientes: str = Form("0"), contrato: str = Form(""), ): conn = get_connection() q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()} conn.close() rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin) if rows is None: return JSONResponse({"success": False, "message": err}) if not rows: return JSONResponse({"success": False, "message": "Sin datos"}) excluded = load_excluded_set() contrato_map = load_contrato_map() sin_contrato_set_val = load_sin_contrato_set() grupos_all = _agrupar(rows) if contrato: grupos = {k: v for k, v in grupos_all.items() if str(v[0].get("CODCONTRATO") or "").strip() == contrato.strip()} else: grupos = {k: v for k, v in grupos_all.items() if str(v[0].get("CODCONTRATO") or "").strip() not in excluded} if solo_pendientes == "1": grupos = {k: v for k, v in grupos.items() if not _is_sent(k)} excluidos_count = len(grupos_all) - len(grupos) token, token_err = await get_tns_token( cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "") ) if not token: return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"}) headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"} api_sucursal = cfg.get("api_sucursal", "") or "00" endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}" resultados = [] async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client: for key, grupo_rows in grupos.items(): rda_json, _, _, factura_display = _build_rda(grupo_rows, cfg, contrato_map, sin_contrato_set_val) raw_resp = "" ok_rda = False msg_tns = "" try: r = await client.post(endpoint, json=rda_json, headers=headers) raw_resp = r.text try: data = r.json() ok_rda = bool(data.get("status") or (data.get("data") or {}).get("success", False)) msg_tns = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300]) except Exception: ok_rda = r.status_code < 300 msg_tns = raw_resp[:300] except Exception as ex: msg_tns = str(ex) conn = get_connection() conn.execute(""" INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato, fecha_inicio, fecha_fin, pacientes_count, servicios_count, status, json_enviado, respuesta_api, mensaje_tns, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, ( user["user_id"], "transaccion", factura_display, key, str(grupo_rows[0].get("CODCONTRATO", "")), fecha_inicio, fecha_fin, 1, len(grupo_rows), "success" if ok_rda else "error", json_lib.dumps(rda_json, ensure_ascii=False)[:10000], raw_resp[:2000], msg_tns, datetime.now().isoformat(), )) conn.commit() conn.close() resultados.append({"idrecepcion": key, "success": ok_rda, "msg": msg_tns}) ok_count = sum(1 for r in resultados if r["success"]) err_count = len(resultados) - ok_count log_activity(user["user_id"], user["username"], "rda_masivo", f"Enviados: {ok_count} OK, {err_count} errores, {excluidos_count} excluidos | {fecha_inicio} → {fecha_fin}", get_ip(request)) return JSONResponse({ "success": err_count == 0, "total_enviados": ok_count, "total_errores": err_count, "total_excluidos": excluidos_count, "resultados": resultados, }) @router.post("/send-direct") async def send_direct( request: Request, user: dict = Depends(get_current_user), prefijo: str = Form(...), numero: str = Form(...), query_id: int = Form(...), ): conn = get_connection() q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()} conn.close() if not q: return JSONResponse({"success": False, "message": "Consulta no encontrada"}) prefijo = prefijo.strip().upper() numero = numero.strip() factura_str = f"{prefijo}{numero}" rows, err, _ = _query_rows(cfg, q["query_text"], factura_str, "2000-01-01", "2099-12-31") if rows is None: return JSONResponse({"success": False, "message": err}) if not rows: return JSONResponse({"success": False, "message": f"No se encontró factura {factura_str} en Firebird"}) grupos = _agrupar(rows) if not grupos: return JSONResponse({"success": False, "message": "Sin datos agrupables"}) excluded = load_excluded_set() primer_contrato = str(list(grupos.values())[0][0].get("CODCONTRATO") or "").strip() if primer_contrato in excluded: return JSONResponse({"success": False, "message": f"Contrato {primer_contrato} está excluido del envío TNS"}) token, token_err = await get_tns_token( cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "") ) if not token: return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"}) headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"} api_sucursal = cfg.get("api_sucursal", "") or "00" endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}" contrato_map = load_contrato_map() sin_contrato_set_val = load_sin_contrato_set() resultados = [] async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client: for key, grupo_rows in grupos.items(): rda_json, _, _, factura_display = _build_rda(grupo_rows, cfg, contrato_map, sin_contrato_set_val) raw_resp = "" ok_rda = False msg_tns = "" try: r = await client.post(endpoint, json=rda_json, headers=headers) raw_resp = r.text try: data = r.json() ok_rda = bool(data.get("status") or (data.get("data") or {}).get("success", False)) msg_tns = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300]) except Exception: ok_rda = r.status_code < 300 msg_tns = raw_resp[:300] except Exception as ex: msg_tns = str(ex) conn = get_connection() conn.execute(""" INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato, fecha_inicio, fecha_fin, pacientes_count, servicios_count, status, json_enviado, respuesta_api, mensaje_tns, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, ( user["user_id"], "transaccion", factura_display, key, str(grupo_rows[0].get("CODCONTRATO", "")), "2000-01-01", "2099-12-31", 1, len(grupo_rows), "success" if ok_rda else "error", json_lib.dumps(rda_json, ensure_ascii=False)[:10000], raw_resp[:2000], msg_tns, datetime.now().isoformat(), )) conn.commit() conn.close() resultados.append({ "idrecepcion": key, "factura": factura_display, "success": ok_rda, "message": msg_tns, "raw_tns": raw_resp[:1000], "json": rda_json, }) ok_count = sum(1 for r in resultados if r["success"]) log_activity(user["user_id"], user["username"], "rda_directo", f"Factura {factura_str} | {'OK' if ok_count > 0 else 'ERROR'}", get_ip(request)) return JSONResponse({ "success": ok_count > 0, "total": len(resultados), "resultados": resultados, })