.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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
|
||||
Reference in New Issue
Block a user