"""Индекс и резолв контрагентов (§3 п. 4; spec osv-result-mapping «Резолв контрагента»; design D-8; Review Focus 3). Интеграционные тесты на `db_session` с фабриками: загрузка индекса двумя запросами по всему справочнику (без policy), ИНН → точное имя → нормализованное имя/алиас, неоднозначность → `None`, `owner_names` для `counterparty_check`. """ import datetime import uuid import pytest from sqlalchemy.ext.asyncio import AsyncSession from src.domain.aliases import normalize_alias from src.models.directories import LegalEntityAlias from src.models.enums import CounterpartyMatchKind from src.repositories.counterparties import CounterpartyIndex, Resolution from tests.factories import make_legal_entity pytestmark = pytest.mark.integration def _entity(resolution: Resolution | None) -> uuid.UUID | None: """Идентификатор ЮЛ из результата резолва (шаг резолва проверяют отдельные тесты).""" return resolution.entity_id if resolution is not None else None async def _alias(session: AsyncSession, entity_id, alias: str) -> None: session.add(LegalEntityAlias(legal_entity_id=entity_id, alias=alias, normalized_alias=normalize_alias(alias))) await session.flush() async def test_inn_alias_and_ambiguous(db_session: AsyncSession) -> None: """Сценарий спеки: по ИНН, по алиасу, неоднозначно.""" by_inn = await make_legal_entity(db_session, name="АО «Хлебозавод Ломоносов»", inn="3528052718") by_alias = await make_legal_entity(db_session, name="ООО «РБ Холдинг»") await _alias(db_session, by_alias.id, "Русский бисквит ООО") await make_legal_entity(db_session, name="Альфаомега") await make_legal_entity(db_session, name="АО «Альфаомега»") index = await CounterpartyIndex.load(db_session) assert _entity(index.resolve("3528052718", "что угодно")) == by_inn.id assert _entity(index.resolve(None, "Русский бисквит")) == by_alias.id # точного имени нет, два ЮЛ с нормализованным именем `альфаомега` assert index.resolve(None, "ООО Альфаомега") is None async def test_inn_is_compared_without_whitespace(db_session: AsyncSession) -> None: entity = await make_legal_entity(db_session, name="ООО Пробел", inn="7701234567") index = await CounterpartyIndex.load(db_session) assert _entity(index.resolve(" 7701 234 567 ", None)) == entity.id assert index.resolve("7701234568", None) is None assert index.resolve("", "") is None assert index.resolve(None, None) is None assert index.resolve(None, " ") is None async def test_unknown_inn_falls_through_to_alias_of_other_entity(db_session: AsyncSession) -> None: """Review Focus 3: ИНН строки нет в справочнике — это не блокирует поиск по имени/алиасу другого ЮЛ.""" owner = await make_legal_entity(db_session, name="ООО Совсем Другое", inn="1111111111") await _alias(db_session, owner.id, "Пекарня Счастливочная") index = await CounterpartyIndex.load(db_session) assert _entity(index.resolve("9999999999", "Пекарня Счастливочная ООО")) == owner.id async def test_exact_name_unique_and_ambiguous(db_session: AsyncSession) -> None: unique = await make_legal_entity(db_session, name="ООО Точное Имя Один") await make_legal_entity(db_session, name="ООО Дубль Имени", inn="2222222221") await make_legal_entity(db_session, name="ООО Дубль Имени", inn="2222222222") index = await CounterpartyIndex.load(db_session) assert _entity(index.resolve(None, " ООО Точное Имя Один ")) == unique.id # >1 кандидата на шаге точного имени → None, без перехода дальше assert index.resolve(None, "ООО Дубль Имени") is None async def test_unique_exact_name_wins_over_ambiguous_normalized(db_session: AsyncSession) -> None: """Приоритет шагов: единственное точное имя побеждает неоднозначность нормализованного (оба ЮЛ нормализуются в одно имя, но точное совпадение у одного).""" ooo = await make_legal_entity(db_session, name="ООО Альфа Приоритет") await make_legal_entity(db_session, name="АО Альфа Приоритет") index = await CounterpartyIndex.load(db_session) assert _entity(index.resolve(None, "ООО Альфа Приоритет")) == ooo.id # без точного совпадения нормализованное имя неоднозначно → None assert index.resolve(None, "Альфа Приоритет") is None async def test_exact_name_zero_candidates_goes_to_normalized(db_session: AsyncSession) -> None: entity = await make_legal_entity(db_session, name="ООО «Нормализуемое Имя»") index = await CounterpartyIndex.load(db_session) assert _entity(index.resolve(None, "Нормализуемое имя, ООО")) == entity.id async def test_alias_ambiguity_between_name_and_alias_of_different_entities(db_session: AsyncSession) -> None: first = await make_legal_entity(db_session, name="ООО Гамма Сервис") second = await make_legal_entity(db_session, name="ООО Дельта") await _alias(db_session, second.id, "Гамма сервис") index = await CounterpartyIndex.load(db_session) assert first.id != second.id assert index.resolve(None, "Гамма Сервис") is None async def test_same_entity_by_name_and_alias_is_not_ambiguous(db_session: AsyncSession) -> None: entity = await make_legal_entity(db_session, name="ООО Эпсилон") await _alias(db_session, entity.id, "Эпсилон") index = await CounterpartyIndex.load(db_session) assert _entity(index.resolve(None, "эпсилон ооо")) == entity.id async def test_closed_entity_is_resolved(db_session: AsyncSession) -> None: closed = await make_legal_entity( db_session, name="ООО Закрытое Зета", inn="3333333333", close_date=datetime.date(2023, 1, 1) ) index = await CounterpartyIndex.load(db_session) assert _entity(index.resolve("3333333333", None)) == closed.id assert _entity(index.resolve(None, "закрытое зета, ООО")) == closed.id async def test_owner_names(db_session: AsyncSession) -> None: owner = await make_legal_entity(db_session, name="ООО «Пекарня Счастливочная»") await _alias(db_session, owner.id, "Счастливочная пекарня") other = await make_legal_entity(db_session, name="ООО Чужое") await _alias(db_session, other.id, "Чужой алиас") index = await CounterpartyIndex.load(db_session) assert index.owner_names(owner) == frozenset({"пекарня счастливочная", "счастливочная пекарня"}) assert index.owner_names(other) == frozenset({"чужое", "чужой алиас"}) async def test_owner_names_without_aliases_and_empty_normalization(db_session: AsyncSession) -> None: only_form = await make_legal_entity(db_session, name="ООО") plain = await make_legal_entity(db_session, name="ИП Иванов") index = await CounterpartyIndex.load(db_session) assert index.owner_names(only_form) == frozenset() assert index.owner_names(plain) == frozenset({"иванов"}) # нормализация в пустую строку не даёт совпадений: пустой ключ в индексе не хранится assert index.resolve(None, "АО") is None async def test_resolution_kind_per_step(db_session: AsyncSession) -> None: """v5.16: результат резолва несёт шаг — ИНН → `INN`, точное имя → `NAME`, нормализованное имя/алиас → `ALIAS`.""" by_inn = await make_legal_entity(db_session, name="ООО Шаг Инн", inn="4401000001") by_name = await make_legal_entity(db_session, name="ООО Шаг Имя", inn="4401000002") by_normalized = await make_legal_entity(db_session, name="ООО «Шаг Нормализация»", inn="4401000003") by_alias = await make_legal_entity(db_session, name="ООО Совсем Другое Имя", inn="4401000004") await _alias(db_session, by_alias.id, "Шаг Алиас") index = await CounterpartyIndex.load(db_session) assert index.resolve("4401000001", "ООО Шаг Имя") == Resolution(by_inn.id, CounterpartyMatchKind.INN) assert index.resolve(None, " ООО Шаг Имя ") == Resolution(by_name.id, CounterpartyMatchKind.NAME) # ИНН не найден, точного имени нет, нормализованное — единственное assert index.resolve("9999999999", "Шаг нормализация, ООО") == Resolution( by_normalized.id, CounterpartyMatchKind.ALIAS ) assert index.resolve(None, "шаг алиас") == Resolution(by_alias.id, CounterpartyMatchKind.ALIAS) assert index.resolve(None, "Нет такого") is None