Skip to content

Support for initiate call using process tracker#192

Merged
swaroopvarma1 merged 1 commit intojuspay:releasefrom
badri-singhal:Support-for-initiate-call-using-process-tracker
Sep 11, 2025
Merged

Support for initiate call using process tracker#192
swaroopvarma1 merged 1 commit intojuspay:releasefrom
badri-singhal:Support-for-initiate-call-using-process-tracker

Conversation

@badri-singhal
Copy link
Contributor

@badri-singhal badri-singhal commented Sep 10, 2025

Summary by CodeRabbit

  • New Features
    • Lead-based call scheduling with automatic retries and outcome tracking.
    • Cron initiation endpoint to process call backlogs asynchronously.
    • Dynamic outbound number selection per provider for improved call capacity.
  • API Changes
    • Order confirmation now queues a lead and returns a tracker ID with next-attempt time.
    • New endpoints to create and fetch call-execution configurations.
    • WebSocket telephony flow streamlined with built-in completion handling.
  • Bug Fixes
    • Clearer errors and explicit failure handling during call initiation.
  • Refactor
    • Transitioned from call-data to a lead-tracker model in responses and workflows.

@coderabbitai
Copy link

coderabbitai bot commented Sep 10, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Replaces call_data with lead_call_tracker across DB schemas, queries, accessors, and decoders. Updates providers to accept a customer number string. Introduces a cron-driven backlog processor and completion handler. Refactors API endpoints and WebSocket flow to the new lead-tracker model. Removes CallQueueManager and related main/app wiring.

Changes

Cohort / File(s) Summary of Changes
Lead tracker model, queries, accessors
app/schemas.py, app/database/queries/breeze_buddy/lead_call_tracker.py, app/database/decoder/breeze_buddy/lead_call_tracker.py, app/database/accessor/breeze_buddy/lead_call_tracker.py, app/database/accessor/main.py, app/scripts/create_tables.py
Adds LeadCallTracker schema/enums; introduces lead_call_tracker SQL builders, decoder, and accessors; updates main accessor exports; adds table creation (and indexes).
Remove call_data stack
app/database/queries/breeze_buddy/call_data.py, app/database/accessor/breeze_buddy/call_data.py, app/database/accessor/decoder.py, app/scripts/create_tables.py (call_data parts)
Deletes call_data queries, accessors, and shared decoder; removes call_data table creation.
Provider API changes
app/agents/voice/breeze_buddy/call_providers/main.py, app/agents/voice/breeze_buddy/call_providers/exotel.py, app/agents/voice/breeze_buddy/call_providers/twillio.py
Changes abstract and concrete make_call(...) to accept customer_mobile_number: str; adjusts payload/usage; Twilio adds explicit return dict and HTTPException on failure.
Cron processing & completion
app/agents/voice/breeze_buddy/cron_manager.py
Adds process_backlog_leads and handle_call_completion with provider resolution, number selection, call initiation, status/outcome updates, and retry scheduling.
API refactor to lead tracker
app/api/routers/breeze_buddy.py
Reworks order confirmation to create lead_call_tracker; adds POST /cron/initiate; updates WebSocket telephony handler to resolve providers and attach completion handler; removes CallQueueManager dependencies.
WebSocket bot data source switch
app/agents/voice/breeze_buddy/breeze/order_confirmation/websocket_bot.py
Switches from get_call_data_by_call_id to get_lead_by_call_id; reads lead.payload; adjusts completion call to omit call_end_time.
Decoders import relocation
app/database/accessor/breeze_buddy/call_execution_config.py, app/database/accessor/breeze_buddy/outbound_number.py, app/database/decoder/breeze_buddy/call_execution_config.py, app/database/decoder/breeze_buddy/outbound_number.py
Moves decoding to new decoder modules; adds decoder implementations for call_execution_config and outbound_number.
Remove CallQueueManager wiring
app/services/call_queue_manager.py, app/main.py
Removes CallQueueManager service and its app wiring (global, DI override, getter).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant API as API (POST /order-confirmation)
  participant DB as DB (lead_call_tracker)
  participant Cfg as DB (call_execution_config)

  API->>Cfg: get config for merchant/workflow
  alt config found
    API->>API: compute next_attempt_at (initial_offset)
    API->>DB: create lead_call_tracker (BACKLOG, payload)
    DB-->>API: lead id
    API-->>API: respond queued with lead id
  else no config
    API-->>API: 404 Not Found
  end
Loading
sequenceDiagram
  autonumber
  participant Cron as Cron (process_backlog_leads)
  participant DB as DB (lead_call_tracker, outbound_numbers)
  participant Prov as Voice Provider
  participant Tel as Telephony

  Cron->>DB: get BACKLOG leads <= now
  loop per lead
    Cron->>DB: get merchant call config
    Cron->>DB: select outbound number (by provider/channels)
    Cron->>DB: mark number IN_USE
    Cron->>Prov: resolve provider (aiohttp session)
    Cron->>Prov: make_call(customer_mobile_number)
    Prov->>Tel: initiate call
    Tel-->>Prov: sid
    Prov-->>Cron: {status, sid}
    Cron->>DB: update lead (PROCESSING, call_id)
  end
Loading
sequenceDiagram
  autonumber
  participant WS as WebSocket (telephony)
  participant Bot as WebsocketBot
  participant DB as DB (lead_call_tracker)
  participant Cron as handle_call_completion

  WS->>Bot: events (transcripts, status)
  Bot->>DB: get_lead_by_call_id(call_sid)
  alt lead found
    Bot->>Cron: handle_call_completion(call_id, outcome, transcription)
    Cron->>DB: update lead (release number, FINISHED/outcome)
    alt outcome BUSY/NO_ANSWER and retry allowed
      Cron->>DB: schedule RETRY (increment attempt_count, next_attempt_at)
    end
  else lead missing
    Bot-->>WS: log/close
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • murdore

Poem

I thump my paw—new tracks we lay,
From queues to leads, we hop away.
A cron bell rings, the calls take flight,
Providers dial into the night.
When websockets whisper, outcomes gleam—
A hare-brained hop to a tidier stream. 🐇📞

✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
app/agents/voice/breeze_buddy/call_providers/twillio.py (2)

33-50: Validate E.164 input and map Twilio errors to accurate HTTP status codes

Preempt bad inputs and surface TwilioRestException.status instead of a blanket 400.

-from fastapi import WebSocket, HTTPException
+from fastapi import WebSocket, HTTPException
+from twilio.base.exceptions import TwilioRestException
@@
-    def make_call(self, customer_mobile_number: str):
+    def make_call(self, customer_mobile_number: str):
         ws_url = self.config.TWILIO_WEBSOCKET_URL
@@
-        try:
+        try:
+            if not isinstance(customer_mobile_number, str) or not customer_mobile_number.strip():
+                raise HTTPException(status_code=422, detail="customer_mobile_number is required")
+            if not customer_mobile_number.startswith("+"):
+                # Twilio expects E.164; enforce to reduce provider errors.
+                raise HTTPException(status_code=422, detail="customer_mobile_number must be E.164 (+<countrycode><number>)")
             call = self.client.calls.create(
                 to=customer_mobile_number,
                 from_=self.config.TWILIO_FROM_NUMBER,
                 twiml=str(voice_call_payload)
             )
             return {"status": "call_initiated", "sid": call.sid}
-        except Exception as e:
-            raise HTTPException(status_code=400, detail=str(e))
+        except TwilioRestException as e:
+            # Preserve provider status if available; default to 502 (upstream error).
+            raise HTTPException(status_code=(e.status or 502), detail=e.msg)
+        except HTTPException:
+            raise
+        except Exception as e:
+            raise HTTPException(status_code=500, detail=f"Unexpected error: {e}")

14-16: Ensure hangup_callback is invoked on all exit paths
Overriding _hang_up_call disables serializer-driven hangups, but telephony_websocket_conn only calls your hangup_function in the end‐conversation handler. Early returns on JSON parse failures, missing lead, invalid price/order data, and the on_client_disconnected event never invoke hangup_function, risking orphaned calls. Add explicit hangup_function(self.call_sid) in those branches or wrap run() in a top‐level try/finally to guarantee hang‐up on every terminal path.

app/database/accessor/breeze_buddy/outbound_number.py (1)

36-37: Avoid logging full objects/phone numbers (PII).

Current info logs may include the raw outbound number. Prefer logging identifiers and metadata only, or mask numbers.

Example change:

-logger.info(f"Outbound number created successfully: {decoded_result}")
+logger.info("Outbound number created successfully: id=%s provider=%s status=%s",
+            decoded_result.id, decoded_result.provider, decoded_result.status)

Helper (outside diff) you can add once:

def mask_msisdn(num: str) -> str:
    return f"{'*' * (len(num)-4)}{num[-4:]}" if num else num

Also applies to: 51-52, 73-74, 95-96, 117-118, 139-140, 161-162

app/agents/voice/breeze_buddy/call_providers/exotel.py (1)

25-41: Don’t embed API credentials in the URL; add timeouts; avoid logging PII.

  • Credentials in the URL can leak via proxies/logs. Use HTTP Basic Auth.
  • Add an explicit timeout to prevent hangs.
  • Mask the customer number in logs.

Apply:

@@
-from fastapi import WebSocket, HTTPException
+from fastapi import WebSocket, HTTPException
+from requests.auth import HTTPBasicAuth
@@
-        flow_url = f"http://my.exotel.com/{self.config.EXOTEL_ACCOUNT_SID}/exoml/start_voice/{self.config.EXOTEL_APPLET_APP_ID}"
+        flow_url = f"https://my.exotel.com/{self.config.EXOTEL_ACCOUNT_SID}/exoml/start_voice/{self.config.EXOTEL_APPLET_APP_ID}"
@@
-        payload = {
-            "From": customer_mobile_number,
+        masked = f"{'*' * (len(customer_mobile_number)-4)}{customer_mobile_number[-4:]}" if customer_mobile_number and len(customer_mobile_number) > 4 else customer_mobile_number
+        payload = {
+            "From": customer_mobile_number,
             "CallerId": self.config.EXOTEL_FROM_NUMBER,
             "Url": flow_url
         }
-        url = f"https://{self.config.EXOTEL_API_KEY}:{self.config.EXOTEL_API_TOKEN}@{self.config.EXOTEL_SUBDOMAIN}/v1/Accounts/{self.config.EXOTEL_ACCOUNT_SID}/Calls/connect.json"
+        url = f"https://{self.config.EXOTEL_SUBDOMAIN}/v1/Accounts/{self.config.EXOTEL_ACCOUNT_SID}/Calls/connect.json"
@@
-        logger.info(f"Making Exotel API call to: {self.config.EXOTEL_SUBDOMAIN}")
-        logger.info(f"Payload: {payload}")
+        logger.info(f"Making Exotel API call to: {self.config.EXOTEL_SUBDOMAIN}")
+        logger.info(f"Payload: {{'From': '{masked}', 'CallerId': '***', 'Url': '...'}}")
@@
-            resp = requests.post(url, data=payload)
+            resp = requests.post(
+                url,
+                data=payload,
+                auth=HTTPBasicAuth(self.config.EXOTEL_API_KEY, self.config.EXOTEL_API_TOKEN),
+                timeout=getattr(self.config, 'EXOTEL_TIMEOUT', 10),
+            )
app/api/routers/breeze_buddy.py (2)

124-126: Bug: Enum compared to string always triggers 404.

Compare to the enum value.

-    if identity != "breeze":
+    if identity != RequestedBy.BREEZE:
         raise HTTPException(status_code=404, detail="Feature not supported")

139-150: Bug: payload not JSON-serializable due to Enum instance.

Use identity.value.

-            "identity": identity,
+            "identity": identity.value,
🧹 Nitpick comments (24)
app/agents/voice/breeze_buddy/call_providers/main.py (1)

22-27: Clarify make_call contract (return type + E.164 expectation) and keep providers consistent

Recommend locking down the interface: declare the return type and document number format and failure mode so Exotel/Twilio behave uniformly.

 from abc import ABC, abstractmethod
 from fastapi import WebSocket
+from typing import Dict, Any
 
@@
     @abstractmethod
-    def make_call(self, customer_mobile_number: str):
+    def make_call(self, customer_mobile_number: str) -> Dict[str, Any]:
         """
-        Initiate a call.
+        Initiate a call to the given E.164-formatted number (e.g., +14155552671).
+        Returns a dict like {"status": "call_initiated", "sid": "<provider-call-id>"} on success.
+        Implementations SHOULD raise HTTPException on failure, with an appropriate status code.
         """
         pass
app/agents/voice/breeze_buddy/call_providers/twillio.py (1)

21-22: Harden hangup_call with basic error handling

Twilio SDK can raise; avoid silent failures.

     def hangup_call(self, call_sid: str):
-        self.client.calls(call_sid).update(status="completed")
+        try:
+            self.client.calls(call_sid).update(status="completed")
+        except Exception as e:
+            logger.error(f"Failed to hang up Twilio call {call_sid}: {e}")
+            # Optional: surface to caller
+            # raise HTTPException(status_code=502, detail="Failed to hang up call")
app/schemas.py (3)

58-58: Normalize metaData to snake_case with alias

Keep JSON compatibility while making the Python field idiomatic.

-    metaData: Optional[Dict[str, Any]] = None
+    from pydantic import Field
+    meta_data: Optional[Dict[str, Any]] = Field(default=None, alias="metaData")

64-67: Specify units for call_duration and currency for cost

Ambiguity here causes downstream bugs and reporting errors. Add docstrings or rename to call_duration_seconds and cost_in_minor_units (or document currency).


24-29: Consolidate status/outcome enums to avoid drift

  • Migrate all uses of CallStatus/CallOutcome to LeadCallStatus/LeadCallOutcome (e.g. in app/agents/voice/breeze_buddy/breeze/order_confirmation/utils.py, websocket_bot.py, cron_manager.py and related database accessor/query modules).
  • In app/schemas.py, add aliases or deprecation warnings for CallStatus/CallOutcome pointing to the new enums, then phase out the old ones once all references have been updated.
  • Ensure tests and any serialization logic are updated to use the unified enums.
app/database/accessor/breeze_buddy/call_execution_config.py (1)

16-20: De-duplicate get_row_count helper across accessors.

This helper appears in multiple accessors; consider centralizing in a shared db utils module.

app/agents/voice/breeze_buddy/call_providers/exotel.py (1)

50-61: Unify success response shape.

Elsewhere (Twilio) returns {"status": "call_initiated", "sid": ...}. When Exotel body is empty or Sid missing, consider consistent "status" values to simplify callers.

app/database/decoder/breeze_buddy/lead_call_tracker.py (1)

9-35: Add a list decoder for batch queries.

Many accessors use list decoders; add parity for LeadCallTracker.

Example:

def decode_lead_call_tracker_list(result: List[asyncpg.Record]) -> List[LeadCallTracker]:
    if not result:
        return []
    return [decode_lead_call_tracker([row]) for row in result]
app/agents/voice/breeze_buddy/breeze/order_confirmation/websocket_bot.py (2)

121-127: Close WS when lead is missing to avoid dangling connection

If lead lookup fails, also close the websocket with a reason, mirroring other error paths.

-        lead = await get_lead_by_call_id(self.call_sid)
-        if not lead:
-            logger.error(f"Could not find lead for call_sid: {self.call_sid}")
-            return
+        lead = await get_lead_by_call_id(self.call_sid)
+        if not lead:
+            logger.error(f"Could not find lead for call_sid: {self.call_sid}")
+            try:
+                if self.ws.client_state.name != "DISCONNECTED":
+                    await self.ws.close(code=4000, reason="Lead not found")
+            except Exception as close_error:
+                logger.warning(f"Could not close websocket (likely already closed): {close_error}")
+            return

271-276: Pass call_end_time on disconnect for parity with normal completion

Keep the DB/completion payloads consistent in both code paths.

                         await self.completion_function(
                             call_id=self.call_sid,
                             outcome=CallOutcome.BUSY,
-                            transcription={"messages": transcription, "call_sid": self.call_sid}
+                            transcription={"messages": transcription, "call_sid": self.call_sid},
+                            call_end_time=datetime.now().isoformat()
                         )
-                        logger.info(f"Updated database for call_id: {self.call_sid} with outcome: INTERRUPTED")
+                        logger.info(f"Updated database for call_id: {self.call_sid} with outcome: BUSY")
app/database/decoder/breeze_buddy/outbound_number.py (1)

27-46: Prefer list decoder usage in accessors when multiple rows are expected

If upstream queries can return multiple rows, ensure the accessor uses decode_outbound_number_list rather than decode_outbound_number to avoid accidental truncation to the first row.

Would you like me to patch the accessor to consume decode_outbound_number_list and return List[OutboundNumber]?

app/scripts/create_tables.py (3)

68-86: Optional: strengthen outbound_number constraints

Consider CHECKs for non-negative channels and channels <= maximum_channels to prevent invalid states.

         CREATE TABLE IF NOT EXISTS "{OUTBOUND_NUMBERS_TABLE}" (
             "id" VARCHAR(255) PRIMARY KEY,
             "number" VARCHAR(20) NOT NULL UNIQUE,
             "provider" VARCHAR(50) CHECK ("provider" IN ('TWILIO', 'EXOTEL')) NOT NULL,
             "status" VARCHAR(50) CHECK ("status" IN ('AVAILABLE', 'IN_USE', 'DISABLED')) NOT NULL,
-            "channels" INTEGER,
-            "maximum_channels" INTEGER,
+            "channels" INTEGER,
+            "maximum_channels" INTEGER,
+            CHECK (channels IS NULL OR channels >= 0),
+            CHECK (maximum_channels IS NULL OR maximum_channels >= 0),
+            CHECK (
+              channels IS NULL OR maximum_channels IS NULL OR channels <= maximum_channels
+            ),

116-129: Narrow exception handling or log tracebacks

Catching bare Exception hides actionable errors; at least log stack traces with traceback.print_exc() or narrow to DB exceptions.

-    except Exception as e:
-        print(f"Error creating lead_call_tracker table: {e}")
+    except Exception as e:
+        import traceback
+        print(f"Error creating lead_call_tracker table: {e}")
+        traceback.print_exc()
         return False

141-146: Stop on partial failures to simplify ops

If a table creation fails, consider short-circuiting rather than continuing; avoids half-initialized states.

-        if outbound_numbers_success and call_execution_config_success and lead_call_tracker_success:
+        if outbound_numbers_success and call_execution_config_success and lead_call_tracker_success:
             print("All database tables created successfully")
             return True
         else:
             print("Failed to create some database tables")
-            return False
+            return False

(Alternatively, raise after printing to fail fast.)

app/agents/voice/breeze_buddy/cron_manager.py (3)

26-31: Narrow broad exception handlers and log tracebacks

Catching bare Exception twice hides actionable failures; at least log stack traces with logger.exception.

-        try:
+        try:
             leads = await get_leads_based_on_status_and_next_attempt(LeadCallStatus.BACKLOG, datetime.now(timezone.utc))
@@
-                except Exception as e:
-                    logger.error(f"Error processing lead {lead.id}: {e}")
+                except Exception:
+                    logger.exception(f"Error processing lead {lead.id}")
@@
-        except Exception as e:
-            logger.error(f"Error processing backlog leads: {e}")
+        except Exception:
+            logger.exception("Error processing backlog leads")

Also applies to: 67-71


73-73: Unused parameter: transcription

If not needed, rename to _transcription or store it with the lead for auditing.


21-27: Aiohttp session is unused by providers

Providers use requests/Twilio SDK; the aiohttp session is currently unused. Either switch providers to async aiohttp or drop the session wrapper.

app/database/accessor/breeze_buddy/lead_call_tracker.py (2)

55-62: Minor: collapse the post-query error branch into an else.

Improves readability.

-        if result and get_row_count(result) > 0:
+        if result and get_row_count(result) > 0:
             decoded_result = decode_lead_call_tracker(result)
             logger.info(f"Lead call tracker created successfully: {decoded_result}")
             return decoded_result
-        
-        logger.error("Failed to create lead call tracker")
-        return None
+        else:
+            logger.error("Failed to create lead call tracker")
+            return None

64-66: Narrow exception handling and log tracebacks.

Catch Exception is broad; at least add exc_info=True to preserve stack traces.

-    except Exception as e:
-        logger.error(f"Error creating lead call tracker: {e}")
+    except Exception as e:
+        logger.error(f"Error creating lead call tracker: {e}", exc_info=True)
         return None

Apply similarly to other try/except blocks in this file.

Also applies to: 80-82, 101-104, 122-125, 141-145

app/database/queries/breeze_buddy/lead_call_tracker.py (2)

5-5: Use timezone-aware timestamps for created_at/updated_at.

Prevents mixed naive/aware datetime issues.

-from datetime import datetime
+from datetime import datetime, timezone
...
-        datetime.now(),
-        datetime.now()
+        datetime.now(timezone.utc),
+        datetime.now(timezone.utc)

Also applies to: 57-58


67-73: Order backlog fetch by next_attempt_at.

Deterministic processing and better fairness.

     text = f"""
         SELECT * FROM "{LEAD_CALL_TRACKER_TABLE}"
         WHERE "status" = $1
-        AND "next_attempt_at" <= $2;
+        AND "next_attempt_at" <= $2
+        ORDER BY "next_attempt_at" ASC;
     """
app/api/routers/breeze_buddy.py (3)

244-245: Fix log message.

Wrong context string.

-        logger.error("Error disabling outbound number", exc_info=True)
+        logger.error("Error adding call execution config", exc_info=True)

247-258: Consider POST for cron initiation.

Side-effecting action; GET is semantically odd. Keep GET if you rely on idempotent triggers, otherwise switch to POST.


200-209: Broadened exception handling; consider special-casing known errors.

Handle ValueError from unsupported providers separately for clearer logs.

-        except Exception as e:
+        except ValueError as e:
+            logger.error(f"Unsupported provider: {e}")
+            try:
+                if websocket.client_state.name != "DISCONNECTED":
+                    await websocket.close(code=1003, reason="Unsupported provider")
+            except Exception as close_error:
+                logger.warning(f"Could not close websocket (likely already closed): {close_error}")
+        except Exception as e:
             error_type = type(e).__name__
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b79dd1 and 37ce394.

📒 Files selected for processing (21)
  • app/agents/voice/breeze_buddy/breeze/order_confirmation/websocket_bot.py (3 hunks)
  • app/agents/voice/breeze_buddy/call_providers/exotel.py (1 hunks)
  • app/agents/voice/breeze_buddy/call_providers/main.py (1 hunks)
  • app/agents/voice/breeze_buddy/call_providers/twillio.py (2 hunks)
  • app/agents/voice/breeze_buddy/cron_manager.py (1 hunks)
  • app/api/routers/breeze_buddy.py (6 hunks)
  • app/database/accessor/breeze_buddy/call_data.py (0 hunks)
  • app/database/accessor/breeze_buddy/call_execution_config.py (1 hunks)
  • app/database/accessor/breeze_buddy/lead_call_tracker.py (1 hunks)
  • app/database/accessor/breeze_buddy/outbound_number.py (1 hunks)
  • app/database/accessor/decoder.py (0 hunks)
  • app/database/accessor/main.py (2 hunks)
  • app/database/decoder/breeze_buddy/call_execution_config.py (1 hunks)
  • app/database/decoder/breeze_buddy/lead_call_tracker.py (1 hunks)
  • app/database/decoder/breeze_buddy/outbound_number.py (1 hunks)
  • app/database/queries/breeze_buddy/call_data.py (0 hunks)
  • app/database/queries/breeze_buddy/lead_call_tracker.py (1 hunks)
  • app/main.py (0 hunks)
  • app/schemas.py (4 hunks)
  • app/scripts/create_tables.py (3 hunks)
  • app/services/call_queue_manager.py (0 hunks)
💤 Files with no reviewable changes (5)
  • app/database/accessor/breeze_buddy/call_data.py
  • app/database/queries/breeze_buddy/call_data.py
  • app/main.py
  • app/services/call_queue_manager.py
  • app/database/accessor/decoder.py
🧰 Additional context used
🧬 Code graph analysis (15)
app/database/decoder/breeze_buddy/call_execution_config.py (1)
app/schemas.py (3)
  • Workflow (34-35)
  • CallExecutionConfig (95-106)
  • CallProvider (14-16)
app/database/accessor/breeze_buddy/call_execution_config.py (1)
app/database/decoder/breeze_buddy/call_execution_config.py (2)
  • decode_call_execution_config (32-52)
  • decode_call_execution_config_list (8-30)
app/database/accessor/breeze_buddy/outbound_number.py (1)
app/database/decoder/breeze_buddy/outbound_number.py (2)
  • decode_outbound_number (8-25)
  • decode_outbound_number_list (27-46)
app/database/decoder/breeze_buddy/lead_call_tracker.py (2)
app/schemas.py (1)
  • LeadCallTracker (50-67)
app/utils/common.py (1)
  • parse_json (49-50)
app/database/decoder/breeze_buddy/outbound_number.py (1)
app/schemas.py (3)
  • OutboundNumber (75-83)
  • OutboundNumberStatus (8-11)
  • CallProvider (14-16)
app/agents/voice/breeze_buddy/call_providers/exotel.py (2)
app/agents/voice/breeze_buddy/call_providers/main.py (1)
  • make_call (23-27)
app/agents/voice/breeze_buddy/call_providers/twillio.py (1)
  • make_call (33-50)
app/agents/voice/breeze_buddy/cron_manager.py (7)
app/database/accessor/breeze_buddy/lead_call_tracker.py (5)
  • get_leads_based_on_status_and_next_attempt (68-82)
  • update_lead_status_and_call_id (84-103)
  • get_lead_by_call_id (105-124)
  • update_lead_status_and_outcome (126-145)
  • create_lead_call_tracker (26-66)
app/database/accessor/breeze_buddy/call_execution_config.py (1)
  • get_call_execution_config_by_merchant_id (64-84)
app/database/accessor/breeze_buddy/outbound_number.py (2)
  • get_outbound_number_based_on_status_and_provider (149-169)
  • update_outbound_number_status (83-103)
app/schemas.py (3)
  • LeadCallStatus (37-41)
  • OutboundNumberStatus (8-11)
  • LeadCallOutcome (43-48)
app/agents/voice/breeze_buddy/call_providers/factory.py (1)
  • get_voice_provider (5-10)
app/agents/voice/breeze_buddy/call_providers/exotel.py (1)
  • make_call (24-77)
app/agents/voice/breeze_buddy/call_providers/twillio.py (1)
  • make_call (33-50)
app/database/queries/breeze_buddy/lead_call_tracker.py (1)
app/schemas.py (4)
  • Workflow (34-35)
  • LeadCallStatus (37-41)
  • LeadCallOutcome (43-48)
  • RequestedBy (30-32)
app/database/accessor/main.py (1)
app/database/accessor/breeze_buddy/lead_call_tracker.py (5)
  • create_lead_call_tracker (26-66)
  • get_leads_based_on_status_and_next_attempt (68-82)
  • update_lead_status_and_call_id (84-103)
  • get_lead_by_call_id (105-124)
  • update_lead_status_and_outcome (126-145)
app/agents/voice/breeze_buddy/call_providers/twillio.py (2)
app/agents/voice/breeze_buddy/call_providers/exotel.py (1)
  • make_call (24-77)
app/agents/voice/breeze_buddy/call_providers/main.py (1)
  • make_call (23-27)
app/database/accessor/breeze_buddy/lead_call_tracker.py (4)
app/schemas.py (5)
  • LeadCallTracker (50-67)
  • LeadCallStatus (37-41)
  • Workflow (34-35)
  • RequestedBy (30-32)
  • LeadCallOutcome (43-48)
app/database/queries/main.py (1)
  • run_parameterized_query (10-24)
app/database/decoder/breeze_buddy/lead_call_tracker.py (1)
  • decode_lead_call_tracker (9-35)
app/database/queries/breeze_buddy/lead_call_tracker.py (5)
  • insert_lead_call_tracker_query (13-61)
  • get_leads_based_on_status_and_next_attempt_query (63-73)
  • update_lead_status_and_call_id_query (75-86)
  • get_lead_by_call_id_query (88-97)
  • update_lead_status_and_outcome_query (99-110)
app/scripts/create_tables.py (1)
app/database/__init__.py (1)
  • get_db_connection (44-52)
app/agents/voice/breeze_buddy/call_providers/main.py (2)
app/agents/voice/breeze_buddy/call_providers/exotel.py (1)
  • make_call (24-77)
app/agents/voice/breeze_buddy/call_providers/twillio.py (1)
  • make_call (33-50)
app/agents/voice/breeze_buddy/breeze/order_confirmation/websocket_bot.py (1)
app/database/accessor/breeze_buddy/lead_call_tracker.py (1)
  • get_lead_by_call_id (105-124)
app/api/routers/breeze_buddy.py (10)
app/core/security/jwt.py (1)
  • get_current_user (121-141)
app/schemas.py (3)
  • TokenData (126-131)
  • RequestedBy (30-32)
  • Workflow (34-35)
app/agents/voice/breeze_buddy/breeze/order_confirmation/types.py (1)
  • BreezeOrderData (11-20)
app/agents/voice/breeze_buddy/cron_manager.py (2)
  • process_backlog_leads (21-71)
  • handle_call_completion (73-106)
app/agents/voice/breeze_buddy/call_providers/factory.py (1)
  • get_voice_provider (5-10)
app/database/accessor/breeze_buddy/call_execution_config.py (1)
  • get_call_execution_config_by_merchant_id (64-84)
app/database/accessor/breeze_buddy/lead_call_tracker.py (1)
  • create_lead_call_tracker (26-66)
app/agents/voice/breeze_buddy/call_providers/main.py (2)
  • set_completion_callback (29-33)
  • handle_websocket (16-20)
app/agents/voice/breeze_buddy/call_providers/exotel.py (1)
  • handle_websocket (17-22)
app/agents/voice/breeze_buddy/call_providers/twillio.py (1)
  • handle_websocket (24-31)
🪛 Ruff (0.12.2)
app/agents/voice/breeze_buddy/cron_manager.py

67-67: Do not catch blind exception: Exception

(BLE001)


70-70: Do not catch blind exception: Exception

(BLE001)


73-73: Unused function argument: transcription

(ARG001)

app/database/queries/breeze_buddy/lead_call_tracker.py

27-44: Possible SQL injection vector through string-based query construction

(S608)


67-71: Possible SQL injection vector through string-based query construction

(S608)


79-84: Possible SQL injection vector through string-based query construction

(S608)


92-95: Possible SQL injection vector through string-based query construction

(S608)


103-108: Possible SQL injection vector through string-based query construction

(S608)

app/database/accessor/breeze_buddy/lead_call_tracker.py

62-62: Consider moving this statement to an else block

(TRY300)


64-64: Do not catch blind exception: Exception

(BLE001)


79-79: Consider moving this statement to an else block

(TRY300)


80-80: Do not catch blind exception: Exception

(BLE001)


99-99: Consider moving this statement to an else block

(TRY300)


101-101: Do not catch blind exception: Exception

(BLE001)


120-120: Consider moving this statement to an else block

(TRY300)


122-122: Do not catch blind exception: Exception

(BLE001)


141-141: Consider moving this statement to an else block

(TRY300)


143-143: Do not catch blind exception: Exception

(BLE001)

app/scripts/create_tables.py

126-126: Do not catch blind exception: Exception

(BLE001)

app/api/routers/breeze_buddy.py

118-118: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


133-133: Abstract raise to an inner function

(TRY301)


137-137: Abstract raise to an inner function

(TRY301)


175-175: Abstract raise to an inner function

(TRY301)


200-200: Do not catch blind exception: Exception

(BLE001)


207-207: Do not catch blind exception: Exception

(BLE001)


250-250: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🔇 Additional comments (14)
app/database/accessor/breeze_buddy/outbound_number.py (1)

9-9: Import path change looks good.

The decoder reorg alignment is correct; no functional impact.

app/database/accessor/breeze_buddy/call_execution_config.py (1)

10-10: Decoder import rewire is fine.

Consistent with the new decoder package layout.

app/agents/voice/breeze_buddy/call_providers/exotel.py (2)

25-25: Use HTTPS for flow_url (Exotel supports and recommends it)
Exotel accepts HTTPS endpoints for its start_voice (Exoml applet) URLs and recommends using HTTPS in production for security.


24-24: Approve code changes All make_call usages now match the updated signature and no lingering CallDataResponse references remain.

app/database/decoder/breeze_buddy/lead_call_tracker.py (1)

30-31: No action needed: call_initaited_time matches existing DB schema
The column name call_initaited_time is defined in app/scripts/create_tables.py and consistently used in accessors, decoders, queries, and schemas—leaving it as-is avoids a breaking migration.

app/database/decoder/breeze_buddy/outbound_number.py (1)

8-26: LGTM: Single-record decoder is correct and aligns with schema

Decoding and enum mapping look good, including optional timestamps.

app/database/decoder/breeze_buddy/call_execution_config.py (2)

8-31: LGTM: List decoder correctly maps enums and timestamps


32-53: LGTM: Single-record decoder mirrors list decoder faithfully

app/database/accessor/main.py (2)

18-24: Swap to lead_call_tracker accessors: looks good.

Imports are correct and align with the new accessor module.


35-39: No lingering call_data references detected
Ripgrep search for *_call_data returned no matches, confirming all accessors have been removed.

app/database/accessor/breeze_buddy/lead_call_tracker.py (2)

10-17: Schema/decoder field mismatch: metaData vs meta_data will raise validation errors.

LeadCallTracker uses metaData (camelCase) but the decoder constructs with meta_data=.... Fix the decoder to use metaData=....

Outside this file, apply in app/database/decoder/breeze_buddy/lead_call_tracker.py:

-        meta_data=parse_json(row, "meta_data"),
+        metaData=parse_json(row, "meta_data"),

Optionally add Field(..., alias="meta_data") to the schema if you want to accept either.

Likely an incorrect or invalid review comment.


76-79: Fix decoder call: pass a list (row wrapper) and filter None.

decode_lead_call_tracker expects a list, not a single row.

-        if result:
-            return [decode_lead_call_tracker(row) for row in result]
+        if result:
+            decoded = [decode_lead_call_tracker([row]) for row in result]
+            return [d for d in decoded if d is not None]
         return []

Likely an incorrect or invalid review comment.

app/database/queries/breeze_buddy/lead_call_tracker.py (2)

27-44: S608 (SQL injection) note: safe as written.

Table name is a constant; all user data is parameterized. No change needed.

Also applies to: 63-87, 88-98, 99-110


37-41: Spelling is consistent: “call_initaited_time” matches the table DDL and all code references.

Comment on lines 28 to 31
"From": customer_mobile_number,
"CallerId": self.config.EXOTEL_FROM_NUMBER,
"Url": flow_url
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate and normalize the mobile number (E.164).

Exotel typically expects E.164 format. Normalize/validate before sending to reduce provider errors.

Example:

def to_e164(n: str, default_cc: str) -> str:
    n = n.strip().replace(' ', '')
    if n.startswith('+'): return n
    if n.startswith('00'): return '+' + n[2:]
    return f'+{default_cc}{n}'

Use before building payload.

🤖 Prompt for AI Agents
In app/agents/voice/breeze_buddy/call_providers/exotel.py around lines 28 to 31,
the "From" phone number is used raw; validate and normalize it to E.164 before
building the payload to avoid provider errors. Add a small helper that strips
whitespace, handles leading '+' and '00', and prepends a default country code
when missing; call this helper on customer_mobile_number, validate the result
(ensure it starts with '+', contains only digits after '+', and meets length
expectations), and if invalid either log and abort the call creation or raise a
clear error so you never send a malformed number to Exotel. Ensure this
normalization runs before constructing the payload and that failures are handled
consistently with existing error/logging patterns.

Comment on lines 43 to 58
numbers = await get_outbound_number_based_on_status_and_provider(OutboundNumberStatus.AVAILABLE, config.calling_provider)
if not numbers:
logger.warning(f"No available outbound numbers found for provider: {config.calling_provider}")
continue

number_to_use = None
if config.calling_provider == "EXOTEL":
for number in numbers:
if number.channels < number.maximum_channels:
number_to_use = number
break
else:
number_to_use = numbers[0]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Accessor returns a single number, but code treats it as a list

get_outbound_number_based_on_status_and_provider (per accessor snippet) returns Optional[OutboundNumber], not a list. Iteration/indexing will break.

Immediate fix (treat as single value):

-                    numbers = await get_outbound_number_based_on_status_and_provider(OutboundNumberStatus.AVAILABLE, config.calling_provider)
-                    if not numbers:
+                    number = await get_outbound_number_based_on_status_and_provider(OutboundNumberStatus.AVAILABLE, config.calling_provider)
+                    if not number:
                         logger.warning(f"No available outbound numbers found for provider: {config.calling_provider}")
                         continue

-                    number_to_use = None
-                    if config.calling_provider == "EXOTEL":
-                        for number in numbers:
-                            if number.channels < number.maximum_channels:
-                                number_to_use = number
-                                break
-                    else:
-                        number_to_use = numbers[0]
+                    number_to_use = number

Preferred fix (outside this file): make the accessor return List[OutboundNumber] and use decode_outbound_number_list.

📝 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.

Suggested change
numbers = await get_outbound_number_based_on_status_and_provider(OutboundNumberStatus.AVAILABLE, config.calling_provider)
if not numbers:
logger.warning(f"No available outbound numbers found for provider: {config.calling_provider}")
continue
number_to_use = None
if config.calling_provider == "EXOTEL":
for number in numbers:
if number.channels < number.maximum_channels:
number_to_use = number
break
else:
number_to_use = numbers[0]
number = await get_outbound_number_based_on_status_and_provider(
OutboundNumberStatus.AVAILABLE,
config.calling_provider
)
if not number:
logger.warning(
f"No available outbound numbers found for provider: {config.calling_provider}"
)
continue
number_to_use = number
🤖 Prompt for AI Agents
In app/agents/voice/breeze_buddy/cron_manager.py around lines 43 to 56, the
accessor get_outbound_number_based_on_status_and_provider returns an
Optional[OutboundNumber] (a single object) but the code treats it like a list
(iterating and indexing). Replace the list handling with a single-value flow:
assign the returned value to number_to_use directly, check for None and
warn/continue, and if config.calling_provider == "EXOTEL" verify number.channels
< number.maximum_channels before using it (warn/continue if it fails);
alternatively, if you want list semantics change the accessor to return
List[OutboundNumber] and use decode_outbound_number_list elsewhere.

Comment on lines 49 to 58
if config.calling_provider == "EXOTEL":
for number in numbers:
if number.channels < number.maximum_channels:
number_to_use = number
break
else:
number_to_use = numbers[0]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enum vs string comparison bug

config.calling_provider is CallProvider, but you're comparing to "EXOTEL". This branch never executes.

If you keep single-number selection (above), drop this branch. If you later return lists, compare with the enum:

-                    if config.calling_provider == "EXOTEL":
+                    if config.calling_provider == CallProvider.EXOTEL:

(Ensure from app.schemas import CallProvider is imported.)

📝 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.

Suggested change
if config.calling_provider == "EXOTEL":
for number in numbers:
if number.channels < number.maximum_channels:
number_to_use = number
break
else:
number_to_use = numbers[0]
# at the top of cron_manager.py, ensure you import the enum
from app.schemas import CallProvider
# (other code)
if config.calling_provider == CallProvider.EXOTEL:
for number in numbers:
if number.channels < number.maximum_channels:
number_to_use = number
break
else:
number_to_use = numbers[0]
# (following code)
🤖 Prompt for AI Agents
In app/agents/voice/breeze_buddy/cron_manager.py around lines 49 to 56, the code
compares config.calling_provider to the string "EXOTEL" but
config.calling_provider is a CallProvider enum so that branch never runs; fix by
either removing the EXOTEL-specific branch if you will always choose a single
number, or change the comparison to use the enum (e.g. compare to
CallProvider.EXOTEL) and ensure you add "from app.schemas import CallProvider"
at the top of the file; update logic accordingly so number_to_use is selected
correctly when provider is EXOTEL.

Comment on lines 61 to 74
await update_outbound_number_status(number_to_use.id, OutboundNumberStatus.IN_USE)

call_provider = get_voice_provider(config.calling_provider.value, session)
call = await call_provider.make_call(lead.payload.get("customer_mobile_number"))
await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid"))

except Exception as e:
logger.error(f"Error processing lead {lead.id}: {e}")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle call initiation failures robustly

If the provider fails or returns no SID, you still mark the lead PROCESSING and keep the number IN_USE. Add failure handling and rollback.

-                    call = call_provider.make_call(lead.payload.get("customer_mobile_number"))
-                    await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid"))
+                    call = call_provider.make_call(lead.payload.get("customer_mobile_number"))
+                    if not call or call.get("status") != "call_initiated" or not call.get("sid"):
+                        logger.error(f"Call initiation failed for lead {lead.id}: {call}")
+                        await update_outbound_number_status(number_to_use.id, OutboundNumberStatus.AVAILABLE)
+                        continue
+                    await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid"))

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.12.2)

67-67: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In app/agents/voice/breeze_buddy/cron_manager.py around lines 61-69, the code
marks the lead PROCESSING and sets the outbound number to IN_USE before ensuring
a call was actually created; if the provider fails or returns no SID we must
roll back those changes. Change the flow so you first reserve the number,
attempt to initiate the call, then check the call response: if a valid SID is
returned, update the lead status to PROCESSING and save the call_id; if the
provider raises or returns no SID, update the lead to a failure/error status (or
keep as PENDING per domain rules) and set the outbound number back to AVAILABLE
(use update_outbound_number_status), and ensure this rollback also runs inside
the except block so any exception releases the number; only mark PROCESSING and
persist call_id after confirming a non-empty SID.

Comment on lines 61 to 71
await update_outbound_number_status(number_to_use.id, OutboundNumberStatus.IN_USE)

call_provider = get_voice_provider(config.calling_provider.value, session)
call = await call_provider.make_call(lead.payload.get("customer_mobile_number"))
await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid"))

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Persist chosen outbound_number_id on the lead

handle_call_completion releases lead.outbound_number_id, but it’s never set here. This will be None, breaking release.

If your accessor supports it, update outbound_number_id together with status and call_id:

-                    await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid"))
+                    await update_lead_status_and_call_id(
+                        id=lead.id,
+                        status=LeadCallStatus.PROCESSING,
+                        call_id=call.get("sid"),
+                        outbound_number_id=number_to_use.id,  # requires accessor support
+                    )

If not supported, add a new accessor update_lead_status_call_id_and_number(...) and use it here.

📝 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.

Suggested change
await update_outbound_number_status(number_to_use.id, OutboundNumberStatus.IN_USE)
call_provider = get_voice_provider(config.calling_provider.value, session)
call = await call_provider.make_call(lead.payload.get("customer_mobile_number"))
await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid"))
await update_outbound_number_status(number_to_use.id, OutboundNumberStatus.IN_USE)
call_provider = get_voice_provider(config.calling_provider.value, session)
call = await call_provider.make_call(lead.payload.get("customer_mobile_number"))
await update_lead_status_and_call_id(
id=lead.id,
status=LeadCallStatus.PROCESSING,
call_id=call.get("sid"),
outbound_number_id=number_to_use.id, # requires accessor support
)
🤖 Prompt for AI Agents
In app/agents/voice/breeze_buddy/cron_manager.py around lines 61 to 66, the
chosen outbound_number_id is never persisted to the lead before making the call
which causes handle_call_completion to fail when releasing the number; update
the lead record together with its status and call_id to include
outbound_number_id. If your existing data accessor can update status and call_id
together, extend that call to also set outbound_number_id; otherwise add a new
accessor update_lead_status_call_id_and_number(lead_id, status, call_id,
outbound_number_id) and call it here with number_to_use.id,
LeadCallStatus.PROCESSING, and the returned call sid so the
lead.outbound_number_id is persisted.

Comment on lines 24 to 34
payload=parse_json(row, "payload"),
meta_data=parse_json(row, "meta_data"),
recording_url=row["recording_url"],
status=row["status"],
outcome=row["outcome"],
call_id=row["call_id"],
call_initaited_time=row["call_initaited_time"],
call_duration=row["call_duration"],
cost=row["cost"],
created_at=row["created_at"],
updated_at=row["updated_at"],
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Wrong field name: meta_data vs metaData (constructor will fail).

Pydantic model uses metaData; passing meta_data raises an unexpected keyword error.

Apply:

-        meta_data=parse_json(row, "meta_data"),
+        metaData=parse_json(row, "meta_data"),
📝 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.

Suggested change
payload=parse_json(row, "payload"),
meta_data=parse_json(row, "meta_data"),
recording_url=row["recording_url"],
status=row["status"],
outcome=row["outcome"],
call_id=row["call_id"],
call_initaited_time=row["call_initaited_time"],
call_duration=row["call_duration"],
cost=row["cost"],
created_at=row["created_at"],
updated_at=row["updated_at"],
)
payload=parse_json(row, "payload"),
metaData=parse_json(row, "meta_data"),
recording_url=row["recording_url"],
status=row["status"],
outcome=row["outcome"],
call_id=row["call_id"],
call_initaited_time=row["call_initaited_time"],
call_duration=row["call_duration"],
cost=row["cost"],
created_at=row["created_at"],
updated_at=row["updated_at"],
)
🤖 Prompt for AI Agents
In app/database/decoder/breeze_buddy/lead_call_tracker.py around lines 24 to 35,
the code passes meta_data to the Pydantic constructor but the model expects
metaData; change the argument name to metaData while still parsing the same
source key (e.g., metaData=parse_json(row, "meta_data")) so the constructor
receives the correct field name and no unexpected keyword error occurs.

Comment on lines 53 to 55
merchant_id: RequestedBy
workflow: Workflow
attempt_count: int = 0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

merchant_id typed as RequestedBy (semantic/type mismatch)

merchant_id should be an identifier (str/UUID). RequestedBy denotes the source (breeze/shopify). This will break consumers and conflate two concepts.

 class LeadCallTracker(BaseModel):
     id: str
     outbound_number_id: Optional[str] = None
-    merchant_id: RequestedBy
+    merchant_id: str
+    # Track source separately to preserve prior semantics
+    requested_by: Optional[RequestedBy] = None
     workflow: Workflow
📝 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.

Suggested change
merchant_id: RequestedBy
workflow: Workflow
attempt_count: int = 0
class LeadCallTracker(BaseModel):
id: str
outbound_number_id: Optional[str] = None
merchant_id: str
# Track source separately to preserve prior semantics
requested_by: Optional[RequestedBy] = None
workflow: Workflow
attempt_count: int = 0
🤖 Prompt for AI Agents
In app/schemas.py around lines 53-55 the field merchant_id is incorrectly typed
as RequestedBy (which represents the source, e.g., breeze/shopify) instead of an
identifier; change merchant_id to the appropriate identifier type (e.g., str or
UUID) and leave RequestedBy as a separate field (or add a separate requested_by:
RequestedBy field if missing). Update the type annotation and any necessary
imports, run static type checks, and adjust any downstream code/tests that
expect merchant_id to be a RequestedBy to use the new requested_by field
instead.

app/schemas.py Outdated
assigned_number: Optional[str] = None
created_at: str
updated_at: str
call_initaited_time: Optional[datetime] = None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Typo: call_initaited_time → call_initiated_time (add alias for backward-compat)

The current misspelling will leak into DB/JSON and downstream code.

-    call_initaited_time: Optional[datetime] = None
+    from pydantic import Field
+    call_initiated_time: Optional[datetime] = Field(default=None, alias="call_initaited_time")
+
+    class Config:
+        allow_population_by_field_name = True

Note: After this change, serialize with by_alias=True only if you must keep the old key during a transition window.

📝 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.

Suggested change
call_initaited_time: Optional[datetime] = None
from pydantic import Field
# rename the field to the correct spelling, but keep the old key for backwards compatibility
call_initiated_time: Optional[datetime] = Field(default=None, alias="call_initaited_time")
class Config:
# allow initializing via the new “call_initiated_time” name as well as the old alias
allow_population_by_field_name = True
🤖 Prompt for AI Agents
In app/schemas.py at line 63, rename the misspelled field to call_initiated_time
and keep backward compatibility by defining the field with the correct Python
attribute name and a Pydantic alias equal to the old key (call_initaited_time);
update the default/typing (Optional[datetime] = None) accordingly and ensure any
serialization during transition only uses by_alias=True when you need to emit
the old key.

Comment on lines 16 to 39
def create_lead_call_tracker_table_query() -> str:
"""
Generate query to create lead_call_tracker table.
"""
return f"""
CREATE TABLE IF NOT EXISTS "{LEAD_CALL_TRACKER_TABLE}" (
"id" VARCHAR(255) PRIMARY KEY,
"outbound_number_id" VARCHAR(255),
"merchant_id" VARCHAR(100) NOT NULL,
"workflow" VARCHAR(50) CHECK ("workflow" IN ('order-confirmation')) NOT NULL,
"attempt_count" INTEGER DEFAULT 0,
"next_attempt_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
"payload" JSONB,
"meta_data" JSONB,
"recording_url" VARCHAR(500),
"status" VARCHAR(50) CHECK ("status" IN ('BACKLOG', 'PROCESSING', 'FINISHED', 'RETRY')) NOT NULL,
"outcome" VARCHAR(50) CHECK ("outcome" IN ('NO_ANSWER', 'BUSY', 'CANCEL', 'CONFIRM', 'UNKNOWN')),
"call_id" VARCHAR(100),
"call_initaited_time" TIMESTAMP WITH TIME ZONE,
"call_duration" INTEGER,
"cost" REAL,
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix column name typo: call_initaited_time → call_initiated_time (schema change)

This is a persistent schema typo. Correct before it ships; retrofitting later is costly.

-            "call_initaited_time" TIMESTAMP WITH TIME ZONE,
+            "call_initiated_time" TIMESTAMP WITH TIME ZONE,

Follow-ups (outside this file):

  • Update accessors/queries/models using the same misspelling.
📝 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.

Suggested change
def create_lead_call_tracker_table_query() -> str:
"""
Generate query to create lead_call_tracker table.
"""
return f"""
CREATE TABLE IF NOT EXISTS "{LEAD_CALL_TRACKER_TABLE}" (
"id" VARCHAR(255) PRIMARY KEY,
"outbound_number_id" VARCHAR(255),
"merchant_id" VARCHAR(100) NOT NULL,
"workflow" VARCHAR(50) CHECK ("workflow" IN ('order-confirmation')) NOT NULL,
"attempt_count" INTEGER DEFAULT 0,
"next_attempt_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
"payload" JSONB,
"meta_data" JSONB,
"recording_url" VARCHAR(500),
"status" VARCHAR(50) CHECK ("status" IN ('BACKLOG', 'PROCESSING', 'FINISHED', 'RETRY')) NOT NULL,
"outcome" VARCHAR(50) CHECK ("outcome" IN ('NO_ANSWER', 'BUSY', 'CANCEL', 'CONFIRM', 'UNKNOWN')),
"call_id" VARCHAR(100),
"call_initaited_time" TIMESTAMP WITH TIME ZONE,
"call_duration" INTEGER,
"cost" REAL,
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
def create_lead_call_tracker_table_query() -> str:
"""
Generate query to create lead_call_tracker table.
"""
return f"""
CREATE TABLE IF NOT EXISTS "{LEAD_CALL_TRACKER_TABLE}" (
"id" VARCHAR(255) PRIMARY KEY,
"outbound_number_id" VARCHAR(255),
"merchant_id" VARCHAR(100) NOT NULL,
"workflow" VARCHAR(50) CHECK ("workflow" IN ('order-confirmation')) NOT NULL,
"attempt_count" INTEGER DEFAULT 0,
"next_attempt_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
"payload" JSONB,
"meta_data" JSONB,
"recording_url" VARCHAR(500),
"status" VARCHAR(50) CHECK ("status" IN ('BACKLOG', 'PROCESSING', 'FINISHED', 'RETRY')) NOT NULL,
"outcome" VARCHAR(50) CHECK ("outcome" IN ('NO_ANSWER', 'BUSY', 'CANCEL', 'CONFIRM', 'UNKNOWN')),
"call_id" VARCHAR(100),
"call_initiated_time" TIMESTAMP WITH TIME ZONE,
"call_duration" INTEGER,
"cost" REAL,
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
🤖 Prompt for AI Agents
In app/scripts/create_tables.py around lines 16 to 39, the column name
"call_initaited_time" is misspelled; change it to "call_initiated_time" in the
CREATE TABLE query to correct the schema, and update the column name in any
related migration or deployment scripts to apply the change. Also search the
codebase for the misspelled identifier and update all accessors, queries,
models, and tests to use "call_initiated_time" (or add a migration that renames
the column in existing DBs if data must be preserved). Ensure any references in
SELECT/INSERT/UPDATE statements and ORM mappings are updated and run
schema/migration tests.

Comment on lines 40 to 44
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_merchant_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("merchant_id");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_status" ON "{LEAD_CALL_TRACKER_TABLE}" ("status");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_outcome" ON "{LEAD_CALL_TRACKER_TABLE}" ("outcome");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_created_at" ON "{LEAD_CALL_TRACKER_TABLE}" ("created_at");
"""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add an index on call_id to speed up get_lead_by_call_id lookups

This path is hot; an index materially reduces latency.

         CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_merchant_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("merchant_id");
         CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_status" ON "{LEAD_CALL_TRACKER_TABLE}" ("status");
         CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_outcome" ON "{LEAD_CALL_TRACKER_TABLE}" ("outcome");
         CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_created_at" ON "{LEAD_CALL_TRACKER_TABLE}" ("created_at");
+        CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_call_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("call_id");
📝 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.

Suggested change
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_merchant_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("merchant_id");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_status" ON "{LEAD_CALL_TRACKER_TABLE}" ("status");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_outcome" ON "{LEAD_CALL_TRACKER_TABLE}" ("outcome");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_created_at" ON "{LEAD_CALL_TRACKER_TABLE}" ("created_at");
"""
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_merchant_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("merchant_id");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_status" ON "{LEAD_CALL_TRACKER_TABLE}" ("status");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_outcome" ON "{LEAD_CALL_TRACKER_TABLE}" ("outcome");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_created_at" ON "{LEAD_CALL_TRACKER_TABLE}" ("created_at");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_call_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("call_id");
"""
🤖 Prompt for AI Agents
In app/scripts/create_tables.py around lines 40 to 44, the SQL block creates
several indexes but is missing an index on call_id used by get_lead_by_call_id;
add a new line to the CREATE INDEX list like CREATE INDEX IF NOT EXISTS
"idx_lead_call_tracker_call_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("call_id")
before the closing triple-quote so the migration creates the call_id index (then
save and run the migration to apply).

@badri-singhal badri-singhal force-pushed the Support-for-initiate-call-using-process-tracker branch 7 times, most recently from 6f9de57 to 9e086c1 Compare September 11, 2025 08:30
@badri-singhal badri-singhal force-pushed the Support-for-initiate-call-using-process-tracker branch from 9e086c1 to 97a3d7d Compare September 11, 2025 09:09
@swaroopvarma1 swaroopvarma1 merged commit ad6087d into juspay:release Sep 11, 2025
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Sep 24, 2025
@coderabbitai coderabbitai bot mentioned this pull request Dec 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants