arm-core/api/tests/test_domain_consolidation_periods.py
Andrey Epifancev d3c4964e92 fix(consolidation): документ за Q1 равнозначен нарастающему и квартальному (R-14)
Сквозной тест на фикстурах РБ показал: парсер помечает ОСВ за Q1 нарастающей, а
привязка (`fits`) брала нарастающий только за квартал версии, `select_sources` не
считал его квартальным Q1 — для версии Q3 привязано 17 из 22, пары 60/62(/76) в
пробеле [Q1], свод пуст.

- consolidation_periods.effective_cumulative: для Q1 — квартальный при любом флаге;
  одна функция для `fits`, ключа замены `_key`, `select_sources` (версии Q2+) и
  адаптера чек-листа; для версии Q1 поведение прежнее (№50)
- тесты: таблица effective_cumulative, select_sources/build/чек-лист с нарастающим
  Q1, fits любой версии года, подтверждение и подхват (нарастающий и квартальный Q1 —
  один слот, побеждает поздний)
- test_consolidation_fixtures: xfail снят, сценарий зелёный (22 привязки, 1385 строк)
- спеки documents-api и consolidated-osv: фраза о равнозначности Q1; design #81;
  план Task 13 Step 2–4 и tasks.md 5.1 отмечены

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

169 lines
7.4 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): отчётная дата (OQ-40) и выбор источников ОСВ на пару (ЮЛ, счёт) — D-2, §2."""
import datetime
import uuid
from dataclasses import dataclass
import pytest
from src.domain.consolidation_periods import (
QUARTER_ORDER,
Gap,
Sources,
counts_as_cumulative,
effective_cumulative,
quarters_through,
reporting_date,
select_sources,
)
from src.models.enums import PeriodQuarter
Q1, Q2, Q3, Q4 = PeriodQuarter.Q1, PeriodQuarter.Q2, PeriodQuarter.Q3, PeriodQuarter.Q4
@pytest.mark.parametrize(
("year", "quarter", "expected"),
[
(2024, PeriodQuarter.Q1, datetime.date(2024, 4, 1)),
(2024, PeriodQuarter.Q2, datetime.date(2024, 7, 1)),
(2024, PeriodQuarter.Q3, datetime.date(2024, 10, 1)),
(2024, PeriodQuarter.Q4, datetime.date(2025, 1, 1)),
(2100, PeriodQuarter.Q4, datetime.date(2101, 1, 1)),
],
)
def test_reporting_date_is_first_day_after_quarter(year: int, quarter: PeriodQuarter, expected: datetime.date) -> None:
assert reporting_date(year, quarter) == expected
@dataclass(frozen=True)
class _Doc:
"""Документ для `select_sources`: структурно подходит под `DocLike` (как `Document` и `SourceDocument`)."""
period_quarter: PeriodQuarter | None
is_cumulative: bool | None
id: uuid.UUID = uuid.UUID(int=0)
def _doc(quarter: PeriodQuarter | None, cumulative: bool | None = False) -> _Doc:
return _Doc(quarter, cumulative, uuid.uuid4())
def test_quarter_order_and_quarters_through() -> None:
assert QUARTER_ORDER == (Q1, Q2, Q3, Q4)
assert quarters_through(Q1) == (Q1,)
assert quarters_through(Q3) == (Q1, Q2, Q3)
assert quarters_through(Q4) == QUARTER_ORDER
def test_counts_as_cumulative_null_is_cumulative() -> None:
"""R-1, решение №10: NULL (ББ/ОФР) — нарастающий; только `False` — квартальный."""
assert counts_as_cumulative(True) is True
assert counts_as_cumulative(None) is True
assert counts_as_cumulative(False) is False
def test_cumulative_of_version_quarter_is_single_source() -> None:
cumulative = _doc(Q3, True)
assert select_sources(Q3, [cumulative]) == Sources(documents=(cumulative,), cumulative=True)
def test_null_cumulative_flag_of_version_quarter_counts_as_cumulative() -> None:
document = _doc(Q3, None)
assert select_sources(Q3, [document]) == Sources(documents=(document,), cumulative=True)
def test_quarterly_q1_to_q3_are_sources_in_quarter_order() -> None:
q1, q2, q3 = _doc(Q1), _doc(Q2), _doc(Q3)
assert select_sources(Q3, [q3, q1, q2]) == Sources(documents=(q1, q2, q3), cumulative=False)
def test_missing_middle_quarter_is_gap() -> None:
assert select_sources(Q3, [_doc(Q1), _doc(Q3)]) == Gap(missing_quarters=(Q2,))
def test_q1_version_quarterly_q1_is_enough() -> None:
"""Для версии Q1 квартальный Q1 равнозначен нарастающему: источник есть, пробела нет."""
q1 = _doc(Q1)
assert select_sources(Q1, [q1]) == Sources(documents=(q1,), cumulative=False)
def test_cumulative_beats_quarterly_of_same_pair() -> None:
"""Review Focus 2: квартальные Q1–Q3 и нарастающий Q3 — источник один, нарастающий."""
cumulative = _doc(Q3, True)
assert select_sources(Q3, [_doc(Q1), _doc(Q2), _doc(Q3), cumulative]) == Sources((cumulative,), cumulative=True)
def test_cumulative_beats_quarterly_with_gap() -> None:
cumulative = _doc(Q3, True)
assert select_sources(Q3, [_doc(Q1), cumulative]) == Sources((cumulative,), cumulative=True)
def test_cumulative_of_earlier_quarter_is_ignored() -> None:
"""Review Focus 2 (защитно): нарастающий Q2 при версии Q3 не привязывается, но и функция его не берёт."""
assert select_sources(Q3, [_doc(Q2, True)]) == Gap(missing_quarters=(Q1, Q2, Q3))
def test_cumulative_of_earlier_quarter_does_not_fill_gap_of_quarterly() -> None:
assert select_sources(Q3, [_doc(Q1), _doc(Q2, True), _doc(Q3)]) == Gap(missing_quarters=(Q2,))
def test_quarterly_after_version_and_without_quarter_are_ignored() -> None:
q1, q2, q3 = _doc(Q1), _doc(Q2), _doc(Q3)
assert select_sources(Q3, [q1, q2, q3, _doc(Q4), _doc(None), _doc(None, True)]) == Sources((q1, q2, q3), False)
assert select_sources(Q2, [_doc(Q3)]) == Gap(missing_quarters=(Q1, Q2))
def test_no_documents_is_gap_of_all_quarters() -> None:
assert select_sources(Q3, []) == Gap(missing_quarters=(Q1, Q2, Q3))
assert select_sources(Q1, []) == Gap(missing_quarters=(Q1,))
# ------------------------------------------------------------------------- R-14: документ за Q1 равнозначен обоим видам
@pytest.mark.parametrize(
("flag", "quarter", "expected"),
[
(True, Q1, False), # нарастающий с начала года за Q1 — это и есть квартал Q1
(None, Q1, False),
(False, Q1, False),
(True, Q2, True),
(None, Q3, True), # ББ/ОФР вне Q1 — нарастающие (решение №10)
(False, Q3, False),
(True, None, True), # квартал не определён — по флагу (такой документ не привязывается)
],
)
def test_effective_cumulative(flag: bool | None, quarter: PeriodQuarter | None, expected: bool) -> None:
assert effective_cumulative(flag, quarter) is expected
def test_cumulative_q1_is_quarterly_q1_source_for_later_version() -> None:
"""Фикстуры РБ: парсер помечает ОСВ за Q1 нарастающей, Q2/Q3 — квартальными; для версии Q3 это полный комплект."""
q1, q2, q3 = _doc(Q1, True), _doc(Q2), _doc(Q3)
assert select_sources(Q3, [q3, q2, q1]) == Sources(documents=(q1, q2, q3), cumulative=False)
assert select_sources(Q2, [q2, q1]) == Sources(documents=(q1, q2), cumulative=False)
def test_null_flag_q1_is_quarterly_q1_source_for_later_version() -> None:
q1, q2 = _doc(Q1, None), _doc(Q2)
assert select_sources(Q2, [q1, q2]) == Sources(documents=(q1, q2), cumulative=False)
def test_cumulative_q1_does_not_fill_other_quarters() -> None:
assert select_sources(Q2, [_doc(Q1, True)]) == Gap(missing_quarters=(Q2,))
# нарастающий Q2 при версии Q3 — не квартальный Q2: не хватает квартальных Q2 и Q3
assert select_sources(Q3, [_doc(Q1, True), _doc(Q2, True)]) == Gap(missing_quarters=(Q2, Q3))
def test_cumulative_of_version_quarter_still_beats_cumulative_q1() -> None:
cumulative = _doc(Q3, True)
assert select_sources(Q3, [_doc(Q1, True), _doc(Q2), _doc(Q3), cumulative]) == Sources((cumulative,), True)
def test_q1_version_cumulative_q1_is_taken_as_is() -> None:
"""Версия Q1: нарастающий Q1 — источник «как есть» (решение №50), квартальный — склейка из одного квартала."""
cumulative, quarterly = _doc(Q1, True), _doc(Q1, False)
assert select_sources(Q1, [cumulative]) == Sources((cumulative,), cumulative=True)
assert select_sources(Q1, [quarterly, cumulative]) == Sources((cumulative,), cumulative=True)
assert select_sources(Q1, [quarterly]) == Sources((quarterly,), cumulative=False)