"""Справочники нормализатора (spec «Справочники нормализации — данные с редакциями форм», D-7).""" from datetime import date from pathlib import Path import pytest import yaml from src.core.config import get_settings from src.normalizer.rules import Edition, OcrRules, load_rules API_DIR = Path(__file__).resolve().parents[3] RULES_DIR = API_DIR / "rules" def _write_rules(tmp_path: Path, lines: dict | None = None) -> Path: (tmp_path / "statement_lines.yaml").write_text( yaml.safe_dump( lines or { "editions": [ {"id": "66n", "valid_until": "2024-12-31", "lines": {"1150": "Основные средства"}}, {"id": "fsbu4", "valid_from": "2025-01-01", "lines": {}}, ] }, allow_unicode=True, ), encoding="utf-8", ) (tmp_path / "ocr_lookalikes.yaml").write_text(yaml.safe_dump({"digits": {"О": "0", "l": "1"}}), encoding="utf-8") (tmp_path / "statement_labels.yaml").write_text( yaml.safe_dump({"labels": {"бухгалтерский баланс": "BALANCE"}}, allow_unicode=True), encoding="utf-8" ) return tmp_path class TestRealRules: def test_loads_repository_rules(self) -> None: rules = load_rules(RULES_DIR) ids = [edition.id for edition in rules.editions] assert ids == ["66n", "fsbu4"] assert rules.editions[0].lines["1150"] == "Основные средства" assert rules.editions[0].lines["1120"] == "Результаты исследований и разработок" assert rules.lookalikes["О"] == "0" assert rules.labels["бухгалтерский баланс"] == "BALANCE" assert rules.labels["отчет о финансовых результатах"] == "INCOME" def test_codes_are_four_or_five_digits_without_duplicates(self) -> None: rules = load_rules(RULES_DIR) for edition in rules.editions: codes = list(edition.lines) assert all(code.isdigit() and 4 <= len(code) <= 5 for code in codes), edition.id assert len(codes) == len(set(codes)), edition.id assert all(name.strip() for name in edition.lines.values()), edition.id def test_fsbu4_edition_exists_even_if_empty(self) -> None: rules = load_rules(RULES_DIR) fsbu4 = next(edition for edition in rules.editions if edition.id == "fsbu4") assert fsbu4.valid_from == date(2025, 1, 1) def test_66n_edition_data_integrity(self) -> None: rules = load_rules(RULES_DIR) edition_66n = next(edition for edition in rules.editions if edition.id == "66n") # Проверка точного количества кодов в редакции 66n assert len(edition_66n.lines) == 63, f"66n должна содержать 63 кода, а содержит {len(edition_66n.lines)}" # Проверка специфических кодов, упомянутых в брифе (опечатка и нестандартные) assert edition_66n.lines["1220"] == "Налог на добавленную стоимость по приобретённым ценностям" assert edition_66n.lines["1105"] == "Гудвил" assert edition_66n.lines["2411"] == "в т.ч. текущий налог на прибыль" assert edition_66n.lines["2412"] == "отложенный налог на прибыль" class TestEditionChoice: def test_by_report_date(self, tmp_path: Path) -> None: rules = load_rules(_write_rules(tmp_path)) assert rules.edition_for(date(2024, 12, 31)).id == "66n" assert rules.edition_for(date(2025, 6, 30)).id == "fsbu4" assert rules.edition_for(None) is None def test_by_codes_prefers_more_hits(self, tmp_path: Path) -> None: rules = load_rules( _write_rules( tmp_path, { "editions": [ {"id": "66n", "valid_until": "2024-12-31", "lines": {"1150": "ОС", "1110": "НМА"}}, {"id": "fsbu4", "valid_from": "2025-01-01", "lines": {"1150": "ОС", "1231": "ДЗ долгосрочная"}}, ] }, ) ) assert rules.edition_by_codes(["1150", "1231"]).id == "fsbu4" assert rules.edition_by_codes(["1150"]).id == "66n" # равенство — первая assert rules.edition_by_codes(["9999"]) is None def test_known_codes_and_editions_with(self, tmp_path: Path) -> None: rules = load_rules(_write_rules(tmp_path)) assert rules.known_codes() == frozenset({"1150"}) assert rules.editions_with("1150") == ["66n"] assert rules.editions_with("1110") == [] class TestValidation: def test_bad_code_fails_with_file_name(self, tmp_path: Path) -> None: _write_rules(tmp_path, {"editions": [{"id": "66n", "lines": {"11A0": "x"}}]}) with pytest.raises(ValueError, match="statement_lines.yaml"): load_rules(tmp_path) def test_missing_file_fails(self, tmp_path: Path) -> None: _write_rules(tmp_path) (tmp_path / "ocr_lookalikes.yaml").unlink() with pytest.raises(ValueError, match="ocr_lookalikes.yaml"): load_rules(tmp_path) def test_lookalike_must_map_to_single_digit(self, tmp_path: Path) -> None: _write_rules(tmp_path) (tmp_path / "ocr_lookalikes.yaml").write_text(yaml.safe_dump({"digits": {"О": "00"}}), encoding="utf-8") with pytest.raises(ValueError, match="ocr_lookalikes.yaml"): load_rules(tmp_path) def test_settings_defaults() -> None: settings = get_settings() assert settings.SCAN_CLEANUP is False assert settings.SCAN_RULES_DIR == "rules" assert isinstance(Edition, type) and isinstance(OcrRules, type)