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]