diff --git a/backend/main.py b/backend/main.py index bf0f84a..bcf52cd 100644 --- a/backend/main.py +++ b/backend/main.py @@ -5,28 +5,14 @@ from fastapi.middleware.cors import CORSMiddleware from datetime import datetime import uvicorn -import os -from dotenv import load_dotenv - -# Cargar variables de entorno desde .env -load_dotenv() - import services from cache_manager import start_background_refresh, cache_stats app = FastAPI(title="Dashboard Leads API", version="1.0") - -# Orígenes CORS configurables (por defecto permite todo o el dominio configurado) -cors_env = os.getenv("CORS_ORIGINS", "*") -origins = [o.strip() for o in cors_env.split(",") if o.strip()] if cors_env != "*" else ["*"] - app.add_middleware( - CORSMiddleware, - allow_origins=origins if origins else ["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], + CORSMiddleware, allow_origins=["*"], allow_credentials=True, + allow_methods=["*"], allow_headers=["*"], ) @@ -49,7 +35,7 @@ def _startup(): except Exception as e: print(f"[startup] Precarga Otros falló: {e}") threading.Thread(target=_precarga_otros_bg, daemon=True).start() - start_background_refresh(services.refrescar_todo, interval=1800) + start_background_refresh(services.refrescar_todo, interval=1800) # 30 min (con horario) @app.get("/api/health") @@ -145,6 +131,164 @@ def get_alertas_dash(): raise HTTPException(status_code=500, detail=str(e)) +@app.get("/api/plantillas") +def get_plantillas(): + """Lista las plantillas de WhatsApp del inbox de Chatwoot (nombre + categoría).""" + import os, requests + CHATWOOT_URL = os.getenv("CHATWOOT_URL", "https://gestor.escueladerefrigeracion.edu.pe") + TOKEN = os.getenv("CHATWOOT_TOKEN", "4anazHvZnvKLtup8biu5Zuoh") + ACCOUNT_ID = os.getenv("CHATWOOT_ACCOUNT_ID", "1") + INBOX_ID = os.getenv("CHATWOOT_INBOX_ID", "8") + headers = {"api_access_token": TOKEN, "Content-Type": "application/json"} + try: + r = requests.get(f"{CHATWOOT_URL}/api/v1/accounts/{ACCOUNT_ID}/inboxes/{INBOX_ID}", + headers=headers, timeout=15) + r.raise_for_status() + inbox = r.json() + tpls = inbox.get("message_templates") or [] + out = [] + for t in tpls: + # El texto está en el componente tipo BODY (components[].text). + texto = "" + for comp in (t.get("components") or []): + if str(comp.get("type", "")).upper() == "BODY": + texto = comp.get("text", "") or "" + break + out.append({ + "nombre": t.get("name", ""), + "categoria": t.get("category", ""), + "texto": texto, + }) + out.sort(key=lambda x: x["nombre"]) + return {"plantillas": out} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/plantillas/enviar") +def post_plantillas_enviar(payload: dict = Body(...)): + """Envía una plantilla de WhatsApp (Chatwoot) a una lista de números. + payload: {nombre, categoria, idioma, texto, telefonos:[...]}. + Solo se envía a números peruanos con código 51 (formato 51XXXXXXXXX).""" + import os, requests, time + CHATWOOT_URL = os.getenv("CHATWOOT_URL", "https://gestor.escueladerefrigeracion.edu.pe") + TOKEN = os.getenv("CHATWOOT_TOKEN", "4anazHvZnvKLtup8biu5Zuoh") + ACCOUNT_ID = os.getenv("CHATWOOT_ACCOUNT_ID", "1") + INBOX_ID = int(os.getenv("CHATWOOT_INBOX_ID", "8")) + headers = {"api_access_token": TOKEN, "Content-Type": "application/json"} + + nombre = payload.get("nombre", "") + categoria = payload.get("categoria", "MARKETING") + idioma = payload.get("idioma", "es_PE") + texto = payload.get("texto", "") + telefonos_raw = payload.get("telefonos", []) or [] + + def ok(s): return s in (200, 201) + + def normaliza(v): + s = str(v or "").strip() + if s.endswith(".0"): s = s[:-2] + for x in ("+", " ", "-", "(", ")"): s = s.replace(x, "") + return s + + # Validar: debe empezar con 51 y tener 11 dígitos (51 + 9) + validos, invalidos = [], [] + for t in telefonos_raw: + s = normaliza(t) + if s.isdigit() and s.startswith("51") and len(s) == 11: + validos.append(s) + else: + invalidos.append(str(t)) + + def chat_id(tel): + tel_p = "+" + tel + try: + r = requests.get(f"{CHATWOOT_URL}/api/v1/accounts/{ACCOUNT_ID}/contacts/search", + headers=headers, params={"q": tel_p}, timeout=20) + data = r.json() + except Exception: + return None + cid = None + if ok(r.status_code) and data.get("payload"): + cid = data["payload"][0]["id"] + else: + rc = requests.post(f"{CHATWOOT_URL}/api/v1/accounts/{ACCOUNT_ID}/contacts", + headers=headers, json={"phone_number": tel_p, "inbox_id": INBOX_ID}, timeout=20) + if ok(rc.status_code): + cid = rc.json()["payload"]["contact"]["id"] + else: + return None + rconv = requests.get(f"{CHATWOOT_URL}/api/v1/accounts/{ACCOUNT_ID}/contacts/{cid}/conversations", + headers=headers, timeout=20) + for conv in rconv.json().get("payload", []): + if conv.get("inbox_id") == INBOX_ID: + return conv["id"] + rn = requests.post(f"{CHATWOOT_URL}/api/v1/accounts/{ACCOUNT_ID}/conversations", + headers=headers, json={"source_id": str(cid), "inbox_id": INBOX_ID, "contact_id": cid}, timeout=20) + return rn.json().get("id") if ok(rn.status_code) else None + + enviados, fallidos = 0, [] + for tel in validos: + cid = chat_id(tel) + if not cid: + fallidos.append(tel); continue + body = {"content": texto, "private": False, + "template_params": {"name": nombre, "category": categoria, + "language": idioma, "processed_params": {}}} + try: + r = requests.post(f"{CHATWOOT_URL}/api/v1/accounts/{ACCOUNT_ID}/conversations/{cid}/messages", + headers=headers, json=body, timeout=30) + if ok(r.status_code): + enviados += 1 + else: + fallidos.append(tel) + except Exception: + fallidos.append(tel) + time.sleep(1) + + return {"status": "ok", "enviados": enviados, + "fallidos": fallidos, "invalidos": invalidos, + "total": len(telefonos_raw)} + + +@app.get("/api/plantillas/campanias") +def get_plantillas_campanias(): + """Lista las campañas (código + frase) para el checklist.""" + try: + return services.plantillas_campanias() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/plantillas/asesores") +def get_plantillas_asesores(): + """Lista los asesores (para el checklist de filtro).""" + try: + return services.plantillas_asesores() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/plantillas/estados") +def get_plantillas_estados(): + """Lista los estados/objeción (para el checklist de filtro).""" + try: + return services.plantillas_estados() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/plantillas/base") +def post_plantillas_base(payload: dict = Body(...)): + """Devuelve la base de números (teléfono + 2 fechas + asesor + estado) de los + códigos elegidos, filtrando por asesores ('__VACIO__' = sin asesor) y estados.""" + try: + return services.plantillas_base_por_codigos( + payload.get("codigos", []), payload.get("asesores"), payload.get("estados")) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + @app.get("/api/roas") def get_roas( ano: str = Query("TODOS"),