test(organization): add analytics pass 4 contract tests

This commit is contained in:
2026-04-14 10:59:06 +02:00
parent 903312670c
commit f0c4f501a6
2 changed files with 169 additions and 26 deletions

View File

@@ -22,7 +22,7 @@
- [x] **Pass 1 — Discovery & контрактный каркас:** подготовка сериализаторов контрактов, уточнение форматов, матрица соответствий.
- [x] **Pass 2 — Пользователи и аутентификация:** доработка `users/me` + user-management.
- [x] **Pass 3 — Формы:** выравнивание upload F-2…F-6.
- [ ] **Pass 4 — Аналитика:** financial-summary / economics / personnel / equipment / products / risk / forecast.
- [x] **Pass 4 — Аналитика:** financial-summary / economics / personnel / equipment / products / risk / forecast.
- [ ] **Pass 5 — Внешние контуры:** industrial/prosecutor/procurements/arbitration/security registries.
- [ ] **Pass 6 — Финализация:** OpenAPI + массовое тестирование + smoke.
@@ -45,6 +45,10 @@
- выровнен request/response контракт upload-эндпоинтов (sync/async, report-период, `upload_id`, `job_id`).
- добавлена единая схема ошибок валидации multipart.
- добавлены contract tests на upload endpoints F-2…F-6.
- **Pass 4 — Аналитика (2026-04-14): завершён**
- добавлены contract checks для всех analytics endpointов:
- `financial-summary`, `economics`, `personnel`, `equipment`, `products`, `forecast`, `risk-profile`, `dashboard`.
- дополнена проверка query-валидации для invalid `economics`-запроса.
---
@@ -235,10 +239,10 @@
- [x] Добавить общий error serializer для валидации multipart.
### Pass 4. Аналитика
- [ ] Финализировать `financial-summary` и добавить расчёты deltas/period.
- [ ] Вынести/довести `economics`, `personnel`, `equipment`, `products`.
- [ ] Проверить/документировать `risk-profile`, `forecast`.
- [ ] Добавить dashboard фильтрацию и стабильные `cluster` метрики.
- [x] Финализировать `financial-summary` и добавить расчёты deltas/period. (2026-04-14)
- [x] Вынести/довести `economics`, `personnel`, `equipment`, `products`. (2026-04-14)
- [x] Проверить/документировать `risk-profile`, `forecast`. (2026-04-14)
- [x] Добавить dashboard фильтрацию и стабильные `cluster` метрики. (2026-04-14)
### Pass 5. Внешние данные
- [ ] Довести внешние реестры к единообразным фильтрам/ответам.

View File

@@ -170,7 +170,7 @@ class OrganizationAnalyticsApiTest(APITestCase):
avg_shift_work=Decimal("1.80"),
)
def test_financial_summary_endpoint(self):
def test_financial_summary_contract(self):
response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/financial-summary/"
"?report_year=2026&report_quarter=1"
@@ -181,47 +181,160 @@ class OrganizationAnalyticsApiTest(APITestCase):
self.assertEqual(response.data["revenue"]["previous_amount"], 760000000)
self.assertEqual(response.data["taxes_paid"]["amount"], 18900000)
self.assertEqual(response.data["insurance_contributions"]["amount"], 302000)
self.assertEqual(response.data["organization_id"], str(self.organization.id))
self.assertEqual(response.data["report_period"], {"year": 2026, "quarter": 1})
self.assertEqual(set(response.data["revenue"]), {"amount", "previous_amount", "delta_percent"})
self.assertEqual(set(response.data["net_profit"]), {"amount", "previous_amount", "delta_percent"})
self.assertEqual(set(response.data["taxes_paid"]), {"amount", "previous_amount", "delta_percent"})
self.assertEqual(
set(response.data["insurance_contributions"]),
{"amount", "previous_amount", "delta_percent"},
)
def test_personnel_and_equipment_endpoints(self):
def test_economics_contract(self):
response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/economics/"
"?group=efficiency&from_year=2025&to_year=2026"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["organization_id"], str(self.organization.id))
self.assertEqual(response.data["group"], "efficiency")
self.assertEqual(response.data["periods"], [2025, 2026])
self.assertEqual(
response.data["kpis"].keys(),
{"revenue", "ebitda", "net_profit"},
)
self.assertIn("series", response.data)
for series in response.data["series"]:
self.assertIn("metric", series)
self.assertIn("unit", series)
self.assertIn("points", series)
self.assertEqual(series["unit"], "rub_thousands")
self.assertEqual(len(series["points"]), 2)
self.assertEqual(set(series["points"][0]), {"period", "value"})
self.assertEqual(len(response.data["ratios"]), 2)
self.assertIn("ros", response.data["ratios"][0])
self.assertIn("roa", response.data["ratios"][0])
self.assertIn("roe", response.data["ratios"][0])
def test_personnel_contract(self):
personnel_response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/personnel/"
"?report_year=2026&history_years=2"
)
equipment_response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/equipment/"
"?report_year=2026"
)
self.assertEqual(personnel_response.status_code, status.HTTP_200_OK)
self.assertEqual(personnel_response.data["organization_id"], str(self.organization.id))
self.assertEqual(personnel_response.data["report_year"], 2026)
self.assertEqual(
personnel_response.data["headcount"]["average_employees"],
1050,
)
self.assertEqual(len(personnel_response.data["history"]), 2)
self.assertEqual(equipment_response.status_code, status.HTTP_200_OK)
self.assertEqual(equipment_response.data["summary"]["total_equipment"], 187)
self.assertEqual(len(personnel_response.data["age_distribution"]), 3)
self.assertEqual(
equipment_response.data["categories"][0]["category"],
"Станочное оборудование",
personnel_response.data["age_distribution"][0]["age_group"],
"under_30",
)
self.assertIn("employees_count", personnel_response.data["age_distribution"][0])
def test_equipment_contract(self):
response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/equipment/"
"?report_year=2026"
)
def test_products_and_risk_profile_endpoints(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["organization_id"], str(self.organization.id))
self.assertEqual(response.data["report_year"], 2026)
self.assertEqual(
set(response.data["summary"]),
{
"total_equipment",
"domestic_equipment",
"imported_equipment",
"physical_wear_percent",
"utilization_rate",
"avg_shift_work",
"equipment_needed",
},
)
self.assertEqual(response.data["summary"]["total_equipment"], 187)
self.assertEqual(response.data["summary"]["physical_wear_percent"], 32.0)
self.assertEqual(response.data["summary"]["utilization_rate"], 0.92)
self.assertEqual(
response.data["age_distribution"][0]["bucket"], "under_5_years"
)
self.assertEqual(len(response.data["age_distribution"]), 5)
self.assertGreaterEqual(len(response.data["categories"]), 1)
self.assertEqual(
response.data["categories"][0],
{
"category": "Станочное оборудование",
"total_equipment": 1,
"domestic_equipment": 1,
"imported_equipment": 0,
"physical_wear_percent": 28.4,
},
)
def test_products_contract(self):
products_response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/products/"
"?frequency=quarterly&price_mode=actual&report_year=2026"
)
risk_response = self.client.get(
self.assertEqual(products_response.status_code, status.HTTP_200_OK)
self.assertEqual(products_response.data["organization_id"], str(self.organization.id))
self.assertEqual(products_response.data["report_year"], 2026)
self.assertEqual(products_response.data["frequency"], "quarterly")
self.assertEqual(products_response.data["price_mode"], "actual")
self.assertEqual(products_response.data["summary"]["military_output_amount"], 11000000)
self.assertEqual(products_response.data["summary"]["civilian_output_amount"], 7000000)
self.assertEqual(products_response.data["summary"]["hightech_output_amount"], 1500000)
self.assertEqual(products_response.data["summary"]["rd_volume_amount"], 900000)
self.assertEqual(len(products_response.data["production_series"]), 1)
self.assertEqual(len(products_response.data["sales_series"]), 1)
first_production = products_response.data["production_series"][0]
first_sales = products_response.data["sales_series"][0]
self.assertEqual(first_production["period"], "2026-Q1")
self.assertEqual(first_production["military_output_amount"], 11000000)
self.assertEqual(first_sales["military_domestic_amount"], 9000000)
self.assertEqual(first_sales["civilian_export_amount"], 2000000)
def test_forecast_contract(self):
response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/forecast/"
"?scenario=base&horizon_years=3"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["organization_id"], str(self.organization.id))
self.assertEqual(response.data["scenario"], "base")
self.assertEqual(response.data["horizon_years"], 3)
self.assertEqual(response.data["base_year"], 2026)
self.assertEqual(len(response.data["forecast"]), 3)
self.assertEqual(response.data["forecast"][0]["year"], 2027)
self.assertIn("revenue_amount", response.data["forecast"][0])
self.assertIn("net_profit_amount", response.data["forecast"][0])
self.assertIn("margin_percent", response.data["forecast"][0])
self.assertGreaterEqual(len(response.data["risk_factors"]), 1)
self.assertIn("code", response.data["risk_factors"][0])
self.assertIn("name", response.data["risk_factors"][0])
self.assertIn("impact_level", response.data["risk_factors"][0])
def test_risk_profile_contract(self):
response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/risk-profile/"
)
self.assertEqual(products_response.status_code, status.HTTP_200_OK)
self.assertEqual(
products_response.data["summary"]["military_output_amount"],
11000000,
)
self.assertEqual(risk_response.status_code, status.HTTP_200_OK)
self.assertEqual(risk_response.data["risk_level"], "low")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["organization_id"], str(self.organization.id))
self.assertTrue(response.data["financial_reports_available"])
self.assertTrue(response.data["tax_reports_available"])
self.assertIn("risk_level", response.data)
self.assertIn("updated_at", response.data)
def test_dashboard_endpoint(self):
response = self.client.get(
@@ -230,6 +343,32 @@ class OrganizationAnalyticsApiTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["corporation_scope"], "rosatom")
self.assertIn("distribution_by_cluster", response.data)
self.assertIn("executors_by_cluster", response.data)
self.assertIn("headcount_growth_by_cluster", response.data)
self.assertIn("bankruptcy_free_share_by_cluster", response.data)
self.assertEqual(
response.data["distribution_by_cluster"][0]["cluster"], "radioelectronics"
)
self.assertEqual(
response.data["executors_by_cluster"][0]["cluster"], "radioelectronics"
)
self.assertIn(
"executors_count",
response.data["executors_by_cluster"][0],
)
self.assertIn("growth_percent", response.data["headcount_growth_by_cluster"][0])
def test_analytics_query_validation(self):
response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/economics/"
"?group=efficiency&from_year=2026&to_year=2025"
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(response.data["success"])
self.assertEqual(response.data["errors"][0]["code"], "validation_error")
self.assertEqual(
response.data["errors"][0]["message"],
"Validation failed",
)