arm-core/api/tests/test_schemas_consolidations.py
Andrey Epifancev a2c2135811 feat(consolidation): API сводной ОСВ — страница с фильтрами и сводка с пробелами
GET /consolidations/versions/{id}/osv и /osv/summary (CONSOLIDATION_VIEW): ConsolidatedOsvService собирает свод
чистой функцией от версии (периметр, привязанные ОСВ, load_osv_rows, один CounterpartyIndex, один SELECT legal_entity)
и режет страницу в памяти; чтение любой версии, включая CANCELLED, без блокировок и аудита.
OsvFilters отделён от OsvPageParams (T2.7), суммы всегда с двумя знаками; account_sort_key устойчив к «6²»/«①» (T9.a).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 06:23:24 +04:00

124 lines
5.2 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.

"""Схемы консолидаций (Срез 4, D-16): границы валидации, суммы-строки в JSON, неизменяемость доменного состояния."""
import dataclasses
import uuid
from decimal import Decimal
from typing import Any
import pytest
from pydantic import ValidationError
from src.domain.checklist import CellStatus, EntityFilesState
from src.domain.consolidated_osv import CounterpartyType as DomainCounterpartyType
from src.models.enums import CounterpartyMatchKind, FileStatus, PeriodQuarter
from src.schemas import consolidations as schemas
from src.schemas.consolidations import (
ChecklistCellOut,
ConsolidatedRowOut,
ConsolidationCreate,
MapCounterpartyIn,
OsvFilters,
OsvPageParams,
StatusIn,
)
MAX_LIMIT = 2000
def test_consolidation_create_year_bounds() -> None:
ids = {"company_group_id": uuid.uuid4(), "related_group_id": uuid.uuid4(), "quarter": "Q3"}
assert ConsolidationCreate(year=2024, **ids).quarter == PeriodQuarter.Q3
for year in (1999, 2101):
with pytest.raises(ValidationError):
ConsolidationCreate(year=year, **ids)
def test_osv_page_params_limit_bounds() -> None:
assert (OsvPageParams().limit, OsvPageParams().offset) == (500, 0) # noqa: PLR2004
assert OsvPageParams(limit=MAX_LIMIT).limit == MAX_LIMIT
for bad in ({"limit": MAX_LIMIT + 1}, {"limit": 0}, {"offset": -1}):
with pytest.raises(ValidationError):
OsvPageParams(**bad)
def test_osv_filters_repeated_match_kind_and_no_pagination() -> None:
"""Фильтры — отдельная модель от пагинации (T2.7): `limit`/`offset` в ней нет."""
assert "limit" not in OsvFilters.model_fields
assert "offset" not in OsvFilters.model_fields
with pytest.raises(ValidationError):
OsvFilters(counterparty_type="OTHER") # type: ignore[arg-type]
assert OsvFilters(match_kind=["NAME", "ALIAS"]).match_kind == [
CounterpartyMatchKind.NAME,
CounterpartyMatchKind.ALIAS,
]
def test_defaults_of_input_schemas() -> None:
assert MapCounterpartyIn(legal_entity_id=uuid.uuid4()).move_alias is False
assert StatusIn(to_status="CANCELLED").comment is None
def _row(**amounts: Decimal | None) -> ConsolidatedRowOut:
fields: dict[str, Any] = {
"entry_ids": [uuid.uuid4()],
"legal_entity_id": uuid.uuid4(),
"legal_entity_name": "ООО",
"legal_entity_inn": "7701000001",
"account_code": "60",
"account_root": "60",
"period": "2024 Q3",
"is_cumulative": False,
"row_number": 1,
"counterparty_name": "А",
"counterparty_inn": None,
"counterparty_id": None,
"counterparty_entity_name": None,
"counterparty_type": "EXTERNAL",
"match_kind": None,
"match_stored": False,
"opening_debit": None,
"opening_credit": None,
"turnover_debit": None,
"turnover_credit": None,
"closing_debit": None,
"closing_credit": None,
"warnings": [],
}
return ConsolidatedRowOut(**{**fields, **amounts})
def test_consolidated_row_serializes_money_as_strings() -> None:
row = _row(opening_debit=Decimal("100.50"), turnover_debit=Decimal("0.00"))
dumped = row.model_dump(mode="json")
assert (dumped["opening_debit"], dumped["opening_credit"], dumped["turnover_debit"]) == ("100.50", None, "0.00")
def test_consolidated_row_money_has_two_decimals() -> None:
"""Суммы свода — всегда с двумя знаками (`"0.00"`, не `"0"`): ноль склейки и сумма Decimal не меняют вид."""
row = _row(opening_debit=Decimal("0"), turnover_debit=Decimal("30.00"), closing_debit=Decimal("7"))
dumped = row.model_dump(mode="json")
assert (dumped["opening_debit"], dumped["turnover_debit"], dumped["closing_debit"]) == ("0.00", "30.00", "7.00")
def test_entity_files_state_is_frozen() -> None:
state = EntityFilesState(has_in_progress=True, last_file_status=FileStatus.ERROR)
with pytest.raises(dataclasses.FrozenInstanceError):
state.has_in_progress = False # type: ignore[misc]
def test_checklist_cell_status_is_the_domain_enum() -> None:
"""Статус ячейки в схеме — `domain.checklist.CellStatus` (одно определение, T2.8): все значения принимаются."""
for status in CellStatus:
cell = ChecklistCellOut(status=status.value, missing_quarters=[], document_ids=[]) # type: ignore[arg-type]
assert cell.status is status
with pytest.raises(ValidationError):
ChecklistCellOut(status="UNKNOWN", missing_quarters=[], document_ids=[]) # type: ignore[arg-type]
def test_counterparty_type_is_the_domain_enum() -> None:
"""Признак контрагента в схеме — `domain.consolidated_osv.CounterpartyType` (одно определение, T2.8)."""
assert schemas.CounterpartyType is DomainCounterpartyType
assert OsvFilters(counterparty_type="GK").counterparty_type is DomainCounterpartyType.GK
with pytest.raises(ValidationError):
OsvFilters(counterparty_type="OTHER") # type: ignore[arg-type]