SPB Git

spb/air Public MIT

AIR — The Language of Accounting.

Python 100%
4.6 KB · 129 lines python
Raw Blame History
1# =============================================================================2# Projet : AIR — Accounting Intermediate Representation3# Auteur : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : test_incremental.py6# Description : Tests — incremental compilation: diff, reversal + replacement entries.7# =============================================================================8"""Incremental compilation tests.910The scenario CLAUDE.md names explicitly: "if an invoice changes, recompile11only the delta (like Git), with reversal entries generated automatically."12"""13from __future__ import annotations1415from datetime import date16from decimal import Decimal17from pathlib import Path1819from aic.incremental import diff_documents, event_fingerprint, recompile20from alsl.loader import load_policy_set21from core.events import AirDocument22from core.invariant import entry_imbalances23from kernel.ledger import Ledger2425ROOT = Path(__file__).resolve().parents[1]26POLICIES = load_policy_set(ROOT / "alsl" / "policies" / "ca-qc-2026.yaml")272829def _doc(*events: dict) -> AirDocument:30    return AirDocument.model_validate({"events": list(events)})313233SALE = {34    "id": "evt_inv_001", "type": "Sale", "date": date(2026, 7, 15),35    "amount": {"amount": "1000.00", "currency": "CAD"},36    "tax": {"jurisdiction": "CA-QC"},37}38PURCHASE = {39    "id": "evt_bill_001", "type": "Purchase", "date": date(2026, 7, 16),40    "amount": {"amount": "400.00", "currency": "CAD"},41    "tax": {"jurisdiction": "CA-QC"},42}43SALE_CORRECTED = {**SALE, "amount": {"amount": "1200.00", "currency": "CAD"}}444546def test_fingerprint_is_content_sensitive() -> None:47    a = _doc(SALE).events[0]48    b = _doc(SALE_CORRECTED).events[0]49    c = _doc(SALE).events[0]50    assert event_fingerprint(a) != event_fingerprint(b)51    assert event_fingerprint(a) == event_fingerprint(c)525354def test_diff_classification() -> None:55    old = _doc(SALE, PURCHASE)56    new = _doc(SALE_CORRECTED, {**PURCHASE, "id": "evt_bill_002"})57    diff = diff_documents(old, new)58    assert diff.changed == ("evt_inv_001",)59    assert diff.removed == ("evt_bill_001",)60    assert diff.added == ("evt_bill_002",)61    assert diff.unchanged == ()626364def test_unchanged_document_produces_empty_delta() -> None:65    old = _doc(SALE, PURCHASE)66    new = _doc(SALE, PURCHASE)67    result = recompile(old, new, POLICIES)68    assert result.diff.is_empty()69    assert result.journal.entries == []707172def test_corrected_invoice_yields_reversal_plus_replacement() -> None:73    result = recompile(_doc(SALE), _doc(SALE_CORRECTED), POLICIES)7475    assert len(result.reversals) == 176    assert len(result.new_entries) == 177    reversal, replacement = result.reversals[0], result.new_entries[0]7879    # reversal is the exact contra of the original 1000.00 compile80    assert reversal.id == "rev_je_evt_inv_001"81    assert reversal.reverses == "je_evt_inv_001"82    credit_ar = [l for l in reversal.lines83                 if l.account.code == "1100" and l.side.value == "credit"]84    assert credit_ar and credit_ar[0].amount.amount == Decimal("1149.75")8586    # replacement carries the corrected figures under a revisioned id87    assert replacement.id.startswith("je_evt_inv_001_r")88    debit_ar = [l for l in replacement.lines89                if l.account.code == "1100" and l.side.value == "debit"]90    assert debit_ar and debit_ar[0].amount.amount == Decimal("1379.70")  # 1200 * 1.149759192    # every delta entry balances93    for entry in result.journal.entries:94        assert entry_imbalances(entry) == {}959697def test_removed_event_yields_reversal_only() -> None:98    result = recompile(_doc(SALE, PURCHASE), _doc(SALE), POLICIES)99    assert [e.id for e in result.reversals] == ["rev_je_evt_bill_001"]100    assert result.new_entries == []101102103def test_delta_posts_cleanly_onto_the_ledger() -> None:104    """Full lifecycle: post v1, post the delta, net effect == direct v2 compile."""105    from aic.compiler import compile_document106107    old, new = _doc(SALE, PURCHASE), _doc(SALE_CORRECTED, PURCHASE)108109    ledger = Ledger()110    v1, _ = compile_document(old, POLICIES)111    ledger.post_journal(v1, "v1")112    delta = recompile(old, new, POLICIES)113    ledger.post_journal(delta.journal, "v2-delta")114115    direct = Ledger()116    v2, _ = compile_document(new, POLICIES)117    direct.post_journal(v2, "v2")118119    assert ledger.balances() == direct.balances()120    assert ledger.verify_chain()121122123def test_recompile_is_deterministic() -> None:124    a = recompile(_doc(SALE, PURCHASE), _doc(SALE_CORRECTED), POLICIES)125    b = recompile(_doc(SALE, PURCHASE), _doc(SALE_CORRECTED), POLICIES)126    ids_a = [e.id for e in a.journal.entries]127    ids_b = [e.id for e in b.journal.entries]128    assert ids_a == ids_b129