feat: expand registry ingestion and demo exchange
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 36s
CI/CD Pipeline / Build and Push Images (push) Successful in 15s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 24s

This commit is contained in:
2026-07-16 12:59:23 +02:00
parent cc5aa84556
commit 007cecc8d5
43 changed files with 3723 additions and 352 deletions

View File

@@ -34,7 +34,7 @@ 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 organizations.models import Organization
from organizations.models import Organization, OrganizationSourceRecord
class StateCorpExchangeError(ValueError):
@@ -395,6 +395,42 @@ class StateCorpExchangeService:
"ogrn": cls._digits(record.ogrn),
}
)
for record in cls._canonical_records(
allowed_inns,
sources=[ParserLoadLog.Source.INDUSTRIAL],
):
payload = cls._record_payload(record)
certificate_number = cls._payload_lookup(
payload, ["certificate_number", "registry_number"]
)
if not certificate_number:
continue
issue_date = cls._coerce_date(
None,
cls._payload_lookup(payload, ["issue_date_normalized", "issue_date"])
or record.record_date,
)
expiry_date = cls._coerce_date(
None,
cls._payload_lookup(payload, ["expiry_date_normalized", "expiry_date"]),
)
items.append(
{
"organization_inn": cls._digits(record.inn),
"certificate_number": certificate_number,
"issue_date": issue_date.isoformat() if issue_date else None,
"expiry_date": expiry_date.isoformat() if expiry_date else None,
"certificate_file_url": cls._payload_lookup(
payload, ["certificate_file_url"]
)
or record.url,
"organisation_name": cls._payload_lookup(
payload, ["organisation_name"]
)
or record.registry_organization.name,
"ogrn": cls._digits(record.ogrn),
}
)
return items
@classmethod
@@ -405,7 +441,7 @@ class StateCorpExchangeService:
queryset = ManufacturerRecord.objects.filter(inn__in=allowed_inns).order_by(
"id"
)
return [
items = [
{
"organization_inn": cls._digits(record.inn),
"full_legal_name": record.full_legal_name,
@@ -415,6 +451,26 @@ class StateCorpExchangeService:
}
for record in queryset
]
for record in cls._canonical_records(
allowed_inns,
sources=[ParserLoadLog.Source.MANUFACTURES],
):
payload = cls._record_payload(record)
items.append(
{
"organization_inn": cls._digits(record.inn),
"full_legal_name": cls._payload_lookup(
payload, ["full_legal_name", "organisation_name"]
)
or record.registry_organization.full_name
or record.registry_organization.name,
"inn": cls._digits(record.inn),
"ogrn": cls._digits(record.ogrn),
"address": cls._payload_lookup(payload, ["address"])
or record.registry_organization.legal_address,
}
)
return items
@classmethod
def _serialize_industrial_products(
@@ -424,7 +480,7 @@ class StateCorpExchangeService:
queryset = IndustrialProductRecord.objects.filter(
inn__in=allowed_inns
).order_by("id")
return [
items = [
{
"organization_inn": cls._digits(record.inn),
"product_name": record.product_name,
@@ -437,6 +493,27 @@ class StateCorpExchangeService:
}
for record in queryset
]
for record in cls._canonical_records(
allowed_inns,
sources=[ParserLoadLog.Source.INDUSTRIAL_PRODUCTS],
):
payload = cls._record_payload(record)
product_name = cls._payload_lookup(payload, ["product_name"])
items.append(
{
"organization_inn": cls._digits(record.inn),
"product_name": product_name or record.title,
"product_class": cls._payload_lookup(
payload, ["product_model", "regulatory_document"]
)
or "Промышленная продукция",
"okpd2_code": cls._payload_lookup(payload, ["okpd2_code"]),
"tnved_code": cls._payload_lookup(payload, ["tnved_code"]),
"registry_number": cls._payload_lookup(payload, ["registry_number"])
or record.external_id,
}
)
return items
@classmethod
def _serialize_prosecutor_checks(
@@ -463,6 +540,36 @@ class StateCorpExchangeService:
"status": record.status,
}
)
for record in cls._canonical_records(
allowed_inns,
sources=[ParserLoadLog.Source.INSPECTIONS],
):
payload = cls._record_payload(record)
start_date = cls._coerce_date(
None,
cls._payload_lookup(payload, ["start_date_normalized", "start_date"])
or record.record_date,
)
if start_date is None:
continue
items.append(
{
"organization_inn": cls._digits(record.inn),
"registration_number": cls._payload_lookup(
payload, ["registration_number"]
)
or record.external_id,
"law_type": cls._payload_lookup(payload, ["legal_basis"]),
"control_authority": cls._payload_lookup(
payload, ["control_authority"]
),
"prosecutor_office": cls._payload_lookup(
payload, ["prosecutor_office"]
),
"start_date": start_date.isoformat(),
"status": record.status or cls._payload_lookup(payload, ["status"]),
}
)
return items
@classmethod
@@ -504,6 +611,7 @@ class StateCorpExchangeService:
for record in cls._generic_records(
allowed_inns,
sources=[
ParserLoadLog.Source.PROCUREMENTS,
ParserLoadLog.Source.PROCUREMENTS_44FZ,
ParserLoadLog.Source.PROCUREMENTS_223FZ,
ParserLoadLog.Source.CONTRACTS,
@@ -585,7 +693,7 @@ class StateCorpExchangeService:
.prefetch_related("lines")
.order_by("id")
)
return [
items = [
{
"organization_inn": allowed_ogrn_to_inn[report.ogrn],
"external_id": report.external_id,
@@ -611,6 +719,33 @@ class StateCorpExchangeService:
for report in queryset
if report.external_id
]
for record in cls._canonical_records(
set(allowed_ogrn_to_inn.values()),
sources=[ParserLoadLog.Source.FNS_REPORTS],
):
if not record.external_id:
continue
payload = cls._record_payload(record)
items.append(
{
"organization_inn": cls._digits(record.inn),
"external_id": record.external_id,
"ogrn": cls._digits(record.ogrn),
"file_name": cls._payload_lookup(payload, ["file_name"]),
"file_hash": cls._payload_lookup(payload, ["file_hash"]),
"load_batch": record.load_batch,
"status": record.status,
"source": cls._payload_lookup(payload, ["source"]) or record.source,
"error_message": cls._payload_lookup(payload, ["error_message"]),
"lines": [
cls._serialize_financial_report_line(line)
for line in record.financial_lines.all().order_by(
"year", "form_code", "line_code"
)
],
}
)
return items
@staticmethod
def _serialize_financial_report_line(
@@ -631,11 +766,10 @@ class StateCorpExchangeService:
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:
for record in cls._generic_records(
allowed_inns,
sources=[ParserLoadLog.Source.ARBITRATION],
):
payload = record.payload if isinstance(record.payload, dict) else {}
target = payload.get("target")
if not isinstance(target, dict):
@@ -843,7 +977,7 @@ class StateCorpExchangeService:
return items
@staticmethod
def _record_payload(record: GenericParserRecord) -> dict[str, Any]:
def _record_payload(record: Any) -> dict[str, Any]:
return record.payload if isinstance(record.payload, dict) else {}
@classmethod
@@ -854,11 +988,35 @@ class StateCorpExchangeService:
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")
return []
legacy_records = list(
GenericParserRecord.objects.filter(
source__in=sources,
inn__in=allowed_inns,
).order_by("id")
)
return legacy_records + list(
cls._canonical_records(allowed_inns, sources=sources)
)
@staticmethod
def _canonical_records(
allowed_inns: set[str],
*,
sources: list[str],
):
"""Return canonical-only records, excluding legacy-backed mirrors."""
if not allowed_inns:
return OrganizationSourceRecord.objects.none()
return (
OrganizationSourceRecord.objects.filter(
source__in=sources,
extension__organization__inn__in=allowed_inns,
legacy_model="",
)
.select_related("extension__organization")
.order_by("created_at", "uid")
)
@staticmethod
def _payload_lookup(payload: dict[str, Any], candidates: list[str]) -> str: