Files
call-center/tests/test_sales_service.py
T
2026-05-09 09:16:06 +05:00

301 lines
11 KiB
Python

from fastapi.testclient import TestClient
import services.sales_service.app as sales_module
def _headers() -> dict[str, str]:
return {"X-User": "admin", "X-Role": "admin"}
def _create_lead(client: TestClient) -> tuple[dict, str]:
created = client.post(
"/api/v1/leads",
json={
"source_type": "crm",
"source_channel": "telegram",
"full_name": "Test Buyer",
"company_name": "Sales QA",
"phone": "+77001234567",
"email": "buyer@test.local",
"lead_temperature": "hot",
"lead_score": 82,
"initial_need_summary": "Need a commercial offer and a callback.",
"preferred_channel": "telegram",
"assigned_agent_type": "text_ai",
"status": "new_qualified_lead",
"priority": 3,
"title": "Lead: Test Buyer",
},
headers=_headers(),
)
assert created.status_code == 200
lead = created.json()
deals = client.get("/api/v1/deals", headers=_headers())
assert deals.status_code == 200
deal_id = next(item["deal_id"] for item in deals.json() if item["lead_id"] == lead["lead_id"])
return lead, deal_id
class _DummySyncResponse:
def __init__(self, payload: dict[str, str]):
self._payload = payload
self.status_code = 200
self.text = ""
def raise_for_status(self) -> None:
return None
def json(self) -> dict[str, str]:
return self._payload
class _DummyHttpxClient:
requests: list[dict] = []
response_payload: dict[str, str] = {}
def __init__(self, timeout: float):
self.timeout = timeout
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return None
def post(self, url: str, json: dict | None = None, headers: dict | None = None):
self.__class__.requests.append(
{
"url": url,
"json": json or {},
"headers": headers or {},
"timeout": self.timeout,
}
)
return _DummySyncResponse(self.__class__.response_payload)
def test_sales_lead_creation_auto_creates_deal_and_workspace():
client = TestClient(sales_module.app)
lead, deal_id = _create_lead(client)
workspace = client.get(f"/api/v1/deals/{deal_id}/workspace", headers=_headers())
assert workspace.status_code == 200
payload = workspace.json()
assert payload["lead"]["lead_id"] == lead["lead_id"]
assert payload["deal"]["deal_id"] == deal_id
assert payload["deal"]["preferred_channel"] == "telegram"
assert payload["timeline"]
def test_sales_bind_telegram_and_send_outbound_message(monkeypatch):
client = TestClient(sales_module.app)
_, deal_id = _create_lead(client)
_DummyHttpxClient.requests = []
_DummyHttpxClient.response_payload = {"ok": True}
monkeypatch.setattr(sales_module.httpx, "Client", _DummyHttpxClient)
monkeypatch.setenv("SALES_TELEGRAM_BRIDGE_ENABLED", "1")
created_communication = client.post(
f"/api/v1/deals/{deal_id}/communications/text",
json={
"channel_type": "text",
"direction": "outbound",
"agent_type": "text_ai",
"subject": "Telegram follow-up",
},
headers=_headers(),
)
assert created_communication.status_code == 200
communication = created_communication.json()
bound = client.post(
f"/api/v1/communications/{communication['communication_id']}/bind-external",
json={
"channel_provider": "telegram",
"telegram_thread_id": "tg-thread-42",
"telegram_chat_id": "tg-chat-42",
},
headers=_headers(),
)
assert bound.status_code == 200
assert bound.json()["metadata"]["telegram_thread_id"] == "tg-thread-42"
outbound = client.post(
"/api/v1/messages/outbound",
json={
"deal_id": deal_id,
"communication_id": communication["communication_id"],
"channel_provider": "telegram",
"sender_type": "human",
"sender_id": "admin",
"body": "Please check the offer in Telegram.",
"metadata": {"telegram_thread_id": "tg-thread-42"},
},
headers=_headers(),
)
assert outbound.status_code == 200
assert outbound.json()["delivery_status"] == "sent"
assert _DummyHttpxClient.requests
assert _DummyHttpxClient.requests[0]["url"].endswith("/integrations/telegram/threads/tg-thread-42/messages")
assert _DummyHttpxClient.requests[0]["json"]["text"] == "Please check the offer in Telegram."
switched = client.post(
f"/api/v1/communications/{communication['communication_id']}/switch-channel",
json={
"to_channel": "voice",
"reason_for_channel_switch": "Need to clarify details by phone",
},
headers=_headers(),
)
assert switched.status_code == 200
workspace = client.get(f"/api/v1/deals/{deal_id}/workspace", headers=_headers())
assert workspace.status_code == 200
payload = workspace.json()
assert payload["deal"]["current_channel"] == "voice"
assert payload["channel_switches"][0]["to_channel"] == "voice"
def test_sales_inbound_call_bridges_voice_runtime(monkeypatch):
client = TestClient(sales_module.app)
_, deal_id = _create_lead(client)
_DummyHttpxClient.requests = []
_DummyHttpxClient.response_payload = {"voice_session_id": "avs_sales_case_01"}
monkeypatch.setattr(sales_module.httpx, "Client", _DummyHttpxClient)
monkeypatch.setenv("SALES_VOICE_RUNTIME_ENABLED", "1")
response = client.post(
"/api/v1/calls/inbound-webhook",
json={
"deal_id": deal_id,
"phone_number": "+77001234567",
"provider": "asterisk",
"external_call_id": "call-sales-01",
"subject": "Inbound sales call",
},
headers=_headers(),
)
assert response.status_code == 200
assert _DummyHttpxClient.requests
assert _DummyHttpxClient.requests[0]["url"].endswith("/internal/voice-ai/sessions")
assert _DummyHttpxClient.requests[0]["json"]["call_id"] == "call-sales-01"
workspace = client.get(f"/api/v1/deals/{deal_id}/workspace", headers=_headers())
assert workspace.status_code == 200
communication = workspace.json()["communications"][0]
assert communication["channel_type"] == "voice"
assert communication["metadata"]["voice_session_id"] == "avs_sales_case_01"
def test_sales_internal_telegram_sync_auto_creates_workspace():
client = TestClient(sales_module.app)
response = client.post(
"/internal/sales-sync/telegram",
json={
"thread_id": "tg-thread-auto-01",
"chat_id": "tg-chat-auto-01",
"interaction_id": "int_tg_auto_01",
"phone_number": "+77005550101",
"display_name": "Telegram Prospect",
"queue_id": "q_sales",
"status": "new",
"ai_state": "queued",
"message_id": "msg_tg_auto_01",
"external_message_id": "ext_tg_auto_01",
"text": "Здравствуйте, нужен расчет и коммерческое предложение.",
"direction": "inbound",
"author_type": "customer",
"author_id": "tg-user-01",
"metadata": {"username": "sales_prospect"},
},
headers=_headers(),
)
assert response.status_code == 200
workspace = response.json()
assert workspace["deal"]["current_channel"] == "telegram"
assert workspace["deal"]["preferred_channel"] == "telegram"
assert workspace["communications"][0]["channel_provider"] == "telegram"
assert workspace["communications"][0]["metadata"]["telegram_thread_id"] == "tg-thread-auto-01"
assert workspace["messages"][0]["external_message_id"] == "ext_tg_auto_01"
def test_sales_internal_voice_sync_creates_call_and_transcript():
client = TestClient(sales_module.app)
response = client.post(
"/internal/sales-sync/voice",
json={
"call_id": "call-auto-voice-01",
"interaction_id": "int_voice_auto_01",
"queue_id": "q_voice_sales",
"queue_code": "voice_sales",
"caller_number": "+77005550202",
"caller_name": "Voice Prospect",
"voice_session_id": "avs_auto_01",
"ai_session_id": "ais_auto_01",
"ai_state": "completed",
"telephony_status": "ended",
"call_status": "completed",
"started_at": "2026-05-08T10:00:00Z",
"ended_at": "2026-05-08T10:03:00Z",
"summary": "Клиент запросил прайс и условия подключения.",
"transcript_text": "customer: Добрый день, нужен прайс.\nassistant: Подготовлю информацию.",
"metadata": {"agent_profile": "voice_sales"},
},
headers=_headers(),
)
assert response.status_code == 200
workspace = response.json()
assert workspace["deal"]["current_channel"] == "voice"
assert workspace["communications"][0]["channel_type"] == "voice"
assert workspace["communications"][0]["metadata"]["voice_session_id"] == "avs_auto_01"
assert workspace["calls"][0]["external_call_id"] == "call-auto-voice-01"
assert workspace["transcripts"][0]["transcript_text"].startswith("customer:")
def test_sales_list_deals_query_matches_external_links():
client = TestClient(sales_module.app)
voice_workspace = client.post(
"/internal/sales-sync/voice",
json={
"call_id": "call-search-voice-01",
"interaction_id": "int_search_voice_01",
"caller_number": "+77005550303",
"caller_name": "Voice Search Prospect",
"voice_session_id": "avs_search_01",
"call_status": "completed",
},
headers=_headers(),
)
assert voice_workspace.status_code == 200
telegram_workspace = client.post(
"/internal/sales-sync/telegram",
json={
"thread_id": "tg-thread-search-01",
"chat_id": "tg-chat-search-01",
"interaction_id": "int_search_tg_01",
"display_name": "Telegram Search Prospect",
"text": "Need pricing details",
},
headers=_headers(),
)
assert telegram_workspace.status_code == 200
by_voice_session = client.get("/api/v1/deals?query=avs_search_01", headers=_headers())
assert by_voice_session.status_code == 200
assert any(item["deal_id"] == voice_workspace.json()["deal"]["deal_id"] for item in by_voice_session.json())
by_call_id = client.get("/api/v1/deals?query=call-search-voice-01", headers=_headers())
assert by_call_id.status_code == 200
assert any(item["deal_id"] == voice_workspace.json()["deal"]["deal_id"] for item in by_call_id.json())
by_thread_id = client.get("/api/v1/deals?query=tg-thread-search-01", headers=_headers())
assert by_thread_id.status_code == 200
assert any(item["deal_id"] == telegram_workspace.json()["deal"]["deal_id"] for item in by_thread_id.json())