test(organization): add analytics pass 4 contract tests
This commit is contained in:
@@ -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. Внешние данные
|
||||
- [ ] Довести внешние реестры к единообразным фильтрам/ответам.
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user