feat: address QA analytics and report upload feedback
All checks were successful
All checks were successful
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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="АО «Бета»",
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user