chore: refactor status processor (#465)

* chore: refactor status processor

* fix: fix billing duration when billsec is None for Cloudonix
This commit is contained in:
Abhishek 2026-06-24 22:07:35 +05:30 committed by GitHub
parent d817d50056
commit 29c5be298c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 910 additions and 809 deletions

View file

@ -11,8 +11,9 @@ from unittest.mock import AsyncMock, patch
import pytest
from starlette.requests import Request
from api.enums import TelephonyCallStatus
from api.services.telephony.providers.cloudonix.provider import CloudonixProvider
from api.services.telephony.providers.cloudonix.routes import handle_cloudonix_cdr
from api.services.telephony.status_processor import StatusCallbackRequest
def _json_request(body: bytes) -> Request:
@ -79,33 +80,33 @@ async def test_cdr_route_handles_string_session():
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."""
def test_parse_cloudonix_cdr_tolerates_missing_session_and_disposition():
"""Cloudonix CDR parsing must not crash on a partial payload."""
# Missing both session and disposition.
req = StatusCallbackRequest.from_cloudonix_cdr({"domain": "acme.cloudonix.io"})
assert req.call_id == ""
assert req.status == ""
req = CloudonixProvider.parse_cdr_status_callback({"domain": "acme.cloudonix.io"})
assert req["call_id"] == ""
assert req["status"] == ""
# Explicit null values.
req = StatusCallbackRequest.from_cloudonix_cdr(
req = CloudonixProvider.parse_cdr_status_callback(
{"session": None, "disposition": None}
)
assert req.call_id == ""
assert req.status == ""
assert req["call_id"] == ""
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(
def test_parse_cloudonix_cdr_tolerates_string_session():
"""Cloudonix CDR parsing treats a non-object session as missing call_id."""
req = CloudonixProvider.parse_cdr_status_callback(
{"session": "abc", "disposition": "ANSWER"}
)
assert req.call_id == ""
assert req.status == "completed"
assert req["call_id"] == ""
assert req["status"] == TelephonyCallStatus.COMPLETED
def test_from_cloudonix_cdr_maps_disposition_and_session_token():
def test_parse_cloudonix_cdr_maps_disposition_and_session_token():
"""Normal, well-formed CDR payloads still map correctly."""
req = StatusCallbackRequest.from_cloudonix_cdr(
req = CloudonixProvider.parse_cdr_status_callback(
{
"session": {"token": "abc123"},
"disposition": "BUSY",
@ -114,6 +115,20 @@ def test_from_cloudonix_cdr_maps_disposition_and_session_token():
"billsec": 12,
}
)
assert req.call_id == "abc123"
assert req.status == "busy"
assert req.duration == "12"
assert req["call_id"] == "abc123"
assert req["status"] == TelephonyCallStatus.BUSY
assert req["duration"] == "12"
def test_parse_cloudonix_cdr_preserves_zero_billsec():
"""A zero billed duration must not fall back to total call duration."""
req = CloudonixProvider.parse_cdr_status_callback(
{
"session": {"token": "abc123"},
"disposition": "ANSWER",
"billsec": 0,
"duration": 42,
}
)
assert req["duration"] == "0"

View file

@ -0,0 +1,98 @@
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
import pytest
from api.enums import TelephonyCallStatus, WorkflowRunState
from api.services.telephony.status_processor import (
StatusCallbackRequest,
_process_status_update,
)
from api.tasks.function_names import FunctionNames
@pytest.mark.asyncio
async def test_initialized_no_answer_enqueues_workflow_completion():
workflow_run = SimpleNamespace(
id=123,
campaign_id=None,
queued_run_id=None,
state=WorkflowRunState.INITIALIZED.value,
is_completed=False,
logs={"telephony_status_callbacks": []},
gathered_context={"call_tags": ["existing"]},
)
status = StatusCallbackRequest(
call_id="call-123",
status="No-Answer",
)
with (
patch("api.services.telephony.status_processor.db_client") as mock_db,
patch(
"api.services.telephony.status_processor.enqueue_job",
new_callable=AsyncMock,
) as mock_enqueue,
):
mock_db.get_workflow_run_by_id = AsyncMock(return_value=workflow_run)
mock_db.update_workflow_run = AsyncMock()
await _process_status_update(123, status)
log_update = mock_db.update_workflow_run.await_args_list[0].kwargs
callback_log = log_update["logs"]["telephony_status_callbacks"][0]
assert callback_log["status"] == "no-answer"
assert callback_log["call_id"] == "call-123"
completion_update = mock_db.update_workflow_run.await_args_list[1].kwargs
assert completion_update["run_id"] == 123
assert completion_update["is_completed"] is True
assert completion_update["state"] == WorkflowRunState.COMPLETED.value
assert completion_update["usage_info"] == {"call_duration_seconds": 0}
assert completion_update["gathered_context"] == {
"call_tags": ["existing", "not_connected", "telephony_no-answer"],
"call_disposition": "no-answer",
"mapped_call_disposition": "no-answer",
"call_id": "call-123",
}
mock_enqueue.assert_awaited_once_with(
FunctionNames.RUN_INTEGRATIONS_POST_WORKFLOW_RUN, 123
)
@pytest.mark.asyncio
async def test_running_terminal_status_does_not_enqueue_workflow_completion():
workflow_run = SimpleNamespace(
id=456,
campaign_id=None,
queued_run_id=None,
state=WorkflowRunState.RUNNING.value,
is_completed=False,
logs={"telephony_status_callbacks": []},
gathered_context={"call_tags": ["not_connected"]},
)
status = StatusCallbackRequest(
call_id="call-456",
status=TelephonyCallStatus.FAILED,
duration="7",
)
with (
patch("api.services.telephony.status_processor.db_client") as mock_db,
patch(
"api.services.telephony.status_processor.enqueue_job",
new_callable=AsyncMock,
) as mock_enqueue,
):
mock_db.get_workflow_run_by_id = AsyncMock(return_value=workflow_run)
mock_db.update_workflow_run = AsyncMock()
await _process_status_update(456, status)
completion_update = mock_db.update_workflow_run.await_args_list[1].kwargs
assert "usage_info" not in completion_update
assert completion_update["gathered_context"]["call_tags"] == [
"not_connected",
"telephony_failed",
]
mock_enqueue.assert_not_awaited()