dodil/erp-brick-payments
DODIL ERP brick: payments
tests/test_doors.py
"""Both doors serve the same use cases: every route has its tool (but the machine's seams), a tool is a use case and
never table CRUD, and the same call has the same outcome through the API and through MCP."""
import re
from conftest import APPROVER, BOSS, CASHIER, NOBODY, SALES, api, must, receipt, set_control, tool, tools
from brick_payments.application.use_cases import PAY
MACHINE_ONLY = {"ingest_provider_payment", "ingest_statement", "suggest_match", "allocate_from_source", "collections_tick", "load_payments_from_odoo"}
def test_every_use_case_is_a_route_and_a_tool_except_the_machine_seams():
names = {u.name for u in PAY.use_cases if u.tool}
listed = set(tools(BOSS))
assert names <= listed
assert MACHINE_ONLY.isdisjoint(listed) and MACHINE_ONLY <= {u.name for u in PAY.use_cases}
for u in PAY.use_cases:
if not u.tool:
assert u.permission is None or u.permission in PAY.machine_permissions or u.name == "show_process_bpmn", u.name
def test_tools_are_use_cases_never_table_crud():
for name in tools(BOSS):
assert not re.match(r"^(create|update|delete|insert|upsert|put|patch)_(pay_|receipt_row|allocation_row|table)", name), name
assert {"record_receipt", "import_statement", "allocate", "decide_match", "unallocate", "reverse_receipt", "request_refund",
"approve_refund", "pay_refund", "overdue_list", "record_promise", "produce_statement", "credit_status", "credit_ok",
"set_credit_limit", "set_pay_control", "settlement", "open_items"} <= set(tools(BOSS))
def test_a_cashier_calls_what_a_cashier_may_and_is_refused_the_rest():
assert tool(CASHIER, "approve_refund", {"refund_id": "nope"})[0] == "refused"
assert "pay:refund:approve" in tool(CASHIER, "approve_refund", {"refund_id": "nope"})[1]
assert "pay:settings:controls" in tool(CASHIER, "set_pay_control", {"setting_key": "match_tolerance", "value": "1.00"})[1]
assert "pay:credit:read" in tool(CASHIER, "credit_status", {"bp_id": 1})[1]
def test_the_same_refusal_through_both_doors(nb):
body = {"bp_id": 7311, "amount": "10.00", "currency": "GBP", "received_on": "2026-11-21"}
status, api_body = api("POST", "/receipts", CASHIER, body)
kind, text = tool(CASHIER, "record_receipt", body)
assert status == 409 and kind == "refused" and api_body["detail"] in text
def test_the_same_record_through_both_doors(nb):
a = must(api("POST", "/receipts", CASHIER, {"bp_id": 7311, "amount": "10.00", "currency": "GBP", "received_on": "2026-11-20", "bank_reference": "A"}))
kind, b = tool(CASHIER, "record_receipt", {"bp_id": 7311, "amount": "10.00", "currency": "GBP", "received_on": "2026-11-20", "bank_reference": "B"})
assert kind == "ok"
strip = lambda d: {k: v for k, v in d.items() if k not in ("payment_id", "bank_reference", "suggestion_result", "recorded_by")} # noqa: E731
assert strip(a) == strip(b)
def test_credit_status_through_mcp_for_sales_and_the_refusal_without_the_permission(nb):
nb.invoice("OLD", 7311, "100.00", due_date="2026-09-01")
kind, got = tool(SALES, "credit_status", {"bp_id": 7311})
assert kind == "ok" and got["hold"] is True and got["readiness"][0]["gate"] == "credit_ok"
kind, why = tool(SALES, "list_receipts", {})
assert kind == "refused" and "pay:receipt:read" in why
assert tool(NOBODY, "credit_status", {"bp_id": 7311})[0] == "refused"
def test_the_whole_cashier_flow_through_mcp(nb):
nb.invoice("INV-2026-0041", 7311, "100.00", number="INV-2026-0041")
r = receipt(amount="100.00", reference="INV41")
kind, got = tool(CASHIER, "decide_match", {"payment_id": r["payment_id"], "decision": "accept"})
assert kind == "ok" and got["allocated"] == "100.00"
kind, s = tool(CASHIER, "settlement", {"invoice_ids": "INV-2026-0041"})
assert kind == "ok" and s["invoices"][0]["state"] == "paid"
kind, why = tool(CASHIER, "allocate", {"payment_id": r["payment_id"], "lines": [{"invoice_id": "INV-2026-0041", "amount": "1.00"}]})
assert kind == "refused" and "has 0.00 left" in why
def test_a_control_through_mcp_is_the_owners_alone(nb):
assert tool(CASHIER, "set_pay_control", {"setting_key": "match_tolerance", "value": "5.00"})[0] == "refused"
kind, got = tool(APPROVER, "set_pay_control", {"setting_key": "match_tolerance", "value": "5.00"})
assert kind == "ok" and got["value"] == "5.00"
set_control("match_tolerance", "5.00")
def test_the_process_is_served_as_bpmn_for_each_entity():
for entity, task in (("receipt", "Task_record_customer_receipt"), ("refund", "Task_request_refund"), ("collection", "Task_send_reminder")):
got = must(api("GET", "/process/bpmn", CASHIER, {"process_id": entity}))
assert task in got["bpmn"] and got["processes"] == [entity]