121 lines
3.7 KiB
Python
121 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from realtime_voice_service.crm_client import _issue_service_token
|
|
|
|
|
|
LOGGER = logging.getLogger("uvicorn.error")
|
|
|
|
|
|
def _enabled() -> bool:
|
|
return os.getenv("CALLBACK_DISPATCH_ENABLED", "1").strip().lower() in {"1", "true", "yes"}
|
|
|
|
|
|
def _bridge_base_url() -> str:
|
|
return str(
|
|
os.getenv("ASTERISK_BRIDGE_SERVICE_URL", "http://asterisk-bridge-service:8000")
|
|
).rstrip("/")
|
|
|
|
|
|
def _secret() -> str:
|
|
return str(os.getenv("CRM_APP_TOKEN_SECRET", "")).strip()
|
|
|
|
|
|
def _request_headers() -> dict[str, str] | None:
|
|
secret = _secret()
|
|
if not secret:
|
|
return None
|
|
return {"Authorization": f"Bearer {_issue_service_token(secret)}"}
|
|
|
|
|
|
async def schedule_callback(
|
|
*,
|
|
phone: str,
|
|
in_minutes: int | None = None,
|
|
scheduled_for: str | None = None,
|
|
reason: str | None = None,
|
|
customer_name: str | None = None,
|
|
source_call_id: str | None = None,
|
|
source_interaction_id: str | None = None,
|
|
idempotency_key: str | None = None,
|
|
) -> dict[str, Any] | None:
|
|
if not _enabled():
|
|
LOGGER.info("callback dispatch disabled by env")
|
|
return None
|
|
|
|
headers = _request_headers()
|
|
if headers is None:
|
|
LOGGER.warning("callback client: CRM_APP_TOKEN_SECRET is not configured")
|
|
return None
|
|
|
|
payload: dict[str, Any] = {"phone": phone}
|
|
if in_minutes is not None:
|
|
payload["in_minutes"] = int(in_minutes)
|
|
if scheduled_for:
|
|
payload["scheduled_for"] = scheduled_for
|
|
if reason:
|
|
payload["reason"] = reason
|
|
if customer_name:
|
|
payload["customer_name"] = customer_name
|
|
if source_call_id:
|
|
payload["source_call_id"] = source_call_id
|
|
if source_interaction_id:
|
|
payload["source_interaction_id"] = source_interaction_id
|
|
if idempotency_key:
|
|
payload["idempotency_key"] = idempotency_key
|
|
|
|
url = f"{_bridge_base_url()}/asterisk/callbacks"
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8.0) as client:
|
|
resp = await client.post(url, json=payload, headers=headers)
|
|
if resp.status_code in (200, 201):
|
|
data = resp.json()
|
|
LOGGER.info(
|
|
"callback scheduled: job_id=%s phone=%s scheduled_for=%s",
|
|
data.get("job_id"),
|
|
data.get("phone_e164"),
|
|
data.get("scheduled_for"),
|
|
)
|
|
return data
|
|
if resp.status_code == 409:
|
|
LOGGER.info("callback already scheduled for phone=%s: %s", phone, resp.text[:200])
|
|
return {"status": "conflict", "detail": resp.text}
|
|
LOGGER.warning(
|
|
"callback schedule failed: status=%s body=%s",
|
|
resp.status_code,
|
|
resp.text[:300],
|
|
)
|
|
return None
|
|
except Exception:
|
|
LOGGER.exception("callback client: request failed phone=%s", phone)
|
|
return None
|
|
|
|
|
|
async def cancel_callback(*, job_id: str) -> dict[str, Any] | None:
|
|
if not _enabled():
|
|
return None
|
|
headers = _request_headers()
|
|
if headers is None:
|
|
return None
|
|
url = f"{_bridge_base_url()}/asterisk/callbacks/{job_id}"
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8.0) as client:
|
|
resp = await client.delete(url, headers=headers)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
LOGGER.warning(
|
|
"callback cancel failed: job_id=%s status=%s body=%s",
|
|
job_id,
|
|
resp.status_code,
|
|
resp.text[:300],
|
|
)
|
|
return None
|
|
except Exception:
|
|
LOGGER.exception("callback client: cancel failed job_id=%s", job_id)
|
|
return None
|