feat: import additional exchange sections
All checks were successful
CI/CD Pipeline / Code Quality Checks (push) Successful in 2m41s
CI/CD Pipeline / Run Tests (push) Successful in 2m47s
CI/CD Pipeline / Build Docker Images (push) Successful in 2m24s
CI/CD Pipeline / Push to Gitea Registry (push) Successful in 0s
CI/CD Pipeline / Deploy to Server (push) Successful in 0s

This commit is contained in:
2026-05-12 15:19:26 +02:00
parent ed34603116
commit bd8e1a8400
25 changed files with 1117 additions and 72 deletions

View File

@@ -16,7 +16,11 @@ from apps.exchange.models import ExchangeDeliveryChannel, ExchangePackageImport
from apps.exchange.services import ExchangePackageImportService
from apps.external_data.models import (
ArbitrationCase,
BankruptcyProcedure,
DefenseUnreliableSupplier,
IndustrialProduct,
InformationSecurityRegistryEntry,
LaborVacancy,
ProsecutorCheck,
PublicProcurement,
)
@@ -194,6 +198,53 @@ def build_exchange_payload() -> dict[str, list[dict[str, object]]]:
"decision_date": "2026-03-25",
}
],
"bankruptcy_procedures": [
{
"organization_inn": "7707083893",
"external_id": "fedresurs:001",
"message_type": "Сообщение о намерении",
"message_date": "2026-03-26",
"case_number": "А40-555/2026",
"status": "published",
"source_url": "https://fedresurs.ru/message/001",
}
],
"defense_unreliable_suppliers": [
{
"organization_inn": "7707083893",
"external_id": "fas-goz:001",
"registry_source": "fas_goz",
"registry_number": "ГОЗ-001",
"supplier_name": "АО Альфа Обновленная",
"reason": "Уклонение от заключения контракта",
"included_at": "2026-02-20",
"status": "active",
"source_url": "https://fas.gov.ru/register/001",
}
],
"information_security_registries": [
{
"organization_inn": "7707083893",
"external_id": "fstec:001",
"registry_name": "Реестр лицензий ФСТЭК",
"presence_status": "present",
"entry_number": "77-001234",
"issued_at": "2026-01-10",
"expires_at": "2027-01-10",
}
],
"labor_vacancies": [
{
"organization_inn": "7707083893",
"external_id": "trudvsem:001",
"vacancy_source": "trudvsem",
"title": "Инженер-испытатель",
"status": "open",
"published_at": "2026-04-01",
"salary_amount": "175000.00",
"source_url": "https://trudvsem.ru/vacancy/001",
}
],
}
@@ -234,6 +285,23 @@ class ExchangePackageApiTest(APITestCase):
self.assertEqual(ProsecutorCheck.objects.count(), 1)
self.assertEqual(PublicProcurement.objects.count(), 1)
self.assertEqual(ArbitrationCase.objects.count(), 1)
self.assertEqual(BankruptcyProcedure.objects.count(), 1)
self.assertEqual(DefenseUnreliableSupplier.objects.count(), 1)
self.assertEqual(InformationSecurityRegistryEntry.objects.count(), 1)
self.assertEqual(LaborVacancy.objects.count(), 1)
self.assertEqual(
response.data["result"]["bankruptcy_procedures"]["created"],
1,
)
self.assertEqual(
response.data["result"]["defense_unreliable_suppliers"]["created"],
1,
)
self.assertEqual(
response.data["result"]["information_security_registries"]["created"],
1,
)
self.assertEqual(response.data["result"]["labor_vacancies"]["created"], 1)
organization = Organization.objects.get(inn="7707083893")
self.assertEqual(organization.name, "АО Альфа Обновленная")

View File

@@ -3,10 +3,13 @@
import factory
from apps.external_data.models import (
ArbitrationCase,
BankruptcyProcedure,
DefenseUnreliableSupplier,
IndustrialProduct,
InformationSecurityRegistryEntry,
LaborVacancy,
ProsecutorCheck,
PublicProcurement,
InformationSecurityRegistryEntry,
)
from faker import Faker
@@ -71,9 +74,7 @@ class ArbitrationCaseFactory(factory.django.DjangoModelFactory):
decision_date = factory.LazyAttribute(lambda _: fake.date_this_year())
class InformationSecurityRegistryEntryFactory(
factory.django.DjangoModelFactory
):
class InformationSecurityRegistryEntryFactory(factory.django.DjangoModelFactory):
class Meta:
model = InformationSecurityRegistryEntry
@@ -83,3 +84,45 @@ class InformationSecurityRegistryEntryFactory(
entry_number = "77-001234"
issued_at = factory.LazyAttribute(lambda _: fake.date_this_year())
expires_at = factory.LazyAttribute(lambda _: fake.date_this_year())
class BankruptcyProcedureFactory(factory.django.DjangoModelFactory):
class Meta:
model = BankruptcyProcedure
organization = factory.SubFactory(OrganizationFactory)
external_id = factory.Sequence(lambda n: f"fedresurs:{n}")
message_type = "Сообщение о намерении"
message_date = factory.LazyAttribute(lambda _: fake.date_this_year())
case_number = factory.Sequence(lambda n: f"А40-{20_000 + n}/2026")
status = "published"
source_url = factory.Sequence(lambda n: f"https://fedresurs.ru/message/{n}")
class DefenseUnreliableSupplierFactory(factory.django.DjangoModelFactory):
class Meta:
model = DefenseUnreliableSupplier
organization = factory.SubFactory(OrganizationFactory)
external_id = factory.Sequence(lambda n: f"fas-goz:{n}")
registry_source = "fas_goz"
registry_number = factory.Sequence(lambda n: f"ГОЗ-{n:04d}")
supplier_name = factory.LazyAttribute(lambda obj: obj.organization.name)
reason = "Уклонение от заключения контракта"
included_at = factory.LazyAttribute(lambda _: fake.date_this_year())
status = "active"
source_url = factory.Sequence(lambda n: f"https://fas.gov.ru/register/{n}")
class LaborVacancyFactory(factory.django.DjangoModelFactory):
class Meta:
model = LaborVacancy
organization = factory.SubFactory(OrganizationFactory)
external_id = factory.Sequence(lambda n: f"trudvsem:{n}")
vacancy_source = "trudvsem"
title = "Инженер-испытатель"
status = "open"
published_at = factory.LazyAttribute(lambda _: fake.date_this_year())
salary_amount = "175000.00"
source_url = factory.Sequence(lambda n: f"https://trudvsem.ru/vacancy/{n}")

View File

@@ -10,10 +10,13 @@ from rest_framework.test import APITestCase
from tests.apps.external_data.factories import (
ArbitrationCaseFactory,
BankruptcyProcedureFactory,
DefenseUnreliableSupplierFactory,
IndustrialProductFactory,
InformationSecurityRegistryEntryFactory,
LaborVacancyFactory,
ProsecutorCheckFactory,
PublicProcurementFactory,
InformationSecurityRegistryEntryFactory,
)
from tests.apps.organization.factories import OrganizationFactory
from tests.apps.user.factories import UserFactory
@@ -116,3 +119,47 @@ class ExternalDataApiTest(APITestCase):
self.assertEqual(result["presence_status"], "present")
self.assertIn("registry_name", result)
self.assertIn("entry_number", result)
def test_additional_exchange_sections_are_exposed(self):
BankruptcyProcedureFactory.create(
organization=self.organization,
message_type="Сообщение о намерении",
message_date=date(2026, 3, 26),
)
DefenseUnreliableSupplierFactory.create(
organization=self.organization,
registry_source="fas_goz",
included_at=date(2026, 2, 20),
)
LaborVacancyFactory.create(
organization=self.organization,
vacancy_source="trudvsem",
published_at=date(2026, 4, 1),
)
BankruptcyProcedureFactory.create(organization=self.other_organization)
DefenseUnreliableSupplierFactory.create(organization=self.other_organization)
LaborVacancyFactory.create(organization=self.other_organization)
bankruptcy_response = self.client.get(
f"/api/v1/bankruptcy-procedures/?organization={self.organization.id}"
"&message_date_from=2026-01-01&message_date_to=2026-12-31"
)
defense_response = self.client.get(
f"/api/v1/defense-unreliable-suppliers/?organization={self.organization.id}"
"&registry_source=fas_goz"
)
vacancies_response = self.client.get(
f"/api/v1/labor-vacancies/?organization={self.organization.id}"
"&vacancy_source=trudvsem"
)
self.assertEqual(bankruptcy_response.status_code, status.HTTP_200_OK)
self.assertEqual(defense_response.status_code, status.HTTP_200_OK)
self.assertEqual(vacancies_response.status_code, status.HTTP_200_OK)
self.assertEqual(bankruptcy_response.data["count"], 1)
self.assertEqual(defense_response.data["count"], 1)
self.assertEqual(vacancies_response.data["count"], 1)
self.assertEqual(
vacancies_response.data["results"][0]["title"],
"Инженер-испытатель",
)

View File

@@ -193,19 +193,18 @@ class FormUploadContractsApiTest(APITestCase):
def test_upload_processing_error_contract(self):
for _, case in self.CASES.items():
with self.subTest(form=case["form"]):
with patch(
case["parse_target"],
side_effect=RuntimeError("parse failed"),
) as parse_mock:
response = self.client.post(
case["url"],
self._build_payload(case["payload"], file_size=256),
format="multipart",
)
with self.subTest(form=case["form"]), patch(
case["parse_target"],
side_effect=RuntimeError("parse failed"),
) as parse_mock:
response = self.client.post(
case["url"],
self._build_payload(case["payload"], file_size=256),
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["error_code"], "processing_error")
self.assertEqual(response.data["error_message"], "parse failed")
self.assertEqual(response.data["details"], [])
parse_mock.assert_called_once()
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["error_code"], "processing_error")
self.assertEqual(response.data["error_message"], "parse failed")
self.assertEqual(response.data["details"], [])
parse_mock.assert_called_once()

View File

@@ -188,9 +188,18 @@ class OrganizationAnalyticsApiTest(APITestCase):
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["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"},
@@ -228,7 +237,12 @@ class OrganizationAnalyticsApiTest(APITestCase):
set(response.data["ratio_normatives"]),
{"ros", "roa", "roe", "ebitda_margin"},
)
self.assertTrue(all(value is not None for value in response.data["ratio_normatives"].values()))
self.assertTrue(
all(
value is not None
for value in response.data["ratio_normatives"].values()
)
)
def test_personnel_contract(self):
personnel_response = self.client.get(
@@ -236,7 +250,9 @@ class OrganizationAnalyticsApiTest(APITestCase):
"?report_year=2026&history_years=2"
)
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["organization_id"], str(self.organization.id)
)
self.assertEqual(personnel_response.data["report_year"], 2026)
self.assertEqual(
personnel_response.data["headcount"]["average_employees"],
@@ -312,15 +328,25 @@ class OrganizationAnalyticsApiTest(APITestCase):
"?frequency=quarterly&price_mode=actual&report_year=2026"
)
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["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"]["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(products_response.data["summary"]["shipped_goods_amount"], 18000000)
self.assertEqual(
products_response.data["summary"]["shipped_goods_amount"], 18000000
)
self.assertEqual(len(products_response.data["production_series"]), 1)
self.assertEqual(len(products_response.data["sales_series"]), 1)
self.assertEqual(len(products_response.data["rd_volume_series"]), 1)
@@ -381,7 +407,9 @@ class OrganizationAnalyticsApiTest(APITestCase):
self.assertEqual(monthly_response.status_code, status.HTTP_200_OK)
self.assertEqual(monthly_response.data["frequency"], "monthly")
self.assertEqual(len(monthly_response.data["production_series"]), 6)
self.assertEqual(monthly_response.data["production_series"][0]["period"], "2026-01")
self.assertEqual(
monthly_response.data["production_series"][0]["period"], "2026-01"
)
self.assertEqual(
monthly_response.data["production_series"][0]["military_output_amount"],
3666666,

View File

@@ -230,15 +230,21 @@ class OrganizationApiTest(APITestCase):
response = self.client.get("/api/v1/organizations/?registry_category=goz")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual([item["id"] for item in response.data["results"]], [str(goz_org.id)])
self.assertEqual(
[item["id"] for item in response.data["results"]], [str(goz_org.id)]
)
response = self.client.get("/api/v1/organizations/?registryCategory=opk")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual([item["id"] for item in response.data["results"]], [str(opk_org.id)])
self.assertEqual(
[item["id"] for item in response.data["results"]], [str(opk_org.id)]
)
response = self.client.get("/api/v1/organizations/?registry_category=other")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn(str(other_org.id), [item["id"] for item in response.data["results"]])
self.assertIn(
str(other_org.id), [item["id"] for item in response.data["results"]]
)
class OrganizationDictionaryApiTest(APITestCase):