arm-core/api/tests/test_counterparty_resolve.py
Andrey Epifancev 75feab6015 test: гигиена тестов Среза 2c и нормализация email seed в одном месте
- 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
2026-10-06 00:39:40 +04:00

139 lines
7.5 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_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