From 5cfdbeff02cb45c9dea8b39a5edf3aaf2425d7bf Mon Sep 17 00:00:00 2001 From: Abhishek Date: Thu, 30 Apr 2026 17:33:16 +0530 Subject: [PATCH] chore: new telephony config as default (#260) - Mark first new telephony config as default - Show telephony config in campaign details --- api/db/telephony_configuration_client.py | 11 ++- api/routes/campaign.py | 94 ++++++++++++++++++++-- ui/src/app/campaigns/[campaignId]/page.tsx | 15 ++++ ui/src/client/types.gen.ts | 38 ++++++--- 4 files changed, 139 insertions(+), 19 deletions(-) diff --git a/api/db/telephony_configuration_client.py b/api/db/telephony_configuration_client.py index f70148b..b35a684 100644 --- a/api/db/telephony_configuration_client.py +++ b/api/db/telephony_configuration_client.py @@ -7,7 +7,7 @@ Each row represents one provider account that an organization has connected from typing import Any, Dict, List, Optional -from sqlalchemy import update +from sqlalchemy import func, update from sqlalchemy.exc import IntegrityError from sqlalchemy.future import select @@ -140,7 +140,14 @@ class TelephonyConfigurationClient(BaseDBClient): ) async with self.async_session() as session: - if is_default_outbound: + existing_count = await session.scalar( + select(func.count(TelephonyConfigurationModel.id)).where( + TelephonyConfigurationModel.organization_id == organization_id, + ) + ) + if existing_count == 0: + is_default_outbound = True + elif is_default_outbound: await self._clear_default_outbound(session, organization_id) row = TelephonyConfigurationModel( diff --git a/api/routes/campaign.py b/api/routes/campaign.py index c91a65e..6078ab2 100644 --- a/api/routes/campaign.py +++ b/api/routes/campaign.py @@ -194,6 +194,8 @@ class CampaignResponse(BaseModel): total_queued_count: int = 0 parent_campaign_id: Optional[int] = None redialed_campaign_id: Optional[int] = None + telephony_configuration_id: Optional[int] = None + telephony_configuration_name: Optional[str] = None class CampaignsResponse(BaseModel): @@ -239,6 +241,7 @@ def _build_campaign_response( workflow_name: str, executed_count: int = 0, total_queued_count: int = 0, + telephony_configuration_name: Optional[str] = None, ) -> CampaignResponse: """Build a CampaignResponse from a campaign model.""" # Get retry_config from campaign or use defaults @@ -293,6 +296,8 @@ def _build_campaign_response( total_queued_count=total_queued_count, parent_campaign_id=parent_campaign_id, redialed_campaign_id=redialed_campaign_id, + telephony_configuration_id=campaign.telephony_configuration_id, + telephony_configuration_name=telephony_configuration_name, ) @@ -303,6 +308,22 @@ async def _get_campaign_stats(campaign_id: int) -> tuple[int, int]: return s.get("executed", 0), s.get("total", 0) +async def _get_telephony_configuration_name( + config_id: Optional[int], organization_id: int +) -> Optional[str]: + """Resolve the display name for a campaign's telephony configuration. + + Org-scoped lookup so a stale FK from another org (shouldn't happen, but + cheap to enforce) doesn't leak across tenants. + """ + if config_id is None: + return None + cfg = await db_client.get_telephony_configuration_for_org( + config_id, organization_id + ) + return cfg.name if cfg else None + + @router.post("/create") async def create_campaign( request: CreateCampaignRequest, @@ -412,7 +433,12 @@ async def create_campaign( telephony_configuration_id=telephony_configuration_id, ) - return _build_campaign_response(campaign, workflow_name) + cfg_name = await _get_telephony_configuration_name( + campaign.telephony_configuration_id, user.selected_organization_id + ) + return _build_campaign_response( + campaign, workflow_name, telephony_configuration_name=cfg_name + ) @router.get("/") @@ -433,12 +459,22 @@ async def get_campaigns( [c.id for c in campaigns] ) + # Build {config_id: name} map by fetching all configs for the org once, + # rather than one lookup per campaign. + org_configs = await db_client.list_telephony_configurations( + user.selected_organization_id + ) + config_name_map = {cfg.id: cfg.name for cfg in org_configs} + campaign_responses = [ _build_campaign_response( c, workflow_map.get(c.workflow_id, "Unknown"), executed_count=stats_map.get(c.id, {}).get("executed", 0), total_queued_count=stats_map.get(c.id, {}).get("total", 0), + telephony_configuration_name=config_name_map.get( + c.telephony_configuration_id + ), ) for c in campaigns ] @@ -459,8 +495,15 @@ async def get_campaign( workflow_name = await db_client.get_workflow_name(campaign.workflow_id, user.id) executed, total = await _get_campaign_stats(campaign.id) + cfg_name = await _get_telephony_configuration_name( + campaign.telephony_configuration_id, user.selected_organization_id + ) return _build_campaign_response( - campaign, workflow_name or "Unknown", executed, total + campaign, + workflow_name or "Unknown", + executed, + total, + telephony_configuration_name=cfg_name, ) @@ -502,8 +545,15 @@ async def start_campaign( workflow_name = await db_client.get_workflow_name(campaign.workflow_id, user.id) executed, total = await _get_campaign_stats(campaign.id) + cfg_name = await _get_telephony_configuration_name( + campaign.telephony_configuration_id, user.selected_organization_id + ) return _build_campaign_response( - campaign, workflow_name or "Unknown", executed, total + campaign, + workflow_name or "Unknown", + executed, + total, + telephony_configuration_name=cfg_name, ) @@ -529,8 +579,15 @@ async def pause_campaign( workflow_name = await db_client.get_workflow_name(campaign.workflow_id, user.id) executed, total = await _get_campaign_stats(campaign.id) + cfg_name = await _get_telephony_configuration_name( + campaign.telephony_configuration_id, user.selected_organization_id + ) return _build_campaign_response( - campaign, workflow_name or "Unknown", executed, total + campaign, + workflow_name or "Unknown", + executed, + total, + telephony_configuration_name=cfg_name, ) @@ -592,8 +649,15 @@ async def update_campaign( workflow_name = await db_client.get_workflow_name(campaign.workflow_id, user.id) executed, total = await _get_campaign_stats(campaign.id) + cfg_name = await _get_telephony_configuration_name( + campaign.telephony_configuration_id, user.selected_organization_id + ) return _build_campaign_response( - campaign, workflow_name or "Unknown", executed, total + campaign, + workflow_name or "Unknown", + executed, + total, + telephony_configuration_name=cfg_name, ) @@ -753,7 +817,16 @@ async def redial_campaign( workflow_name = await db_client.get_workflow_name(child.workflow_id, user.id) executed, total = await _get_campaign_stats(child.id) - return _build_campaign_response(child, workflow_name or "Unknown", executed, total) + cfg_name = await _get_telephony_configuration_name( + child.telephony_configuration_id, user.selected_organization_id + ) + return _build_campaign_response( + child, + workflow_name or "Unknown", + executed, + total, + telephony_configuration_name=cfg_name, + ) @router.post("/{campaign_id}/resume") @@ -794,8 +867,15 @@ async def resume_campaign( workflow_name = await db_client.get_workflow_name(campaign.workflow_id, user.id) executed, total = await _get_campaign_stats(campaign.id) + cfg_name = await _get_telephony_configuration_name( + campaign.telephony_configuration_id, user.selected_organization_id + ) return _build_campaign_response( - campaign, workflow_name or "Unknown", executed, total + campaign, + workflow_name or "Unknown", + executed, + total, + telephony_configuration_name=cfg_name, ) diff --git a/ui/src/app/campaigns/[campaignId]/page.tsx b/ui/src/app/campaigns/[campaignId]/page.tsx index 1c92885..8c26097 100644 --- a/ui/src/app/campaigns/[campaignId]/page.tsx +++ b/ui/src/app/campaigns/[campaignId]/page.tsx @@ -614,6 +614,21 @@ export default function CampaignDetailPage() { )} +
+
Telephony Configuration
+
+ {campaign.telephony_configuration_id ? ( + + ) : ( + Not assigned + )} +
+
State
{campaign.state}
diff --git a/ui/src/client/types.gen.ts b/ui/src/client/types.gen.ts index 961dcf6..d70528f 100644 --- a/ui/src/client/types.gen.ts +++ b/ui/src/client/types.gen.ts @@ -96,12 +96,6 @@ export type AriConfigurationRequest = { * websocket_client.conf connection name for externalMedia (e.g., dograh_staging) */ ws_client_name?: string; - /** - * Inbound Workflow Id - * - * Workflow ID for inbound calls - */ - inbound_workflow_id?: number | null; /** * From Numbers * @@ -136,10 +130,6 @@ export type AriConfigurationResponse = { * Ws Client Name */ ws_client_name?: string; - /** - * Inbound Workflow Id - */ - inbound_workflow_id?: number | null; /** * From Numbers */ @@ -483,6 +473,14 @@ export type CampaignResponse = { * Redialed Campaign Id */ redialed_campaign_id?: number | null; + /** + * Telephony Configuration Id + */ + telephony_configuration_id?: number | null; + /** + * Telephony Configuration Name + */ + telephony_configuration_name?: string | null; }; /** @@ -703,6 +701,12 @@ export type CloudonixConfigurationRequest = { * Cloudonix Domain ID */ domain_id: string; + /** + * Application Name + * + * Cloudonix Voice Application name. The application's url is updated when inbound workflows are attached to numbers on this domain. + */ + application_name: string; /** * From Numbers * @@ -729,6 +733,10 @@ export type CloudonixConfigurationResponse = { * Domain Id */ domain_id: string; + /** + * Application Name + */ + application_name: string; /** * From Numbers */ @@ -4114,6 +4122,12 @@ export type VobizConfigurationRequest = { * Vobiz Auth Token */ auth_token: string; + /** + * Application Id + * + * Vobiz Application ID. The application's answer_url is updated when inbound workflows are attached to numbers on this account. + */ + application_id: string; /** * From Numbers * @@ -4140,6 +4154,10 @@ export type VobizConfigurationResponse = { * Auth Token */ auth_token: string; + /** + * Application Id + */ + application_id: string; /** * From Numbers */