Add organizations v2 API and registry enrichment
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 26s
CI/CD Pipeline / Build and Push Images (push) Successful in 6s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Successful in 1s

This commit is contained in:
2026-05-06 19:04:46 +02:00
parent f54aa4cb0b
commit 0f17ff6773
62 changed files with 10311 additions and 430 deletions

131
src/organizations/models.py Normal file
View File

@@ -0,0 +1,131 @@
"""Models for the canonical organizations directory."""
import uuid
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from organizations.name_normalization import normalize_organization_name
class Organization(models.Model):
"""Canonical organization without source-specific relations."""
uid = models.UUIDField(
_("UID"),
primary_key=True,
default=uuid.uuid4,
editable=False,
)
name = models.CharField(
_("наименование"),
max_length=1024,
db_index=True,
help_text=_("Наименование организации или ИП"),
)
inn = models.CharField(
_("ИНН"),
max_length=12,
blank=True,
db_index=True,
help_text=_("ИНН ЮЛ или ИП"),
)
kpp = models.CharField(
_("КПП"),
max_length=9,
blank=True,
db_index=True,
help_text=_("КПП только для юридических лиц"),
)
ogrn = models.CharField(
_("ОГРН"),
max_length=13,
blank=True,
db_index=True,
help_text=_("ОГРН только для юридических лиц"),
)
ogrip = models.CharField(
_("ОГРИП"),
max_length=15,
blank=True,
db_index=True,
help_text=_("ОГРИП только для индивидуальных предпринимателей"),
)
class Meta:
db_table = "organizations_organization"
verbose_name = _("организация")
verbose_name_plural = _("организации")
ordering = ["name"]
indexes = [
models.Index(fields=["inn", "kpp"]),
models.Index(fields=["inn", "ogrn"]),
models.Index(fields=["inn", "ogrip"]),
]
constraints = [
models.UniqueConstraint(
fields=["inn", "kpp"],
condition=~Q(inn="") & ~Q(kpp=""),
name="unique_org_inn_kpp_not_blank",
),
models.UniqueConstraint(
fields=["inn"],
condition=~Q(inn="") & Q(kpp="") & Q(ogrip=""),
name="unique_org_inn_without_kpp",
),
models.UniqueConstraint(
fields=["ogrip"],
condition=~Q(ogrip=""),
name="unique_organizations_ogrip_not_blank",
),
models.CheckConstraint(
check=Q(ogrip="") | (Q(kpp="") & Q(ogrn="")),
name="check_entrepreneur_has_no_kpp_ogrn",
),
]
def __str__(self) -> str:
identifier = self.inn or self.ogrn or self.ogrip
if identifier:
return f"{self.name} ({identifier})"
return self.name
@property
def normalized_name(self) -> str:
return normalize_organization_name(self.name)
class OrganizationDataSnapshot(models.Model):
"""Precomputed API v2 data payload for one canonical organization."""
organization = models.OneToOneField(
Organization,
on_delete=models.CASCADE,
primary_key=True,
related_name="data_snapshot",
verbose_name=_("организация"),
)
data = models.JSONField(
_("данные источников"),
default=dict,
help_text=_("Готовый JSON data для API v2"),
)
registries = models.JSONField(
_("реестры"),
default=list,
help_text=_("Готовый JSON registries для API v2"),
)
updated_at = models.DateTimeField(
_("дата обновления"),
auto_now=True,
db_index=True,
)
class Meta:
db_table = "organizations_data_snapshot"
verbose_name = _("снапшот данных организации")
verbose_name_plural = _("снапшоты данных организаций")
def __str__(self) -> str:
return f"Snapshot for {self.organization_id}"