feat: import organization exchange v3
All checks were successful
All checks were successful
This commit is contained in:
@@ -9,6 +9,7 @@ import struct
|
||||
import tempfile
|
||||
import zlib
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
from zipfile import ZIP_DEFLATED, ZipFile
|
||||
|
||||
@@ -29,7 +30,6 @@ from apps.external_data.models import (
|
||||
PublicProcurement,
|
||||
)
|
||||
from apps.organization.models import Organization
|
||||
from apps.registers.models import RegisterUpload, RegistryMembershipPeriod
|
||||
from apps.user.models import User
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from django.conf import settings
|
||||
@@ -53,15 +53,17 @@ def build_exchange_archive(
|
||||
archive_name: str = "exchange_package_20260407.zip",
|
||||
bin_name: str = "exchange_package_20260407.bin",
|
||||
data: dict[str, list[dict[str, object]]] | None = None,
|
||||
schema_version: int = ExchangePackageImportService.SUPPORTED_SCHEMA_VERSION,
|
||||
) -> SimpleUploadedFile:
|
||||
"""Build encrypted exchange archive compatible with import service."""
|
||||
payload = {
|
||||
"format": ExchangePackageImportService.PAYLOAD_FORMAT,
|
||||
"schema_version": 1,
|
||||
"schema_version": schema_version,
|
||||
"manifest": {
|
||||
"package_id": package_id,
|
||||
"source_system": "mostovik-dev",
|
||||
"produced_at": "2026-04-07T12:00:00+00:00",
|
||||
"schema_version": schema_version,
|
||||
"sections": list((data or {}).keys()),
|
||||
},
|
||||
"data": data or {},
|
||||
@@ -84,7 +86,7 @@ def build_exchange_archive(
|
||||
"nonce": _b64url(nonce),
|
||||
"aad": _b64url(aad),
|
||||
"package_id": package_id,
|
||||
"schema_version": 1,
|
||||
"schema_version": schema_version,
|
||||
"plaintext_sha256": hashlib.sha256(payload_bytes).hexdigest(),
|
||||
"compressed_sha256": hashlib.sha256(compressed_payload).hexdigest(),
|
||||
"ciphertext_sha256": hashlib.sha256(encrypted_payload).hexdigest(),
|
||||
@@ -122,41 +124,110 @@ def build_exchange_payload() -> dict[str, list[dict[str, object]]]:
|
||||
return {
|
||||
"organizations": [
|
||||
{
|
||||
"mostovik_uid": "11111111-1111-4111-8111-111111111111",
|
||||
"rn": 1001,
|
||||
"inn": "7707083893",
|
||||
"name": "АО Альфа Обновленная",
|
||||
"full_name": "Акционерное общество Альфа Обновленная",
|
||||
"short_name": "АО Альфа",
|
||||
"organization_type": "ao",
|
||||
"cluster": "radioelectronics",
|
||||
"pn_name": "АО Альфа",
|
||||
"pn_name_en": "Alpha JSC",
|
||||
"ogrn": "1027700132195",
|
||||
"kpp": "770701001",
|
||||
"okpo": "12345678",
|
||||
"ogrip": "",
|
||||
"identity_status": "complete",
|
||||
"primary_identity": "inn:7707083893|ogrn:1027700132195|kpp:770701001",
|
||||
"gk_code": "1",
|
||||
"gk_name": "Росатом",
|
||||
"in_korp_code": "head",
|
||||
"in_korp_name": "Головная организация",
|
||||
"filial": ".F.",
|
||||
"is_branch": False,
|
||||
"registration_date": "2024-02-15",
|
||||
"create_date": "2024",
|
||||
"organizational_legal_form": "12267",
|
||||
"organizational_legal_form1": "Акционерное общество",
|
||||
"ownership_form": "61",
|
||||
"ownership_form1": "Федеральная собственность",
|
||||
"authorized_capital": "1500000.50",
|
||||
"legal_address": "г. Москва, ул. Тверская, д. 1",
|
||||
"activity_type": "Производство электронных компонентов",
|
||||
"founder_name": "Госкорпорация Пример",
|
||||
"ownership_type": "Федеральная собственность",
|
||||
"legal_form": "Акционерное общество",
|
||||
"charter_capital_amount": "1500000.50",
|
||||
"general_director_name": "Иванов Иван Иванович",
|
||||
"general_director_inn": "123456789012",
|
||||
"general_director_appointment_date": "2025-01-10",
|
||||
"executors_count": 175,
|
||||
"financial_reports_available": True,
|
||||
"tax_reports_available": True,
|
||||
"in_defense_unreliable_suppliers_registry": False,
|
||||
"in_275_fz_registry": True,
|
||||
"bankruptcy_messages_found": False,
|
||||
"business_act_cod": "26.11",
|
||||
"business_activity": "Производство электронных компонентов",
|
||||
"general_director": "Иванов Иван Иванович",
|
||||
"general_director_tax_id": "123456789012",
|
||||
"uk": "АО Управляющая компания",
|
||||
"inn_uk": "7707000000",
|
||||
"appointment_date": "2025-01-10",
|
||||
"cf_fl_rn": "director-001",
|
||||
"akc_fs": "100",
|
||||
"akc_sf": "0",
|
||||
"re_za": True,
|
||||
"re_zasf": False,
|
||||
"goz_participation": True,
|
||||
"opk_registry_membership": True,
|
||||
"ropk_num": "РОПК-001",
|
||||
"ropk_razdel_num": "opk-1",
|
||||
"ropk_razdel_name": "Росатом ОПК",
|
||||
"min": "Минпромторг",
|
||||
"dep": "Департамент радиоэлектроники",
|
||||
"otr": "Радиоэлектроника",
|
||||
"integrated_structure": "Интегрированная структура Альфа",
|
||||
"state_sector_code": "10",
|
||||
"state_sector_name": "Государственный сектор",
|
||||
},
|
||||
{
|
||||
"mostovik_uid": "22222222-2222-4222-8222-222222222222",
|
||||
"rn": 1002,
|
||||
"inn": "7707083894",
|
||||
"name": "АО Бета",
|
||||
"full_name": "Акционерное общество Бета",
|
||||
"short_name": "АО Бета",
|
||||
"organization_type": "pao",
|
||||
"cluster": "space",
|
||||
"pn_name": "АО Бета",
|
||||
"pn_name_en": "Beta JSC",
|
||||
"ogrn": "1027700132196",
|
||||
"kpp": "770701002",
|
||||
"okpo": "12345679",
|
||||
"ogrip": "",
|
||||
"identity_status": "complete",
|
||||
"primary_identity": "inn:7707083894|ogrn:1027700132196|kpp:770701002",
|
||||
"gk_code": "2",
|
||||
"gk_name": "Роскосмос",
|
||||
"in_korp_code": "member",
|
||||
"in_korp_name": "Организация внутри ГК",
|
||||
"filial": ".F.",
|
||||
"is_branch": False,
|
||||
"registration_date": "2023-09-01",
|
||||
"create_date": "2023",
|
||||
"organizational_legal_form": "12267",
|
||||
"organizational_legal_form1": "Акционерное общество",
|
||||
"ownership_form": "61",
|
||||
"ownership_form1": "Федеральная собственность",
|
||||
"authorized_capital": None,
|
||||
"legal_address": "",
|
||||
"business_act_cod": "",
|
||||
"business_activity": "",
|
||||
"general_director": "",
|
||||
"general_director_tax_id": "",
|
||||
"uk": "",
|
||||
"inn_uk": "",
|
||||
"appointment_date": None,
|
||||
"cf_fl_rn": "",
|
||||
"akc_fs": "",
|
||||
"akc_sf": "",
|
||||
"re_za": None,
|
||||
"re_zasf": None,
|
||||
"goz_participation": False,
|
||||
"opk_registry_membership": False,
|
||||
"ropk_num": "",
|
||||
"ropk_razdel_num": "",
|
||||
"ropk_razdel_name": "",
|
||||
"min": "",
|
||||
"dep": "",
|
||||
"otr": "",
|
||||
"integrated_structure": "",
|
||||
"state_sector_code": "",
|
||||
"state_sector_name": "",
|
||||
},
|
||||
],
|
||||
"industrial_products": [
|
||||
@@ -169,20 +240,6 @@ def build_exchange_payload() -> dict[str, list[dict[str, object]]]:
|
||||
"registry_number": "prod-001",
|
||||
}
|
||||
],
|
||||
"registry_memberships": [
|
||||
{
|
||||
"organization_inn": "7707083893",
|
||||
"registry_name": "Реестр госкорпорации Росатом",
|
||||
"started_at": "2026-01-01",
|
||||
"ended_at": None,
|
||||
},
|
||||
{
|
||||
"organization_inn": "7707083894",
|
||||
"registry_name": "Реестр госкорпорации Роскосмос",
|
||||
"started_at": "2026-02-01",
|
||||
"ended_at": None,
|
||||
},
|
||||
],
|
||||
"industrial_certificates": [
|
||||
{
|
||||
"organization_inn": "7707083893",
|
||||
@@ -341,13 +398,8 @@ class ExchangePackageApiTest(APITestCase):
|
||||
self.assertFalse(response.data["result"]["duplicate"])
|
||||
self.assertEqual(response.data["result"]["organizations"]["created"], 1)
|
||||
self.assertEqual(response.data["result"]["organizations"]["updated"], 1)
|
||||
self.assertEqual(
|
||||
response.data["result"]["registry_memberships"]["opened"],
|
||||
2,
|
||||
)
|
||||
self.assertNotIn("registry_memberships", response.data["result"])
|
||||
self.assertEqual(Organization.objects.count(), 2)
|
||||
self.assertEqual(RegisterUpload.objects.count(), 2)
|
||||
self.assertEqual(RegistryMembershipPeriod.objects.count(), 2)
|
||||
self.assertEqual(IndustrialCertificate.objects.count(), 1)
|
||||
self.assertEqual(ManufacturerRegistryEntry.objects.count(), 1)
|
||||
self.assertEqual(IndustrialProduct.objects.count(), 1)
|
||||
@@ -380,16 +432,34 @@ class ExchangePackageApiTest(APITestCase):
|
||||
|
||||
organization = Organization.objects.get(inn="7707083893")
|
||||
self.assertEqual(organization.name, "АО Альфа Обновленная")
|
||||
self.assertEqual(
|
||||
organization.full_name, "Акционерное общество Альфа Обновленная"
|
||||
)
|
||||
self.assertEqual(organization.short_name, "АО Альфа")
|
||||
self.assertEqual(organization.organization_type, "ao")
|
||||
self.assertEqual(organization.cluster, "radioelectronics")
|
||||
self.assertEqual(
|
||||
str(organization.mostovik_uid), "11111111-1111-4111-8111-111111111111"
|
||||
)
|
||||
self.assertEqual(organization.rn, 1001)
|
||||
self.assertEqual(organization.gk_name, "Росатом")
|
||||
self.assertEqual(organization.in_korp_name, "Головная организация")
|
||||
self.assertEqual(organization.registration_date, date(2024, 2, 15))
|
||||
self.assertEqual(organization.executors_count, 175)
|
||||
self.assertTrue(organization.tax_reports_available)
|
||||
self.assertTrue(organization.in_275_fz_registry)
|
||||
self.assertEqual(organization.appointment_date, date(2025, 1, 10))
|
||||
self.assertEqual(
|
||||
organization.business_activity, "Производство электронных компонентов"
|
||||
)
|
||||
self.assertEqual(
|
||||
organization.organizational_legal_form1, "Акционерное общество"
|
||||
)
|
||||
self.assertEqual(organization.ownership_form1, "Федеральная собственность")
|
||||
self.assertEqual(organization.authorized_capital, Decimal("1500000.50"))
|
||||
self.assertEqual(organization.general_director, "Иванов Иван Иванович")
|
||||
self.assertEqual(organization.general_director_tax_id, "123456789012")
|
||||
self.assertTrue(organization.goz_participation)
|
||||
self.assertTrue(organization.opk_registry_membership)
|
||||
self.assertEqual(organization.ropk_razdel_name, "Росатом ОПК")
|
||||
self.assertEqual(
|
||||
organization.get_active_registry_names(),
|
||||
["Реестр госкорпорации Росатом"],
|
||||
["Росатом", "Участие в ГОЗ", "Росатом ОПК"],
|
||||
)
|
||||
|
||||
package_import = ExchangePackageImport.objects.get()
|
||||
@@ -411,6 +481,75 @@ class ExchangePackageApiTest(APITestCase):
|
||||
self.assertEqual(ExchangePackageImport.objects.count(), 0)
|
||||
self.assertEqual(Organization.objects.count(), 0)
|
||||
|
||||
def test_upload_rejects_legacy_schema_version(self):
|
||||
archive = build_exchange_archive(
|
||||
package_id="pkg-legacy-schema-version",
|
||||
schema_version=2,
|
||||
data=build_exchange_payload(),
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{"file": archive},
|
||||
format="multipart",
|
||||
HTTP_X_EXCHANGE_TOKEN=TEST_TOKEN,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("schema_version 3", response.data["file"][0])
|
||||
self.assertEqual(Organization.objects.count(), 0)
|
||||
|
||||
def test_upload_rejects_legacy_registry_memberships_section(self):
|
||||
payload = build_exchange_payload()
|
||||
payload["registry_memberships"] = [
|
||||
{
|
||||
"organization_inn": "7707083893",
|
||||
"registry_name": "Реестр госкорпорации Росатом",
|
||||
"started_at": "2026-01-01",
|
||||
"ended_at": None,
|
||||
}
|
||||
]
|
||||
archive = build_exchange_archive(
|
||||
package_id="pkg-legacy-registry-memberships",
|
||||
data=payload,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{"file": archive},
|
||||
format="multipart",
|
||||
HTTP_X_EXCHANGE_TOKEN=TEST_TOKEN,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("registry_memberships", response.data["file"][0])
|
||||
self.assertEqual(Organization.objects.count(), 0)
|
||||
|
||||
def test_upload_rejects_legacy_organization_row_schema(self):
|
||||
archive = build_exchange_archive(
|
||||
package_id="pkg-legacy-organization-row",
|
||||
data={
|
||||
"organizations": [
|
||||
{
|
||||
"inn": "7707083893",
|
||||
"name": "АО Альфа",
|
||||
"ogrn": "1027700132195",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{"file": archive},
|
||||
format="multipart",
|
||||
HTTP_X_EXCHANGE_TOKEN=TEST_TOKEN,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("schema_version 3", response.data["file"][0])
|
||||
self.assertEqual(Organization.objects.count(), 0)
|
||||
|
||||
def test_upload_rejects_external_rows_for_organization_absent_from_package(self):
|
||||
Organization.objects.create(
|
||||
inn="7707083893",
|
||||
|
||||
Reference in New Issue
Block a user