feat: export state corp package from backup endpoint
Some checks failed
CI/CD Pipeline / Quality Gate (push) Successful in 33s
CI/CD Pipeline / Build and Push Images (push) Successful in 10s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Failing after 9s

This commit is contained in:
2026-05-12 15:12:56 +02:00
parent 15360a3c8e
commit 75c1d4cf1a
11 changed files with 925 additions and 301 deletions

View File

@@ -19,14 +19,17 @@ from zipfile import ZIP_DEFLATED, ZipFile
import requests
from apps.parsers.models import (
GenericParserRecord,
IndustrialProductRecord,
InspectionRecord,
ParserLoadLog,
ProcurementRecord,
)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from django.conf import settings
from django.db.models import Q
from django.utils import timezone
from registers.models import Organization
from registers.models import Organization, RegistryMembershipPeriod
class StateCorpExchangeError(ValueError):
@@ -52,6 +55,14 @@ class StateCorpExchangeService:
AAD = b"state-corp-exchange-v1"
PAYLOAD_FORMAT = "state-corp-exchange-payload"
BIN_FORMAT = "state-corp-exchange-bin"
ROSATOM_ROSCOSMOS_REGISTRY_NAMES = (
"Реестр госкорпорации Роскосмос",
"Реестр госкорпорации Роскосмос ГОЗ",
"Реестр госкорпорации Роскосмос ОПК",
"Реестр госкорпорации Росатом",
"Реестр госкорпорации Росатом ГОЗ",
"Реестр госкорпорации Росатом ОПК",
)
@classmethod
def build_package(
@@ -60,18 +71,32 @@ class StateCorpExchangeService:
package_id: str | None = None,
source_system: str = "mostovik",
organization_inns: list[str] | None = None,
actual_date: date | str | None = None,
) -> StateCorpExchangePackage:
"""Build encrypted archive for state-corp import."""
produced_at = timezone.now()
snapshot_date = cls._coerce_actual_date(actual_date)
normalized_inns = cls._normalize_inn_list(organization_inns)
organizations = cls._get_organizations(normalized_inns)
organizations = (
cls._get_organizations(normalized_inns)
if normalized_inns
else cls._get_rosatom_roscosmos_organizations(snapshot_date)
)
allowed_inns = {str(item.mn_inn) for item in organizations}
data = {
"organizations": cls._serialize_organizations(organizations),
"industrial_products": cls._serialize_industrial_products(allowed_inns),
"prosecutor_checks": cls._serialize_prosecutor_checks(allowed_inns),
"public_procurements": cls._serialize_public_procurements(allowed_inns),
"arbitration_cases": [],
"arbitration_cases": cls._serialize_arbitration_cases(allowed_inns),
"bankruptcy_procedures": cls._serialize_bankruptcy_procedures(allowed_inns),
"defense_unreliable_suppliers": (
cls._serialize_defense_unreliable_suppliers(allowed_inns)
),
"information_security_registries": (
cls._serialize_information_security_registries(allowed_inns)
),
"labor_vacancies": cls._serialize_labor_vacancies(allowed_inns),
}
payload_counts = {key: len(value) for key, value in data.items()}
package_id = package_id or cls._build_package_id()
@@ -248,6 +273,23 @@ class StateCorpExchangeService:
)
return list(queryset)
@classmethod
def _get_rosatom_roscosmos_organizations(
cls,
actual_date: date,
) -> list[Organization]:
organization_ids = (
RegistryMembershipPeriod.objects.filter(
registry__name__in=cls.ROSATOM_ROSCOSMOS_REGISTRY_NAMES,
started_at__lte=actual_date,
)
.filter(Q(ended_at__isnull=True) | Q(ended_at__gt=actual_date))
.order_by("organization_id")
.values_list("organization_id", flat=True)
.distinct()
)
return list(Organization.objects.filter(id__in=organization_ids).order_by("id"))
@classmethod
def _serialize_organizations(
cls,
@@ -350,6 +392,249 @@ class StateCorpExchangeService:
)
return items
@classmethod
def _serialize_arbitration_cases(
cls,
allowed_inns: set[str],
) -> list[dict[str, str]]:
items: list[dict[str, str]] = []
queryset = GenericParserRecord.objects.filter(
source=ParserLoadLog.Source.ARBITRATION,
inn__in=allowed_inns,
).order_by("id")
for record in queryset:
payload = record.payload if isinstance(record.payload, dict) else {}
target = payload.get("target")
if not isinstance(target, dict):
target = {}
decision_date = cls._coerce_date(
None,
payload.get("result_date")
or payload.get("decision_date")
or payload.get("filing_date")
or record.record_date,
)
case_number = str(
payload.get("case_number") or record.title or record.external_id or ""
).strip()
if not case_number or decision_date is None:
continue
items.append(
{
"organization_inn": cls._digits(record.inn),
"case_number": case_number,
"court_name": str(payload.get("court_name") or "").strip(),
"party_role": str(
target.get("role") or payload.get("party_role") or ""
).strip(),
"status": str(payload.get("status") or record.status or "").strip(),
"decision_date": decision_date.isoformat(),
}
)
return items
@classmethod
def _serialize_bankruptcy_procedures(
cls,
allowed_inns: set[str],
) -> list[dict[str, str | None]]:
items: list[dict[str, str | None]] = []
for record in cls._generic_records(
allowed_inns,
sources=[ParserLoadLog.Source.FEDRESURS_BANKRUPTCY],
):
payload = cls._record_payload(record)
message_date = cls._coerce_date(
None,
cls._payload_lookup(payload, ["message_date", "date", "Дата"])
or record.record_date,
)
message_type = cls._payload_lookup(payload, ["message_type", "type"])
items.append(
{
"organization_inn": cls._digits(record.inn),
"external_id": record.external_id,
"message_type": message_type or record.title or record.status,
"message_date": message_date.isoformat() if message_date else None,
"case_number": cls._payload_lookup(payload, ["case_number"]),
"status": record.status,
"source_url": record.url,
}
)
return items
@classmethod
def _serialize_defense_unreliable_suppliers(
cls,
allowed_inns: set[str],
) -> list[dict[str, str | None]]:
items: list[dict[str, str | None]] = []
for record in cls._generic_records(
allowed_inns,
sources=[
ParserLoadLog.Source.UNFAIR_SUPPLIERS,
ParserLoadLog.Source.FAS_GOZ,
],
):
payload = cls._record_payload(record)
included_at = cls._coerce_date(
None,
cls._payload_lookup(
payload,
[
"included_at",
"date",
"Дата вступления постановления",
"Включено",
],
)
or record.record_date,
)
items.append(
{
"organization_inn": cls._digits(record.inn),
"external_id": record.external_id,
"registry_source": record.source,
"registry_number": cls._payload_lookup(
payload,
[
"registry_number",
"number",
"Номер реестровой записи",
"Номер реестровой записи в ЕРУЗ",
],
),
"supplier_name": cls._payload_lookup(
payload,
[
"supplier_name",
"organisation_name",
"Полное наименование лица",
"Фирменное наименование лица",
"Наименование ФИО недобросовестного поставщика",
],
)
or record.organisation_name,
"reason": record.title,
"included_at": included_at.isoformat() if included_at else None,
"status": record.status,
"source_url": record.url,
}
)
return items
@classmethod
def _serialize_information_security_registries(
cls,
allowed_inns: set[str],
) -> list[dict[str, str | None]]:
items: list[dict[str, str | None]] = []
for record in cls._generic_records(
allowed_inns,
sources=[ParserLoadLog.Source.FSTEC],
):
payload = cls._record_payload(record)
issued_at = cls._coerce_date(
None,
cls._payload_lookup(
payload,
["issued_at", "date", "Дата предоставления лицензии"],
)
or record.record_date,
)
expires_at = cls._coerce_date(
None,
cls._payload_lookup(
payload,
["expires_at", "Срок действия сертификата", "res_valid_till"],
),
)
items.append(
{
"organization_inn": cls._digits(record.inn),
"external_id": record.external_id,
"registry_name": cls._payload_lookup(payload, ["registry_name"])
or record.title
or "Реестр ФСТЭК",
"presence_status": "present",
"entry_number": cls._payload_lookup(
payload,
[
"entry_number",
"registration_number",
"Регистрационный номер лицензии",
"№ сертификата",
],
)
or record.external_id,
"issued_at": issued_at.isoformat() if issued_at else None,
"expires_at": expires_at.isoformat() if expires_at else None,
}
)
return items
@classmethod
def _serialize_labor_vacancies(
cls,
allowed_inns: set[str],
) -> list[dict[str, str | None]]:
items: list[dict[str, str | None]] = []
for record in cls._generic_records(
allowed_inns,
sources=[ParserLoadLog.Source.TRUDVSEM],
):
payload = cls._record_payload(record)
published_at = cls._coerce_date(
None,
cls._payload_lookup(
payload,
["published_at", "creation-date", "date", "published_at"],
)
or record.record_date,
)
items.append(
{
"organization_inn": cls._digits(record.inn),
"external_id": record.external_id,
"vacancy_source": cls._payload_lookup(payload, ["vacancy_source"])
or "trudvsem",
"title": record.title,
"status": record.status,
"published_at": published_at.isoformat() if published_at else None,
"salary_amount": cls._serialize_decimal(record.amount),
"source_url": record.url,
}
)
return items
@staticmethod
def _record_payload(record: GenericParserRecord) -> dict[str, Any]:
return record.payload if isinstance(record.payload, dict) else {}
@classmethod
def _generic_records(
cls,
allowed_inns: set[str],
*,
sources: list[str],
):
if not allowed_inns:
return GenericParserRecord.objects.none()
return GenericParserRecord.objects.filter(
source__in=sources,
inn__in=allowed_inns,
).order_by("id")
@staticmethod
def _payload_lookup(payload: dict[str, Any], candidates: list[str]) -> str:
for key in candidates:
value = payload.get(key)
if value not in (None, ""):
return str(value).strip()
return ""
@staticmethod
def _normalize_inn_list(organization_inns: list[str] | None) -> list[str]:
normalized_items: list[str] = []
@@ -359,6 +644,20 @@ class StateCorpExchangeService:
normalized_items.append(normalized)
return normalized_items
@staticmethod
def _coerce_actual_date(value: date | str | None) -> date:
if value is None:
return timezone.localdate()
if isinstance(value, date):
return value
raw_text = str(value).strip()
try:
return date.fromisoformat(raw_text)
except ValueError as exc:
raise StateCorpExchangeError(
"actual_date должен быть датой в формате YYYY-MM-DD"
) from exc
@staticmethod
def _digits(value: str | int) -> str:
return "".join(char for char in str(value).strip() if char.isdigit())