diff --git a/backend/tests/test_analytics_phase5.py b/backend/tests/test_analytics_phase5.py index db8dd5db..6992e0a8 100644 --- a/backend/tests/test_analytics_phase5.py +++ b/backend/tests/test_analytics_phase5.py @@ -454,3 +454,227 @@ class TestFlowQualityEndpoint: flows = response.json()["flows"] usage_counts = [f["usage_count"] for f in flows] assert usage_counts == sorted(usage_counts, reverse=True) + + +# ─── PSA metrics endpoint tests ─────────────────────────────────────────────── + +class TestPsaMetrics: + """Tests for GET /api/v1/analytics/flowpilot/psa-metrics.""" + + async def test_requires_auth(self, client: AsyncClient, test_db: AsyncSession): + """Unauthenticated requests are rejected.""" + response = await client.get("/api/v1/analytics/flowpilot/psa-metrics") + assert response.status_code == 401 + + async def test_empty_state( + self, + client: AsyncClient, + test_db: AsyncSession, + team_admin_headers: dict, + ): + """When no PSA activity logs exist, returns zeros gracefully.""" + response = await client.get( + "/api/v1/analytics/flowpilot/psa-metrics", + headers=team_admin_headers, + ) + assert response.status_code == 200 + data = response.json() + + assert data["total_time_entries"] == 0 + assert data["total_hours_logged"] == 0.0 + assert data["avg_hours_per_session"] == 0.0 + assert data["daily_trend"] == [] + + funnel = data["push_funnel"] + assert funnel["total_sessions"] == 0 + assert funnel["linked_to_ticket"] == 0 + assert funnel["doc_pushed"] == 0 + assert funnel["time_entry_logged"] == 0 + + async def test_returns_time_entry_metrics( + self, + client: AsyncClient, + test_db: AsyncSession, + team_admin: dict, + team_admin_headers: dict, + ): + """Seeded time_entry_posted logs produce correct totals and averages.""" + from app.models.psa_activity_log import PsaActivityLog + + account_id = team_admin["user"].account_id + + # 3 time entries: 1.5 + 2.0 + 0.5 = 4.0 hours total, avg = 4.0/3 ≈ 1.33 + for hours in (1.5, 2.0, 0.5): + log = PsaActivityLog( + id=uuid.uuid4(), + account_id=account_id, + activity_type="time_entry_posted", + hours_logged=hours, + ) + test_db.add(log) + await test_db.commit() + + response = await client.get( + "/api/v1/analytics/flowpilot/psa-metrics", + headers=team_admin_headers, + ) + assert response.status_code == 200 + data = response.json() + + assert data["total_time_entries"] == 3 + assert data["total_hours_logged"] == pytest.approx(4.0, abs=0.01) + assert data["avg_hours_per_session"] == pytest.approx(4.0 / 3, abs=0.01) + + async def test_non_time_entry_logs_excluded( + self, + client: AsyncClient, + test_db: AsyncSession, + team_admin: dict, + team_admin_headers: dict, + ): + """Activity logs with a different activity_type do not count as time entries.""" + from app.models.psa_activity_log import PsaActivityLog + + account_id = team_admin["user"].account_id + + # One real time entry, one "note_posted" that should be ignored + test_db.add(PsaActivityLog( + id=uuid.uuid4(), + account_id=account_id, + activity_type="time_entry_posted", + hours_logged=1.0, + )) + test_db.add(PsaActivityLog( + id=uuid.uuid4(), + account_id=account_id, + activity_type="note_posted", + hours_logged=5.0, + )) + await test_db.commit() + + response = await client.get( + "/api/v1/analytics/flowpilot/psa-metrics", + headers=team_admin_headers, + ) + assert response.status_code == 200 + data = response.json() + assert data["total_time_entries"] == 1 + assert data["total_hours_logged"] == pytest.approx(1.0, abs=0.01) + + async def test_funnel_counts( + self, + client: AsyncClient, + test_db: AsyncSession, + team_admin: dict, + team_admin_headers: dict, + ): + """Funnel steps count the correct subset of sessions.""" + from app.models.ai_session import AISession + from app.models.psa_activity_log import PsaActivityLog + from app.models.psa_post_log import PsaPostLog + + account_id = team_admin["user"].account_id + user_id = team_admin["user"].id + + # 4 total sessions — 2 linked to a ticket + session_ids = [uuid.uuid4() for _ in range(4)] + for i, sid in enumerate(session_ids): + s = AISession( + id=sid, + user_id=user_id, + account_id=account_id, + status="resolved", + psa_ticket_id="TICKET-123" if i < 2 else None, + ) + test_db.add(s) + await test_db.commit() + + # 1 session with a successful doc push + push_session_id = session_ids[0] + post_log = PsaPostLog( + id=uuid.uuid4(), + ai_session_id=push_session_id, + ticket_id="TICKET-123", + note_type="internal", + content_posted="Session summary", + status="success", + posted_by=user_id, + ) + test_db.add(post_log) + + # 1 session with a time entry logged (same session as push for realism) + activity_log = PsaActivityLog( + id=uuid.uuid4(), + account_id=account_id, + session_id=push_session_id, + activity_type="time_entry_posted", + hours_logged=1.0, + ) + test_db.add(activity_log) + await test_db.commit() + + response = await client.get( + "/api/v1/analytics/flowpilot/psa-metrics", + headers=team_admin_headers, + ) + assert response.status_code == 200 + funnel = response.json()["push_funnel"] + + assert funnel["total_sessions"] == 4 + assert funnel["linked_to_ticket"] == 2 + assert funnel["doc_pushed"] == 1 + assert funnel["time_entry_logged"] == 1 + + async def test_daily_trend( + self, + client: AsyncClient, + test_db: AsyncSession, + team_admin: dict, + team_admin_headers: dict, + ): + """Time entries grouped by date produce the correct daily trend array.""" + from app.models.psa_activity_log import PsaActivityLog + + account_id = team_admin["user"].account_id + now = datetime.now(timezone.utc) + + # Day -2: 2 entries totalling 3.0 hours + # Day -1: 1 entry with 1.5 hours + entries = [ + (now - timedelta(days=2), 1.5), + (now - timedelta(days=2), 1.5), + (now - timedelta(days=1), 1.5), + ] + for created_at, hours in entries: + log = PsaActivityLog( + id=uuid.uuid4(), + account_id=account_id, + activity_type="time_entry_posted", + hours_logged=hours, + created_at=created_at, + ) + test_db.add(log) + await test_db.commit() + + response = await client.get( + "/api/v1/analytics/flowpilot/psa-metrics?period=30d", + headers=team_admin_headers, + ) + assert response.status_code == 200 + trend = response.json()["daily_trend"] + + assert len(trend) == 2 + + # Trend is ordered by date ascending + dates = [t["date"] for t in trend] + assert dates == sorted(dates) + + # Oldest day: 2 entries, 3.0 hours + oldest = trend[0] + assert oldest["entries"] == 2 + assert oldest["hours"] == pytest.approx(3.0, abs=0.01) + + # Most recent day: 1 entry, 1.5 hours + newest = trend[1] + assert newest["entries"] == 1 + assert newest["hours"] == pytest.approx(1.5, abs=0.01)