329 lines
11 KiB
Python
329 lines
11 KiB
Python
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 _create_invoice(client: TestClient, deal_id: str, tenant_id: str = "tenant_events") -> dict:
|
|
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={"stage_code": "offer_sent", "reason": "offer sent"},
|
|
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"] == "offer_sent"
|
|
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)
|
|
|
|
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)
|
|
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
|