arm-core/api/tests/test_counterparty_resolve.py
Andrey Epifancev 9658f809e7 feat(mapping): индекс и резолв контрагентов по ИНН/имени/алиасу (§3 п. 4)
CounterpartyIndex.load — два запроса по всему справочнику без policy (включая закрытые ЮЛ);
resolve — ИНН, затем точное имя, затем normalize_alias по именам и алиасам, >1 кандидата → None;
неизвестный ИНН не блокирует поиск по имени. owner_names — для counterparty_check.
Отмечены шаги Task 6 и пункт 3.1 tasks.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 22:27:42 +04:00

127 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Индекс и резолв контрагентов (§3 п. 4; spec osv-result-mapping «Резолв контрагента»; design D-8; Review Focus 3).
Интеграционные тесты на `db_session` с фабриками: загрузка индекса двумя запросами по всему справочнику (без policy),
ИНН → точное имя → нормализованное имя/алиас, неоднозначность → `None`, `owner_names` для `counterparty_check`.
"""
import datetime
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.aliases import normalize_alias
from src.models.directories import LegalEntityAlias
from src.repositories.counterparties import CounterpartyIndex
from tests.factories import make_legal_entity
pytestmark = pytest.mark.integration
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 index.resolve("3528052718", "что угодно") == by_inn.id
assert 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 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 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 index.resolve(None, " ООО Точное Имя Один ") == unique.id
# >1 кандидата на шаге точного имени → 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 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 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 index.resolve("3333333333", None) == closed.id
assert 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