"""API tests for analytics endpoints.""" from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta, timezone from decimal import Decimal import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from app.core.security import jwt_service from app.models.contact import Contact from app.models.deal import Deal, DealStage, DealStatus from app.models.organization import Organization from app.models.organization_member import OrganizationMember, OrganizationRole from app.models.user import User @dataclass(slots=True) class AnalyticsScenario: organization_id: int user_id: int user_email: str token: str async def prepare_analytics_scenario(session_factory: async_sessionmaker[AsyncSession]) -> AnalyticsScenario: async with session_factory() as session: org = Organization(name="Analytics Org") user = User(email="analytics@example.com", hashed_password="hashed", name="Analyst", is_active=True) session.add_all([org, user]) await session.flush() membership = OrganizationMember( organization_id=org.id, user_id=user.id, role=OrganizationRole.OWNER, ) contact = Contact( organization_id=org.id, owner_id=user.id, name="Client", email="client@example.com", ) session.add_all([membership, contact]) await session.flush() now = datetime.now(timezone.utc) deals = [ Deal( organization_id=org.id, contact_id=contact.id, owner_id=user.id, title="Qual 1", amount=Decimal("100"), status=DealStatus.NEW, stage=DealStage.QUALIFICATION, created_at=now - timedelta(days=5), ), Deal( organization_id=org.id, contact_id=contact.id, owner_id=user.id, title="Proposal", amount=Decimal("200"), status=DealStatus.IN_PROGRESS, stage=DealStage.PROPOSAL, created_at=now - timedelta(days=15), ), Deal( organization_id=org.id, contact_id=contact.id, owner_id=user.id, title="Negotiation Won", amount=Decimal("500"), status=DealStatus.WON, stage=DealStage.NEGOTIATION, created_at=now - timedelta(days=2), ), Deal( organization_id=org.id, contact_id=contact.id, owner_id=user.id, title="Closed Lost", amount=Decimal("300"), status=DealStatus.LOST, stage=DealStage.CLOSED, created_at=now - timedelta(days=40), ), ] session.add_all(deals) await session.commit() token = jwt_service.create_access_token( subject=str(user.id), expires_delta=timedelta(minutes=30), claims={"email": user.email}, ) return AnalyticsScenario( organization_id=org.id, user_id=user.id, user_email=user.email, token=token, ) def _headers(token: str, organization_id: int) -> dict[str, str]: return {"Authorization": f"Bearer {token}", "X-Organization-Id": str(organization_id)} @pytest.mark.asyncio async def test_deals_summary_endpoint_returns_metrics( session_factory: async_sessionmaker[AsyncSession], client: AsyncClient ) -> None: scenario = await prepare_analytics_scenario(session_factory) response = await client.get( "/api/v1/analytics/deals/summary?days=30", headers=_headers(scenario.token, scenario.organization_id), ) assert response.status_code == 200 payload = response.json() assert payload["total_deals"] == 4 by_status = {entry["status"]: entry for entry in payload["by_status"]} assert by_status[DealStatus.NEW.value]["count"] == 1 assert by_status[DealStatus.WON.value]["amount_sum"] == "500" assert payload["won"]["average_amount"] == "500" assert payload["new_deals"]["count"] == 3 @pytest.mark.asyncio async def test_deals_summary_respects_days_filter( session_factory: async_sessionmaker[AsyncSession], client: AsyncClient ) -> None: scenario = await prepare_analytics_scenario(session_factory) response = await client.get( "/api/v1/analytics/deals/summary?days=3", headers=_headers(scenario.token, scenario.organization_id), ) assert response.status_code == 200 payload = response.json() assert payload["new_deals"]["count"] == 1 # только сделки моложе трёх дней @pytest.mark.asyncio async def test_deals_funnel_returns_breakdown( session_factory: async_sessionmaker[AsyncSession], client: AsyncClient ) -> None: scenario = await prepare_analytics_scenario(session_factory) response = await client.get( "/api/v1/analytics/deals/funnel", headers=_headers(scenario.token, scenario.organization_id), ) assert response.status_code == 200 payload = response.json() assert len(payload["stages"]) == 4 qualification = next(item for item in payload["stages"] if item["stage"] == DealStage.QUALIFICATION.value) assert qualification["total"] == 1 proposal = next(item for item in payload["stages"] if item["stage"] == DealStage.PROPOSAL.value) assert proposal["conversion_to_next"] == 100.0