Register all active platform services and base features in core DB so admin can manage them, add delivery/hospitality/sports-center backend phases, update apps catalog and production deploy tooling. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.7 KiB
Python
121 lines
3.7 KiB
Python
"""Phase 12.8 Analytics API / tenant / validation tests."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.events.publisher import get_event_publisher
|
|
from app.events.types import HospitalityEventType
|
|
from app.tests.conftest import TENANT_A, TENANT_B, tenant_headers
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_analytics_snapshot_refresh_counts_local_tables(client):
|
|
h = tenant_headers(TENANT_A)
|
|
|
|
venue = await client.post(
|
|
"/api/v1/venues",
|
|
json={"code": "an-v1", "name": "Analytics Venue", "status": "active"},
|
|
headers=h,
|
|
)
|
|
assert venue.status_code == 201, venue.text
|
|
venue_id = venue.json()["id"]
|
|
|
|
menu = await client.post(
|
|
"/api/v1/menus",
|
|
json={"venue_id": venue_id, "code": "an-menu", "name": "Analytics Menu"},
|
|
headers=h,
|
|
)
|
|
assert menu.status_code == 201, menu.text
|
|
|
|
report = await client.post(
|
|
"/api/v1/analytics-reports",
|
|
json={
|
|
"code": "daily-ops",
|
|
"name": "Daily Ops",
|
|
"metric_keys": ["venues_count", "menus_count"],
|
|
},
|
|
headers=h,
|
|
)
|
|
assert report.status_code == 201, report.text
|
|
report_id = report.json()["id"]
|
|
|
|
snapshot = await client.post(
|
|
"/api/v1/analytics-snapshots/refresh",
|
|
json={
|
|
"report_id": report_id,
|
|
"period_start": "2026-07-01",
|
|
"period_end": "2026-07-31",
|
|
},
|
|
headers=h,
|
|
)
|
|
assert snapshot.status_code == 201, snapshot.text
|
|
metrics = snapshot.json()["metrics"]
|
|
assert metrics["venues_count"] == 1
|
|
assert metrics["menus_count"] == 1
|
|
|
|
published = {e.event_type for e in get_event_publisher().published}
|
|
assert HospitalityEventType.ANALYTICS_REPORT_DEFINITION_CREATED.value in published
|
|
assert HospitalityEventType.ANALYTICS_SNAPSHOT_REFRESHED.value in published
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_analytics_report_rejects_unknown_metric_key(client):
|
|
h = tenant_headers(TENANT_A)
|
|
|
|
bad = await client.post(
|
|
"/api/v1/analytics-reports",
|
|
json={
|
|
"code": "bad-report",
|
|
"name": "Bad Report",
|
|
"metric_keys": ["not_a_real_metric"],
|
|
},
|
|
headers=h,
|
|
)
|
|
assert bad.status_code == 422
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_analytics_report_code_unique_per_tenant(client):
|
|
h = tenant_headers(TENANT_A)
|
|
|
|
first = await client.post(
|
|
"/api/v1/analytics-reports",
|
|
json={"code": "dup-report", "name": "Report A", "metric_keys": ["venues_count"]},
|
|
headers=h,
|
|
)
|
|
assert first.status_code == 201, first.text
|
|
|
|
duplicate = await client.post(
|
|
"/api/v1/analytics-reports",
|
|
json={"code": "dup-report", "name": "Report B", "metric_keys": ["venues_count"]},
|
|
headers=h,
|
|
)
|
|
assert duplicate.status_code == 409
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_analytics_report_tenant_isolation(client):
|
|
h = tenant_headers(TENANT_A)
|
|
|
|
report = await client.post(
|
|
"/api/v1/analytics-reports",
|
|
json={"code": "iso-report", "name": "Iso Report", "metric_keys": ["venues_count"]},
|
|
headers=h,
|
|
)
|
|
assert report.status_code == 201, report.text
|
|
report_id = report.json()["id"]
|
|
|
|
denied = await client.get(
|
|
f"/api/v1/analytics-reports/{report_id}", headers=tenant_headers(TENANT_B)
|
|
)
|
|
assert denied.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_capabilities_phase_12_8(client):
|
|
resp = await client.get("/capabilities")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["features"]["analytics"] is True
|
|
assert body["features"]["ai"] is False
|