"""Проверка схемы среза ЕХ ex-payload-v1 (docs #208): примеры проходят, типовые ошибки — нет. Запуск: .venv/bin/python -I scripts/kfo_contract/test_ex_payload.py """ import copy import json import sys from pathlib import Path from jsonschema import Draft202012Validator ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "scripts")) from validate_contracts import make_format_checker # noqa: E402 — та же проверка форматов, что в make check SCHEMA = ROOT / "contracts" / "ex-payload-v1.schema.json" EXAMPLES = sorted((ROOT / "contracts" / "examples").glob("ex-payload-*.json")) REQUIRED_TYPES = {"summary_osv", "vgo_pairs", "chess", "kfo_balance", "kfo_income", "goodwill", "corrections", "version_card"} def main(): schema = json.loads(SCHEMA.read_text(encoding="utf-8")) Draft202012Validator.check_schema(schema) v = Draft202012Validator(schema, format_checker=make_format_checker()) by_type = {} for p in EXAMPLES: doc = json.loads(p.read_text(encoding="utf-8")) errs = list(v.iter_errors(doc)) assert not errs, f"{p.name}: {errs[0].message[:200]}" by_type[doc["item_type"]] = doc missing = REQUIRED_TYPES - set(by_type) assert not missing, f"нет примеров: {sorted(missing)}" def rejected(doc, why): assert list(v.iter_errors(doc)), f"схема пропустила: {why}" # 1. item_type конверта не совпадает с формой data bad = copy.deepcopy(by_type["kfo_balance"]) bad["data"] = copy.deepcopy(by_type["goodwill"]["data"]) rejected(bad, "kfo_balance с данными гудвилла") # 2. kfo_balance без сводки корректировок в реквизитах bad = copy.deepcopy(by_type["kfo_balance"]) del bad["data"]["requisites"]["corrections_balance"] rejected(bad, "нет requisites.corrections_balance") # 4. деньги числом bad = copy.deepcopy(by_type["kfo_balance"]) bad["data"]["balance"]["lines"][0]["bank_amount"] = 2500.0 rejected(bad, "деньги числом") # конверт: лишнее поле и неизвестная единица bad = copy.deepcopy(by_type["version_card"]) bad["extra"] = 1 rejected(bad, "лишнее поле конверта") bad = copy.deepcopy(by_type["summary_osv"]) bad["units"] = "mln_rub" rejected(bad, "неизвестная единица") # единица не соответствует типу bad = copy.deepcopy(by_type["summary_osv"]) bad["units"] = "thousand_rub" rejected(bad, "summary_osv в тысячах") # ревью #208: легитимные ответы API проходят ok = copy.deepcopy(by_type["summary_osv"]) ok["data"]["summary"]["gaps"][0]["missing_quarters"] = ["Q1", "Q2"] assert not list(v.iter_errors(ok)), "C-1: пробелы свода как в API (Q1…Q4)" ok = copy.deepcopy(by_type["chess"]) ok["data"]["matrix_all"]["cells"][0]["variance_percent"] = "10199959.95" assert not list(v.iter_errors(ok)), "C-2: большой процент шахматки" ok = copy.deepcopy(by_type["vgo_pairs"]) ok["data"]["balance"]["aggregates"][0]["deviation_percent"] = "10199959.95" assert not list(v.iter_errors(ok)), "C-2: большой процент агрегата" # ревью #208: то, что нужно выгрузкам, обязательно for path, why in [(("chess", "matrix_all", "total_debit"), "I-1 итоги шахматки"), (("vgo_pairs", "balance", "rules"), "I-1 правила таблицы корректировок"), (("chess", "presets", 0, "rule"), "I-1 правило пресета")]: bad = copy.deepcopy(by_type[path[0]]) node = bad["data"] for k in path[1:-1]: node = node[k] del node[path[-1]] rejected(bad, why) bad = copy.deepcopy(by_type["corrections"]) del bad["data"]["items"][0]["correction"]["balance"] rejected(bad, "I-1 balance корректировки") bad = copy.deepcopy(by_type["vgo_pairs"]) bad["data"]["balance"]["aggregates"][0]["side_1"]["entries"][0]["entry_ids"] = [] rejected(bad, "I-1 строка агрегата без entry_ids") bad = copy.deepcopy(by_type["corrections"]) bad["data"]["items"][0]["correction"]["updated_by"] = {"user_id": "x", "full_name": "y"} rejected(bad, "I-2 updated_by не Ref") # ревью #208: пустой и незамороженный payload bad = copy.deepcopy(by_type["version_card"]) bad["data"]["members"] = [] rejected(bad, "M-1 карточка без состава") bad = copy.deepcopy(by_type["kfo_balance"]) bad["data"]["is_frozen"] = False rejected(bad, "M-1 КФО не заморожена") bad = copy.deepcopy(by_type["version_card"]) bad["data"]["quarter"] = 3 rejected(bad, "M-3 квартал числом") # форматы проверяются bad = copy.deepcopy(by_type["version_card"]) bad["data"]["verified_at"] = "вчера" rejected(bad, "M-6 date-time") print(f"OK: примеров {len(EXAMPLES)}, отрицательных случаев 17, положительных граничных 3") return 0 if __name__ == "__main__": sys.exit(main())