Get all configs by merchantidentifier#577
Get all configs by merchantidentifier#577sharifajahanshaik wants to merge 2 commits intojuspay:releasefrom
Conversation
WalkthroughThis PR systematically renames identifier fields across the Breeze Buddy system, replacing merchant-centric identifiers (merchant_id, shop_identifier) with reseller-centric ones (reseller_id, merchant_identifier). Changes span API routes, database schema and accessors, authorization logic, agent workflows, and schemas. Backward compatibility is maintained through database migrations, COALESCE-based query logic, dual-field support, and validation fallbacks. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR standardizes Breeze Buddy’s “merchant/shop” identifiers to the new naming scheme “reseller_id / merchant_identifier” across schemas, RBAC, database queries/decoders, and API handlers, while attempting to preserve backward compatibility via COALESCE and dual-field support.
Changes:
- Introduces
reseller_idandmerchant_identifierfields across API schemas/models and updates RBAC/token payload usage. - Updates SQL queries, accessors, and decoders to read/write new columns while maintaining legacy columns.
- Adds a migration to add/populate new DB columns and indexes; updates architecture docs accordingly.
Reviewed changes
Copilot reviewed 46 out of 46 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/BREEZE_BUDDY_ARCHITECTURE.md | Renames docs references from merchant/shop to reseller/merchant_identifier. |
| app/schemas/breeze_buddy/template.py | Adds reseller/merchant_identifier fields with backward-compat fields. |
| app/schemas/breeze_buddy/core.py | Extends core models (LeadCallTracker, OutboundNumber, CallExecutionConfig) for new identifiers. |
| app/schemas/breeze_buddy/auth.py | Extends UserInfo/User models for reseller_ids + merchant_identifiers (legacy aliases retained). |
| app/schemas/breeze_buddy/analytics.py | Adds new analytics filters and CallDetailResult backward-compatible dump behavior. |
| app/database/migrations/add_reseller_id_and_merchant_identifier_columns.sql | Adds new columns + backfills + indexes for reseller/merchant_identifier fields. |
| app/database/queries/breeze_buddy/template.py | Updates template SQL to COALESCE new/old identifiers and modifies insert/update semantics. |
| app/database/queries/breeze_buddy/call_execution_config.py | Updates call_execution_config SQL to accept merchant_identifier and write new columns. |
| app/database/queries/breeze_buddy/outbound_number.py | Writes new outbound_number columns and adds COALESCE projections in select queries. |
| app/database/decoder/breeze_buddy/* | Decodes records using reseller_id / merchant_identifier (with legacy fallbacks). |
| app/database/accessor/breeze_buddy/* | Threads new identifiers through accessor APIs and adjusts list/fallback logic. |
| app/api/security/breeze_buddy/rbac_token.py | Switches JWT claims to reseller_ids / merchant_identifiers. |
| app/api/security/breeze_buddy/authorization.py | Updates RBAC helper naming and fallback logic for new identifiers. |
| app/api/routers/breeze_buddy/*/handlers.py | Updates lead/template/config/number handlers to use new fields and backward-compat fallbacks. |
| app/ai/voice/agents/breeze_buddy/* | Updates agent/template loading/config retrieval paths for merchant_identifier. |
Comments suppressed due to low confidence (2)
app/database/decoder/breeze_buddy/outbound_number.py:68
- decode_outbound_number_list computes reseller_id/merchant_identifier from only the first row, then reuses those values for every OutboundNumber in the list comprehension. This will return incorrect reseller/shop metadata for all but the first record. Compute these fields per-row inside the comprehension/loop.
app/api/routers/breeze_buddy/numbers/handlers.py:52 - create_number_handler now accepts reseller_id (with merchant_id as fallback), but the 400 error detail still says "merchant_id is required". Update the message to reflect the accepted fields (reseller_id/merchant_id).
if not reseller:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="merchant_id is required",
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| template_obj = await self._load_template_from_db( | ||
| merchant_id, template, shop_identifier | ||
| ) | ||
| template_obj = await self._load_template_from_db(template, merchant_identifier) |
There was a problem hiding this comment.
FlowConfigLoader.load_template passes arguments to _load_template_from_db in the wrong order, so the DB lookup uses the template name as the merchant_identifier and vice‑versa. This will cause consistent template misses (and confusing error logs). Swap the arguments (or change the helper signature) so _load_template_from_db receives (merchant_identifier, name).
| template_obj = await self._load_template_from_db(template, merchant_identifier) | |
| template_obj = await self._load_template_from_db(merchant_identifier, template) |
| raise HTTPException( | ||
| status_code=status.HTTP_404_NOT_FOUND, | ||
| detail=f"Template '{name}' not found for merchant: {merchant_id}", | ||
| detail=f"Template '{name}' not found for merrchant_identifier {merchant_identifier}", |
There was a problem hiding this comment.
Typo in the 404 detail message: merrchant_identifier should be merchant_identifier. This is user-facing and will surface in API error responses.
| detail=f"Template '{name}' not found for merrchant_identifier {merchant_identifier}", | |
| detail=f"Template '{name}' not found for merchant_identifier {merchant_identifier}", |
| def get_template_by_merchant_query( | ||
| merchant_id: str, shop_identifier: Optional[str] = None, name: Optional[str] = None | ||
| merchant_identifier: str, | ||
| name: Optional[str] = None, | ||
| ) -> Tuple[str, List[Any]]: | ||
| """Generate query to get a template by merchant ID and optional filters.""" | ||
| conditions = ["merchant_id = $1"] | ||
| values = [merchant_id] | ||
| """Generate query to get a template by reseller ID and optional filters.""" | ||
|
|
||
| if shop_identifier: | ||
| conditions.append(f"shop_identifier = ${len(values) + 1}") | ||
| values.append(shop_identifier) | ||
| else: | ||
| conditions.append("shop_identifier IS NULL") | ||
| conditions = [f"COALESCE(merchant_identifier, shop_identifier) = $1"] | ||
| values = [merchant_identifier] | ||
|
|
||
| if name: | ||
| conditions.append(f"name = ${len(values) + 1}") | ||
| values.append(name) | ||
|
|
||
| query = f""" | ||
| SELECT id, merchant_id, shop_identifier, name, flow, expected_payload_schema, expected_callback_response_schema, configurations, secrets, outbound_number_id, is_active, created_at, updated_at | ||
| SELECT id, | ||
| COALESCE(reseller_id, merchant_id) AS reseller_id, | ||
| COALESCE(merchant_identifier, shop_identifier) AS merchant_identifier, | ||
| name, flow, expected_payload_schema, expected_callback_response_schema, configurations, secrets, outbound_number_id, is_active, created_at, updated_at | ||
| FROM {TEMPLATE_TABLE} | ||
| WHERE {" AND ".join(conditions)} | ||
| """ |
There was a problem hiding this comment.
get_template_by_merchant_query filters only by merchant_identifier (COALESCE(merchant_identifier, shop_identifier)) and does not scope by reseller_id/merchant_id. Since the schema allows the same merchant_identifier under different resellers (unique indexes include reseller_id), this query can return the wrong reseller’s template (data leak / incorrect behavior). Include reseller_id (COALESCE(reseller_id, merchant_id)) as part of the WHERE clause and update call sites accordingly.
| if not reseller_id or not req.identifier: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_400_BAD_REQUEST, | ||
| detail="reseller (or merchant for backward compatibility) is required", | ||
| ) |
There was a problem hiding this comment.
push_lead_handler validates not reseller_id or not req.identifier but the error message only mentions reseller/merchant. If identifier (merchant_identifier/shop_identifier) is missing, this returns a misleading error. Split the validation so each missing field returns the correct message.
| if not reseller_id or not req.identifier: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="reseller (or merchant for backward compatibility) is required", | |
| ) | |
| if not reseller_id: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="reseller (or merchant for backward compatibility) is required", | |
| ) | |
| if not req.identifier: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="identifier (merchant_identifier/shop_identifier) is required", | |
| ) |
| @@ -108,8 +116,8 @@ async def list_templates( | |||
| Automatically filters based on user's RBAC permissions. | |||
|
|
|||
| Query Parameters: | |||
| - merchant_id: Optional filter by specific merchant ID | |||
| - shop_identifier: Optional filter by specific shop identifier | |||
| - reseller_id: Optional filter by specific reseller ID | |||
| - merchant_identifier: Optional filter by specific shop identifier | |||
| - include_inactive: Include inactive templates (default: false) | |||
|
|
|||
| RBAC Behavior: | |||
| @@ -127,15 +135,14 @@ async def list_templates( | |||
|
|
|||
| Example Requests: | |||
| GET /templates/list # All accessible templates | |||
| GET /templates/list?merchant_id=shop_123 # Templates for specific merchant | |||
| GET /templates/list?reseller_id=reseller_123 # Templates for specific reseller | |||
| GET /templates/list?include_inactive=true # Include inactive templates | |||
|
|
|||
| Returns: | |||
| TemplateListResponse with templates array and total count | |||
| """ | |||
| return await list_templates_handler( | |||
| merchant_id, shop_identifier, include_inactive, current_user | |||
| ) | |||
| identifier = merchant_identifier or shop_identifier | |||
| return await list_templates_handler(identifier, include_inactive, current_user) | |||
There was a problem hiding this comment.
list_templates endpoint docs mention a reseller_id query parameter, but the actual signature only accepts shop_identifier/merchant_identifier and never passes a reseller_id filter. Either add reseller_id (and merchant_id legacy) params and thread them through, or update the docstring/examples to match the real API.
|
|
||
| if result: | ||
| decoded_result = decode_call_execution_config_list(result) | ||
| logger.info(f"decoded_result: {decoded_result}") |
There was a problem hiding this comment.
get_all_call_execution_configs logs the entire decoded_result object. This can be very large and may include customer/shop identifiers or other sensitive config data, increasing log volume and exposure. Consider logging only counts/IDs or moving this to debug-level logging.
| logger.info(f"decoded_result: {decoded_result}") | |
| logger.debug(f"decoded_result: {decoded_result}") |
| if not template_data.identifier: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_400_BAD_REQUEST, | ||
| detail="merchant_identifier (or shop_identifier for backward compatibility) is required", | ||
| ) | ||
| # Check if template already exists |
There was a problem hiding this comment.
create_template_handler currently rejects requests where identifier is omitted, but CreateTemplateRequest.identifier is optional and the DB schema supports reseller-level (generic) templates with merchant_identifier NULL. If generic templates are still needed, this validation should be relaxed (and uniqueness enforced via reseller_id + name + NULL identifier).
| if not template_data.identifier: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="merchant_identifier (or shop_identifier for backward compatibility) is required", | |
| ) | |
| # Check if template already exists | |
| # Check if template already exists. `identifier` is optional to support | |
| # reseller-level (generic) templates with NULL merchant_identifier. |
| def get_templates_list_query(filters: Dict[str, Any]) -> Tuple[str, List[Any]]: | ||
| """ | ||
| Generate query to list multiple templates (metadata only, no flow). | ||
|
|
||
| Supports RBAC filtering by merchant_ids and shop_identifiers arrays. | ||
| Supports RBAC filtering by reseller_ids and merchant_identifiers arrays. | ||
|
|
||
| Args: | ||
| filters: Dictionary containing: | ||
| - merchant_ids (optional): List of merchant IDs to filter by | ||
| - shop_identifiers (optional): List of shop identifiers to filter by | ||
| - merchant_identifiers (optional): List of shop identifiers to filter by | ||
| - is_active (optional): Filter by active status | ||
| - merchant_id (optional): Single merchant ID to filter by | ||
| - shop_identifier (optional): Single shop identifier to filter by | ||
| - merchant_identifier (optional): Single shop identifier to filter by | ||
|
|
||
| Returns: | ||
| Tuple of (query string, values list) | ||
| """ | ||
| conditions = [] | ||
| values = [] | ||
|
|
||
| # Handle merchant filtering (supports both single and multiple) | ||
| if "merchant_ids" in filters and filters["merchant_ids"]: | ||
| values.append(filters["merchant_ids"]) | ||
| conditions.append(f"merchant_id = ANY(${len(values)})") | ||
| elif "merchant_id" in filters and filters["merchant_id"]: | ||
| values.append(filters["merchant_id"]) | ||
| conditions.append(f"merchant_id = ${len(values)}") | ||
|
|
||
| # Handle shop filtering (supports both single and multiple) | ||
| if "shop_identifiers" in filters and filters["shop_identifiers"]: | ||
| values.append(filters["shop_identifiers"]) | ||
| conditions.append(f"shop_identifier = ANY(${len(values)})") | ||
| elif "shop_identifier" in filters and filters["shop_identifier"]: | ||
| values.append(filters["shop_identifier"]) | ||
| conditions.append(f"shop_identifier = ${len(values)}") | ||
| # Handle shop filtering (supports both single and multiple) with COALESCE for backward compatibility | ||
| if "merchant_identifiers" in filters and filters["merchant_identifiers"]: | ||
| values.append(filters["merchant_identifiers"]) | ||
| conditions.append( | ||
| f"COALESCE(merchant_identifier, shop_identifier) = ANY(${len(values)})" | ||
| ) | ||
| elif "merchant_identifier" in filters and filters["merchant_identifier"]: | ||
| values.append(filters["merchant_identifier"]) | ||
| conditions.append( | ||
| f"COALESCE(merchant_identifier, shop_identifier) = ${len(values)}" | ||
| ) | ||
|
|
There was a problem hiding this comment.
Template RBAC filters inject reseller_id(s) into filters, but get_templates_list_query ignores reseller_id/reseller_ids entirely. This can bypass reseller scoping (e.g., users with wildcard merchant_identifiers could see templates across all resellers). Add reseller_id/reseller_ids filtering (with COALESCE for legacy merchant_id) to the SQL WHERE clause.
| if merchant_identifier: | ||
| values.append(merchant_identifier) | ||
| merchant_identifier_param = param_count | ||
| where_clause = f'"merchant_id" = ${reseller_id_param} AND "template" = ${template_param} AND "shop_identifier" = ${merchant_identifier_param}' | ||
| else: | ||
| where_clause = f'"merchant_id" = ${merchant_id_param} AND "template" = ${template_param} AND "shop_identifier" IS NULL' | ||
| where_clause = f'"merchant_id" = ${reseller_id_param} AND "template" = ${template_param} AND "shop_identifier" IS NULL' | ||
|
|
There was a problem hiding this comment.
update_call_execution_config_query builds the WHERE clause using legacy columns ("merchant_id" and "shop_identifier") even though the write path now uses reseller_id/merchant_identifier and other queries use COALESCE for compatibility. This can fail to update rows where only the new columns are populated. Use COALESCE(reseller_id, merchant_id) and COALESCE(merchant_identifier, shop_identifier) (or update both new+old columns consistently) in the WHERE clause.
| -- filepath: /Users/sharifajahan.shaik/Desktop/clairvoyance/app/database/migrations/009_add_reseller_id_and_merchant_identifier_columns.sql | ||
| -- Migration: Add reseller_id and merchant_identifier columns (backward compatible) | ||
| -- Description: |
There was a problem hiding this comment.
Migration file header includes an absolute local filepath and the filename doesn’t follow the repo’s numbered migration convention (001_, 002_, …). Remove the machine-specific path and rename the migration to the next sequence number to ensure deterministic ordering in deployments.
There was a problem hiding this comment.
Actionable comments posted: 13
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (10)
app/database/queries/breeze_buddy/analytics.py (1)
539-582:⚠️ Potential issue | 🔴 CriticalFix: Lead-based grouped query must apply COALESCE aliases in CTE to match summary query backward-compatibility logic.
When
group_by="merchant_identifier"or"reseller_id", line 545 selectslct.{group_by}directly without COALESCE fallback. For rows where only the legacy column (shop_identifier/merchant_id) is populated, the new column will beNULL, causing rows to be incorrectly grouped underNULLand effectively excluded from results.The summary query (lines 244–245) correctly applies
COALESCE(lct.merchant_identifier, lct.shop_identifier) as merchant_identifierandCOALESCE(lct.reseller_id, lct.merchant_id) as reseller_idin its CTE. The lead-based query must apply the same pattern.Proposed fix
text = f""" WITH filtered_data AS ( SELECT lct.request_id, - lct.{group_by}, + COALESCE(lct.merchant_identifier, lct.shop_identifier) as merchant_identifier, + COALESCE(lct.reseller_id, lct.merchant_id) as reseller_id, + lct.template, + lct.shop_identifier, + lct.merchant_id, lct.status, lct.outcome, lct.payload FROM "{LEAD_CALL_TRACKER_TABLE}" lct🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/queries/breeze_buddy/analytics.py` around lines 539 - 582, The grouped lead-based query builds filtered_data without applying the COALESCE aliases used elsewhere, so when group_by is "merchant_identifier" or "reseller_id" rows using legacy columns get NULL-grouped; update the filtered_data CTE selection (the SELECT inside filtered_data in this block) to use the same COALESCE expressions as the summary query (e.g., COALESCE(lct.merchant_identifier, lct.shop_identifier) AS merchant_identifier and COALESCE(lct.reseller_id, lct.merchant_id) AS reseller_id) so that the {group_by} column in filtered_data, unique_leads, outcome_counts and subsequent joins/projections matches the backward-compatible aliasing used by the rest of the query.app/api/routers/breeze_buddy/telephony/inbound/handlers.py (1)
117-134:⚠️ Potential issue | 🟠 MajorInconsistent error handling:
HTTPExceptionvsResponsewithin the same handler.The new validation at lines 119–122 raises
HTTPException(producing{"detail": "..."}responses), while every other error path in this function (lines 106–110, 130–134, 151–155, etc.) returns aResponse(content='{"error": "..."}'). API consumers will receive different JSON shapes depending on which error is triggered.Use the same
Responsepattern for consistency:Proposed fix
if not lead.merchant_identifier: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="merchant_identifier (or shop_identifier for backward compatibility) is required", + logger.error(f"[Voicebot] Lead {lead.id} missing merchant_identifier") + return Response( + content='{"error": "merchant_identifier is required"}', + media_type="application/json", + status_code=400, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/routers/breeze_buddy/telephony/inbound/handlers.py` around lines 117 - 134, The handler currently raises HTTPException when lead.merchant_identifier is missing (using HTTPException) while other error paths return a fastapi.Response with a JSON {"error": "..."} shape; change the merchant_identifier validation to return a Response (not raise HTTPException) using the same pattern as other errors: construct a Response with content='{"error": "merchant_identifier (or shop_identifier for backward compatibility) is required"}', media_type="application/json", and status_code=status.HTTP_400_BAD_REQUEST so the JSON shape is consistent with other branches (reference lead.merchant_identifier check, HTTPException, Response, and existing error-return pattern around get_template_by_merchant and logger.error).app/api/routers/breeze_buddy/numbers/handlers.py (1)
48-52:⚠️ Potential issue | 🟡 MinorStale error message — still references
merchant_idinstead ofreseller_id.The validation now checks the resolved
reseller(preferringreseller_id), but the error detail still says"merchant_id is required". Update to reflect the new field name (with backward-compat note if desired).Suggested fix
if not reseller: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="merchant_id is required", + detail="reseller_id (or merchant_id for backward compatibility) is required", )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/routers/breeze_buddy/numbers/handlers.py` around lines 48 - 52, The error message raised when no reseller is found still says "merchant_id is required"; update the HTTPException detail to mention "reseller_id is required" (or "reseller_id or merchant_id is required" to keep backwards compatibility) in the block that checks the resolved reseller variable (where reseller is evaluated and the exception is raised). Change the detail string in that HTTPException so it accurately reflects the current field names used by the resolver.app/database/decoder/breeze_buddy/outbound_number.py (1)
47-67:⚠️ Potential issue | 🔴 CriticalBug: All records in the list get the first record's
reseller_idandmerchant_identifier.
reseller_idandmerchant_identifierare derived fromresult[0](lines 47-52) but then reused for everyrowin the list comprehension (lines 54-67). Each row should compute its own values.Proposed fix
def decode_outbound_number_list(result: List[asyncpg.Record]) -> List[OutboundNumber]: """ Decode multiple outbound number records from database result using Pydantic models. """ if not result: return [] - row = result[0] - - # Explicit None check (safe) - reseller_id = row["reseller_id"] or row["merchant_id"] - - merchant_identifier = row["merchant_identifier"] or row["shop_identifier"] - return [ OutboundNumber( id=row["id"], number=row["number"], provider=CallProvider(row["provider"]), status=OutboundNumberStatus(row["status"]), channels=row["channels"], maximum_channels=row["maximum_channels"], - reseller_id=reseller_id, - merchant_identifier=merchant_identifier, + reseller_id=row["reseller_id"] or row["merchant_id"], + merchant_identifier=row["merchant_identifier"] or row["shop_identifier"], created_at=row["created_at"], updated_at=row["updated_at"], ) for row in result ]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/decoder/breeze_buddy/outbound_number.py` around lines 47 - 67, The code computes reseller_id and merchant_identifier once from result[0] and reuses them for every OutboundNumber in the list comprehension, causing all records to carry the first row's values; move the per-row logic inside the list comprehension so each iteration computes reseller_id = row["reseller_id"] or row["merchant_id"] and merchant_identifier = row["merchant_identifier"] or row["shop_identifier"] for that row before constructing OutboundNumber (the list comprehension that builds OutboundNumber instances should reference these per-row computed values rather than the previously defined variables).app/database/accessor/breeze_buddy/template.py (1)
228-261:⚠️ Potential issue | 🔴 Critical
HTTPExceptionswallowed by broadexcept Exception— will never reach the client.The
HTTPExceptionraised at line 232 is inside thetryblock whoseexcept Exceptionclause at line 259 catches it, logs it, and returns[]. The 400 response will never propagate to the caller.Either re-raise
HTTPExceptionbefore the general catch, or add anexcept HTTPException: raiseguard.Proposed fix
except HTTPException: raise except Exception as e: logger.error(f"Error getting templates list: {e}", exc_info=True) return []Replace lines 259–261 with the above to let
HTTPExceptionpropagate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/accessor/breeze_buddy/template.py` around lines 228 - 261, The HTTPException raised for missing reseller/merchant is being caught by the broad except Exception in the get-templates block, preventing the 400 from propagating; modify the exception handling around the try/except that wraps the loop (the block that appends TemplateMetadata using reseller_id_value and merchant_identifier_value) to explicitly let HTTPException propagate—either add an early except HTTPException: raise before the general except Exception or re-raise HTTPException inside the general handler so the HTTP 400 is returned to the caller; keep the existing logger.error for other exceptions.app/ai/voice/agents/breeze_buddy/managers/calls.py (1)
60-70:⚠️ Potential issue | 🔴 Critical
HTTPExceptionraised in background/cron task contexts — semantically incorrect and misleading.
_get_lead_config(line 66),_get_available_number(line 138),_retry_call(line 282), andprocess_backlog_leads(line 408) raiseHTTPExceptionwithin functions called from background tasks (process_backlog_leadsis registered viaBackgroundTasks.add_task()and_cleanup_stuck_leads).
HTTPExceptionis a FastAPI construct for HTTP request handlers; itsstatus_codeanddetailfields are meaningless in background/cron contexts. While genericexcept Exceptionblocks prevent crashes, they mask the semantic mismatch. Replace with appropriate logging and early return patterns:if not merchant_identifier: logger.error(f"Lead {lead.id} missing merchant_identifier/shop_identifier") return NoneApply to all four locations.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/ai/voice/agents/breeze_buddy/managers/calls.py` around lines 60 - 70, Replace HTTPException raises used inside background/cron contexts with logging and early returns: in _get_lead_config (replace the merchant_identifier missing HTTPException) log an error (including lead.id) and return None; do the same in _get_available_number, _retry_call, and process_backlog_leads (and the helper _cleanup_stuck_leads) where HTTPException is raised — use the module logger to record a descriptive error message and return an appropriate sentinel (e.g., None or False) instead of raising HTTPException so background tasks do not emit HTTP semantics.app/api/routers/breeze_buddy/templates/handlers.py (1)
199-204:⚠️ Potential issue | 🟠 MajorBare
except Exceptionre-raises as 500, masking the already-raisedHTTPException.If
get_template_by_merchantreturnsNoneand theHTTPException(404)on line 194 is raised, theexcept Exceptionon line 199 catches it, logs it, and re-raises a generic 500. This swallows the intended 404.Add an
except HTTPException: raisebefore the generic handler, consistent with other handlers in this file.🐛 Proposed fix
+ except HTTPException: + raise except Exception as e: logger.error(f"Error getting template: {e}", exc_info=True) raise HTTPException(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/routers/breeze_buddy/templates/handlers.py` around lines 199 - 204, The generic except in the template handler is catching and transforming already-raised HTTPException (e.g., the 404 raised when get_template_by_merchant returns None) into a 500; update the exception handling in the same handler to add an explicit "except HTTPException: raise" before the broad "except Exception as e" so that HTTPException instances from get_template_by_merchant (or any other HTTPException) are re-raised unchanged and only other exceptions are logged and mapped to a 500.app/api/security/breeze_buddy/rbac_token.py (1)
111-119:⚠️ Potential issue | 🔴 CriticalBreaking change: existing JWT tokens will decode with empty access lists.
Old tokens contain
"merchant_ids"and"shop_identifiers"keys. This code only reads"reseller_ids"and"merchant_identifiers"— so any user with an unexpired legacy token will get empty lists and effectively lose all access.Add fallback extraction to maintain backward compatibility during the migration window:
🐛 Proposed fix
user_info = UserInfo( id=user_id, username=username, role=UserRole(payload.get("role")), email=payload.get("email"), - reseller_ids=payload.get("reseller_ids", []), - merchant_identifiers=payload.get("merchant_identifiers", []), + reseller_ids=payload.get("reseller_ids") or payload.get("merchant_ids", []), + merchant_identifiers=payload.get("merchant_identifiers") or payload.get("shop_identifiers", []), permissions=payload.get("permissions", []), )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/security/breeze_buddy/rbac_token.py` around lines 111 - 119, The JWT payload extraction for UserInfo currently only reads "reseller_ids" and "merchant_identifiers", causing legacy tokens with "merchant_ids" and "shop_identifiers" to produce empty access lists; update the construction of user_info (UserInfo(...)) to fallback to legacy keys by using payload.get("reseller_ids", payload.get("merchant_ids", [])) for reseller_ids and payload.get("merchant_identifiers", payload.get("shop_identifiers", [])) for merchant_identifiers (ensure defaults are empty lists) so existing tokens remain compatible during migration.app/database/queries/breeze_buddy/template.py (2)
260-276:⚠️ Potential issue | 🟡 MinorUpdate
shop_identifierin addition tomerchant_identifierinreplace_template_query
create_template_querydual-writesmerchant_identifier → shop_identifieron INSERT to keep both columns in sync for backward compatibility. However,replace_template_queryonly updatesmerchant_identifierand leavesshop_identifierunchanged. While this doesn't break reads (which use COALESCE), it introduces data inconsistency.Add
shop_identifier = $9to the SET clause to match the create behavior:🔧 Proposed fix
SET name = $1, flow = $2::jsonb, expected_payload_schema = $3::jsonb, expected_callback_response_schema = $4::jsonb, configurations = $5::jsonb, secrets = $6::jsonb, outbound_number_id = $7, is_active = $8, merchant_identifier = $9, + shop_identifier = $9, updated_at = $10 WHERE id = $11🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/queries/breeze_buddy/template.py` around lines 260 - 276, The UPDATE in replace_template_query leaves shop_identifier unchanged causing inconsistency with create_template_query; modify the SET clause of the query built in replace_template_query to also assign shop_identifier = $9 (matching merchant_identifier) so both columns stay in sync, and ensure RETURNING/COALESCE logic remains unchanged; locate the query string variable that references TEMPLATE_TABLE and update its SET list to include shop_identifier = $9 alongside merchant_identifier = $9.
11-33:⚠️ Potential issue | 🔴 CriticalAdd reseller_id to WHERE clause for proper tenant isolation
The query filters only on
COALESCE(merchant_identifier, shop_identifier)without scoping toreseller_id. The database schema enforces unique constraints on(reseller_id, merchant_identifier, name)and(reseller_id, name)(lines 124–131 ofadd_reseller_id_and_merchant_identifier_columns.sql), provingmerchant_identifieris scoped per reseller, not globally unique. Two resellers can have templates with identicalmerchant_identifiervalues; filtering onmerchant_identifieralone returns data across tenants.The accessor function
get_template_by_merchantreceives no reseller context from callers and cannot enforce isolation. Compare withget_templates_list(line 138–199 in the same file), which correctly handles bothreseller_idandmerchant_identifierin filters and usesget_reseller_id_by_merchant_identifier_from_config_queryto resolve missing context.Modify the query function and accessor to accept and filter by
reseller_id, or resolve it from context before the query, to ensure templates returned belong only to the requesting tenant.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/queries/breeze_buddy/template.py` around lines 11 - 33, The get_template_by_merchant_query currently only filters by COALESCE(merchant_identifier, shop_identifier) and can return templates across resellers; update it to enforce tenant isolation by accepting a reseller_id (or resolving it beforehand using get_reseller_id_by_merchant_identifier_from_config_query like get_templates_list does) and adding a WHERE condition for "reseller_id = $N" (or COALESCE(reseller_id, merchant_id) = $N) with the reseller_id appended to values; also update the accessor get_template_by_merchant to pass or resolve reseller_id before calling get_template_by_merchant_query so returned templates are scoped to the correct reseller.
🟡 Minor comments (14)
app/ai/voice/agents/breeze_buddy/types/models.py-12-13 (1)
12-13:⚠️ Potential issue | 🟡 MinorMove
reseller/merchantvalidation from handler to Pydantic model.The handler in
app/api/routers/breeze_buddy/leads/handlers.py(line 117–123) currently validates that at least one ofresellerormerchantis provided. While this validation does prevent both fields from beingNoneat runtime, placing this check in the Pydantic model is cleaner and more maintainable.Suggested improvement (Pydantic v2)
from typing import Any, Dict, List, Optional -from pydantic import BaseModel +from pydantic import BaseModel, model_validator from app.schemas.breeze_buddy.core import ExecutionMode class PushLeadRequest(BaseModel): request_id: str payload: Dict[str, Any] template: str reseller: Optional[str] = None merchant: Optional[str] = None identifier: Optional[str] = None reporting_webhook_url: str | None = None execution_mode: Optional[ExecutionMode] = ( None # Defaults to TELEPHONY if not provided ) + + `@model_validator`(mode="after") + def require_reseller_or_merchant(self) -> "PushLeadRequest": + if not self.reseller and not self.merchant: + raise ValueError("At least one of 'reseller' or 'merchant' must be provided.") + return selfThen remove the corresponding check from the handler.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/ai/voice/agents/breeze_buddy/types/models.py` around lines 12 - 13, Add a Pydantic model-level validator in the model class that declares the reseller and merchant fields (the Pydantic model in app/ai/voice/agents/breeze_buddy/types/models.py) that raises a ValueError if both reseller and merchant are None/empty (use Pydantic v2 `@model_validator`(mode="after") to inspect the populated values and enforce "at least one present"); after adding this validator, remove the duplicate runtime check that enforces the same rule from the Breeze Buddy leads handler (the validation block in the leads handler) so the model is the single source of truth.app/database/queries/breeze_buddy/outbound_number.py-72-78 (1)
72-78:⚠️ Potential issue | 🟡 Minor
SELECT *combined withCOALESCE(...) AS reseller_idcreates duplicate column names in the result set—unreliable for name-based access.The query at lines 72–78 executes:
SELECT *, COALESCE(reseller_id, merchant_id) AS reseller_id, COALESCE(merchant_identifier, shop_identifier) AS merchant_identifier FROM "outbound_number"Since the table contains both
reseller_idandmerchant_id(and bothmerchant_identifierandshop_identifier),SELECT *includes all four columns. TheCOALESCE ... ASclauses then add two more columns with the same names as existing columns, producing duplicate column labels in the result set.PostgreSQL permits duplicate output column names, but asyncpg's
Recordbehavior when accessing fields by name from duplicate column labels is undefined and implementation-dependent. While the decoder code includes a fallback (row["reseller_id"] or row["merchant_id"]), this is fragile and should not be relied upon.Fix: Either explicitly list only the needed columns (avoiding
SELECT *), or use unique alias names (e.g.,AS resolved_reseller_id) and adjust the decoder accordingly.This pattern also appears at lines 207–210 in the same file and in
lead_call_tracker.py(lines 180–183, 196–199).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/queries/breeze_buddy/outbound_number.py` around lines 72 - 78, The query uses SELECT * plus COALESCE(... ) AS reseller_id / merchant_identifier which creates duplicate column labels; change the SQL to explicitly list the required columns from OUTBOUND_NUMBER_TABLE (omit the original reseller_id/merchant_id and merchant_identifier/shop_identifier or give the coalesced columns unique aliases like resolved_reseller_id and resolved_merchant_identifier), and then update the decoder logic that currently falls back to row["reseller_id"] or row["merchant_id"] to read the new explicit column names (e.g., resolved_reseller_id) or the explicit original column names you kept; apply the same change pattern to the other occurrences mentioned (lead_call_tracker.py and the later block in this file) so no result set has duplicate column names.app/database/queries/breeze_buddy/call_execution_config.py-282-300 (1)
282-300:⚠️ Potential issue | 🟡 MinorInconsistent documentation:
get_all_merchants_querydocstring conflates merchant_identifiers with resellers.The docstring says "get all unique resellers (merchant_identifiers)" and "Each merchant_identifier represents a distinct reseller." Based on the domain learning,
merchant_identifier(formerlyshop_identifier) represents individual merchant locations, whilereseller_id(formerlymerchant_id) is the parent/group identifier. This documentation inverts the hierarchy.Also, the table name is unquoted on line 295 (
FROM {CALL_EXECUTION_CONFIG_TABLE}) unlike other queries that useFROM "{CALL_EXECUTION_CONFIG_TABLE}". While safe for this table name, it's inconsistent.Based on learnings: In Breeze Buddy,
shop_identifier(nowmerchant_identifier) identifies individual merchant locations, whilemerchant_id(nowreseller_id) is the parent/group identifier.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/queries/breeze_buddy/call_execution_config.py` around lines 282 - 300, Update the get_all_merchants_query docstring to accurately describe that merchant_identifier (formerly shop_identifier) represents individual merchant locations—not resellers—and that reseller_id (formerly merchant_id) is the parent/group identifier; also make the SQL consistent by quoting the table identifier like the other queries (use FROM "{CALL_EXECUTION_CONFIG_TABLE}"). Ensure you update occurrences of "reseller" vs "merchant" wording in the docstring and keep function name get_all_merchants_query and column names merchant_identifier/shop_identifier and reseller_id/merchant_id referenced for clarity.app/api/routers/breeze_buddy/leads/rbac.py-106-106 (1)
106-106:⚠️ Potential issue | 🟡 MinorRemove extraneous
fprefix from string literal.
f"Lead not found"contains no placeholders. This was also flagged by Ruff (F541).Proposed fix
- status_code=status.HTTP_404_NOT_FOUND, detail=f"Lead not found" + status_code=status.HTTP_404_NOT_FOUND, detail="Lead not found"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/routers/breeze_buddy/leads/rbac.py` at line 106, The HTTPException raised with status_code=status.HTTP_404_NOT_FOUND uses an unnecessary f-string (f"Lead not found"); update the call that constructs the exception (the raise HTTPException(...) in the RBAC check) to use a plain string "Lead not found" instead of an f-string to remove the extraneous `f` prefix flagged by Ruff.app/api/routers/breeze_buddy/leads/handlers.py-152-156 (1)
152-156:⚠️ Potential issue | 🟡 MinorRedundant
req.identifiercheck — already validated at line 120.This block is dead code since
req.identifierwas already checked as part of the condition on line 120. If it were falsy, an exception would have been raised before reaching this point. Remove it to avoid confusion.Proposed fix
logger.info(f"Payload validation successful for merchant {req.identifier}") - if not req.identifier: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="merchant_identifier (or shop_identifier for backward compatibility) is required", - ) # Get call execution config🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/routers/breeze_buddy/leads/handlers.py` around lines 152 - 156, Remove the redundant runtime check for req.identifier in the handler (the if block that raises HTTPException for missing merchant_identifier/shop_identifier), because req.identifier is already validated earlier (the condition at line 120). Locate the conditional referencing req.identifier in this handler and delete that entire if/raise block (including the HTTPException instantiation) so the code is not checking the same condition twice; ensure no other logic depends on that block before committing.app/api/routers/breeze_buddy/templates/__init__.py-118-121 (1)
118-121:⚠️ Potential issue | 🟡 MinorDocstring references
reseller_idquery parameter that doesn't exist in the function signature.The query parameters section at line 119 mentions
reseller_idas a filter, but the function signature only definesshop_identifierandmerchant_identifier. This will confuse API consumers reading the OpenAPI docs.Proposed fix
Query Parameters: - - reseller_id: Optional filter by specific reseller ID - merchant_identifier: Optional filter by specific shop identifier + - shop_identifier: Optional filter by specific shop identifier (backward compatibility, use merchant_identifier) - include_inactive: Include inactive templates (default: false)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/routers/breeze_buddy/templates/__init__.py` around lines 118 - 121, The docstring lists a non-existent query parameter reseller_id which doesn’t match the function signature (it defines shop_identifier and merchant_identifier); update the Query Parameters section to remove reseller_id and instead document the actual params shop_identifier, merchant_identifier, and include_inactive so the OpenAPI docs match the function signature (ensure the symbols shop_identifier, merchant_identifier, include_inactive are described and reseller_id is not referenced).app/database/accessor/breeze_buddy/template.py-39-63 (1)
39-63:⚠️ Potential issue | 🟡 MinorUnused parameter
should_prioritize_merchant_specific.This parameter is accepted in the function signature but never referenced in the function body. The caller explicitly passes
should_prioritize_merchant_specific=False, indicating intent to use it, but the function ignores this parameter entirely. Either remove it if no longer needed, or implement the prioritization logic.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/accessor/breeze_buddy/template.py` around lines 39 - 63, The parameter should_prioritize_merchant_specific on get_template_by_merchant is unused; either remove it from the signature and update all callers who pass it, or implement the prioritization by propagating the flag into get_template_by_merchant_query (update that function's signature) or by branching inside get_template_by_merchant to choose/modify the SQL query before calling run_parameterized_query; ensure any query change yields merchant-specific rows first, keep decode_template and result handling the same, and update all callers to match the new signature or removed param accordingly.app/database/migrations/add_reseller_id_and_merchant_identifier_columns.sql-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorLocal filesystem path leaked in migration comment.
Line 1 contains
/Users/sharifajahan.shaik/Desktop/clairvoyance/...— remove the local path.📝 Proposed fix
--- filepath: /Users/sharifajahan.shaik/Desktop/clairvoyance/app/database/migrations/009_add_reseller_id_and_merchant_identifier_columns.sql +-- Migration: Add reseller_id and merchant_identifier columns (backward compatible)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/migrations/add_reseller_id_and_merchant_identifier_columns.sql` at line 1, The migration file add_reseller_id_and_merchant_identifier_columns.sql contains a leaked local filesystem path in its header comment (the string starting with /Users/sharifajahan.shaik/Desktop/clairvoyance); remove that local path from the comment so the migration contains only neutral metadata or a generic description, then save the file ensuring no other absolute local paths remain in the file.app/api/routers/breeze_buddy/templates/handlers.py-196-196 (1)
196-196:⚠️ Potential issue | 🟡 MinorTypo in user-facing error message: "merrchant_identifier".
📝 Proposed fix
- detail=f"Template '{name}' not found for merrchant_identifier {merchant_identifier}", + detail=f"Template '{name}' not found for merchant_identifier {merchant_identifier}",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/routers/breeze_buddy/templates/handlers.py` at line 196, The user-facing error message in the TemplateNotFound response contains a typo "merrchant_identifier"; locate the formatted message (detail=f"Template '{name}' not found for merrchant_identifier {merchant_identifier}") in handlers.py and correct the spelling to "merchant_identifier" (or better: "merchant identifier") while keeping the existing variables name and merchant_identifier; update the f-string so the message reads e.g. f"Template '{name}' not found for merchant identifier {merchant_identifier}" to ensure clarity for users.app/api/routers/breeze_buddy/templates/handlers.py-89-92 (1)
89-92:⚠️ Potential issue | 🟡 MinorMissing space between concatenated f-strings in error detail.
The two f-strings concatenate directly, producing e.g.
"Template already exists for merchant XYZand template name: foo".📝 Proposed fix
- detail=f"Template already exists for merchant {template_data.identifier}" - f"and template name: {template_data.template_name}", + detail=f"Template already exists for merchant {template_data.identifier} " + f"and template name: {template_data.template_name}",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/routers/breeze_buddy/templates/handlers.py` around lines 89 - 92, The error detail f-strings in the raise HTTPException call produce a missing space because they are concatenated: update the detail expression in the raise (the HTTPException with status_code=status.HTTP_409_CONFLICT) to include a separating space — either merge into a single f-string or add a trailing/leading space between the two f-strings so the message correctly reads "Template already exists for merchant {template_data.identifier} and template name: {template_data.template_name}".app/database/accessor/breeze_buddy/call_execution_config.py-290-312 (1)
290-312:⚠️ Potential issue | 🟡 MinorMisleading docstring:
get_all_merchantsreturns merchant_identifiers (shop-level), not resellers.The docstring says "Get all unique resellers (merchant_identifiers)" and "Each merchant_identifier represents a distinct reseller in the system." Based on the system's domain model,
merchant_identifierrepresents a distinct merchant/shop (the oldshop_identifier), whilereseller_idrepresents the parent/group. The function name and return semantics are correct, but the docstring wrongly equates merchant_identifiers with resellers.📝 Proposed fix
async def get_all_merchants() -> List[str]: """ - Get all unique resellers (merchant_identifiers). + Get all unique merchants (merchant_identifiers). - Each merchant_identifier represents a distinct reseller in the system. - This assumes every merchant has at least one call execution config. + Each merchant_identifier represents a distinct merchant/shop in the system. + This assumes every merchant/shop has at least one call execution config. Returns: List of unique merchant_identifier strings """ - logger.info("Getting all resellers (merchant_identifiers)") + logger.info("Getting all merchants (merchant_identifiers)")Based on learnings: "In the Breeze Buddy system, each
shop_identifierrepresents a distinct merchant. Themerchant_idis a parent/group identifier, whileshop_identifieruniquely identifies individual merchant locations/shops."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/accessor/breeze_buddy/call_execution_config.py` around lines 290 - 312, Update the docstring for get_all_merchants to correctly reflect that it returns shop-level merchant identifiers (merchant_identifier) rather than resellers; mention that merchant_identifier maps to an individual merchant/shop and that reseller_id (or merchant_id) is the parent/group identifier, and adjust the summary and Returns description accordingly so callers aren’t misled; reference get_all_merchants(), merchant_identifier, and get_all_merchants_query() so the change is applied to the correct function.app/api/security/breeze_buddy/rbac_token.py-44-45 (1)
44-45:⚠️ Potential issue | 🟡 MinorMisleading docstring: descriptions reference the old terminology.
reseller_idsis described as "List of accessible merchant IDs" andmerchant_identifiersas "List of accessible shop identifiers". These should describe resellers and merchants respectively to match the new naming convention.📝 Proposed fix
- reseller_ids: List of accessible merchant IDs (["*"] for all merchants) - merchant_identifiers: List of accessible shop identifiers (["*"] for all shops under the merchant(s)) + reseller_ids: List of accessible reseller IDs (["*"] for all resellers) + merchant_identifiers: List of accessible merchant identifiers (["*"] for all merchants under the reseller(s))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/security/breeze_buddy/rbac_token.py` around lines 44 - 45, Update the misleading docstring in rbac_token.py so the parameter descriptions use the new terminology: change the description for reseller_ids to "List of accessible reseller IDs (['*'] for all resellers)" and change the description for merchant_identifiers to "List of accessible merchant identifiers (['*'] for all merchants)"; locate the docstring that mentions reseller_ids and merchant_identifiers and replace the old "merchant" and "shop" wording to "reseller" and "merchant" respectively to keep naming consistent.app/api/routers/breeze_buddy/templates/handlers.py-164-170 (1)
164-170:⚠️ Potential issue | 🟡 MinorStale docstring: references
reseller_idparameter that doesn't exist in the function signature.
get_template_handleracceptsmerchant_identifier,name, andcurrent_user— there is noreseller_idparameter.📝 Proposed fix
""" - Get template(s) by reseller, shop, and name. + Get template(s) by merchant identifier and name. Args: - reseller_id: Reseller ID - merchant_identifier: Optional shop identifier + merchant_identifier: Merchant identifier name: Optional template name current_user: Current authenticated user🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/routers/breeze_buddy/templates/handlers.py` around lines 164 - 170, The docstring for get_template_handler is stale: it mentions a nonexistent reseller_id parameter; update the docstring to list the actual function parameters (merchant_identifier, name, current_user) and their descriptions, remove or replace any reseller_id references, and ensure the short description ("Get template(s) by ...") matches the function signature and behavior in get_template_handler.app/database/queries/breeze_buddy/template.py-17-17 (1)
17-17:⚠️ Potential issue | 🟡 MinorRemove the extraneous
fprefix — no placeholders presentFlagged by Ruff F541. The string contains no
{...}substitutions so thefprefix is a no-op and should be removed.🔧 Proposed fix
- conditions = [f"COALESCE(merchant_identifier, shop_identifier) = $1"] + conditions = ["COALESCE(merchant_identifier, shop_identifier) = $1"]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/queries/breeze_buddy/template.py` at line 17, The string in the assignment to the variable conditions uses an unnecessary f-string prefix (f"COALESCE(merchant_identifier, shop_identifier) = $1"); remove the leading f so it becomes a plain string literal ("COALESCE(merchant_identifier, shop_identifier) = $1") in template.py to satisfy Ruff F541 and avoid a no-op f-string; keep the rest of the conditions list and variable name unchanged.
| template_obj = await self._load_template_from_db( | ||
| merchant_id, template, shop_identifier | ||
| ) | ||
| template_obj = await self._load_template_from_db(template, merchant_identifier) |
There was a problem hiding this comment.
Critical bug: arguments to _load_template_from_db are swapped.
The method signature is _load_template_from_db(self, merchant_identifier: str, name: str) (lines 21–24), but the call passes template (the template name) as merchant_identifier and merchant_identifier as name. This will query the database with the wrong values — using the template name to look up the merchant, and the merchant identifier as the template name.
🐛 Proposed fix
- template_obj = await self._load_template_from_db(template, merchant_identifier)
+ template_obj = await self._load_template_from_db(merchant_identifier, template)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| template_obj = await self._load_template_from_db(template, merchant_identifier) | |
| template_obj = await self._load_template_from_db(merchant_identifier, template) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/ai/voice/agents/breeze_buddy/template/loader.py` at line 117, Call to
_load_template_from_db has the arguments swapped: the function signature is
_load_template_from_db(self, merchant_identifier: str, name: str) but the code
passes template then merchant_identifier; fix by passing merchant_identifier
first and template second when calling _load_template_from_db (i.e., swap the
two arguments) so the DB query receives the correct merchant_identifier and
template name.
| # Get reseller/merchant IDs - support both new (reseller_ids) and old (merchant_ids) field names | ||
| reseller_ids = current_user.reseller_ids or current_user.merchant_ids or [] | ||
|
|
||
| # Get merchant/shop identifiers - support both new (merchant_identifiers) and old (shop_identifiers) field names | ||
| merchant_identifiers = ( | ||
| current_user.merchant_identifiers or current_user.shop_identifiers or [] | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the UserInfo or User model definition
find . -type f -name "*.py" | head -20
git ls-files | grep -i "model\|schema" | head -20Repository: juspay/clairvoyance
Length of output: 1669
🏁 Script executed:
# Search for UserInfo class definition
rg "class UserInfo" -A 20Repository: juspay/clairvoyance
Length of output: 1455
🏁 Script executed:
# Also search for reseller_ids and merchant_ids field definitions
rg "reseller_ids|merchant_ids" --type py -B 2 -A 2 | head -100Repository: juspay/clairvoyance
Length of output: 7247
🏁 Script executed:
# Look at the actual rbac.py file to understand context better
cat -n app/api/routers/breeze_buddy/analytics/rbac.py | head -50Repository: juspay/clairvoyance
Length of output: 2286
🏁 Script executed:
# Check if there are scenarios where these fields are explicitly set to empty lists
rg "reseller_ids\s*=" app/database -A 1 -B 1 | head -50Repository: juspay/clairvoyance
Length of output: 631
🏁 Script executed:
# Look for any code that explicitly sets these fields to empty lists or revokes access
rg "reseller_ids\s*=\s*\[\]|merchant_ids\s*=\s*\[\]" -t pyRepository: juspay/clairvoyance
Length of output: 45
🏁 Script executed:
# Check user creation and update logic to understand field population
cat -n app/database/queries/breeze_buddy/users.py | grep -A 30 "def create_user\|def update_user"Repository: juspay/clairvoyance
Length of output: 45
🏁 Script executed:
# Check user handlers to see how fields are populated during creation/update
find . -path ./app/api/routers/breeze_buddy/user* -name "*.py" -type fRepository: juspay/clairvoyance
Length of output: 45
🏁 Script executed:
# Look at the actual handlers file mentioned in the learning
cat -n app/api/routers/breeze_buddy/user_accounts/handlers.py | head -150Repository: juspay/clairvoyance
Length of output: 149
🏁 Script executed:
# Find user account related files
find . -type f -name "*.py" | xargs grep -l "user.*account\|user.*handler" | grep breezeRepository: juspay/clairvoyance
Length of output: 158
🏁 Script executed:
# Search for where reseller_ids and merchant_ids are actually assigned
rg "reseller_ids\s*=" --type py -B 3 -A 1 | head -80Repository: juspay/clairvoyance
Length of output: 6126
🏁 Script executed:
# Look at the RBAC file more comprehensively to understand the full context
cat -n app/api/routers/breeze_buddy/analytics/rbac.pyRepository: juspay/clairvoyance
Length of output: 9043
🏁 Script executed:
# Check if these fields are ever explicitly set to empty lists anywhere
rg "reseller_ids\s*=\s*\[\s*\]|merchant_ids\s*=\s*\[\s*\]" -t pyRepository: juspay/clairvoyance
Length of output: 45
🏁 Script executed:
# Check the JWT token creation to understand how fields are populated
cat -n app/api/security/breeze_buddy/rbac_token.py | head -80Repository: juspay/clairvoyance
Length of output: 3187
🏁 Script executed:
# Verify the same pattern exists in other RBAC files
grep -n "reseller_ids or current_user.merchant_ids or" app/api/routers/breeze_buddy/*/rbac.pyRepository: juspay/clairvoyance
Length of output: 866
Clarify: Do reseller_ids and merchant_ids ever have explicit None values, or always default to []?
The security concern is technically valid—[] or ["m1"] evaluates to ["m1"] because empty lists are falsy. However, the schema defines these fields with Field(default_factory=list), meaning they default to [], never None. This makes the or chaining intended for backward compatibility during migration: if the new field is an empty list (default, no explicit value set), fall back to the old field.
The actual vulnerability arises only if:
- A user's
reseller_idsis explicitly revoked (set to[]) - The old
merchant_idsfield still contains values and wasn't cleaned up
This is a real migration risk if cleanup is incomplete. However, lines 97–105 and 143–151 in rbac.py already check len(accessible_merchants) == 0 and raise 403, which partially mitigates the issue.
Using explicit None checks instead (as proposed) would eliminate this ambiguity and be safer:
reseller_ids = (
current_user.reseller_ids
if current_user.reseller_ids is not None
else (current_user.merchant_ids if current_user.merchant_ids is not None else [])
)This pattern appears in 6 locations across multiple RBAC modules, so the fix should be applied systematically.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/routers/breeze_buddy/analytics/rbac.py` around lines 36 - 42, The
current or-chaining for backward-compatibility (e.g., reseller_ids =
current_user.reseller_ids or current_user.merchant_ids or []) treats empty lists
as falsy and can incorrectly fall back to legacy fields; change these
assignments to explicit None checks so an intentionally empty list is preserved
(for example, set reseller_ids to current_user.reseller_ids if
current_user.reseller_ids is not None else current_user.merchant_ids if
current_user.merchant_ids is not None else []), do the same for
merchant_identifiers/shop_identifiers, and apply this pattern consistently
across all similar occurrences in rbac.py (the places that compute reseller_ids,
merchant_ids, merchant_identifiers, shop_identifiers and where
accessible_merchants is derived) to eliminate the migration ambiguity.
| if reseller_id: | ||
| configs = await get_call_execution_config_by_merchant_id(reseller_id) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files app/api/routers/breeze_buddy/configurations/ | head -20Repository: juspay/clairvoyance
Length of output: 226
🏁 Script executed:
# Find where get_call_execution_config_by_merchant_id is defined
rg -n "def get_call_execution_config_by_merchant_id" --type=pyRepository: juspay/clairvoyance
Length of output: 291
🏁 Script executed:
# Find the query function mentioned in the review
rg -n "get_call_execution_config_by_merchant_id_query" --type=py -B 2 -A 15Repository: juspay/clairvoyance
Length of output: 5080
🏁 Script executed:
# Read the handlers.py file around lines 122-123
sed -n '115,135p' app/api/routers/breeze_buddy/configurations/handlers.py | cat -nRepository: juspay/clairvoyance
Length of output: 891
🏁 Script executed:
# Also read more context around those lines to understand the function signature and variable assignments
sed -n '90,140p' app/api/routers/breeze_buddy/configurations/handlers.py | cat -nRepository: juspay/clairvoyance
Length of output: 2168
🏁 Script executed:
# Check what reseller_id and merchant_identifier are set to earlier in the function
sed -n '1,120p' app/api/routers/breeze_buddy/configurations/handlers.py | tail -50 | cat -nRepository: juspay/clairvoyance
Length of output: 2125
🏁 Script executed:
# Check the CallExecutionConfig schema to understand what reseller_id/merchant_id columns exist
rg -n "class CallExecutionConfig" --type=py -A 30Repository: juspay/clairvoyance
Length of output: 1399
🏁 Script executed:
# Also check if there's a query that filters by reseller_id column
rg -n "reseller_id\|merchant_id" app/database/queries/breeze_buddy/call_execution_config.py -B 1 -A 1Repository: juspay/clairvoyance
Length of output: 45
🏁 Script executed:
# List all query functions in the call_execution_config queries file
rg -n "^def " app/database/queries/breeze_buddy/call_execution_config.pyRepository: juspay/clairvoyance
Length of output: 623
🏁 Script executed:
# Check the entire queries file to see all available queries
cat app/database/queries/breeze_buddy/call_execution_config.pyRepository: juspay/clairvoyance
Length of output: 10367
Semantic mismatch: reseller_id passed to function expecting merchant_identifier.
Line 122-123 passes reseller_id (parent/group-level identifier) to get_call_execution_config_by_merchant_id(), which filters by COALESCE(merchant_identifier, shop_identifier) (shop-level identifier). These are distinct columns in the schema—passing reseller_id to a function that filters by shop-level merchant_identifier will return incorrect results or empty results.
Either create a separate query that filters by the reseller_id/merchant_id column, or clarify the intended behavior (e.g., if this should list configs by shop identifier instead).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/routers/breeze_buddy/configurations/handlers.py` around lines 122 -
123, The code is passing reseller_id into
get_call_execution_config_by_merchant_id(), which queries
COALESCE(merchant_identifier, shop_identifier) and thus expects a shop-level
identifier; this causes wrong/empty results. Replace this call with a query that
filters by the reseller/merchant column (e.g., implement and call
get_call_execution_config_by_reseller_id or
get_call_execution_config_by_merchant_column) or, if the intent was to filter by
shop, pass the correct shop identifier variable instead; update the handler to
call the new/appropriate function (or pass shop identifier) and ensure the query
uses merchant_identifier (not shop_identifier) as required.
| # Check merchant identifier access | ||
| if "*" not in user_merchant_identifiers: | ||
| if merchant_identifier and user_merchant_identifiers: | ||
| if merchant_identifier not in user_merchant_identifiers: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_403_FORBIDDEN, | ||
| detail=f"Access denied: cannot {operation} merchant {merchant_identifier}", | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n app/api/routers/breeze_buddy/configurations/rbac.pyRepository: juspay/clairvoyance
Length of output: 4149
🏁 Script executed:
rg -n "class UserInfo|merchant_identifiers|shop_identifiers" --type=py -C2 app/schemasRepository: juspay/clairvoyance
Length of output: 5706
🏁 Script executed:
rg -n "merchant_identifiers|shop_identifiers" --type=py -C2 -g '!**test**' | head -100Repository: juspay/clairvoyance
Length of output: 7744
🏁 Script executed:
find . -type f -name "*test*.py" -path "*/rbac*" -o -type f -name "*test*.py" | xargs grep -l "validate_config_access\|filter_configs_by_rbac" 2>/dev/nullRepository: juspay/clairvoyance
Length of output: 47
🏁 Script executed:
rg -l "validate_config_access|filter_configs_by_rbac" --type=pyRepository: juspay/clairvoyance
Length of output: 226
🏁 Script executed:
rg -n "validate_config_access\|filter_configs_by_rbac" -C5 app/api/routers/breeze_buddy/configurations/handlers.pyRepository: juspay/clairvoyance
Length of output: 45
🏁 Script executed:
cat app/api/routers/breeze_buddy/configurations/handlers.py | head -200Repository: juspay/clairvoyance
Length of output: 6429
🏁 Script executed:
rg -n "validate_config_access" app/api/routers/breeze_buddy/configurations/ -C10Repository: juspay/clairvoyance
Length of output: 13340
🏁 Script executed:
rg -n "filter_configs_by_rbac" app/api/routers/breeze_buddy/configurations/ -C10Repository: juspay/clairvoyance
Length of output: 6907
Inconsistent merchant-level access check between validate_config_access and filter_configs_by_rbac.
When user_merchant_identifiers is an empty list []:
validate_config_access(line 51): the conditionif merchant_identifier and user_merchant_identifiers:isFalse(empty list is falsy), so the membership check is skipped → access granted.filter_configs_by_rbac(lines 93-94):config_merchant_identifier in []evaluates toFalse→ access denied.
A user with reseller-level access but no merchant-identifier restrictions will pass single-resource validation checks (POST, GET, DELETE) but have all configs filtered out by the list endpoint, creating unpredictable behavior. The reseller-level checks avoid this by using explicit membership testing in both functions; align the merchant-level checks to use the same pattern: check membership explicitly rather than list existence.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/routers/breeze_buddy/configurations/rbac.py` around lines 49 - 56,
The merchant-level check in validate_config_access uses truthiness of
user_merchant_identifiers which treats [] as falsy and skips the check, causing
inconsistency with filter_configs_by_rbac; update validate_config_access to use
explicit membership testing like filter_configs_by_rbac: replace the block that
uses "if merchant_identifier and user_merchant_identifiers:" with a check that
handles None and wildcard and explicitly tests membership (e.g., ensure "*" not
in user_merchant_identifiers and merchant_identifier not in
user_merchant_identifiers then raise) so validate_config_access and
filter_configs_by_rbac use the same logic for user_merchant_identifiers,
merchant_identifier, and config_merchant_identifier.
| # Get reseller_id with backward compatibility | ||
| reseller_id = req.reseller or req.merchant | ||
|
|
||
| if not reseller_id: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_400_BAD_REQUEST, | ||
| detail="reseller (or merchant for backward compatibility) is required", | ||
| ) | ||
| # RBAC: | ||
| # RBAC: Check permission to push leads for this reseller/shop | ||
| validate_lead_access( | ||
| current_user, req.merchant, req.identifier, operation="push leads for" | ||
| current_user, reseller_id, req.identifier, operation="push leads for" | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Duplicate reseller_id derivation and validation with push_lead_handler.
reseller_id is derived from req.reseller or req.merchant and validated for presence here (lines 86–92), then the exact same derivation and validation is repeated inside push_lead_handler (handlers.py lines 111, 120–124). This is redundant—either pass the already-validated reseller_id into the handler, or remove the check from one location.
Also, line 93–94 has a duplicate # RBAC: comment.
Proposed fix (remove duplicate comment)
- # RBAC:
- # RBAC: Check permission to push leads for this reseller/shop
+ # RBAC: Check permission to push leads for this reseller/shop📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Get reseller_id with backward compatibility | |
| reseller_id = req.reseller or req.merchant | |
| if not reseller_id: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="reseller (or merchant for backward compatibility) is required", | |
| ) | |
| # RBAC: | |
| # RBAC: Check permission to push leads for this reseller/shop | |
| validate_lead_access( | |
| current_user, req.merchant, req.identifier, operation="push leads for" | |
| current_user, reseller_id, req.identifier, operation="push leads for" | |
| ) | |
| # Get reseller_id with backward compatibility | |
| reseller_id = req.reseller or req.merchant | |
| if not reseller_id: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="reseller (or merchant for backward compatibility) is required", | |
| ) | |
| # RBAC: Check permission to push leads for this reseller/shop | |
| validate_lead_access( | |
| current_user, reseller_id, req.identifier, operation="push leads for" | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/routers/breeze_buddy/leads/__init__.py` around lines 85 - 97,
reseller_id is computed and validated twice (here and inside push_lead_handler);
remove the redundancy by computing and validating reseller_id once and passing
it into push_lead_handler (or vice versa): consolidate the logic so only one
place derives reseller_id = req.reseller or req.merchant and calls
validate_lead_access(current_user, reseller_id, req.identifier, operation="push
leads for"); update the push_lead_handler signature/usages to accept the
already-validated reseller_id (or remove its internal derivation/validation),
and delete the duplicate '# RBAC:' comment so the comment appears only once.
| """ | ||
| accessible_merchants = get_accessible_merchants(current_user.merchant_ids) | ||
| accessible_shops = get_accessible_shops(current_user.shop_identifiers) | ||
|
|
||
| # Determine merchant filter | ||
| merchant_filter = None | ||
| if accessible_merchants is not None: | ||
| # User has specific merchant access | ||
| if requested_merchant_id: | ||
| if requested_merchant_id not in accessible_merchants: | ||
| accessible_resellers = get_accessible_merchants(current_user.reseller_ids) | ||
| accessible_shops = get_accessible_shops(current_user.merchant_identifiers) |
There was a problem hiding this comment.
Same missing fallback in apply_merchant_shop_filter.
Lines 298–299 use only current_user.reseller_ids and current_user.merchant_identifiers without the or current_user.merchant_ids / or current_user.shop_identifiers fallback used elsewhere. Old JWTs will be treated as having no merchant/shop access.
Proposed fix
- accessible_resellers = get_accessible_merchants(current_user.reseller_ids)
- accessible_shops = get_accessible_shops(current_user.merchant_identifiers)
+ accessible_resellers = get_accessible_merchants(current_user.reseller_ids or current_user.merchant_ids)
+ accessible_shops = get_accessible_shops(current_user.merchant_identifiers or current_user.shop_identifiers)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| """ | |
| accessible_merchants = get_accessible_merchants(current_user.merchant_ids) | |
| accessible_shops = get_accessible_shops(current_user.shop_identifiers) | |
| # Determine merchant filter | |
| merchant_filter = None | |
| if accessible_merchants is not None: | |
| # User has specific merchant access | |
| if requested_merchant_id: | |
| if requested_merchant_id not in accessible_merchants: | |
| accessible_resellers = get_accessible_merchants(current_user.reseller_ids) | |
| accessible_shops = get_accessible_shops(current_user.merchant_identifiers) | |
| """ | |
| accessible_resellers = get_accessible_merchants(current_user.reseller_ids or current_user.merchant_ids) | |
| accessible_shops = get_accessible_shops(current_user.merchant_identifiers or current_user.shop_identifiers) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/security/breeze_buddy/authorization.py` around lines 297 - 299, The
calls in apply_merchant_shop_filter use current_user.reseller_ids and
current_user.merchant_identifiers directly, which omits the legacy JWT
fallbacks; update the calls to use the same fallback pattern used elsewhere
(e.g., use current_user.reseller_ids or current_user.merchant_ids for merchants,
and current_user.merchant_identifiers or current_user.shop_identifiers for
shops) so old tokens still grant access; locate the calls to
get_accessible_merchants and get_accessible_shops in apply_merchant_shop_filter
and change the argument expressions to include these OR fallbacks.
| reseller_id = row.get("reseller_id") or row.get("merchant_id") | ||
|
|
||
| if not reseller_id: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_400_BAD_REQUEST, | ||
| detail="reseller (or merchant for backward compatibility) is required", | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "HTTPException" --type=py -g '**/decoder/**'Repository: juspay/clairvoyance
Length of output: 705
🏁 Script executed:
rg -n "decode_lead_call_tracker" --type=py -C3Repository: juspay/clairvoyance
Length of output: 12547
🏁 Script executed:
rg -n "class LeadCallTracker" --type=py -A15Repository: juspay/clairvoyance
Length of output: 1189
🏁 Script executed:
cat -n app/database/decoder/breeze_buddy/lead_call_tracker.py | head -40Repository: juspay/clairvoyance
Length of output: 1361
🏁 Script executed:
rg -n "HTTPException" --type=py app/main.py app/api/ | head -20Repository: juspay/clairvoyance
Length of output: 1884
🏁 Script executed:
find . -name "main.py" -o -name "*exception*" -o -name "*handler*" | grep -E "(main|exception|handler)" | head -10Repository: juspay/clairvoyance
Length of output: 594
🏁 Script executed:
cat -n app/main.py | head -50Repository: juspay/clairvoyance
Length of output: 1884
🏁 Script executed:
cat -n app/main.py | grep -A10 "exception_handler"Repository: juspay/clairvoyance
Length of output: 45
🏁 Script executed:
rg -n "try.*decode_lead_call_tracker|except.*HTTPException" --type=pyRepository: juspay/clairvoyance
Length of output: 1437
🏁 Script executed:
cat -n app/api/routers/breeze_buddy/leads/handlers.py | sed -n '70,90p'Repository: juspay/clairvoyance
Length of output: 851
🏁 Script executed:
cat -n app/api/routers/breeze_buddy/leads/__init__.py | sed -n '160,180p'Repository: juspay/clairvoyance
Length of output: 907
🏁 Script executed:
rg -n "decode_lead_call_tracker|decode_call_execution_config|decode_template" --type=py app/ai/ app/core/background_tasks.py app/core/scheduler.py 2>/dev/null | head -20Repository: juspay/clairvoyance
Length of output: 45
🏁 Script executed:
cat -n app/database/decoder/breeze_buddy/call_execution_config.py | head -35Repository: juspay/clairvoyance
Length of output: 1232
Raising HTTPException in a decoder layer violates separation of concerns and uses a misleading status code.
This decoder function is in the database/data-access layer but raises a FastAPI HTTPException with HTTP 400 ("Bad Request"). This pattern is systematic—also found in call_execution_config.py and template.py. Two problems:
- Layer violation: Decoders should not depend on or emit HTTP-layer concerns. This couples the data layer to FastAPI and makes decoders unsuitable for use in non-HTTP contexts (tests, background tasks, CLI tools).
- Wrong semantics: The
LeadCallTrackerschema already definesreseller_idandmerchant_idasOptional[str] = None, yet the decoder forces both to be non-null. If a DB row lacks both, that's a data integrity issue—not a client error. HTTP 400 ("Bad Request") misleads the caller into thinking they sent an invalid request.
Raise a domain-specific exception (e.g., ValueError or custom DataIntegrityError) in the decoder and let the HTTP layer translate it, or remove the validation entirely and allow the schema's Optional field to remain None.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/database/decoder/breeze_buddy/lead_call_tracker.py` around lines 26 - 32,
The decoder in lead_call_tracker.py currently raises FastAPI's HTTPException
when reseller_id (from row.get("reseller_id") or row.get("merchant_id")) is
missing, which couples the data/access layer to HTTP; replace that HTTPException
with a domain-level error (e.g., raise ValueError or a custom
DataIntegrityError) or remove the check to allow the LeadCallTracker schema's
Optional[str] fields to remain None, and ensure any HTTP translation of that
domain error is handled in the HTTP layer (controller/handler) instead of in the
decoder.
| return OutboundNumber( | ||
| id=row["id"], | ||
| number=row["number"], | ||
| provider=CallProvider(row["provider"]), | ||
| status=OutboundNumberStatus(row["status"]), | ||
| channels=row["channels"], | ||
| maximum_channels=row["maximum_channels"], | ||
| merchant_id=row["merchant_id"], | ||
| shop_identifier=row["shop_identifier"], | ||
| reseller_id=reseller_id, | ||
| merchant_identifier=merchant_identifier, | ||
| created_at=row["created_at"], | ||
| updated_at=row["updated_at"], | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the OutboundNumber schema definition
cat -n app/schemas/breeze_buddy/core.py | sed -n '100,120p'Repository: juspay/clairvoyance
Length of output: 904
🏁 Script executed:
# Check the full decoder implementation to understand the context
cat -n app/database/decoder/breeze_buddy/outbound_number.pyRepository: juspay/clairvoyance
Length of output: 2505
🏁 Script executed:
# Run the provided command to find consumers of merchant_id or shop_identifier fields
rg -n --type=py '\.merchant_id\b|\.shop_identifier\b' -g '!*migration*' -g '!*decoder*' -C2Repository: juspay/clairvoyance
Length of output: 20067
🏁 Script executed:
# Search for direct accesses to OutboundNumber merchant_id or shop_identifier
# Look for patterns that DON'T use "or" (i.e., direct field reads without fallback)
rg -n 'number\.merchant_id|number\.shop_identifier' --type=py -C 2 | grep -v ' or 'Repository: juspay/clairvoyance
Length of output: 853
🏁 Script executed:
# Also check if any code constructs OutboundNumber with explicit merchant_id or shop_identifier assignment
rg -n 'OutboundNumber\(' --type=py -A 15Repository: juspay/clairvoyance
Length of output: 3928
Backward-compat fields merchant_id and shop_identifier are not populated by the decoder.
The OutboundNumber schema exposes merchant_id and shop_identifier as backward-compatible aliases (lines 108-113 in app/schemas/breeze_buddy/core.py), but the decoder only assigns values to reseller_id and merchant_identifier. The old field names remain None on all decoded objects.
While internal code currently uses fallback patterns (config.reseller_id or config.merchant_id), any direct reads of the old field names will incorrectly see null values. Populate the backward-compat fields explicitly in the decoder, or remove them from the schema if the migration to new names is complete.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/database/decoder/breeze_buddy/outbound_number.py` around lines 26 - 37,
The decoder that constructs OutboundNumber returns reseller_id and
merchant_identifier but does not set the backward-compatible fields merchant_id
and shop_identifier; update the OutboundNumber construction in the decoder (the
return that creates OutboundNumber(...)) to also populate
merchant_id=reseller_id and shop_identifier=merchant_identifier so the old
aliases are present on decoded objects (keep existing reseller_id and
merchant_identifier assignments unchanged).
| if merchant_identifier: | ||
| values.append(merchant_identifier) | ||
| merchant_identifier_param = param_count | ||
| where_clause = f'"merchant_id" = ${reseller_id_param} AND "template" = ${template_param} AND "shop_identifier" = ${merchant_identifier_param}' | ||
| else: | ||
| where_clause = f'"merchant_id" = ${merchant_id_param} AND "template" = ${template_param} AND "shop_identifier" IS NULL' | ||
| where_clause = f'"merchant_id" = ${reseller_id_param} AND "template" = ${template_param} AND "shop_identifier" IS NULL' |
There was a problem hiding this comment.
update_call_execution_config_query WHERE clause doesn't use COALESCE — inconsistent with other queries.
Lines 223–225 filter on the old column names ("merchant_id", "shop_identifier") directly, while every other query in this file (e.g., calling_activation_for_merchant_query at line 263, get_call_execution_config_by_merchant_id_query at line 94) uses COALESCE(reseller_id, merchant_id) / COALESCE(merchant_identifier, shop_identifier).
This will fail to match rows where only the new columns (reseller_id, merchant_identifier) are populated and the old columns are NULL. Currently safe because insert_call_execution_config_query writes both old and new columns, but this is fragile and inconsistent.
Proposed fix
if merchant_identifier:
values.append(merchant_identifier)
merchant_identifier_param = param_count
- where_clause = f'"merchant_id" = ${reseller_id_param} AND "template" = ${template_param} AND "shop_identifier" = ${merchant_identifier_param}'
+ where_clause = f'COALESCE("reseller_id", "merchant_id") = ${reseller_id_param} AND "template" = ${template_param} AND COALESCE("merchant_identifier", "shop_identifier") = ${merchant_identifier_param}'
else:
- where_clause = f'"merchant_id" = ${reseller_id_param} AND "template" = ${template_param} AND "shop_identifier" IS NULL'
+ where_clause = f'COALESCE("reseller_id", "merchant_id") = ${reseller_id_param} AND "template" = ${template_param} AND COALESCE("merchant_identifier", "shop_identifier") IS NULL'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/database/queries/breeze_buddy/call_execution_config.py` around lines 220
- 225, The WHERE clause in update_call_execution_config_query currently compares
the old columns directly ("merchant_id" and "shop_identifier"); change it to use
the COALESCE expressions used elsewhere so rows with only the new columns match:
replace references to "merchant_id" with COALESCE(reseller_id, merchant_id) and
"shop_identifier" with COALESCE(merchant_identifier, shop_identifier). When
merchant_identifier is provided, compare COALESCE(merchant_identifier,
shop_identifier) = ${merchant_identifier_param} (using the existing
merchant_identifier_param); when merchant_identifier is None, use
COALESCE(merchant_identifier, shop_identifier) IS NULL. Ensure you still use
reseller_id_param and template_param for the other comparisons.
| reseller_id: Optional[str] = None | ||
| merchant_id: Optional[str] = ( | ||
| None # Backward compatibility: same as reseller_id (will be removed) | ||
| ) | ||
| merchant_identifier: Optional[str] = None | ||
| shop_identifier: Optional[str] = ( | ||
| None # Backward compatibility: same as merchant_identifier (will be removed) | ||
| ) |
There was a problem hiding this comment.
Dual backward-compat fields have no conflict-resolution validator — ambiguous when both are supplied
Every write model (CreateOutboundNumberRequest, CreateCallExecutionConfigRequest, UpdateCallExecutionConfigRequest) and CallExecutionConfig now expose two aliased pairs: reseller_id/merchant_id and merchant_identifier/shop_identifier. If a caller (old or new) populates both aliases with different values there is no @model_validator to canonicalise the pair, so downstream handlers that read only one field silently use whichever was set.
A root-level validator that coalesces the pairs at parse time would eliminate this ambiguity:
from pydantic import model_validator
`@model_validator`(mode="after")
def _coalesce_backward_compat_fields(self) -> "CreateOutboundNumberRequest":
# Prefer new field; fall back to legacy alias
if self.reseller_id is None and self.merchant_id is not None:
self.reseller_id = self.merchant_id
if self.merchant_identifier is None and self.shop_identifier is not None:
self.merchant_identifier = self.shop_identifier
return selfApply the same pattern to OutboundNumber, CreateCallExecutionConfigRequest, UpdateCallExecutionConfigRequest, and CallExecutionConfig.
Also applies to: 107-114, 128-132, 140-144, 165-169
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/schemas/breeze_buddy/core.py` around lines 88 - 95, Add a root-level
Pydantic model validator named _coalesce_backward_compat_fields (use
`@model_validator`(mode="after")) to each of the affected classes (OutboundNumber,
CreateOutboundNumberRequest, CreateCallExecutionConfigRequest,
UpdateCallExecutionConfigRequest, CallExecutionConfig) that canonicalizes the
aliased pairs by preferring the new fields: if reseller_id is None and
merchant_id is set, copy merchant_id into reseller_id; if merchant_identifier is
None and shop_identifier is set, copy shop_identifier into merchant_identifier;
return self. This ensures reseller_id/merchant_id and
merchant_identifier/shop_identifier are coalesced at parse time and prevents
ambiguous conflicting inputs.
Summary by CodeRabbit
Refactor
merchant_id→reseller_idandshop_identifier→merchant_identifierDatabase