"""CRM API and event contract tests — Phase 6.0 + 6.1.""" from __future__ import annotations import pytest from app.events.publisher import get_event_publisher from app.events.types import CrmEventType from app.tests.conftest import TENANT_A, TENANT_B, tenant_headers @pytest.mark.asyncio async def test_health(client): resp = await client.get("/health") assert resp.status_code == 200 body = resp.json() assert body["service"] == "crm-service" assert body["status"] == "ok" @pytest.mark.asyncio async def test_create_lead_publishes_event(client): resp = await client.post( "/api/v1/leads", json={ "title": "New Lead", "first_name": "Ali", "last_name": "Rezaei", "email": "lead@example.com", "mobile": "+989121234567", "source": "web", "owner_user_id": "owner-1", }, headers=tenant_headers(TENANT_A), ) assert resp.status_code == 201, resp.text data = resp.json() assert data["full_name"] assert data["lead_number"] assert data["tenant_id"] == str(TENANT_A) assert data["owner_user_id"] == "owner-1" events = get_event_publisher().published assert any(e.event_type == CrmEventType.LEAD_CREATED.value for e in events) @pytest.mark.asyncio async def test_contact_and_organization_flow(client): org = await client.post( "/api/v1/organizations", json={ "name": "Buyer Co", "industry": "retail", "kind": "company", "legal_name": "Buyer Co Ltd", }, headers=tenant_headers(TENANT_A), ) assert org.status_code == 201, org.text org_id = org.json()["id"] contact = await client.post( "/api/v1/contacts", json={ "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com", "phones": ["+1-555-0100", "+1-555-0101"], "emails": ["ada@example.com", "ada.work@example.com"], "kind": "business", "organization_id": org_id, }, headers=tenant_headers(TENANT_A), ) assert contact.status_code == 201, contact.text assert contact.json()["organization_id"] == org_id assert contact.json()["full_name"] == "Ada Lovelace" @pytest.mark.asyncio async def test_pipeline_opportunity_win_flow(client): pipeline = await client.post( "/api/v1/pipelines", json={"name": "Default", "code": "DEFAULT", "is_default": True}, headers=tenant_headers(TENANT_A), ) assert pipeline.status_code == 201 pipeline_id = pipeline.json()["id"] stage = await client.post( "/api/v1/pipelines/stages", json={ "pipeline_id": pipeline_id, "name": "Proposal", "code": "PROPOSAL", "position": 1, "win_probability": 60, }, headers=tenant_headers(TENANT_A), ) assert stage.status_code == 201 stage_id = stage.json()["id"] opportunity = await client.post( "/api/v1/opportunities", json={ "name": "Big Deal", "pipeline_id": pipeline_id, "stage_id": stage_id, "amount": "150000.0000", "currency_code": "IRR", }, headers=tenant_headers(TENANT_A), ) assert opportunity.status_code == 201 opportunity_id = opportunity.json()["id"] won = await client.post( f"/api/v1/opportunities/{opportunity_id}/win", headers=tenant_headers(TENANT_A), ) assert won.status_code == 200 assert won.json()["status"] == "won" events = get_event_publisher().published assert any(e.event_type == CrmEventType.OPPORTUNITY_WON.value for e in events) @pytest.mark.asyncio async def test_activity_complete_and_quote_create(client): activity = await client.post( "/api/v1/activities", json={"subject": "Discovery call", "activity_type": "call"}, headers=tenant_headers(TENANT_A), ) assert activity.status_code == 201 activity_id = activity.json()["id"] completed = await client.post( f"/api/v1/activities/{activity_id}/complete", headers=tenant_headers(TENANT_A), ) assert completed.status_code == 200 assert completed.json()["status"] == "completed" quote = await client.post( "/api/v1/quotes", json={ "title": "Q1 Offer", "quote_number": "Q-1001", "amount": "25000.0000", }, headers=tenant_headers(TENANT_A), ) assert quote.status_code == 201 events = get_event_publisher().published assert any(e.event_type == CrmEventType.ACTIVITY_COMPLETED.value for e in events) assert any(e.event_type == CrmEventType.QUOTE_CREATED.value for e in events) @pytest.mark.asyncio async def test_lead_update_publishes_event(client): created = await client.post( "/api/v1/leads", json={"title": "Update me", "owner_user_id": "owner-1"}, headers=tenant_headers(TENANT_A), ) assert created.status_code == 201, created.text lead_id = created.json()["id"] updated = await client.patch( f"/api/v1/leads/{lead_id}", json={"status": "contacted"}, headers=tenant_headers(TENANT_A), ) assert updated.status_code == 200 assert updated.json()["status"] == "contacted" events = get_event_publisher().published assert any(e.event_type == CrmEventType.LEAD_UPDATED.value for e in events) @pytest.mark.asyncio async def test_phase61_entities_notes_attachments_tags_custom_fields(client): lead = await client.post( "/api/v1/leads", json={ "first_name": "Sara", "last_name": "Karimi", "owner_user_id": "owner-1", "email": "sara@example.com", }, headers=tenant_headers(TENANT_A), ) assert lead.status_code == 201, lead.text lead_id = lead.json()["id"] source = await client.post( "/api/v1/lookups/lead-sources", json={"code": "WEB", "name": "Website"}, headers=tenant_headers(TENANT_A), ) assert source.status_code == 201 status_def = await client.post( "/api/v1/lookups/lead-statuses", json={"code": "NEW", "name": "New", "position": 1}, headers=tenant_headers(TENANT_A), ) assert status_def.status_code == 201 tag = await client.post( "/api/v1/tags", json={"name": "hot", "color": "#ff0000", "group_name": "priority"}, headers=tenant_headers(TENANT_A), ) assert tag.status_code == 201 tag_id = tag.json()["id"] assigned = await client.post( f"/api/v1/tags/leads/{lead_id}", json={"tag_id": tag_id}, headers=tenant_headers(TENANT_A), ) assert assigned.status_code == 200 address = await client.post( "/api/v1/addresses", json={ "entity_type": "lead", "entity_id": lead_id, "address_type": "office", "line1": "Valiasr St 1", "city": "Tehran", "country": "IR", "latitude": "35.6892000", "longitude": "51.3890000", "is_default": True, }, headers=tenant_headers(TENANT_A), ) assert address.status_code == 201, address.text note = await client.post( "/api/v1/notes", json={ "entity_type": "lead", "entity_id": lead_id, "body": "
First note
", "is_pinned": True, }, headers=tenant_headers(TENANT_A), ) assert note.status_code == 201 note_id = note.json()["id"] note_upd = await client.patch( f"/api/v1/notes/{note_id}", json={"body": "Updated note
"}, headers=tenant_headers(TENANT_A), ) assert note_upd.status_code == 200 assert note_upd.json()["version"] == 2 attachment = await client.post( "/api/v1/attachments", json={ "entity_type": "lead", "entity_id": lead_id, "file_storage_id": "fs-123", "file_name": "contract.pdf", "content_type": "application/pdf", "kind": "contract", }, headers=tenant_headers(TENANT_A), ) assert attachment.status_code == 201 field = await client.post( "/api/v1/custom-fields", json={ "entity_type": "lead", "field_key": "budget_band", "label": "Budget Band", "field_type": "select", "options": ["low", "mid", "high"], "is_required": True, }, headers=tenant_headers(TENANT_A), ) assert field.status_code == 201 field_id = field.json()["id"] value = await client.put( "/api/v1/custom-fields/values", json={ "custom_field_id": field_id, "entity_type": "lead", "entity_id": lead_id, "value_text": "mid", }, headers=tenant_headers(TENANT_A), ) assert value.status_code == 200 assign = await client.post( f"/api/v1/leads/{lead_id}/assign", json={"assigned_user_id": "rep-9", "note": "handoff"}, headers=tenant_headers(TENANT_A), ) assert assign.status_code == 200 assert assign.json()["assigned_user_id"] == "rep-9" audit = await client.get( "/api/v1/audit", params={"entity_type": "lead", "entity_id": lead_id}, headers=tenant_headers(TENANT_A), ) assert audit.status_code == 200 assert len(audit.json()) >= 1 events = get_event_publisher().published assert any(e.event_type == CrmEventType.NOTE_CREATED.value for e in events) assert any(e.event_type == CrmEventType.ATTACHMENT_ADDED.value for e in events) # tenant isolation on note list notes_b = await client.get( "/api/v1/notes", params={"entity_type": "lead", "entity_id": lead_id}, headers=tenant_headers(TENANT_B), ) assert notes_b.status_code == 200 assert notes_b.json() == []