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