from tests.admin.base import * # noqa: F401,F403 class AdminMetricsTemplatesTests(AdminUniversalCrudBase): def test_dashboard_metrics_returns_lawyer_loads(self): headers = self._auth_headers("ADMIN", email="root@example.com") with self.SessionLocal() as db: db.add_all( [ Status(code="NEW", name="Новая", enabled=True, sort_order=0, is_terminal=False), Status(code="IN_PROGRESS", name="В работе", enabled=True, sort_order=1, is_terminal=False), Status(code="CLOSED", name="Закрыта", enabled=True, sort_order=2, is_terminal=True), ] ) lawyer_busy = AdminUser( role="LAWYER", name="Юрист Загруженный", email="busy@example.com", password_hash="hash", avatar_url="https://cdn.example.com/a.png", primary_topic_code="civil-law", is_active=True, ) lawyer_free = AdminUser( role="LAWYER", name="Юрист Свободный", email="free@example.com", password_hash="hash", avatar_url=None, primary_topic_code="family-law", is_active=True, ) db.add_all([lawyer_busy, lawyer_free]) db.flush() db.add_all( [ Request( track_number="TRK-METRICS-1", client_name="Клиент 1", client_phone="+79990000001", topic_code="civil-law", status_code="NEW", assigned_lawyer_id=str(lawyer_busy.id), extra_fields={}, ), Request( track_number="TRK-METRICS-2", client_name="Клиент 2", client_phone="+79990000002", topic_code="civil-law", status_code="CLOSED", assigned_lawyer_id=str(lawyer_busy.id), extra_fields={}, ), ] ) db.commit() response = self.client.get("/api/admin/metrics/overview", headers=headers) self.assertEqual(response.status_code, 200) body = response.json() self.assertIn("lawyer_loads", body) self.assertEqual(len(body["lawyer_loads"]), 2) by_email = {row["email"]: row for row in body["lawyer_loads"]} self.assertEqual(by_email["busy@example.com"]["active_load"], 1) self.assertEqual(by_email["busy@example.com"]["total_assigned"], 2) self.assertEqual(by_email["busy@example.com"]["avatar_url"], "https://cdn.example.com/a.png") self.assertEqual(by_email["free@example.com"]["active_load"], 0) self.assertEqual(by_email["free@example.com"]["total_assigned"], 0) def test_dashboard_metrics_returns_service_request_unread_totals(self): admin_headers = self._auth_headers("ADMIN", email="root@example.com") lawyer_id = str(uuid4()) lawyer_headers = self._auth_headers("LAWYER", sub=lawyer_id, email="lawyer@example.com") with self.SessionLocal() as db: client = Client(full_name="Клиент по запросам", phone="+79990000012", responsible="seed") db.add(client) db.flush() req = Request( track_number="TRK-METRICS-SR-1", client_id=client.id, client_name=client.full_name, client_phone=client.phone, topic_code="consulting", status_code="IN_PROGRESS", assigned_lawyer_id=lawyer_id, extra_fields={}, responsible="seed", ) db.add(req) db.flush() db.add_all( [ RequestServiceRequest( request_id=str(req.id), client_id=str(client.id), assigned_lawyer_id=lawyer_id, type="CURATOR_CONTACT", status="NEW", body="Нужна консультация администратора", created_by_client=True, admin_unread=True, lawyer_unread=True, responsible="Клиент", ), RequestServiceRequest( request_id=str(req.id), client_id=str(client.id), assigned_lawyer_id=lawyer_id, type="LAWYER_CHANGE_REQUEST", status="NEW", body="Прошу сменить юриста", created_by_client=True, admin_unread=True, lawyer_unread=False, responsible="Клиент", ), ] ) db.commit() admin_response = self.client.get("/api/admin/metrics/overview", headers=admin_headers) self.assertEqual(admin_response.status_code, 200) self.assertEqual(int(admin_response.json().get("service_request_unread_total") or 0), 2) lawyer_response = self.client.get("/api/admin/metrics/overview", headers=lawyer_headers) self.assertEqual(lawyer_response.status_code, 200) self.assertEqual(int(lawyer_response.json().get("service_request_unread_total") or 0), 1) def test_dashboard_metrics_returns_dynamic_sla_and_frt(self): headers = self._auth_headers("ADMIN", email="root@example.com") now = datetime.now(timezone.utc) with self.SessionLocal() as db: db.add_all( [ Status(code="NEW", name="Новая", enabled=True, sort_order=0, is_terminal=False), Status(code="CLOSED", name="Закрыта", enabled=True, sort_order=1, is_terminal=True), ] ) req = Request( track_number="TRK-SLA-M-1", client_name="Клиент SLA", client_phone="+79990000003", topic_code="civil-law", status_code="NEW", extra_fields={}, created_at=now - timedelta(hours=30), updated_at=now - timedelta(hours=30), ) db.add(req) db.flush() db.add( Message( request_id=req.id, author_type="LAWYER", author_name="Юрист", body="Ответ", created_at=req.created_at + timedelta(minutes=20), updated_at=req.created_at + timedelta(minutes=20), ) ) db.add( StatusHistory( request_id=req.id, from_status=None, to_status="NEW", changed_by_admin_id=None, created_at=now - timedelta(hours=30), updated_at=now - timedelta(hours=30), ) ) db.commit() response = self.client.get("/api/admin/metrics/overview", headers=headers) self.assertEqual(response.status_code, 200) body = response.json() self.assertGreaterEqual(int(body.get("sla_overdue") or 0), 1) self.assertIsNotNone(body.get("frt_avg_minutes")) self.assertAlmostEqual(float(body["frt_avg_minutes"]), 20.0, places=1) self.assertIn("NEW", body.get("avg_time_in_status_hours") or {}) def test_dashboard_metrics_can_skip_sla_for_fast_overview(self): headers = self._auth_headers("ADMIN", email="root@example.com") now = datetime.now(timezone.utc) with self.SessionLocal() as db: db.add_all( [ Status(code="NEW", name="Новая", enabled=True, sort_order=0, is_terminal=False), Status(code="CLOSED", name="Закрыта", enabled=True, sort_order=1, is_terminal=True), ] ) req = Request( track_number="TRK-SLA-M-FAST-1", client_name="Клиент SLA Fast", client_phone="+79990000013", topic_code="civil-law", status_code="NEW", extra_fields={}, created_at=now - timedelta(hours=30), updated_at=now - timedelta(hours=30), ) db.add(req) db.flush() db.add( Message( request_id=req.id, author_type="LAWYER", author_name="Юрист", body="Ответ", created_at=req.created_at + timedelta(minutes=20), updated_at=req.created_at + timedelta(minutes=20), ) ) db.add( StatusHistory( request_id=req.id, from_status=None, to_status="NEW", changed_by_admin_id=None, created_at=now - timedelta(hours=30), updated_at=now - timedelta(hours=30), ) ) db.commit() response = self.client.get("/api/admin/metrics/overview?include_sla=false", headers=headers) self.assertEqual(response.status_code, 200) body = response.json() self.assertEqual(int(body.get("sla_overdue") or 0), 0) self.assertIsNone(body.get("frt_avg_minutes")) self.assertEqual(body.get("avg_time_in_status_hours"), {}) sla_response = self.client.get("/api/admin/metrics/overview-sla", headers=headers) self.assertEqual(sla_response.status_code, 200) sla_body = sla_response.json() self.assertGreaterEqual(int(sla_body.get("sla_overdue") or 0), 1) self.assertIsNotNone(sla_body.get("frt_avg_minutes")) def test_admin_can_manage_admin_user_topics_only_for_lawyers(self): headers = self._auth_headers("ADMIN", email="root@example.com") with self.SessionLocal() as db: db.add_all( [ Topic(code="civil-law", name="Гражданское право", enabled=True, sort_order=1), Topic(code="tax-law", name="Налоговое право", enabled=True, sort_order=2), ] ) lawyer = AdminUser( role="LAWYER", name="Юрист Профильный", email="lawyer.topics@example.com", password_hash="hash", is_active=True, ) admin = AdminUser( role="ADMIN", name="Администратор", email="admin.topics@example.com", password_hash="hash", is_active=True, ) db.add_all([lawyer, admin]) db.commit() lawyer_id = str(lawyer.id) admin_id = str(admin.id) created = self.client.post( "/api/admin/crud/admin_user_topics", headers=headers, json={"admin_user_id": lawyer_id, "topic_code": "civil-law"}, ) self.assertEqual(created.status_code, 201) body = created.json() self.assertEqual(body["admin_user_id"], lawyer_id) self.assertEqual(body["topic_code"], "civil-law") self.assertEqual(body["responsible"], "root@example.com") relation_id = body["id"] UUID(relation_id) queried = self.client.post( "/api/admin/crud/admin_user_topics/query", headers=headers, json={ "filters": [{"field": "admin_user_id", "op": "=", "value": lawyer_id}], "sort": [{"field": "created_at", "dir": "desc"}], "page": {"limit": 50, "offset": 0}, }, ) self.assertEqual(queried.status_code, 200) self.assertEqual(queried.json()["total"], 1) updated = self.client.patch( f"/api/admin/crud/admin_user_topics/{relation_id}", headers=headers, json={"topic_code": "tax-law"}, ) self.assertEqual(updated.status_code, 200) self.assertEqual(updated.json()["topic_code"], "tax-law") forbidden_for_non_lawyer = self.client.post( "/api/admin/crud/admin_user_topics", headers=headers, json={"admin_user_id": admin_id, "topic_code": "civil-law"}, ) self.assertEqual(forbidden_for_non_lawyer.status_code, 400) deleted = self.client.delete(f"/api/admin/crud/admin_user_topics/{relation_id}", headers=headers) self.assertEqual(deleted.status_code, 200) def test_topic_templates_crud_and_request_required_fields_validation(self): headers = self._auth_headers("ADMIN", email="root@example.com") with self.SessionLocal() as db: db.add(Topic(code="civil-law", name="Гражданское право", enabled=True, sort_order=1)) db.add( FormField( key="passport_series", label="Серия паспорта", type="string", required=False, enabled=True, sort_order=1, ) ) db.commit() required_created = self.client.post( "/api/admin/crud/topic_required_fields", headers=headers, json={ "topic_code": "civil-law", "field_key": "passport_series", "required": True, "enabled": True, "sort_order": 10, }, ) self.assertEqual(required_created.status_code, 201) self.assertEqual(required_created.json()["responsible"], "root@example.com") invalid_required = self.client.post( "/api/admin/crud/topic_required_fields", headers=headers, json={ "topic_code": "civil-law", "field_key": "missing_field", "required": True, "enabled": True, "sort_order": 11, }, ) self.assertEqual(invalid_required.status_code, 400) template_created = self.client.post( "/api/admin/crud/topic_data_templates", headers=headers, json={ "topic_code": "civil-law", "key": "court_file", "label": "Судебный файл", "description": "PDF с материалами", "required": True, "enabled": True, "sort_order": 1, }, ) self.assertEqual(template_created.status_code, 201) self.assertEqual(template_created.json()["topic_code"], "civil-law") blocked = self.client.post( "/api/admin/crud/requests", headers=headers, json={ "client_name": "ООО Проверка", "client_phone": "+79995550001", "topic_code": "civil-law", "status_code": "NEW", "description": "missing required extra field", "extra_fields": {}, }, ) self.assertEqual(blocked.status_code, 400) self.assertIn("passport_series", blocked.json().get("detail", "")) created = self.client.post( "/api/admin/crud/requests", headers=headers, json={ "client_name": "ООО Проверка", "client_phone": "+79995550001", "topic_code": "civil-law", "status_code": "NEW", "description": "required extra field provided", "extra_fields": {"passport_series": "1234"}, }, ) self.assertEqual(created.status_code, 201) request_id = created.json()["id"] with self.SessionLocal() as db: row = db.get(Request, UUID(request_id)) self.assertIsNotNone(row) self.assertEqual(row.extra_fields, {"passport_series": "1234"}) def test_request_data_template_endpoints_for_assigned_lawyer(self): headers_admin = self._auth_headers("ADMIN", email="root@example.com") with self.SessionLocal() as db: db.add(Topic(code="civil-law", name="Гражданское право", enabled=True, sort_order=1)) lawyer = AdminUser( role="LAWYER", name="Юрист Шаблон", email="lawyer.template@example.com", password_hash="hash", is_active=True, ) outsider = AdminUser( role="LAWYER", name="Юрист Чужой", email="lawyer.outside@example.com", password_hash="hash", is_active=True, ) db.add_all([lawyer, outsider]) db.flush() req = Request( track_number="TRK-TEMPLATE-1", client_name="Клиент", client_phone="+79997770013", topic_code="civil-law", status_code="IN_PROGRESS", assigned_lawyer_id=str(lawyer.id), description="template flow", extra_fields={}, ) db.add(req) db.flush() db.add_all( [ TopicDataTemplate( topic_code="civil-law", key="power_of_attorney", label="Доверенность", description="Скан доверенности", required=True, enabled=True, sort_order=1, ), TopicDataTemplate( topic_code="civil-law", key="claim_copy", label="Копия иска", description="Копия заявления", required=False, enabled=True, sort_order=2, ), ] ) db.commit() request_id = str(req.id) lawyer_id = str(lawyer.id) outsider_id = str(outsider.id) headers_lawyer = self._auth_headers("LAWYER", email="lawyer.template@example.com", sub=lawyer_id) headers_outsider = self._auth_headers("LAWYER", email="lawyer.outside@example.com", sub=outsider_id) pre = self.client.get(f"/api/admin/requests/{request_id}/data-template", headers=headers_lawyer) self.assertEqual(pre.status_code, 200) self.assertEqual(len(pre.json()["topic_items"]), 2) self.assertEqual(len(pre.json()["request_items"]), 0) sync = self.client.post(f"/api/admin/requests/{request_id}/data-template/sync", headers=headers_lawyer) self.assertEqual(sync.status_code, 200) self.assertEqual(sync.json()["created"], 2) sync_repeat = self.client.post(f"/api/admin/requests/{request_id}/data-template/sync", headers=headers_lawyer) self.assertEqual(sync_repeat.status_code, 200) self.assertEqual(sync_repeat.json()["created"], 0) created_custom = self.client.post( f"/api/admin/requests/{request_id}/data-template/items", headers=headers_lawyer, json={ "key": "additional_scan", "label": "Дополнительный скан", "description": "Любой дополнительный файл", "required": False, }, ) self.assertEqual(created_custom.status_code, 201) custom_item_id = created_custom.json()["id"] updated_custom = self.client.patch( f"/api/admin/requests/{request_id}/data-template/items/{custom_item_id}", headers=headers_lawyer, json={"label": "Дополнительный скан (обновлено)", "required": True}, ) self.assertEqual(updated_custom.status_code, 200) self.assertEqual(updated_custom.json()["label"], "Дополнительный скан (обновлено)") self.assertTrue(updated_custom.json()["required"]) outsider_forbidden = self.client.get(f"/api/admin/requests/{request_id}/data-template", headers=headers_outsider) self.assertEqual(outsider_forbidden.status_code, 403) admin_access = self.client.get(f"/api/admin/requests/{request_id}/data-template", headers=headers_admin) self.assertEqual(admin_access.status_code, 200) self.assertEqual(len(admin_access.json()["request_items"]), 3) deleted_custom = self.client.delete( f"/api/admin/requests/{request_id}/data-template/items/{custom_item_id}", headers=headers_lawyer, ) self.assertEqual(deleted_custom.status_code, 200) with self.SessionLocal() as db: count = db.query(RequestDataRequirement).filter(RequestDataRequirement.request_id == UUID(request_id)).count() self.assertEqual(count, 2)