- test_api_documents: тавтология по counterparty_name строки 18 ОСВ_66 заменена значениями снимка (T9.3). - test_mapping_golden: план счетов не пуст и есть строки с account_id (T8.6). - e2e: код ответа DELETE проверяется после try/finally — не маскирует причину падения (T11.2). - test_counterparty_resolve: единственное точное имя побеждает неоднозначность нормализованного (T6.1). - test_domain_osv_mapping: шапка «ООО» без ИНН → UNKNOWN (T5.1). - seed_lib/apply: email пользователя нормализуется один раз для upsert и ролей, поведение не меняется (T1.1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
139 lines
7.5 KiB
Python
139 lines
7.5 KiB
Python
"""Индекс и резолв контрагентов (§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_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 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 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
|