dodil/erp-brick-expenses
DODIL ERP brick: expenses
tests/test_seams.py
"""The neighbours - hr, the ledger, payments - each behind one adapter: every answer is a reply, never an exception. Not installed (409),
failed (502), silent (503). And every call this brick makes is checked against the CALLEE's signed spec (conftest.checked_seams)."""
import pytest
from conftest import EMPLOYEE, FINANCE, MANAGER, SYSTEM, add, api, approve, claim_551, must, new_report, refused, report, submit
from erp_platform import seam
from erp_platform.contract import ContractViolation
from brick_expenses.infrastructure import seams
def test_a_call_the_callees_spec_would_refuse_fails_the_test_at_the_call(nb, checked_seams):
with pytest.raises(ContractViolation, match="event"):
seams.LEDGER.post("/journal-entry/journals", {"journal_id": "1", "source_module": "expenses", "source_document": "exp:R:r1", "event": "settle",
"company_code": "UK01", "journal_date": "2026-11-07", "currency": "GBP", "lines": []}, actor="x")
with pytest.raises(ContractViolation):
seams.PAYMENTS.post("/payouts", {"payout_ref": "exp:R:r1:pay"}, actor="x") # required fields missing
with pytest.raises(ContractViolation):
seams.HR.get("/persons/1/salary", params={}, actor="x") # a route the spec does not list
seams.HR.get("/persons/3301/employment", params={"on": "2026-11-07"}, actor="x") # the real one passes
assert len(checked_seams.violations) == 3 # recorded too, so a swallowed exception still fails a test
checked_seams.violations.clear()
def test_hr_down_or_silent_is_a_reply_and_the_claim_is_not_lost(nb):
rid = claim_551()
nb.hr_status = 500
refused(api("POST", f"/reports/{rid}/submit", EMPLOYEE), 502, "hr failed")
assert report(rid)["status"] == "draft" # nothing half-submitted
nb.hr_status = None
assert submit(rid)["status"] == "submitted"
def test_payments_down_leaves_the_claim_posted_and_the_payout_row_retried(nb):
nb.pay_status = 500
rid = claim_551()
submit(rid)
must(approve(rid, MANAGER))
got = must(approve(rid, FINANCE))
assert got["status"] == "posted"
(box,) = [o for o in got["outbox"] if o["target"] == "payments"]
assert box["status"] == "pending" and "failed (500" in box["last_error"]
nb.pay_status = None
out = must(api("POST", "/outbox/drain", SYSTEM))
assert [s["status"] for s in out["sent"]] == ["requested"] and len(nb.payouts) == 1
def test_a_payout_for_a_person_with_no_partner_is_not_requested_and_the_journal_carries_no_payee(nb):
nb.persons[EMPLOYEE_EMAIL]["bp_id"] = None
rid = new_report()["report_id"]
add(rid, amount="20.00")
submit(rid)
must(approve(rid, MANAGER))
assert nb.payouts == {} and report(rid)["status"] == "posted" and report(rid)["payout_ref"] is None
(j,) = nb.journals.values()
assert "bp_id" not in j["lines"][-1]
def test_without_the_ledger_an_approved_claim_waits_approved_with_its_reason(nb):
seam.unregister_local("gl")
rid = claim_551()
submit(rid)
must(approve(rid, MANAGER))
got = must(approve(rid, FINANCE))
assert got["status"] == "approved" and "no ledger is installed" in got["outbox"][0]["last_error"]
EMPLOYEE_EMAIL = "emp3301@uk.example"
def test_the_fx_and_tax_calls_are_checked_against_gls_and_taxs_specs_too(nb, checked_seams):
seam.register_local("tax", nb.tax)
with pytest.raises(ContractViolation):
seams.TAX.post("/documents/compute", {"document_ref": "exp:R:1", "direction": "sale", "tax_point": "2026-11-07", "currency": "GBP",
"treatment_code": "uk_business", "prices_include_tax": True, "lines": []}, actor="x") # purchase only
with pytest.raises(ContractViolation):
seams.TAX.post("/documents/exp:R:1/check-contents", {"direction": "purchase"}, actor="x") # no contents
with pytest.raises(ContractViolation):
seams.LEDGER.get("/fx-rates/EUR", params={"type": "spot"}, actor="x") # `on` is required
seams.LEDGER.get("/fx-rates/EUR", params={"on": "2026-11-07", "type": "spot", "to": "GBP"}, actor="x") # the real one passes
assert len(checked_seams.violations) == 3
checked_seams.violations.clear()