dodil/erp-brick-ar
DODIL ERP brick: accounts receivable
tests/test_audit_musts.py
"""Audit of ar v4 (b121ec0..b4f8956): each test states a MUST and FAILS on the current code for the stated reason.
Business 'today' is 2026-11-20 unless a test moves the day."""
import json
from decimal import Decimal
from conftest import APPROVER, BILLER, OWNER, SYSTEM, api, approved_credit, billing, issue, line, must, on_day
from test_v4a import deposit, final, recharge
from test_v4b import aged, annual, approve, propose, release, sent_invoice
from brick_ar.application import use_cases as uc
from brick_ar.infrastructure import tables
D = Decimal
def _remaining(dep_id):
with uc.repository() as repo:
return uc.sv.deposit_remaining(repo, repo.invoices.get(dep_id)[0])
# ── M1: a credit note on a deposit invoice ignores what finals already deducted ────────────────────
def test_m1_a_deposit_fully_deducted_cannot_also_be_credited(world):
"""5,000.00 deposit, a final deducts all of it, then a 5,000.00 credit on the deposit is approved: the deposit is used twice
(refunded AND deducted) and what remains is -5,000.00. approve_credit checks only invoice.total - credited, never the claims."""
dep = deposit(world)
issue(final(world, "SO-9", "5000.00")["invoice_id"], world=world)
assert _remaining(dep["invoice_id"]) == D("0.00")
d = must(api("POST", "/credit-notes", BILLER, {"invoice_id": dep["invoice_id"], "reason": "refund", "lines": [line("Refund", "1", "5000.00")]}))
world.payments.credit_faces[d["credit_id"]] = Decimal(d["total"])
s, _b = api("POST", f"/credit-notes/{d['credit_id']}/approve", APPROVER, {})
assert s == 409, "a credit on a deposit must stay within what remains of the deposit (deposit_remaining)"
assert _remaining(dep["invoice_id"]) >= 0
# ── M2: a deposit invoice is voided after a final deducted it ──────────────────────────────────────
def test_m2_a_deposit_that_a_final_deducted_cannot_be_voided(world):
dep = deposit(world)
issue(final(world, "SO-9", "3000.00")["invoice_id"], world=world)
s, _b = api("POST", f"/invoices/{dep['invoice_id']}/void", APPROVER, {"reason": "raised in error"})
assert s == 409, "void_gates has no gate on deposit claims: the final now deducts 3,000.00 from a void deposit"
# ── M3: voiding the final never frees its deposit claim ────────────────────────────────────────────
def test_m3_voiding_a_final_gives_its_deduction_back_to_the_deposit(world):
"""DepositRepository.claimed sums every claim whatever the final's lifecycle (its own docstring says 'on ISSUED finals')."""
dep = deposit(world)
fin = issue(final(world, "SO-9", "3000.00")["invoice_id"], world=world)
must(api("POST", f"/invoices/{fin['invoice_id']}/void", APPROVER, {"reason": "raised in error"}))
assert _remaining(dep["invoice_id"]) == D("5000.00")
# ── M4: release and credit take-back are two bounded claims on one line that never collide ─────────
def test_m4_a_credit_racing_the_last_release_cannot_take_back_revenue_already_released(world, monkeypatch):
"""The credit's snapshot does not see the final month's release (committed meanwhile): the claims are keyed (line, period) and
(line, cn:<id>), so nothing collides and released + taken back exceeds the line's net."""
inv = annual(world)
must(release("2027-09", "2027-10-01")) # all 12,000.00 released
real = tables.ReleaseRepository.rows
stale = {"left": 1}
def snapshot_rows(self, invoice_id):
if stale["left"]:
stale["left"] -= 1
return []
return real(self, invoice_id)
monkeypatch.setattr(tables.ReleaseRepository, "rows", snapshot_rows)
with on_day("2027-10-01"):
approved_credit(world, inv["invoice_id"], lines=[{"description": "Support credit", "quantity": "1", "unit_price": "3000.00"}])
monkeypatch.setattr(tables.ReleaseRepository, "rows", real)
with uc.repository() as repo:
total = sum((r["amount"] for r in repo.releases.rows(inv["invoice_id"])), D(0))
assert total <= D("12000.00"), f"released + taken back = {total} on a 12,000.00 line"
# ── M5: void and release race: nothing collides ────────────────────────────────────────────────────
def test_m5_a_void_racing_a_release_collides(world, monkeypatch):
"""void reads releases.any_for in its snapshot; release never writes the invoice row, so a release committed meanwhile does not
conflict with the void's save: the invoice is void with revenue released (and a release journal) behind it."""
inv = annual(world)
must(release("2026-10", "2026-11-01"))
monkeypatch.setattr(tables.ReleaseRepository, "any_for", lambda self, iid: False) # the void's snapshot
s, _b = api("POST", f"/invoices/{inv['invoice_id']}/void", APPROVER, {"reason": "mistake"})
assert s == 409, "a release committed beside the void must make the void collide"
# ── M6: provision runs approved out of order post the allowance twice ──────────────────────────────
def test_m6_an_earlier_periods_run_approved_after_a_later_one_does_not_post_the_allowance_again(world):
aged(world)
dec = must(propose("2026-12", "2027-01-02"))
must(approve(dec["run_id"], day="2027-01-02")) # movement 720.00 from nothing
s, nov = propose("2026-11", "2027-01-02")
if s == 200:
approve(nov["run_id"], day="2027-01-02") # last_approved_before(2026-11) = none -> 720.00 again
expense = sum((D(ln.get("debit", "0")) - D(ln.get("credit", "0")) for j in world.gl.journals.values()
if j["source_document"].startswith("prov:") for ln in j["lines"] if ln["purpose"] == "doubtful_debt_expense"), D(0))
assert expense == D("720.00"), f"the ledger carries an allowance of {expense} for 720.00 of doubtful debt"
# ── M7: an e-invoice status moves the document backwards ───────────────────────────────────────────
def test_m7_a_rejection_after_acceptance_does_not_reject_an_accepted_invoice(world):
"""AP (accepted) and PD (paid) are final Invoice Responses; record_einvoice_status keys only (message, status), so a later RE is
recorded and the invoice shows 'rejected' after the buyer accepted it."""
i = sent_invoice(world)
must(api("POST", "/einvoices/MSG-1/status", SYSTEM, {"status": "AP", "at": "2026-11-20T09:00:00Z"}))
api("POST", "/einvoices/MSG-1/status", SYSTEM, {"status": "RE", "reason": "late", "at": "2026-11-20T10:00:00Z"})
inv = must(api("GET", f"/invoices/{i['invoice_id']}", BILLER))
assert inv["einvoice"]["rejection"] is None
# ── M8: the served process is the v3 one ───────────────────────────────────────────────────────────
def test_m8_the_served_bpmn_is_the_signed_v4_process():
"""process.yaml says version 3: no payment_request / provision_run workflow, and v4 transition keys (the frozen BPMN ids:
approve_doubtful_provision, settle_payment_request, release_deferred_revenue, plan_manual_schedule) are absent."""
s, body = api("GET", "/process/bpmn", OWNER)
text = json.dumps(body)
assert s == 200
missing = [k for k in ("approve_doubtful_provision", "settle_payment_request", "release_deferred_revenue", "plan_manual_schedule",
"raise_settlement_discount", "record_einvoice_status") if k not in text]
assert not missing, f"the signed v4 transitions are not in the served BPMN: {missing}"
# ── M9: a credit note is numbered outside its company's series ─────────────────────────────────────
def test_m9_a_credit_on_another_companys_invoice_numbers_from_that_companys_series(world):
"""approve_credit calls claim_number without company=inv.company_code (use_cases.py:782): every company's credit notes share
one series, so UK02's first credit note is CN-...-0002 after UK01's."""
uk01 = recharge(world)
billing(7312)
d = must(api("POST", "/invoices", BILLER, {"bp_id": 7312, "currency": "GBP", "company_code": "UK02", "lines": [line()]}))
uk02 = issue(d["invoice_id"], world=world)
c1 = approved_credit(world, uk01["invoice_id"], lines=[line("Credit", "1", "10.00")])
c2 = approved_credit(world, uk02["invoice_id"], lines=[line("Credit", "1", "10.00")])
assert c2["credit_no"] == c1["credit_no"], f"UK02's first credit note is {c2['credit_no']}, UK01's is {c1['credit_no']}"