SPB Git

spb/air Public MIT

AIR — The Language of Accounting.

Python 100%
3.3 KB · 94 lines python
Raw Blame History
1# =============================================================================2# Projet : AIR — Accounting Intermediate Representation3# Auteur : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : test_golden.py6# Description : Golden tests — AIR documents compiled against expected journal entries.7# =============================================================================8"""Golden tests.910Each case in tests/golden/cases/*.yaml holds an AIR document and the exact11journal entries the compiler must produce under the ca-qc-2026 policy set.12Expected figures are derived from cited official sources13(docs/research/canada-gst-qst.md, fx-handling.md) — never from memory.14"""15from __future__ import annotations1617from decimal import Decimal18from pathlib import Path1920import pytest21import yaml2223from aic.compiler import compile_document24from alsl.loader import load_policy_set25from core.events import AirDocument2627ROOT = Path(__file__).resolve().parents[2]28CASES_DIR = Path(__file__).parent / "cases"29POLICY_FILE = ROOT / "alsl" / "policies" / "ca-qc-2026.yaml"303132def _load_cases() -> list[dict]:33    cases: list[dict] = []34    for path in sorted(CASES_DIR.glob("*.yaml")):35        data = yaml.safe_load(path.read_text(encoding="utf-8"))36        cases.extend(data["cases"])37    return cases3839CASES = _load_cases()404142@pytest.fixture(scope="module")43def policies():44    return load_policy_set(POLICY_FILE)454647def _lines_as_tuples(lines) -> list[tuple[str, str, Decimal, str]]:48    return sorted(49        (l.account.code, l.side.value, l.amount.amount, l.amount.currency)50        for l in lines51    )525354def _expected_as_tuples(raw) -> list[tuple[str, str, Decimal, str]]:55    return sorted(56        (str(acct), str(side), Decimal(str(amount)), str(ccy))57        for acct, side, amount, ccy in raw58    )596061@pytest.mark.parametrize("case", CASES, ids=[c["name"] for c in CASES])62def test_golden(case: dict, policies) -> None:63    document = AirDocument.model_validate(case["air"])64    journal, _diags = compile_document(document, policies)6566    entries = {e.id: e for e in journal.entries}67    expected = {e["id"]: e for e in case["expected_entries"]}6869    assert sorted(entries) == sorted(expected), (70        f"{case['name']}: produced entries {sorted(entries)} "71        f"!= expected {sorted(expected)}"72    )73    for entry_id, exp in expected.items():74        got = _lines_as_tuples(entries[entry_id].lines)75        want = _expected_as_tuples(exp["lines"])76        assert got == want, (77            f"{case['name']} / {entry_id}:\n  got:  {got}\n  want: {want}"78        )798081def test_all_golden_lines_have_provenance(policies) -> None:82    """Traceability: every posted line points into the provenance graph and83    traces back to a subtotal rooted in the source event."""84    for case in CASES:85        document = AirDocument.model_validate(case["air"])86        journal, _ = compile_document(document, policies)87        for entry in journal.entries:88            for line in entry.lines:89                assert line.provenance_id is not None90                chain = journal.provenance.trace(line.provenance_id)91                roots = [n for n in chain if n.kind == "event_subtotal"]92                assert roots, f"{case['name']}: line has no event_subtotal root"93                assert roots[-1].source_ref == entry.source_event_id94