109 lines
5.4 KiB
Python
109 lines
5.4 KiB
Python
"""Проверка схемы среза ЕХ 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())
|