dograh/api/schemas/telephony_config.py
stefandsl 533a873ab7 feat: add 3CX telephony provider with Asterisk ARA provisioning
Registers a new `three_cx` provider that fronts a 3CX cloud PBX through
an intermediate Asterisk bridge. Save-time hook writes the matching
PJSIP endpoint/aor/auth/registration and dialplan rows to the Asterisk
Realtime Architecture Postgres (via `ASTERISK_ARA_DSN`), so a config
change in the Dograh UI is immediately picked up by Asterisk without a
`pjsip reload`. Strip prefix is honoured at the dialplan layer.

Inbound calls are matched back to a configuration by the dialled
extension (`account_id_credential_field="extension"`), allowing one
shared Asterisk to serve multiple Dograh orgs without collision.

Touches `providers/__init__.py` and `schemas/telephony_config.py` only
— per `providers/AGENTS.md`. Provider/transport/strategies are
duplicated from `ari/` rather than imported, in line with the
cross-provider-import prohibition. See `docs/providers/three_cx.md` for
the Asterisk ARA setup runbook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:07:50 +02:00

159 lines
5 KiB
Python

"""Telephony configuration schemas.
Per-provider request/response classes live next to their providers in
``api/services/telephony/providers/<name>/config.py``. This module re-exports
them and assembles the discriminated union used by API routes.
Adding a new provider requires adding one import here.
"""
from datetime import datetime
from typing import Annotated, List, Optional, Union
from pydantic import BaseModel, ConfigDict, Field
from api.services.telephony.providers.ari.config import (
ARIConfigurationRequest,
ARIConfigurationResponse,
)
from api.services.telephony.providers.cloudonix.config import (
CloudonixConfigurationRequest,
CloudonixConfigurationResponse,
)
from api.services.telephony.providers.plivo.config import (
PlivoConfigurationRequest,
PlivoConfigurationResponse,
)
from api.services.telephony.providers.telnyx.config import (
TelnyxConfigurationRequest,
TelnyxConfigurationResponse,
)
from api.services.telephony.providers.three_cx.config import (
ThreeCxConfigurationRequest,
ThreeCxConfigurationResponse,
)
from api.services.telephony.providers.twilio.config import (
TwilioConfigurationRequest,
TwilioConfigurationResponse,
)
from api.services.telephony.providers.vobiz.config import (
VobizConfigurationRequest,
VobizConfigurationResponse,
)
from api.services.telephony.providers.vonage.config import (
VonageConfigurationRequest,
VonageConfigurationResponse,
)
# Discriminated union for incoming save requests. Pydantic dispatches on the
# ``provider`` Literal field of each request class. Replaces the manual
# if/elif chains that used to live in routes/organization.py.
TelephonyConfigRequest = Annotated[
Union[
ARIConfigurationRequest,
CloudonixConfigurationRequest,
PlivoConfigurationRequest,
TelnyxConfigurationRequest,
ThreeCxConfigurationRequest,
TwilioConfigurationRequest,
VobizConfigurationRequest,
VonageConfigurationRequest,
],
Field(discriminator="provider"),
]
class TelephonyConfigurationResponse(BaseModel):
"""Top-level telephony configuration response.
Keeps the per-provider field shape that the UI client depends on. When
the UI moves to metadata-driven forms, this can be replaced with a
flat discriminated union.
"""
twilio: Optional[TwilioConfigurationResponse] = None
plivo: Optional[PlivoConfigurationResponse] = None
vonage: Optional[VonageConfigurationResponse] = None
vobiz: Optional[VobizConfigurationResponse] = None
cloudonix: Optional[CloudonixConfigurationResponse] = None
ari: Optional[ARIConfigurationResponse] = None
telnyx: Optional[TelnyxConfigurationResponse] = None
three_cx: Optional[ThreeCxConfigurationResponse] = None
# ---------------------------------------------------------------------------
# Multi-config CRUD schemas
# ---------------------------------------------------------------------------
class TelephonyConfigurationCreateRequest(BaseModel):
"""Body for ``POST /telephony-configs``.
``config`` carries the provider-specific credential fields (the same
discriminated union used by the legacy single-config endpoint). Any
``from_numbers`` on the inner config are ignored — phone numbers are
managed via the dedicated phone-numbers endpoints.
"""
name: str = Field(..., min_length=1, max_length=64)
is_default_outbound: bool = False
config: TelephonyConfigRequest
class TelephonyConfigurationUpdateRequest(BaseModel):
"""Body for ``PUT /telephony-configs/{id}``. Partial update."""
name: Optional[str] = Field(default=None, min_length=1, max_length=64)
config: Optional[TelephonyConfigRequest] = None
class TelephonyConfigurationListItem(BaseModel):
"""One row in ``GET /telephony-configs``."""
model_config = ConfigDict(from_attributes=True)
id: int
name: str
provider: str
is_default_outbound: bool
phone_number_count: int = 0
created_at: datetime
updated_at: datetime
class TelephonyConfigurationDetail(BaseModel):
"""Body of ``GET /telephony-configs/{id}`` — credentials are masked."""
id: int
name: str
provider: str
is_default_outbound: bool
credentials: dict
created_at: datetime
updated_at: datetime
class TelephonyConfigurationListResponse(BaseModel):
configurations: List[TelephonyConfigurationListItem]
__all__ = [
"ARIConfigurationRequest",
"ARIConfigurationResponse",
"CloudonixConfigurationRequest",
"CloudonixConfigurationResponse",
"PlivoConfigurationRequest",
"PlivoConfigurationResponse",
"TelephonyConfigRequest",
"TelephonyConfigurationResponse",
"TelnyxConfigurationRequest",
"TelnyxConfigurationResponse",
"ThreeCxConfigurationRequest",
"ThreeCxConfigurationResponse",
"TwilioConfigurationRequest",
"TwilioConfigurationResponse",
"VobizConfigurationRequest",
"VobizConfigurationResponse",
"VonageConfigurationRequest",
"VonageConfigurationResponse",
]