feat: address QA analytics and report upload feedback
All checks were successful
CI/CD Pipeline / Run Tests (push) Successful in 2m26s
CI/CD Pipeline / Code Quality Checks (push) Successful in 5m3s
CI/CD Pipeline / Build and Push Dev Images (push) Successful in 32s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 31s

This commit is contained in:
2026-07-19 13:32:56 +02:00
parent b025f12856
commit 9eb41f6fd4
28 changed files with 938 additions and 82 deletions

View File

@@ -11,6 +11,8 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from tests.apps.form_1.factories import FormF1RecordFactory
from tests.apps.organization.factories import OrganizationFactory
from tests.apps.user.factories import UserFactory
@@ -32,6 +34,8 @@ class ReportUploadApiTest(APITestCase):
),
form="f1",
user=self.user,
report_year=2026,
report_month=9,
)
def test_service_persists_original_bytes_and_finalizes_batch(self):
@@ -101,3 +105,69 @@ class ReportUploadApiTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_410_GONE)
self.assertEqual(response.data["errors"][0]["code"], "source_file_gone")
def test_history_returns_upload_before_records_are_parsed(self):
report_upload = self._create_upload()
ReportUploadService.mark_queued(report_upload.id, job_id="job-42")
self.client.force_authenticate(self.user)
response = self.client.get("/api/v1/forms/uploads/history/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 1)
row = response.data["results"][0]
self.assertEqual(row["upload_id"], str(report_upload.id))
self.assertEqual(row["form"], "f1")
self.assertEqual(row["report_year"], 2026)
self.assertEqual(row["report_month"], 9)
self.assertEqual(row["status"], "queued")
self.assertEqual(row["job_id"], "job-42")
self.assertIsNone(row["organization_id"])
self.assertFalse(row["download_available"])
def test_history_projects_successful_upload_per_organization(self):
organization = OrganizationFactory.create(
name="Тестовая компания 1",
inn="7709000010",
gk_code="rosatom",
gk_name="Госкорпорация Росатом",
)
report_upload = self._create_upload()
FormF1RecordFactory.create(
organization=organization,
load_batch=42,
report_year=2026,
report_month=9,
)
ReportUploadService.finalize(report_upload.id, {"batch_id": 42})
self.client.force_authenticate(self.user)
response = self.client.get(
"/api/v1/forms/uploads/history/",
{
"corporation_scope": "rosatom",
"organization_inn": organization.inn,
"report_year": 2026,
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 1)
row = response.data["results"][0]
self.assertEqual(row["organization_id"], str(organization.id))
self.assertEqual(row["organization_name"], organization.name)
self.assertEqual(row["organization_inn"], organization.inn)
self.assertEqual(row["corporation_scope"], "rosatom")
self.assertEqual(row["report_period_display"], "Сентябрь 2026")
self.assertEqual(row["records_count"], 1)
self.assertTrue(row["download_available"])
self.assertEqual(row["download_url"], "/api/v1/forms/f1/uploads/42/download/")
def test_history_does_not_expose_another_users_uploads(self):
self._create_upload()
self.client.force_authenticate(UserFactory.create_user())
response = self.client.get("/api/v1/forms/uploads/history/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 0)

View File

@@ -33,6 +33,7 @@ class OrganizationFactory(factory.django.DjangoModelFactory):
ogrn = factory.LazyAttribute(lambda _: fake.numerify("#############"))
kpp = factory.LazyAttribute(lambda _: fake.numerify("#########"))
okpo = factory.LazyAttribute(lambda _: fake.numerify("########"))
mostovik_uid = factory.Faker("uuid4")
registration_date = factory.LazyAttribute(lambda _: fake.date_this_century())
legal_address = factory.LazyAttribute(lambda _: fake.address().replace("\n", ", "))
activity_type = factory.LazyAttribute(lambda _: fake.job())

View File

@@ -194,6 +194,32 @@ class OrganizationAnalyticsApiTest(APITestCase):
set(response.data["insurance_contributions"]),
{"amount", "previous_amount", "delta_percent"},
)
self.assertTrue(response.data["data_available"])
self.assertIsNone(response.data["message"])
def test_financial_summary_returns_explicit_empty_state(self):
organization = OrganizationFactory.create()
response = self.client.get(
f"/api/v1/organizations/{organization.id}/analytics/financial-summary/"
"?report_year=2026&report_quarter=3"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(response.data["data_available"])
self.assertEqual(response.data["message"], "Данные отсутствуют")
self.assertEqual(response.data["revenue"]["amount"], 0)
def test_financial_summary_allows_annual_request_without_quarter(self):
response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/financial-summary/"
"?report_year=2026"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data["report_period"], {"year": 2026, "quarter": None}
)
def test_economics_contract(self):
response = self.client.get(
@@ -207,14 +233,29 @@ class OrganizationAnalyticsApiTest(APITestCase):
self.assertEqual(response.data["periods"], [2025, 2026])
self.assertEqual(
response.data["kpis"].keys(),
{"revenue", "ebitda", "net_profit"},
{"revenue", "ebitda", "net_profit", "revenue_per_employee"},
)
self.assertGreater(response.data["kpis"]["revenue_per_employee"]["value"], 0)
self.assertEqual(
response.data["kpis"]["revenue_per_employee"]["unit"],
"rub_per_employee",
)
for kpi in response.data["kpis"].values():
self.assertIn("previous_value", kpi)
self.assertIn("delta_value", kpi)
self.assertIn("delta_percent", kpi)
self.assertIn("direction", kpi)
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")
expected_unit = (
"rub_per_employee"
if series["metric"] == "revenue_per_employee"
else "rub_thousands"
)
self.assertEqual(series["unit"], expected_unit)
self.assertEqual(len(series["points"]), 2)
self.assertEqual(set(series["points"][0]), {"period", "value"})
@@ -234,15 +275,17 @@ class OrganizationAnalyticsApiTest(APITestCase):
)
)
def test_economics_uses_latest_available_year_when_requested_range_is_empty(self):
def test_economics_respects_requested_range_when_it_is_empty(self):
response = self.client.get(
f"/api/v1/organizations/{self.organization.id}/analytics/economics/"
"?group=efficiency&from_year=2022&to_year=2024"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["periods"], [2026])
self.assertEqual(response.data["series"][0]["points"][0]["period"], 2026)
self.assertEqual(response.data["periods"], [])
self.assertEqual(response.data["kpis"], {})
self.assertFalse(response.data["data_available"])
self.assertEqual(response.data["message"], "Данные отсутствуют")
def test_personnel_contract(self):
personnel_response = self.client.get(
@@ -259,6 +302,16 @@ class OrganizationAnalyticsApiTest(APITestCase):
1050,
)
self.assertEqual(len(personnel_response.data["history"]), 2)
self.assertEqual(
set(personnel_response.data["history"][0]),
{
"year",
"average_employees",
"avg_payroll_employees",
"average_age",
"payroll_fund",
},
)
self.assertEqual(len(personnel_response.data["age_distribution"]), 3)
self.assertIn("average_age", personnel_response.data)
self.assertEqual(
@@ -307,6 +360,7 @@ class OrganizationAnalyticsApiTest(APITestCase):
set(response.data["summary"]),
{
"total_equipment",
"machine_tools_and_equipment",
"domestic_equipment",
"imported_equipment",
"physical_wear_percent",
@@ -329,6 +383,17 @@ class OrganizationAnalyticsApiTest(APITestCase):
response.data["age_distribution"][0]["bucket"], "under_5_years"
)
self.assertEqual(len(response.data["age_distribution"]), 5)
self.assertEqual(
set(response.data["age_distribution"][0]),
{
"bucket",
"units_count",
"imported_equipment",
"utilization_rate",
"lease_share_itn_percent",
"weighted_wear_percent",
},
)
self.assertGreaterEqual(len(response.data["categories"]), 1)
self.assertEqual(len(response.data["dynamics_series"]), 3)
self.assertEqual(
@@ -608,6 +673,42 @@ class OrganizationAnalyticsApiTest(APITestCase):
self.assertEqual(
response.data["headcount_growth_by_cluster"][0]["growth_percent"], 50.0
)
self.assertEqual(
response.data["headcount_growth_by_cluster"][0]["from_year"], 2023
)
self.assertEqual(
response.data["headcount_growth_by_cluster"][0]["to_year"], 2024
)
def test_dashboard_growth_ignores_latest_year_without_comparable_history(self):
comparable = OrganizationFactory.create(
cluster="space",
gk_code="1",
gk_name='Госкорпорация "Роскосмос"',
)
latest_only = OrganizationFactory.create(
cluster="space",
gk_code="1",
gk_name='Госкорпорация "Роскосмос"',
)
FormF3RecordFactory.create(
organization=comparable, report_year=2023, avg_employees=100
)
FormF3RecordFactory.create(
organization=comparable, report_year=2024, avg_employees=120
)
FormF3RecordFactory.create(
organization=latest_only, report_year=2025, avg_employees=200
)
response = self.client.get(
"/api/v1/analytics/dashboard/?corporation_scope=roskosmos"
)
growth = response.data["headcount_growth_by_cluster"][0]
self.assertEqual(growth["from_year"], 2023)
self.assertEqual(growth["to_year"], 2024)
self.assertEqual(growth["growth_percent"], 20.0)
def test_analytics_query_validation(self):
response = self.client.get(

View File

@@ -43,6 +43,25 @@ class OrganizationApiTest(APITestCase):
self.assertEqual(response.data["results"][0]["registry_category_label"], "ОПК")
self.assertEqual(response.data["results"][0]["short_name"], "АО «Альфа»")
def test_catalog_hides_form_only_organization_until_mostovik_confirms_it(self):
organization = OrganizationFactory.create(mostovik_uid=None)
list_response = self.client.get("/api/v1/organizations/")
detail_response = self.client.get(f"/api/v1/organizations/{organization.id}/")
self.assertEqual(list_response.status_code, status.HTTP_200_OK)
self.assertEqual(list_response.data["count"], 0)
self.assertEqual(detail_response.status_code, status.HTTP_404_NOT_FOUND)
organization.mostovik_uid = "11111111-1111-4111-8111-111111111111"
organization.save(update_fields=["mostovik_uid", "updated_at"])
list_response = self.client.get("/api/v1/organizations/")
detail_response = self.client.get(f"/api/v1/organizations/{organization.id}/")
self.assertEqual(list_response.data["count"], 1)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
def test_detail_includes_active_registries(self):
organization = OrganizationFactory.create(
short_name="АО «Бета»",

View File

@@ -50,6 +50,10 @@ class DeleteTestCompaniesCommandTest(TestCase):
uploaded_file=SimpleUploadedFile("Ф-1_2025-09.xlsx", b"demo"),
form="f1",
)
historical_demo_upload = ReportUploadService.create(
uploaded_file=SimpleUploadedFile("Ф-1_2022-12.xlsx", b"demo-history"),
form="f1",
)
demo_file_name = demo_upload.original_file.name
demo_storage = demo_upload.original_file.storage
unrelated_upload = ReportUploadService.create(
@@ -64,7 +68,7 @@ class DeleteTestCompaniesCommandTest(TestCase):
self.assertEqual(payload["organizations_deleted"], 2)
self.assertEqual(payload["related_objects_deleted"], 2)
self.assertEqual(payload["report_uploads_deleted"], 1)
self.assertEqual(payload["report_uploads_deleted"], 2)
self.assertFalse(Organization.objects.filter(pk__in=[first.pk, second.pk]))
self.assertTrue(
Organization.objects.filter(pk=same_pattern_but_not_test_identity.pk)
@@ -75,6 +79,9 @@ class DeleteTestCompaniesCommandTest(TestCase):
self.assertFalse(FormF1Record.objects.exists())
self.assertFalse(IndustrialProduct.objects.exists())
self.assertFalse(ReportUpload.objects.filter(pk=demo_upload.pk).exists())
self.assertFalse(
ReportUpload.objects.filter(pk=historical_demo_upload.pk).exists()
)
self.assertFalse(demo_storage.exists(demo_file_name))
self.assertTrue(ReportUpload.objects.filter(pk=unrelated_upload.pk).exists())

View File

@@ -398,6 +398,17 @@ class TokenRefreshViewTest(APITestCase):
# New refresh token should be different
# Refresh token may be the same or different depending on implementation
def test_same_refresh_token_can_handle_parallel_refresh_requests(self):
data = {"refresh": self.tokens["refresh"]}
first_response = self.client.post(self.refresh_url, data, format="json")
second_response = self.client.post(self.refresh_url, data, format="json")
self.assertEqual(first_response.status_code, status.HTTP_200_OK)
self.assertEqual(second_response.status_code, status.HTTP_200_OK)
self.assertEqual(first_response.data["refresh"], self.tokens["refresh"])
self.assertEqual(second_response.data["refresh"], self.tokens["refresh"])
def test_refresh_token_invalid(self):
"""Test token refresh fails with invalid refresh token"""
data = {"refresh": fake.pystr(min_chars=20, max_chars=50)}