feat: use authoritative organization directory
Some checks failed
CI/CD Pipeline / Quality Gate (push) Failing after 9s
CI/CD Pipeline / Build and Push Images (push) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped
CI/CD Pipeline / Internal Notify (push) Successful in 0s

This commit is contained in:
2026-06-07 16:04:43 +02:00
parent 79f0f8ebf7
commit 6ba8fa8d88
41 changed files with 2781 additions and 1058 deletions

View File

@@ -20,6 +20,7 @@ from organizations.models import (
OrganizationSourceRecord,
)
from organizations.name_normalization import normalize_organization_name
from organizations.resolver import OrganizationDirectoryResolver
from organizations.source_cache import invalidate_source_data_cache
from organizations.source_groups import (
SourceGroupDescriptor,
@@ -72,12 +73,16 @@ class OrganizationSourceIngestionResult:
created_financial_lines: int = 0
updated_financial_lines: int = 0
unresolved: int = 0
skipped_unmatched: int = 0
skipped_ambiguous: int = 0
@dataclass(frozen=True)
class _NormalizedRecordInput:
index: int
record: SourceRecordInput
rn: str
okpo: str
inn: str
kpp: str
ogrn: str
@@ -131,8 +136,9 @@ class OrganizationSourceIngestionService:
normalized_records = cls._normalize_records(records)
(
organizations_by_index,
created_organizations,
) = cls._resolve_or_create_organizations(normalized_records)
skipped_unmatched,
skipped_ambiguous,
) = cls._resolve_existing_organizations(normalized_records)
del normalized_records
unresolved = scanned - len(organizations_by_index)
@@ -174,7 +180,7 @@ class OrganizationSourceIngestionService:
return OrganizationSourceIngestionResult(
scanned=scanned,
created_organizations=created_organizations,
created_organizations=0,
created_extensions=created_extensions,
updated_extensions=updated_extensions,
created_records=created_records,
@@ -182,6 +188,8 @@ class OrganizationSourceIngestionService:
created_financial_lines=created_financial_lines,
updated_financial_lines=updated_financial_lines,
unresolved=unresolved,
skipped_unmatched=skipped_unmatched,
skipped_ambiguous=skipped_ambiguous,
)
@staticmethod
@@ -229,6 +237,16 @@ class OrganizationSourceIngestionService:
_NormalizedRecordInput(
index=index,
record=record_input,
rn=OrganizationDirectoryResolver._digits(
(record_input.payload or {}).get("rn")
or (record_input.payload or {}).get("organization_rn"),
max_length=20,
),
okpo=OrganizationDirectoryResolver._digits(
(record_input.payload or {}).get("okpo")
or (record_input.payload or {}).get("mn_okpo"),
max_length=32,
),
inn=inn,
kpp=kpp,
ogrn=ogrn,
@@ -238,42 +256,46 @@ class OrganizationSourceIngestionService:
)
return normalized_records
@classmethod
def _resolve_existing_organizations(
cls,
normalized_records: list[_NormalizedRecordInput],
) -> tuple[dict[int, Organization], int, int]:
organizations_by_index: dict[int, Organization] = {}
skipped_unmatched = 0
skipped_ambiguous = 0
for record in normalized_records:
result = OrganizationDirectoryResolver.resolve(
OrganizationDirectoryResolver.identity(
rn=record.rn,
okpo=record.okpo,
inn=record.inn,
kpp=record.kpp,
ogrn=record.ogrn,
ogrip=record.ogrip,
)
)
if result.organization is not None:
organizations_by_index[record.index] = result.organization
continue
if result.status == "ambiguous":
skipped_ambiguous += 1
else:
skipped_unmatched += 1
return organizations_by_index, skipped_unmatched, skipped_ambiguous
@classmethod
def _resolve_or_create_organizations(
cls,
normalized_records: list[_NormalizedRecordInput],
) -> tuple[dict[int, Organization], int]:
organizations_by_index: dict[int, Organization] = {}
cls._resolve_organizations_by_inn_kpp(
normalized_records,
organizations_by_index,
"""Compatibility wrapper; parser ingestion must not create organizations."""
organizations, _skipped_unmatched, _skipped_ambiguous = (
cls._resolve_existing_organizations(normalized_records)
)
cls._resolve_organizations_by_ogrn_or_ogrip(
normalized_records,
organizations_by_index,
)
cls._resolve_organizations_by_unique_inn(
normalized_records,
organizations_by_index,
)
cls._resolve_organizations_by_exact_name(
normalized_records,
organizations_by_index,
)
(
organizations_by_index,
created_organizations,
) = cls._create_missing_organizations(
normalized_records,
organizations_by_index,
)
cls._update_resolved_organization_identities(
normalized_records,
organizations_by_index,
)
return organizations_by_index, created_organizations
return organizations, 0
@classmethod
def _resolve_organizations_by_inn_kpp(
@@ -875,34 +897,16 @@ class OrganizationSourceIngestionService:
ogrn=record_input.ogrn,
ogrip=record_input.ogrip,
)
organization = cls._resolve_organization(
inn=inn,
kpp=kpp,
ogrn=ogrn,
ogrip=ogrip,
organization_name=record_input.organization_name,
)
if organization is not None:
return organization, False
name = (
str(record_input.organization_name or "").strip()
or str(record_input.title or "").strip()
or str(record_input.external_id or "").strip()
)
if not name:
return None, False
return (
Organization.objects.create(
name=name,
result = OrganizationDirectoryResolver.resolve(
OrganizationDirectoryResolver.from_payload(
payload=record_input.payload,
inn=inn,
kpp=kpp,
ogrn=ogrn,
ogrip=ogrip,
),
True,
)
)
return result.organization, False
@classmethod
def _resolve_organization(
@@ -914,23 +918,16 @@ class OrganizationSourceIngestionService:
ogrip: str,
organization_name: str,
) -> Organization | None:
for resolver in (
cls._resolve_by_inn_kpp,
cls._resolve_by_ogrn_or_ogrip,
cls._resolve_by_ogrip,
cls._resolve_by_unique_inn,
cls._resolve_by_exact_normalized_name,
):
organization = resolver(
result = OrganizationDirectoryResolver.resolve(
OrganizationDirectoryResolver.identity(
inn=inn,
kpp=kpp,
ogrn=ogrn,
ogrip=ogrip,
organization_name=organization_name,
)
if organization is not None:
return organization
return None
)
return result.organization
@staticmethod
def _resolve_by_inn_kpp(