Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/ai/voice/agents/breeze_buddy/managers/calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ async def _get_lead_config(lead: LeadCallTracker) -> Optional[CallExecutionConfi
),
None,
)
if not config:
# Step 3: fall back to the default config (template IS NULL) for this merchant
config = next(
(c for c in configs if c.template is None),
None,
)
if not config:
Comment on lines +109 to 114
logger.warning(
f"No call execution config found for template: {lead.template} (template_id={lead.template_id})"
Expand Down
6 changes: 6 additions & 0 deletions app/api/routers/breeze_buddy/leads/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@
)
req = req.model_copy(update={"merchant_id": template.merchant_id})
if not template and req.template:
template = await get_template_by_merchant(

Check warning on line 177 in app/api/routers/breeze_buddy/leads/handlers.py

View workflow job for this annotation

GitHub Actions / build

Pyrefly deprecated

`app.database.accessor.breeze_buddy.template.get_template_by_merchant` is deprecated Name-based template lookup. Pass template_id and use get_template_by_id_with_fallback() instead.
req.reseller_id, req.merchant_id, req.template
)

Expand Down Expand Up @@ -251,6 +251,12 @@
None,
)

if not config:
# Step 3: fall back to the default config (template IS NULL) for this merchant
config = next(
(c for c in call_execution_configs if c.template is None),
None,
)
if not config:
Comment on lines +255 to 260
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
Expand Down
8 changes: 5 additions & 3 deletions app/database/accessor/breeze_buddy/call_execution_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async def create_call_execution_config(
max_retry: int,
calling_provider: CallProvider,
reseller_id: str,
template: str,
template: Optional[str],
merchant_id: Optional[str],
enable_international_call: bool,
enable_calling: bool = True,
Expand All @@ -72,7 +72,9 @@ async def create_call_execution_config(
pre_checks: Optional[List[Any]] = None,
telephony_config: Optional[TelephonyConfig] = None,
) -> Optional[CallExecutionConfig]:
"""Create a new call execution config record (upsert based on merchant_id + template)."""
"""Create a new call execution config record (upsert based on partial unique indexes).
When template is None the row acts as the DEFAULT config for the merchant/reseller.
"""
logger.info(f"Creating call execution config for reseller ID: {reseller_id}")

try:
Expand Down Expand Up @@ -193,7 +195,7 @@ async def get_all_call_execution_configs() -> List[CallExecutionConfig]:

async def update_call_execution_config(
reseller_id: str,
template: str,
template: Optional[str] = None,
merchant_id: Optional[str] = None,
initial_offset: Optional[int] = None,
retry_offset: Optional[int] = None,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- Migration 029: Allow NULL template in call_execution_config for default configs
--
-- Problem: Every template required its own call_execution_config row, even when
-- all timing/settings were identical.
--
-- Solution: A row with template IS NULL acts as the DEFAULT config for a
-- reseller/merchant. The resolution chain is:
-- 1. Exact template_id match
-- 2. Exact template name match
-- 3. Default config (template IS NULL) <-- NEW
--
-- Changes:
-- 1. Make the `template` column nullable
-- 2. Drop the old non-partial unique constraint (doesn't handle NULLs correctly)
-- 3. Replace with partial unique indexes that cover all four cases:
-- a) template IS NOT NULL, merchant IS NOT NULL → unique on (merchant_id, template)
-- b) template IS NOT NULL, merchant IS NULL → unique on (reseller_id, template)
-- c) template IS NULL, merchant IS NOT NULL → unique on (reseller_id, merchant_id) [DEFAULT per merchant]
-- d) template IS NULL, merchant IS NULL → unique on (reseller_id) [DEFAULT for reseller]

-- Step 1: Make template nullable
ALTER TABLE call_execution_config
ALTER COLUMN "template" DROP NOT NULL;

-- Step 2: Drop the old non-null-aware unique constraint
ALTER TABLE call_execution_config
DROP CONSTRAINT IF EXISTS uq_call_execution_config_merchant_template;

-- Step 3a: Per-template, per-merchant uniqueness (matches old behaviour)
CREATE UNIQUE INDEX IF NOT EXISTS uq_call_execution_config_template_merchant
ON call_execution_config (merchant_id, template)
WHERE template IS NOT NULL AND merchant_id IS NOT NULL;

-- Step 3b: Per-template, no merchant (reseller-level) uniqueness
-- (replaces the old uq_call_execution_config_reseller_template_null_merchant)
DROP INDEX IF EXISTS uq_call_execution_config_reseller_template_null_merchant;
CREATE UNIQUE INDEX IF NOT EXISTS uq_call_execution_config_template_null_merchant
ON call_execution_config (reseller_id, template)
WHERE template IS NOT NULL AND merchant_id IS NULL;

-- Step 3c: Default config per merchant (template IS NULL, merchant IS NOT NULL)
CREATE UNIQUE INDEX IF NOT EXISTS uq_call_execution_config_default_with_merchant
ON call_execution_config (reseller_id, merchant_id)
WHERE template IS NULL AND merchant_id IS NOT NULL;

-- Step 3d: Default config for reseller (template IS NULL, merchant IS NULL)
CREATE UNIQUE INDEX IF NOT EXISTS uq_call_execution_config_default_no_merchant
ON call_execution_config (reseller_id)
WHERE template IS NULL AND merchant_id IS NULL;
44 changes: 34 additions & 10 deletions app/database/queries/breeze_buddy/call_execution_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@
CALL_EXECUTION_CONFIG_TABLE = "call_execution_config"


def _conflict_target(template: Optional[str], merchant_id: Optional[str]) -> str:
"""
Return the ON CONFLICT partial-index target for an upsert into call_execution_config.

The migration created four partial unique indexes that cover all combinations of
nullable template and nullable merchant_id — this helper picks the right one.
"""
if template is None:
return (
"(reseller_id, merchant_id) WHERE template IS NULL AND merchant_id IS NOT NULL"
if merchant_id is not None
else "(reseller_id) WHERE template IS NULL AND merchant_id IS NULL"
)
return (
"(merchant_id, template) WHERE template IS NOT NULL AND merchant_id IS NOT NULL"
if merchant_id is not None
else "(reseller_id, template) WHERE template IS NOT NULL AND merchant_id IS NULL"
)


# Call execution config queries
def insert_call_execution_config_query(
id: str,
Expand All @@ -21,7 +41,7 @@ def insert_call_execution_config_query(
max_retry: int,
calling_provider: CallProvider,
reseller_id: str,
template: str,
template: Optional[str],
merchant_id: Optional[str],
enable_international_call: bool,
enable_calling: bool = True,
Expand All @@ -43,11 +63,11 @@ def insert_call_execution_config_query(
) -> Tuple[str, List[Any]]:
"""
Generate query to insert call execution config record.
Uses ON CONFLICT to upsert based on (merchant_id, template) to prevent duplicates.
Uses ON CONFLICT to upsert based on unique constraints to prevent duplicates.

Args:
template_id: UUID of the template (preferred, for referential integrity)
template: Name of the template (kept for backward compatibility)
template: Name of the template (kept for backwards compatibility)
pre_checks: JSON string of pre-check configurations
telephony_config: JSON string of telephony provider overrides
"""
Expand Down Expand Up @@ -85,7 +105,7 @@ def insert_call_execution_config_query(
"updated_at"
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29)
ON CONFLICT (merchant_id, template) DO UPDATE SET
ON CONFLICT {_conflict_target(template, merchant_id)} DO UPDATE SET
initial_offset = EXCLUDED.initial_offset,
retry_offset = EXCLUDED.retry_offset,
call_start_time = EXCLUDED.call_start_time,
Expand Down Expand Up @@ -208,7 +228,7 @@ def delete_call_execution_config_query(config_id: str) -> Tuple[str, List[Any]]:

def update_call_execution_config_query(
reseller_id: str,
template: str,
template: Optional[str] = None,
merchant_id: Optional[str] = None,
initial_offset: Optional[int] = None,
retry_offset: Optional[int] = None,
Expand Down Expand Up @@ -315,16 +335,20 @@ def _add(col: str, val: Any) -> None:
reseller_id_param = param_count
param_count += 1

values.append(template)
template_param = param_count
param_count += 1
if template is not None:
values.append(template)
template_param = param_count
param_count += 1
template_clause = f'"template" = ${template_param}'
else:
template_clause = '"template" IS NULL'

if merchant_id:
values.append(merchant_id)
merchant_identifier_param = param_count
where_clause = f'reseller_id = ${reseller_id_param} AND "template" = ${template_param} AND merchant_id = ${merchant_identifier_param}'
where_clause = f"reseller_id = ${reseller_id_param} AND {template_clause} AND merchant_id = ${merchant_identifier_param}"
else:
where_clause = f'reseller_id = ${reseller_id_param} AND "template" = ${template_param} AND merchant_id IS NULL'
where_clause = f"reseller_id = ${reseller_id_param} AND {template_clause} AND merchant_id IS NULL"
Comment on lines 346 to +351

text = f"""
UPDATE "{CALL_EXECUTION_CONFIG_TABLE}"
Expand Down
6 changes: 3 additions & 3 deletions app/schemas/breeze_buddy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ class CreateCallExecutionConfigRequest(BaseModel):
max_retry: int
calling_provider: CallProvider
reseller_id: str
template: str
template: Optional[str] = None
merchant_id: Optional[str] = None
enable_international_call: bool = True
enable_calling: Optional[bool] = True
Expand Down Expand Up @@ -262,7 +262,7 @@ class UpdateCallExecutionConfigRequest(BaseModel):
"""Request to update call execution configuration"""

reseller_id: str
template: str
template: Optional[str] = None
merchant_id: Optional[str] = None
initial_offset: Optional[int] = None
retry_offset: Optional[int] = None
Expand Down Expand Up @@ -323,7 +323,7 @@ class CallExecutionConfig(BaseModel):
max_retry: int
calling_provider: CallProvider
reseller_id: str
template: str
template: Optional[str] = None
template_id: Optional[str] = None
merchant_id: Optional[str] = None
enable_international_call: bool = True
Expand Down
Loading