spb/air Public MIT
AIR — The Language of Accounting.
Python 100%
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