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()