fix: number pool initialization in multi telephony setup

If there are multiple telephony configurations, the form number should be initialized from the campaigns given telephonic configuration rather than the organization default telephonic configuration.
This commit is contained in:
Abhishek Kumar 2026-05-08 14:48:53 +05:30
parent 81a363b06e
commit 6d93be3ef6
31 changed files with 1105 additions and 238 deletions

View file

@ -3,7 +3,7 @@
from datetime import datetime
from typing import Any, Dict, List, Optional
from sqlalchemy import Float, Integer, and_, cast, func
from sqlalchemy import Float, Integer, Text, and_, cast, func
from sqlalchemy.dialects.postgresql import JSONB
from api.db.models import WorkflowRunModel
@ -48,8 +48,10 @@ ATTRIBUTE_FIELD_MAPPING = {
"tokenUsage": "cost_info.total_cost_usd",
"runId": "id",
"workflowId": "workflow_id",
"campaignId": "campaign_id",
"callTags": "gathered_context.call_tags",
"phoneNumber": "initial_context.phone",
"callerNumber": "initial_context.caller_number",
"calledNumber": "initial_context.called_number",
}
@ -69,7 +71,8 @@ def apply_workflow_run_filters(
- runId: Filter by workflow run ID (exact match)
- workflowId: Filter by workflow ID (exact match)
- callTags: Filter by gathered_context.call_tags (array of strings)
- phoneNumber: Filter by initial_context.phone (text search)
- callerNumber: Filter by initial_context.caller_number (text search)
- calledNumber: Filter by initial_context.called_number (text search)
Args:
base_query: The base SQLAlchemy query to apply filters to
@ -119,6 +122,12 @@ def apply_workflow_run_filters(
WorkflowRunModel.workflow_id == value["value"]
)
elif filter_type == "number" and field == "campaign_id":
if value.get("value") is not None:
filter_conditions.append(
WorkflowRunModel.campaign_id == value["value"]
)
elif filter_type == "dateRange" and field == "created_at":
# Same as attribute-based dateRange
if value.get("from"):
@ -169,15 +178,30 @@ def apply_workflow_run_filters(
call_tags = gathered_context_jsonb.op("->")("call_tags")
filter_conditions.append(call_tags.op("@>")(func.cast(tags, JSONB)))
elif filter_type == "text" and field == "initial_context.phone":
# Filter by phone number (contains search)
elif filter_type == "text" and field == "initial_context.caller_number":
phone = value.get("value", "").strip()
if phone:
# Use ->> operator for compatibility with all PostgreSQL versions
# Cast ->> result to Text so .contains() emits LIKE,
# not the JSONB @> operator (the default for untyped exprs).
filter_conditions.append(
cast(WorkflowRunModel.initial_context, JSONB)
.op("->>")("phone")
.contains(phone)
cast(
cast(WorkflowRunModel.initial_context, JSONB).op("->>")(
"caller_number"
),
Text,
).contains(phone)
)
elif filter_type == "text" and field == "initial_context.called_number":
phone = value.get("value", "").strip()
if phone:
filter_conditions.append(
cast(
cast(WorkflowRunModel.initial_context, JSONB).op("->>")(
"called_number"
),
Text,
).contains(phone)
)
elif filter_type == "numberRange":

View file

@ -245,7 +245,7 @@ class OrganizationUsageClient(BaseDBClient):
limit: int = 50,
offset: int = 0,
filters: Optional[list[dict]] = None,
) -> tuple[list[dict], int]:
) -> tuple[list[dict], int, float, int]:
"""Get paginated workflow runs with usage for an organization."""
async with self.async_session() as session:
query = (
@ -267,7 +267,15 @@ class OrganizationUsageClient(BaseDBClient):
# Only allow specific filters for usage history endpoint
# This ensures security and prevents unexpected filter attributes
allowed_filters = {"duration", "dispositionCode", "phoneNumber"}
allowed_filters = {
"duration",
"dispositionCode",
"callerNumber",
"calledNumber",
"runId",
"workflowId",
"campaignId",
}
sanitized_filters = []
if filters:
@ -315,13 +323,15 @@ class OrganizationUsageClient(BaseDBClient):
total_tokens += dograh_tokens
total_duration_seconds += int(round(call_duration))
# Extract phone number from initial_context based on call_type.
ic = run.initial_context or {}
caller_number = ic.get("caller_number")
called_number = ic.get("called_number") or ic.get("phone_number")
# DEPRECATED: phone_number — use caller_number/called_number.
# Inbound runs only have caller_number/called_number; the
# caller_number is the customer. Outbound runs use the
# phone_number key written by the dispatchers.
ic = run.initial_context or {}
if run.call_type == "inbound":
phone_number = ic.get("caller_number")
phone_number = caller_number
else:
phone_number = ic.get("phone_number")
@ -341,6 +351,8 @@ class OrganizationUsageClient(BaseDBClient):
"recording_url": run.recording_url,
"transcript_url": run.transcript_url,
"phone_number": phone_number,
"caller_number": caller_number,
"called_number": called_number,
"call_type": run.call_type,
"disposition": disposition,
"initial_context": run.initial_context,
@ -355,6 +367,66 @@ class OrganizationUsageClient(BaseDBClient):
return formatted_runs, total_count, total_tokens, total_duration_seconds
async def get_usage_runs_for_report(
self,
organization_id: int,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
filters: Optional[list[dict]] = None,
) -> list:
"""Get filtered runs for an organization-scoped usage CSV report.
Mirrors the filter allowlist used by `get_usage_history`, but selects
only the columns needed by `build_run_report_csv` and returns every
matching run (no pagination).
"""
async with self.async_session() as session:
query = (
select(
WorkflowRunModel.id,
WorkflowRunModel.workflow_id,
WorkflowRunModel.definition_id,
WorkflowRunModel.campaign_id,
WorkflowRunModel.created_at,
WorkflowRunModel.initial_context,
WorkflowRunModel.gathered_context,
WorkflowRunModel.cost_info,
WorkflowRunModel.public_access_token,
)
.join(WorkflowModel, WorkflowRunModel.workflow_id == WorkflowModel.id)
.join(UserModel, WorkflowModel.user_id == UserModel.id)
.where(
UserModel.selected_organization_id == organization_id,
WorkflowRunModel.cost_info.isnot(None),
)
.order_by(WorkflowRunModel.created_at.desc())
)
if start_date:
query = query.where(WorkflowRunModel.created_at >= start_date)
if end_date:
query = query.where(WorkflowRunModel.created_at <= end_date)
allowed_filters = {
"duration",
"dispositionCode",
"callerNumber",
"calledNumber",
"runId",
"workflowId",
"campaignId",
}
sanitized_filters = []
if filters:
for filter_item in filters:
if filter_item.get("attribute") in allowed_filters:
sanitized_filters.append(filter_item)
query = apply_workflow_run_filters(query, sanitized_filters)
result = await session.execute(query)
return list(result.all())
async def get_daily_usage_breakdown(
self,
organization_id: int,