from fastapi.testclient import TestClient import pytest import services.sales_service.app as sales_module def _headers(tenant_id: str = "tenant_test") -> dict[str, str]: return {"X-User": "admin", "X-Role": "admin", "X-Tenant-ID": tenant_id} 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_telegram_sync_moves_pipeline_every_three_customer_messages(): client = TestClient(sales_module.app) tenant_id = "tenant_tg_autostage" def post_message(index: int) -> dict: response = client.post( "/internal/sales-sync/telegram", json={ "thread_id": "tg-thread-autostage-01", "chat_id": "tg-chat-autostage-01", "interaction_id": "int_tg_autostage_01", "display_name": "Autostage Prospect", "message_id": f"msg_tg_autostage_{index}", "external_message_id": f"ext_tg_autostage_{index}", "text": f"Сообщение клиента {index}", "direction": "inbound", "author_type": "customer", "author_id": "tg-user-autostage", }, headers=_headers(tenant_id), ) assert response.status_code == 200 return response.json() stage_codes = [post_message(index)["stage"]["code"] for index in range(1, 7)] assert stage_codes[0] == "new_qualified_lead" assert stage_codes[1] == "new_qualified_lead" assert stage_codes[2] == "warm_lead" assert stage_codes[3] == "warm_lead" assert stage_codes[4] == "warm_lead" assert stage_codes[5] == "hot_lead" duplicate = post_message(6) assert duplicate["stage"]["code"] == "hot_lead" assert len(duplicate["messages"]) == 6 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())