import json from fastapi.testclient import TestClient from sqlalchemy import select import services.sales_service.app as sales_module from services.shared.core import new_id from services.shared.db import get_session from services.shared.sql_models import EventOutboxRow def _headers(tenant_id: str = "tenant_events") -> dict[str, str]: return {"X-User": "admin", "X-Role": "admin", "X-Tenant-ID": tenant_id} def _events(event_type: str | None = None, tenant_id: str | None = None) -> list[tuple[EventOutboxRow, dict]]: session = get_session() try: stmt = select(EventOutboxRow).where(EventOutboxRow.producer_service == "sales-service").order_by(EventOutboxRow.id.asc()) if event_type: stmt = stmt.where(EventOutboxRow.event_type == event_type) rows = session.execute(stmt).scalars().all() result = [] for row in rows: envelope = json.loads(row.payload_json or "{}") payload = envelope.get("payload") if isinstance(envelope.get("payload"), dict) else {} if tenant_id and payload.get("tenant_id") != tenant_id: continue result.append((row, payload)) return result finally: session.close() def _event_payload(event_type: str, tenant_id: str = "tenant_events") -> dict: rows = _events(event_type, tenant_id) assert rows, f"expected {event_type}" return rows[-1][1] def _lead_payload(seed: str) -> dict: return { "source_type": "website", "source_channel": "webchat", "full_name": f"Event Buyer {seed}", "company_name": "Events QA", "phone": f"+7700{seed[-7:]}", "email": f"{seed}@events.test", "lead_temperature": "warm", "lead_score": 70, "initial_need_summary": "Need sales event coverage.", "preferred_channel": "telegram", "assigned_agent_type": "text_ai", "status": "new_qualified_lead", "priority": 3, "title": f"Event Lead {seed}", } def _create_lead_and_deal(client: TestClient, tenant_id: str = "tenant_events") -> tuple[dict, dict]: seed = new_id("evt").replace("_", "") response = client.post("/api/v1/leads", json=_lead_payload(seed), headers=_headers(tenant_id)) assert response.status_code == 200 lead = response.json() deals = client.get("/api/v1/deals", headers=_headers(tenant_id)) assert deals.status_code == 200 deal = next(item for item in deals.json() if item["lead_id"] == lead["lead_id"]) return lead, deal def _change_stage(client: TestClient, deal_id: str, stage_code: str, tenant_id: str = "tenant_events") -> dict: response = client.post( f"/api/v1/deals/{deal_id}/change-stage", json={"target_stage_code": stage_code, "reason": f"test.{stage_code}"}, headers=_headers(tenant_id), ) assert response.status_code == 200 return response.json() def _prepare_offer_selection(client: TestClient, deal_id: str, tenant_id: str = "tenant_events") -> None: _change_stage(client, deal_id, "hot_lead", tenant_id) _change_stage(client, deal_id, "offer_selection", tenant_id) def _prepare_invoice_deal(client: TestClient, deal_id: str, tenant_id: str = "tenant_events") -> None: updated = client.patch( f"/api/v1/deals/{deal_id}", json={"document_required": False}, headers=_headers(tenant_id), ) assert updated.status_code == 200 _change_stage(client, deal_id, "active_text_communication", tenant_id) _change_stage(client, deal_id, "need_confirmed", tenant_id) def _create_invoice(client: TestClient, deal_id: str, tenant_id: str = "tenant_events") -> dict: _prepare_invoice_deal(client, deal_id, tenant_id) response = client.post( f"/api/v1/deals/{deal_id}/invoices", json={"amount": 150000, "currency": "KZT", "due_date": "2026-05-15"}, headers=_headers(tenant_id), ) assert response.status_code == 200 return response.json() def test_create_lead_publishes_lead_entered_crm(): client = TestClient(sales_module.app) lead, deal = _create_lead_and_deal(client) payload = _event_payload("lead.entered_crm") assert payload["tenant_id"] == "tenant_events" assert payload["lead_id"] == lead["lead_id"] assert payload["deal_id"] == deal["deal_id"] assert payload["initial_stage_code"] == "new_qualified_lead" def test_stage_change_publishes_deal_stage_changed(): client = TestClient(sales_module.app) _, deal = _create_lead_and_deal(client) response = client.post( f"/api/v1/deals/{deal['deal_id']}/change-stage", json={"target_stage_code": "warm_lead", "reason": "lead warmed"}, headers=_headers(), ) assert response.status_code == 200 payload = _event_payload("deal.stage_changed") assert payload["deal_id"] == deal["deal_id"] assert payload["from_stage_code"] == "new_qualified_lead" assert payload["to_stage_code"] == "warm_lead" assert payload["from_stage_id"].startswith("pst_") assert payload["to_stage_id"].startswith("pst_") def test_inbound_message_publishes_message_received(): client = TestClient(sales_module.app) response = client.post( "/api/v1/messages/inbound-webhook", json={ "phone": "+77008880001", "channel_provider": "whatsapp", "external_message_id": "ext-msg-1", "sender_id": "wa-user", "body": "Need pricing", }, headers=_headers(), ) assert response.status_code == 200 payload = _event_payload("message.received") assert payload["message_id"] == response.json()["message_id"] assert payload["channel_provider"] == "whatsapp" assert payload["external_message_id"] == "ext-msg-1" def test_inbound_call_publishes_call_received(): client = TestClient(sales_module.app) response = client.post( "/api/v1/calls/inbound-webhook", json={"phone_number": "+77008880002", "provider": "asterisk", "external_call_id": "ext-call-1"}, headers=_headers(), ) assert response.status_code == 200 payload = _event_payload("call.received") assert payload["call_id"] == response.json()["call_id"] assert payload["provider"] == "asterisk" assert payload["external_call_id"] == "ext-call-1" def test_complete_call_publishes_call_completed(): client = TestClient(sales_module.app) call = client.post( "/api/v1/calls/inbound-webhook", json={"phone_number": "+77008880003", "provider": "asterisk"}, headers=_headers(), ).json() response = client.post( f"/api/v1/calls/{call['call_id']}/complete", json={"summary": "Customer asked for a proposal.", "result_code": "completed"}, headers=_headers(), ) assert response.status_code == 200 payload = _event_payload("call.completed") assert payload["call_id"] == call["call_id"] assert payload["result_code"] == "completed" def test_create_offer_publishes_offer_created(): client = TestClient(sales_module.app) _, deal = _create_lead_and_deal(client) _prepare_offer_selection(client, deal["deal_id"]) response = client.post( f"/api/v1/deals/{deal['deal_id']}/offers", json={"offer_type": "quotation", "title": "Quotation", "total_amount": 150000, "currency": "KZT"}, headers=_headers(), ) assert response.status_code == 200 payload = _event_payload("offer.created") assert payload["offer_id"] == response.json()["offer_id"] assert payload["total_amount"] == 150000 def test_send_offer_publishes_offer_sent(): client = TestClient(sales_module.app) _, deal = _create_lead_and_deal(client) _prepare_offer_selection(client, deal["deal_id"]) offer = client.post( f"/api/v1/deals/{deal['deal_id']}/offers", json={"offer_type": "quotation", "title": "Quotation", "total_amount": 150000, "currency": "KZT"}, headers=_headers(), ).json() response = client.post(f"/api/v1/offers/{offer['offer_id']}/send", headers=_headers()) assert response.status_code == 200 payload = _event_payload("offer.sent") assert payload["offer_id"] == offer["offer_id"] assert payload["deal_id"] == deal["deal_id"] def test_create_invoice_publishes_invoice_created(): client = TestClient(sales_module.app) _, deal = _create_lead_and_deal(client) invoice = _create_invoice(client, deal["deal_id"]) payload = _event_payload("invoice.created") assert payload["invoice_id"] == invoice["invoice_id"] assert payload["amount"] == 150000 def test_send_invoice_publishes_invoice_sent(): client = TestClient(sales_module.app) _, deal = _create_lead_and_deal(client) invoice = _create_invoice(client, deal["deal_id"]) response = client.post(f"/api/v1/invoices/{invoice['invoice_id']}/send", headers=_headers()) assert response.status_code == 200 payload = _event_payload("invoice.sent") assert payload["invoice_id"] == invoice["invoice_id"] assert payload["invoice_number"] == invoice["invoice_number"] def test_payment_webhook_publishes_payment_received(): client = TestClient(sales_module.app) _, deal = _create_lead_and_deal(client) invoice = _create_invoice(client, deal["deal_id"]) response = client.post( "/api/v1/payments/webhook", json={ "deal_id": deal["deal_id"], "invoice_id": invoice["invoice_id"], "payment_provider": "manual", "external_payment_id": "pay-ext-1", "amount": 150000, "currency": "KZT", "status": "success", }, headers=_headers(), ) assert response.status_code == 200 payload = _event_payload("payment.received") assert payload["payment_id"] == response.json()["payment_id"] assert payload["status"] == "success" def test_paid_invoice_publishes_invoice_paid(): client = TestClient(sales_module.app) _, deal = _create_lead_and_deal(client) invoice = _create_invoice(client, deal["deal_id"]) response = client.post( "/api/v1/payments/webhook", json={ "deal_id": deal["deal_id"], "invoice_id": invoice["invoice_id"], "payment_provider": "manual", "external_payment_id": "pay-ext-2", "amount": 150000, "currency": "KZT", "status": "success", }, headers=_headers(), ) assert response.status_code == 200 payload = _event_payload("invoice.paid") assert payload["invoice_id"] == invoice["invoice_id"] assert payload["paid_amount"] == 150000 def test_won_deal_publishes_deal_won(): client = TestClient(sales_module.app) _, deal = _create_lead_and_deal(client) invoice = _create_invoice(client, deal["deal_id"]) response = client.post( "/api/v1/payments/webhook", json={ "deal_id": deal["deal_id"], "invoice_id": invoice["invoice_id"], "payment_provider": "manual", "external_payment_id": "pay-ext-3", "amount": 150000, "currency": "KZT", "status": "success", }, headers=_headers(), ) assert response.status_code == 200 payload = _event_payload("deal.won") assert payload["deal_id"] == deal["deal_id"] assert payload["won_reason"] == "payment_received" def test_sales_events_are_tenant_scoped(): client = TestClient(sales_module.app) _create_lead_and_deal(client, "tenant_events_a") _create_lead_and_deal(client, "tenant_events_b") tenant_a_events = _events("lead.entered_crm", "tenant_events_a") tenant_b_events = _events("lead.entered_crm", "tenant_events_b") assert tenant_a_events assert tenant_b_events assert all(payload["tenant_id"] == "tenant_events_a" for _row, payload in tenant_a_events) assert all(payload["tenant_id"] == "tenant_events_b" for _row, payload in tenant_b_events) def test_failed_business_action_does_not_publish_event(): client = TestClient(sales_module.app) _, deal = _create_lead_and_deal(client) before = len(_events("deal.stage_changed")) response = client.post( f"/api/v1/deals/{deal['deal_id']}/change-stage", json={"stage_id": "pst_missing", "reason": "bad stage"}, headers=_headers(), ) assert response.status_code == 404 assert len(_events("deal.stage_changed")) == before