diff --git a/api/services/telephony/providers/cloudonix/routes.py b/api/services/telephony/providers/cloudonix/routes.py index 1f5f1ea5..facf4bdb 100644 --- a/api/services/telephony/providers/cloudonix/routes.py +++ b/api/services/telephony/providers/cloudonix/routes.py @@ -103,7 +103,8 @@ async def handle_cloudonix_cdr(request: Request): return {"status": "error", "message": "Missing domain field"} # Extract call_id to find workflow run - call_id = (cdr_data.get("session") or {}).get("token") + session = cdr_data.get("session") + call_id = session.get("token") if isinstance(session, dict) else None logger.info(f"Cloudonix CDR data for call id {call_id} - {cdr_data}") if not call_id: logger.warning("Cloudonix CDR missing call_id field") diff --git a/api/services/telephony/status_processor.py b/api/services/telephony/status_processor.py index 426e872c..b93a0d9e 100644 --- a/api/services/telephony/status_processor.py +++ b/api/services/telephony/status_processor.py @@ -116,9 +116,11 @@ class StatusCallbackRequest(BaseModel): disposition = data.get("disposition") or "" status = disposition_map.get(disposition.upper(), disposition.lower()) + session = data.get("session") + call_id = session.get("token") if isinstance(session, dict) else "" return cls( - call_id=(data.get("session") or {}).get("token") or "", + call_id=call_id or "", status=status, from_number=data.get("from"), to_number=data.get("to"), diff --git a/api/tests/telephony/test_cloudonix_cdr.py b/api/tests/telephony/test_cloudonix_cdr.py index 291b5d71..e22b0672 100644 --- a/api/tests/telephony/test_cloudonix_cdr.py +++ b/api/tests/telephony/test_cloudonix_cdr.py @@ -64,6 +64,21 @@ async def test_cdr_route_handles_null_session(): assert result == {"status": "error", "message": "Missing call_id field"} +@pytest.mark.asyncio +async def test_cdr_route_handles_string_session(): + """A CDR payload with a non-object session is handled gracefully.""" + request = _json_request(b'{"domain": "acme.cloudonix.io", "session": "abc"}') + + with patch( + "api.services.telephony.providers.cloudonix.routes.db_client" + ) as db_client: + db_client.get_workflow_run_by_call_id = AsyncMock(return_value=None) + + result = await handle_cloudonix_cdr(request) + + assert result == {"status": "error", "message": "Missing call_id field"} + + def test_from_cloudonix_cdr_tolerates_missing_session_and_disposition(): """``from_cloudonix_cdr`` must not crash on a partial CDR payload.""" # Missing both session and disposition. @@ -79,6 +94,15 @@ def test_from_cloudonix_cdr_tolerates_missing_session_and_disposition(): assert req.status == "" +def test_from_cloudonix_cdr_tolerates_string_session(): + """``from_cloudonix_cdr`` treats a non-object session as missing call_id.""" + req = StatusCallbackRequest.from_cloudonix_cdr( + {"session": "abc", "disposition": "ANSWER"} + ) + assert req.call_id == "" + assert req.status == "completed" + + def test_from_cloudonix_cdr_maps_disposition_and_session_token(): """Normal, well-formed CDR payloads still map correctly.""" req = StatusCallbackRequest.from_cloudonix_cdr(