arm-core/api/tests/test_schemas_consolidations.py

198 lines
8.3 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, DocumentType, FileStatus, PeriodQuarter
from src.schemas import consolidations as schemas
from src.schemas.consolidations import (
MAX_MEMBERS,
MAX_OSV_QUERY,
MAX_STATUS_COMMENT,
ChecklistCellOut,
ConsolidatedRowOut,
ConsolidationCreate,
MapCounterpartyIn,
MembersIn,
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_consolidation_create_selected_perimeter_bounds() -> None:
"""D-1: `legal_entity_ids` необязателен (`None` — периметр из ГСК), не больше `MAX_MEMBERS`."""
ids = {"company_group_id": uuid.uuid4(), "related_group_id": uuid.uuid4(), "year": 2024, "quarter": "Q3"}
assert ConsolidationCreate(**ids).legal_entity_ids is None # type: ignore[arg-type]
selected = [uuid.uuid4() for _ in range(MAX_MEMBERS)]
assert len(ConsolidationCreate(**ids, legal_entity_ids=selected).legal_entity_ids or []) == MAX_MEMBERS # type: ignore[arg-type]
with pytest.raises(ValidationError):
ConsolidationCreate(**ids, legal_entity_ids=[*selected, uuid.uuid4()]) # type: ignore[arg-type]
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_entity_inn": 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_result_table_money_has_two_decimals_with_half_up_rounding() -> None:
company_id = uuid.uuid4()
article = schemas.IntercompanyResultArticleOut(
code="receivables",
name="Дебиторская задолженность",
amounts=[
schemas.IntercompanyResultAmountOut(company_id=company_id, amount=Decimal("0")),
schemas.IntercompanyResultAmountOut(company_id=uuid.uuid4(), amount=Decimal("1.005")),
],
group_total=Decimal("1.005"),
)
dumped = article.model_dump(mode="json")
assert [item["amount"] for item in dumped["amounts"]] == ["0.00", "1.01"]
assert dumped["group_total"] == "1.01"
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(
document_type=DocumentType.OSV,
account="60",
status=status.value, # type: ignore[arg-type]
missing_quarters=[],
document_ids=[],
)
assert cell.status is status
with pytest.raises(ValidationError):
ChecklistCellOut(
document_type=DocumentType.OSV,
account="60",
status="UNKNOWN", # type: ignore[arg-type]
missing_quarters=[],
document_ids=[],
)
def test_checklist_cell_requires_its_rule() -> None:
"""D-2: `document_type` обязателен, `account` — обязателен, но может быть `None` (ББ/ОФР)."""
with pytest.raises(ValidationError):
ChecklistCellOut(status=CellStatus.MISSING, missing_quarters=[], document_ids=[]) # type: ignore[call-arg]
cell = ChecklistCellOut(
document_type=DocumentType.BALANCE,
account=None,
status=CellStatus.MISSING,
missing_quarters=[],
document_ids=[],
)
assert cell.account is None
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]
def test_members_in_upper_bound() -> None:
"""M-6: периметр версии — не больше `MAX_MEMBERS` идентификаторов (до разбора дублей сервисом)."""
ids = [uuid.uuid4() for _ in range(MAX_MEMBERS)]
assert len(MembersIn(legal_entity_ids=ids).legal_entity_ids) == MAX_MEMBERS
with pytest.raises(ValidationError):
MembersIn(legal_entity_ids=[*ids, uuid.uuid4()])
def test_status_comment_and_osv_query_upper_bounds() -> None:
assert StatusIn(to_status="CANCELLED", comment="а" * MAX_STATUS_COMMENT).comment
with pytest.raises(ValidationError):
StatusIn(to_status="CANCELLED", comment="а" * (MAX_STATUS_COMMENT + 1))
assert OsvFilters(q="а" * MAX_OSV_QUERY).q
with pytest.raises(ValidationError):
OsvFilters(q="а" * (MAX_OSV_QUERY + 1))