dodil/erp-brick-purchasing

DODIL ERP brick: purchasing

tests/test_stories_pay.py

"""Payment runs, the bank file, bank details, controls and the slice-1 scenarios — PUR-14 to 16, 19, 29 to 31, 59, SC-PUR-01 to 06."""
import xml.etree.ElementTree as ET
from decimal import Decimal as D

import pytest
from conftest import (
    BUYER,
    CLERK,
    CLERK2,
    OWNER,
    PAYAPPROVER,
    PAYAPPROVER2,
    RECEIVER,
    REQUESTER,
    SUPPLIER_A,
    SUPPLIER_B,
    SYSTEM,
    api,
    as_role,
    bill,
    confirmed,
    must,
    on_day,
    post,
    receive,
    refused,
    set_limit,
    supplier_price,
    world,
)

PAY_DATE = "2026-12-31"


def approved_bill(supplier=SUPPLIER_A, number="INV-5521", qty="10", note="DN-1", price="18.0000", recorder=CLERK):
    supplier_price(bp=supplier, price=price)
    o = confirmed(bp=supplier, lines=[{"product_id": "COLA-075", "quantity": qty}])
    receive(o, quantity=qty, note=note)
    s, b = bill(o, bp=supplier, number=number, who=recorder, quantity=qty, unit_price=price)
    assert s == 200, b
    return o, must(api("POST", f"/bills/{b['bill_id']}/approve", PAYAPPROVER, {}))


def propose(who=CLERK2, **kw):
    return api("POST", "/payment-runs", who, {"bank_account_id": "BARC-01", "pay_date": PAY_DATE, **kw})


def exported_run(fmt=None):
    approved_bill()
    run = must(propose(**({"file_format": fmt} if fmt else {})))
    must(api("POST", f"/payment-runs/{run['run_id']}/approve", PAYAPPROVER, {}))
    f = must(api("POST", f"/payment-runs/{run['run_id']}/export", SYSTEM, {}))
    return run, f


def test_pur19_a_run_lists_one_payment_per_supplier_and_currency_and_names_what_it_left_out():
    world()
    approved_bill(SUPPLIER_A, "A-1")
    approved_bill(SUPPLIER_B, "B-1", note="DN-2")
    o3, b3 = approved_bill(SUPPLIER_A, "A-2", note="DN-3")
    must(api("POST", f"/bills/{b3['bill_id']}/dispute", CLERK, {"reason": "short"}))
    run = must(propose())
    assert run["status"] == "draft" and len(run["payments"]) == 2 and {p["bp_id"] for p in run["payments"]} == {str(SUPPLIER_A), str(SUPPLIER_B)}
    assert [x["bill_id"] for x in run["left_out"]] == [b3["bill_id"]] and "disputed" in run["left_out"][0]["reason"]
    pay_a = [p for p in run["payments"] if p["bp_id"] == str(SUPPLIER_A)][0]
    assert str(pay_a["amount"]) == "216.00" and len(pay_a["bills"]) == 1


def test_pur19_a_supplier_without_approved_bank_details_is_left_out():
    world()
    approved_bill(SUPPLIER_A, "A-1")
    supplier_price(bp=7010)
    o = confirmed(bp=7010, lines=[{"product_id": "COLA-075", "quantity": "10"}])
    receive(o, note="DN-9")
    s, b = bill(o, bp=7010, number="N-1")
    must(api("POST", f"/bills/{b['bill_id']}/approve", PAYAPPROVER, {}))
    run = must(propose())
    assert len(run["payments"]) == 1 and "bank details" in run["left_out"][0]["reason"]


def test_pur19_a_pay_date_before_today_is_refused_and_a_bill_not_yet_due_is_not_proposed():
    world()
    approved_bill()
    assert "not_past" in refused(propose(pay_date="2026-11-19"))
    s, body = propose(pay_date="2026-11-30")
    assert s == 409 and "nothing falls due" in str(body)


def test_pur15_the_proposer_and_the_recorder_of_any_bill_cannot_approve_the_run_under_any_identity():
    world()
    approved_bill(recorder=CLERK)
    run = must(propose(CLERK2))
    clerk_approver = as_role("payables_clerk", "clerk@shop.example", extra=["purchasing:payment:approve"])
    clerk_other_sub = as_role("payables_clerk", "clerk@shop.example", sub="other-sub", extra=["purchasing:payment:approve"])
    proposer_same_sub = as_role("payables_approver", "someone@shop.example", sub="clerk2@shop.example")
    proposer_other_login = as_role("payables_approver", "clerk2@shop.example", sub="zzz")
    for who in (clerk_approver, clerk_other_sub, proposer_same_sub, proposer_other_login):
        assert "second_person" in refused(api("POST", f"/payment-runs/{run['run_id']}/approve", who, {}))
    ok = must(api("POST", f"/payment-runs/{run['run_id']}/approve", PAYAPPROVER, {}))
    assert ok["status"] == "approved" and ok["approved_by_email"] == "finance@shop.example" and ok["approved_on"] == "2026-11-20"


def test_pur15_a_buyer_cannot_release_a_payment_and_a_receiver_cannot_record_a_bill():
    world()
    approved_bill()
    run = must(propose())
    assert api("POST", f"/payment-runs/{run['run_id']}/approve", BUYER, {})[0] == 403
    assert api("POST", "/bills", RECEIVER, {"bp_id": SUPPLIER_A, "supplier_invoice_no": "X", "invoice_date": "2026-11-19", "currency": "GBP",
                                        "lines": [{"quantity": "1", "unit_code": "C62", "unit_price": "1"}]})[0] == 403


def test_pur15_a_run_needs_every_payees_approved_bank_details_in_force_on_the_pay_date():
    world()
    approved_bill()
    run = must(propose())
    # the details are rejected after the proposal: nothing may be released to them
    with on_day("2026-11-20T09:00:00+00:00"):
        must(api("POST", f"/suppliers/{SUPPLIER_A}/bank", CLERK, {"iban_or_account": "GB00BAD", "valid_from": "2026-12-01", "country": "GB"}))
    r = must(api("POST", f"/payment-runs/{run['run_id']}/approve", PAYAPPROVER, {}))
    assert r["status"] == "approved"                                    # the new row is only proposed: the approved row stays in force


def test_pur29_the_file_is_pain001_kept_once_and_a_second_export_returns_the_same(neighbours):
    world()
    run, f = exported_run()
    assert f["file_key"] == f"raw/purchasing/runs/{run['run_id']}.xml" and f["replay"] is False
    root = ET.fromstring(f["content"])
    ns = {"p": root.tag.split("}")[0][1:]}
    assert root.find(".//p:GrpHdr/p:NbOfTxs", ns).text == "1" and root.find(".//p:GrpHdr/p:CtrlSum", ns).text == "216.00"
    assert root.find(".//p:CdtTrfTxInf/p:CdtrAcct/p:Id/p:IBAN", ns).text == "GB29NWBK60161331926819"
    again = must(api("POST", f"/payment-runs/{run['run_id']}/export", SYSTEM, {}))
    assert again["replay"] is True and again["sha256"] == f["sha256"] and again["content"] == f["content"]
    assert neighbours.objects.store[f["file_key"]].decode() == f["content"]
    got = must(api("GET", f"/payment-runs/{run['run_id']}", CLERK))
    assert got["status"] == "exported" and got["file_key"] == f["file_key"]


def test_pur29_only_an_approved_run_is_exported_and_only_the_system_exports():
    world()
    approved_bill()
    run = must(propose())
    assert "approved run" in refused(api("POST", f"/payment-runs/{run['run_id']}/export", SYSTEM, {}))
    assert api("POST", f"/payment-runs/{run['run_id']}/export", CLERK, {})[0] == 403


def test_pur29_confirming_what_the_bank_paid_allocates_posts_and_finishes_the_run(neighbours):
    world()
    run, f = exported_run()
    pid = run["payments"][0]["payment_id"]
    c = must(api("POST", f"/payments/{pid}/confirm", CLERK, {"bank_reference": "BNK-991", "paid_on": "2026-11-20"}))
    assert c["status"] == "confirmed" and c["replay"] is False
    again = must(api("POST", f"/payments/{pid}/confirm", CLERK, {"bank_reference": "BNK-991", "paid_on": "2026-11-20"}))
    assert again["replay"] is True
    bill_id = run["payments"][0]["bills"][0]["bill_id"]
    b = must(api("GET", f"/bills/{bill_id}", CLERK))
    assert str(b["outstanding"]) == "0.00"
    (j,) = neighbours.gl.posted(pid, "pay")
    lines = {("debit" if "debit" in x else "credit"): (x["account_id"], D(x.get("debit") or x.get("credit"))) for x in j["lines"]}
    assert lines["debit"] == ("2000", D("216.00")) and lines["credit"] == ("1200", D("216.00"))
    assert must(api("GET", f"/payment-runs/{run['run_id']}", CLERK))["status"] == "done"


def test_pur29_a_payment_cannot_be_dated_after_today_and_must_name_the_banks_reference():
    world()
    run, f = exported_run()
    pid = run["payments"][0]["payment_id"]
    assert "not_future" in refused(api("POST", f"/payments/{pid}/confirm", CLERK, {"bank_reference": "X", "paid_on": "2026-11-21"}))
    assert api("POST", f"/payments/{pid}/confirm", CLERK, {"bank_reference": "", "paid_on": "2026-11-20"})[0] == 422


def test_pur29_a_payment_the_bank_rejected_is_reversed_and_the_bill_is_open_again(neighbours):
    world()
    run, f = exported_run()
    pid = run["payments"][0]["payment_id"]
    bill_id = run["payments"][0]["bills"][0]["bill_id"]
    must(api("POST", f"/payments/{pid}/confirm", CLERK, {"bank_reference": "B1", "paid_on": "2026-11-20"}))
    rj = must(api("POST", f"/payments/{pid}/reject", CLERK, {"reason": "bank returned it: account closed"}))
    assert rj["status"] == "rejected" and rj["reversed"] is True
    assert str(must(api("GET", f"/bills/{bill_id}", CLERK))["outstanding"]) == "216.00"
    (j,) = neighbours.gl.posted(pid, "reverse")
    assert j["lines"][0].get("credit") == "216.00"
    assert must(api("POST", f"/payments/{pid}/reject", CLERK, {"reason": "again"}))["replay"] is True
    run2 = must(propose())
    assert len(run2["payments"]) == 1


def test_pur66_payments_confirms_from_the_statement_and_tax_reads_what_was_paid(neighbours):
    world()
    run, f = exported_run()
    pid = run["payments"][0]["payment_id"]
    pay_client = as_role("payments_client")
    cand = must(api("GET", "/supplier-payments", pay_client, {"bank_account_id": "BARC-01", "status": "sent"}))["payments"]
    assert [c["payment_id"] for c in cand] == [pid] and cand[0]["status"] == "sent" and cand[0]["bills"][0]["amount"] == "216.00"
    r = must(api("POST", f"/supplier-payments/{pid}/confirm", pay_client, {"bank_ref": "R-2026-44/118", "paid_on": "2026-11-20"}))
    assert r == {"payment_id": pid, "status": "confirmed", "replay": False}
    assert must(api("POST", f"/supplier-payments/{pid}/confirm", pay_client, {"bank_ref": "R", "paid_on": "2026-11-20"}))["replay"] is True
    assert len(neighbours.gl.posted(pid, "pay")) == 1
    tax_client = as_role("tax_client")
    paid = must(api("GET", "/supplier-payments", tax_client, {"from_": "2026-11-01", "to": "2026-11-30"}))["payments"]
    assert [p["payment_id"] for p in paid] == [pid] and paid[0]["paid_on"] == "2026-11-20" and paid[0]["status"] == "confirmed"
    assert "payment_open" in refused(api("POST", "/supplier-payments/run:nope/confirm", pay_client, {"bank_ref": "R", "paid_on": "2026-11-20"}))


def test_pur16_a_payment_outside_a_run_covering_several_bills_allocates_each_and_refuses_over_payment():
    world()
    _, b1 = approved_bill(number="I-1", note="D1")
    _, b2 = approved_bill(number="I-2", note="D2")
    _, b3 = approved_bill(number="I-3", note="D3")
    body = {"bp_id": SUPPLIER_A, "bank_account_id": "BARC-01", "bank_reference": "TRF-1", "paid_on": "2026-11-20", "amount": "500.00", "currency": "GBP"}
    assert "more than the payment" in refused(api("POST", "/payments", CLERK, {**body, "allocations": [{"bill_id": b1["bill_id"], "amount": "300.00"}, {"bill_id": b2["bill_id"], "amount": "300.00"}]}))
    assert "not_over_paid" in refused(api("POST", "/payments", CLERK, {**body, "amount": "1000.00", "allocations": [{"bill_id": b1["bill_id"], "amount": "216.01"}]}))
    p = must(api("POST", "/payments", CLERK, {**body, "allocations": [{"bill_id": b1["bill_id"], "amount": "216.00"}, {"bill_id": b2["bill_id"], "amount": "216.00"},
                                                                         {"bill_id": b3["bill_id"], "amount": "68.00"}]}))
    assert p["payment_id"] == "pay:BARC-01:TRF-1" and p["status"] == "paid"
    outs = [str(must(api("GET", f"/bills/{b['bill_id']}", CLERK))["outstanding"]) for b in (b1, b2, b3)]
    assert outs == ["0.00", "0.00", "148.00"]
    again = must(api("POST", "/payments", CLERK, {**body, "allocations": [{"bill_id": b1["bill_id"], "amount": "1.00"}]}))
    assert again["payment_id"] == p["payment_id"]
    assert str(must(api("GET", f"/bills/{b3['bill_id']}", CLERK))["outstanding"]) == "148.00"


def test_two_allocations_to_one_bill_at_once_cannot_over_pay_it():
    from erp_platform.repository import Conflict

    from brick_purchasing.infrastructure.tables import Claims
    world()
    _, b = approved_bill()
    from brick_purchasing.application import base
    with base.repository() as repo:
        e = repo.conn.engine
    a, c = e.connect(), e.connect()
    ta, tc = a.begin(), c.begin()
    n1, n2 = Claims(a).next_seq(f"alloc:{b['bill_id']}"), Claims(c).next_seq(f"alloc:{b['bill_id']}")
    assert n1 == n2
    Claims(a).claim(f"alloc:{b['bill_id']}", n1)
    ta.commit()
    with pytest.raises(Conflict):
        Claims(c).claim(f"alloc:{b['bill_id']}", n2)
    tc.rollback()


def test_pur30_a_bank_change_is_a_dated_row_that_needs_a_second_person_and_a_call_back():
    world()
    with on_day("2026-11-20T09:00:00+00:00"):
        assert "starts_after_today" in refused(api("POST", f"/suppliers/{SUPPLIER_A}/bank", CLERK, {"iban_or_account": "GB11NEW000000000001", "valid_from": "2026-11-20"}))
        prop = must(api("POST", f"/suppliers/{SUPPLIER_A}/bank", CLERK, {"iban_or_account": "GB11NEW000000000001", "valid_from": "2026-11-21", "country": "GB",
                                                                           "requested_by": "email"}))
        assert prop["status"] == "proposed" and prop["iban_or_account"] == "GB11NEW000000000001" and [s["signal"] for s in prop["signals"]] == ["requested_remotely"]
        clerk_approver = as_role("payables_clerk", "clerk@shop.example", extra=["purchasing:supplier_bank:approve"])
        clerk_other_sub = as_role("payables_clerk", "clerk@shop.example", sub="zzz", extra=["purchasing:supplier_bank:approve"])
        for who in (clerk_approver, clerk_other_sub):
            assert "second_person" in refused(api("POST", f"/suppliers/{SUPPLIER_A}/bank/2026-11-21/approve", who, {"call_back_note": "rang them"}))
        assert api("POST", f"/suppliers/{SUPPLIER_A}/bank/2026-11-21/approve", PAYAPPROVER, {"call_back_note": ""})[0] == 422
        ok = must(api("POST", f"/suppliers/{SUPPLIER_A}/bank/2026-11-21/approve", PAYAPPROVER, {"call_back_note": "rang the number we held, Jo confirmed"}))
        assert ok["status"] == "approved"
        approved_bill()
        run = must(propose())                     # a run proposed today uses today's details
        payee = [r for r in must(api("GET", f"/suppliers/{SUPPLIER_A}/bank", CLERK))["banks"]]
        assert any(b["valid_from"] == "2026-11-21" for b in payee)
        assert run["payments"][0]["payee_bank_valid_from"] == "2026-01-02"


def test_pur30_bank_details_are_masked_for_everyone_but_their_two_roles():
    world()
    rows = must(api("GET", f"/suppliers/{SUPPLIER_A}/bank", as_role("buyer")))["banks"]
    assert rows[0]["iban_or_account"].endswith("6819") and "GB29" not in rows[0]["iban_or_account"]
    rows = must(api("GET", f"/suppliers/{SUPPLIER_A}/bank", CLERK))["banks"]
    assert rows[0]["iban_or_account"] == "GB29NWBK60161331926819"
    from brick_purchasing.application import base
    with base.repository() as repo:
        stored = repo.banks.all(bp_id=SUPPLIER_A)[0]["iban_or_account"]
    assert "GB29" not in stored


def test_pur31_a_payables_approver_cannot_change_a_control_and_the_owner_is_dated():
    world()
    approver = PAYAPPROVER
    assert api("POST", "/settings/controls", approver, {"setting_key": "price_tolerance_pct", "value": "10", "valid_from": "2026-11-21"})[0] == 403
    why = refused(api("POST", "/settings", approver, {"setting_key": "price_tolerance_pct", "value": "10"}), 403)
    assert "control" in why
    must(api("POST", "/settings/controls", OWNER, {"setting_key": "price_tolerance_pct", "value": "2.5", "valid_from": "2026-11-20"}))     # the first row may start today
    assert "starts after today" in refused(api("POST", "/settings/controls", OWNER, {"setting_key": "price_tolerance_pct", "value": "3", "valid_from": "2026-11-20"}))
    must(api("POST", "/settings/controls", OWNER, {"setting_key": "price_tolerance_pct", "value": "3", "valid_from": "2026-12-01"}))
    assert api("POST", "/settings/controls", OWNER, {"setting_key": "price_tolerance_pct", "value": "101", "valid_from": "2026-12-02"})[0] == 422


def test_pur31_the_match_uses_the_tolerance_in_force_on_the_bills_date():
    world()
    supplier_price(product="LAPTOP", unit="C62", price="100.0000")
    o = confirmed(lines=[{"product_id": "LAPTOP", "quantity": "10"}])
    receive(o, note="D1")
    with on_day("2026-11-01T09:00:00+00:00"):
        must(api("POST", "/settings/controls", OWNER, {"setting_key": "price_tolerance_pct", "value": "5", "valid_from": "2026-11-01"}))
    s, early = bill(o, number="E", date_="2026-10-15", unit="C62", unit_price="104.0000")     # before the 5% row: the default 2% applies
    s, late = bill(o, number="L", date_="2026-11-19", unit="C62", unit_price="104.0000")
    assert early["match"]["lines"][0]["checks"][0]["passed"] is False and early["match"]["lines"][0]["checks"][0]["tolerance"]["upper_pct"] == "2"
    assert late["match"]["lines"][0]["checks"][0]["passed"] is True and late["match"]["lines"][0]["checks"][0]["tolerance"]["upper_pct"] == "5"
    s, wide = bill(o, number="W", date_="2026-11-19", unit="C62", unit_price="106.0000")
    assert wide["match"]["lines"][0]["checks"][0]["passed"] is False


def test_pur52_tolerance_keys_per_group_with_the_larger_of_percent_and_amount():
    world()
    supplier_price(product="LAPTOP", unit="C62", price="100.0000")
    o = confirmed(lines=[{"product_id": "LAPTOP", "quantity": "10"}])
    receive(o, note="D1")
    must(api("POST", "/tolerances", OWNER, {"key": "PP", "tolerance_group": "NEW", "upper_pct": "1", "upper_amount": "5.00", "valid_from": "2026-11-20"}))
    must(api("POST", f"/suppliers/{SUPPLIER_A}/profile", BUYER, {"tolerance_group": "NEW", "valid_from": "2026-11-20"}))
    s, b = bill(o, number="G", date_="2026-11-20", unit="C62", unit_price="101.2000")           # 12.00 on the line: over both 1% (10.00) and 5.00
    chk = b["match"]["lines"][0]["checks"][0]
    assert chk["passed"] is False and chk["key"] == "PP" and b["match"]["tolerance_group"] == "NEW"
    s, c = bill(o, number="H", date_="2026-11-20", unit="C62", unit_price="100.8000")           # 8.00: over the 5.00 amount, inside 1% (10.00): the LARGER applies
    assert c["match"]["lines"][0]["checks"][0]["passed"] is True
    assert api("POST", "/tolerances", PAYAPPROVER, {"key": "PP", "upper_pct": "9", "valid_from": "2026-12-01"})[0] == 403


def test_pur52_a_small_total_difference_within_bd_is_accepted_by_the_system_and_posted_to_small_differences(neighbours):
    world()
    supplier_price(product="LAPTOP", unit="C62", price="100.0000")
    o = confirmed(lines=[{"product_id": "LAPTOP", "quantity": "10"}])
    receive(o, note="D1")
    must(api("POST", "/tolerances", OWNER, {"key": "PP", "tolerance_group": "NEW", "upper_pct": "0.05", "valid_from": "2026-11-20"}))
    must(api("POST", "/tolerances", OWNER, {"key": "BD", "tolerance_group": "NEW", "upper_amount": "2.00", "valid_from": "2026-11-20"}))
    must(api("POST", f"/suppliers/{SUPPLIER_A}/profile", BUYER, {"tolerance_group": "NEW", "valid_from": "2026-11-20"}))
    s, b = bill(o, number="S", date_="2026-11-20", unit="C62", unit_price="100.1100")           # 1.10 over in total
    assert b["match"]["small_difference"] == "1.10" and b["match"]["approvable"] is True
    ap = must(api("POST", f"/bills/{b['bill_id']}/approve", PAYAPPROVER, {}))
    assert ap["acceptances"][0]["kind"] == "small_difference" and ap["acceptances"][0]["accepted_by_email"] == "system"
    (j,) = neighbours.gl.posted(b["bill_id"], "approve")
    assert any(x.get("purpose") == "small_difference" and x.get("debit") == "1.10" for x in j["lines"])


# ── scenarios ─────────────────────────────────────────────────────────────────────────────────────

def test_sc_pur01_request_to_paid_in_packs(neighbours):
    world()
    supplier_price()
    r = must(api("POST", "/purchase-requests", REQUESTER, {"cost_centre": "BAR", "reason": "restock", "lines": [{"product_id": "COLA-075", "quantity": "10", "unit_price": "18.0000"}]}))
    post(f"/purchase-requests/{r['purchase_request_id']}/submit", who=REQUESTER)
    must(api("POST", f"/purchase-requests/{r['purchase_request_id']}/approve", as_role("spend_approver", "approver@shop.example"), {}))
    o = must(api("POST", f"/purchase-requests/{r['purchase_request_id']}/raise-order", BUYER, {"bp_id": SUPPLIER_A}))
    o = must(api("POST", f"/purchase-orders/{o['purchase_order_id']}/confirm", BUYER, {}))
    must(api("POST", f"/purchase-orders/{o['purchase_order_id']}/send", BUYER, {"to": "sales@supplier.example"}))
    s, rc = receive(o, quantity="10", note="DN-1")
    assert s == 200 and len(neighbours.inventory.moves) == 1
    s, b = bill(o)
    assert b["match"]["passed"]
    must(api("POST", f"/bills/{b['bill_id']}/approve", PAYAPPROVER, {}))
    run = must(propose())
    must(api("POST", f"/payment-runs/{run['run_id']}/approve", PAYAPPROVER2, {}))
    must(api("POST", f"/payment-runs/{run['run_id']}/export", SYSTEM, {}))
    pid = run["payments"][0]["payment_id"]
    must(api("POST", f"/payments/{pid}/confirm", CLERK, {"bank_reference": "B1", "paid_on": "2026-11-20"}))
    (jb,) = neighbours.gl.posted(b["bill_id"], "approve")
    amounts = sorted((x.get("account_id"), x.get("debit") or x.get("credit")) for x in jb["lines"])
    assert amounts == [("1300", "36.00"), ("2000", "216.00"), ("5000", "180.00")]
    (jp,) = neighbours.gl.posted(pid, "pay")
    assert sorted((x["account_id"], x.get("debit") or x.get("credit")) for x in jp["lines"]) == [("1200", "216.00"), ("2000", "216.00")]
    assert str(must(api("GET", f"/bills/{b['bill_id']}", CLERK))["outstanding"]) == "0.00"


def test_sc_pur02_a_price_rise_caught_and_accepted():
    world()
    supplier_price(product="LAPTOP", unit="C62", price="900.0000")
    o = confirmed(lines=[{"product_id": "LAPTOP", "quantity": "10"}])
    receive(o, note="D1")
    s, b = bill(o, unit="C62", unit_price="950.0000")
    assert b["match"]["lines"][0]["checks"][0]["difference_pct"] == "5.56"
    clerk = as_role("payables_clerk", "clerk@shop.example", extra=["purchasing:bill:approve"])
    assert "second_person" in refused(api("POST", f"/bills/{b['bill_id']}/accept-variance", clerk, {"line_no": 1, "kind": "price", "reason": "fine"}))
    must(api("POST", f"/bills/{b['bill_id']}/accept-variance", PAYAPPROVER, {"line_no": 1, "kind": "price", "reason": "rise agreed"}))
    assert must(api("POST", f"/bills/{b['bill_id']}/approve", PAYAPPROVER, {}))["status"] == "approved"
    assert str(must(api("GET", f"/purchase-orders/{o['purchase_order_id']}", BUYER))["lines"][0]["unit_price"]) == "900.0000"


def test_sc_pur05_the_business_date_not_the_utc_date():
    supplier_price()
    with on_day("2026-07-31T23:30:00+00:00"):                     # 00:30 on 1 August in London (BST)
        world(start="2026-08-01", limits=False)
        o = confirmed(lines=[{"product_id": "COLA-075", "quantity": "10"}])
        assert receive(o, on="2026-08-01", note="D-AUG")[0] == 200                      # the delivery is today in London
        assert receive(o, on="2026-08-02", note="D-AUG2")[0] == 409
        set_limit("approver@shop.example", "10.00", start="2026-08-01")                     # the first row may start today
        assert "starts after today" in refused(api("POST", "/approval-limits", OWNER, {"approver_email": "approver@shop.example", "limit_amount": "9.00", "valid_from": "2026-08-01"}))
        assert api("POST", "/approval-limits", OWNER, {"approver_email": "approver@shop.example", "limit_amount": "9.00", "valid_from": "2026-08-02"})[0] == 200
        s, b = bill(o, date_="2026-08-01")
        assert s == 200
        pay = approved_after(b)
        assert "not_future" in refused(api("POST", f"/payments/{pay}/confirm", CLERK, {"bank_reference": "X", "paid_on": "2026-08-02"}))


def approved_after(b):
    must(api("POST", f"/bills/{b['bill_id']}/approve", PAYAPPROVER, {}))
    run = must(propose(pay_date="2026-09-05"))
    must(api("POST", f"/payment-runs/{run['run_id']}/approve", PAYAPPROVER2, {}))
    must(api("POST", f"/payment-runs/{run['run_id']}/export", SYSTEM, {}))
    return run["payments"][0]["payment_id"]


def test_the_zone_in_force_is_chosen_by_the_business_date_and_today_never_goes_backwards():
    world()
    must(api("POST", "/settings/controls", OWNER, {"setting_key": "business_timezone", "value": "Pacific/Pago_Pago", "valid_from": "2026-11-21"}))
    from brick_purchasing.application import base, requests
    def today_at(when):
        with on_day(when), base.repository() as repo:
            return requests._today(repo).isoformat()
    assert today_at("2026-11-20T23:00:00+00:00") == "2026-11-20"           # London, still the 20th
    assert today_at("2026-11-21T00:30:00+00:00") == "2026-11-21"           # the row takes over; Pago_Pago says the 20th but today holds at the 21st
    assert today_at("2026-11-21T20:00:00+00:00") == "2026-11-21"           # 09:00 on the 21st in Pago_Pago
    assert today_at("2026-11-22T12:00:00+00:00") == "2026-11-22"


def test_sc_pur06_return_and_supplier_credit(neighbours):
    world()
    o = None
    supplier_price()
    o = confirmed(lines=[{"product_id": "COLA-075", "quantity": "7"}])
    s, rc = receive(o, quantity="7", note="DN-881")
    s, b = bill(o, quantity="7")
    must(api("POST", f"/bills/{b['bill_id']}/approve", PAYAPPROVER, {}))
    must(api("POST", f"/receipts/{rc['receipt_id']}/returns", RECEIVER, {"lines": [{"line_no": 1, "quantity": "2"}], "reason": "damaged"}))
    assert len(neighbours.inventory.returns) == 1
    got = must(api("GET", f"/purchase-orders/{o['purchase_order_id']}", BUYER))
    assert str(got["lines"][0]["received"]).startswith("5")
    s, c = bill(o, number="CR-1", kind="credit", quantity="2", corrects_bill_id=b["bill_id"])
    must(api("POST", f"/bills/{c['bill_id']}/approve", PAYAPPROVER, {}))
    assert str(must(api("GET", f"/bills/{b['bill_id']}", CLERK))["outstanding"]) == str(D("151.20") - D("43.20"))
    assert must(api("GET", f"/bills/{b['bill_id']}", CLERK))["total"] == "151.20"


def test_sc_pur04_approving_under_a_second_identity_is_refused_everywhere_and_a_different_person_is_accepted():
    world()
    as_role("payables_clerk", "clerk@shop.example", sub="s-1", extra=["purchasing:payment:approve", "purchasing:supplier_bank:approve"])
    me_b = as_role("payables_clerk", "clerk@shop.example", sub="s-2", extra=["purchasing:payment:approve", "purchasing:supplier_bank:approve"])
    me_c = as_role("payables_clerk", "other@shop.example", sub="s-1", extra=["purchasing:payment:approve", "purchasing:supplier_bank:approve"])
    approved_bill(recorder=as_role("payables_clerk", "clerk@shop.example", sub="s-1"))
    run = must(propose(CLERK2))
    for who in (me_b, me_c):
        assert "second_person" in refused(api("POST", f"/payment-runs/{run['run_id']}/approve", who, {}))
    must(api("POST", f"/suppliers/{SUPPLIER_B}/bank", as_role("payables_clerk", "clerk@shop.example", sub="s-1"), {"iban_or_account": "GB99ZZZ0000000000001", "valid_from": "2026-11-21"}))
    for who in (me_b, me_c):
        assert "second_person" in refused(api("POST", f"/suppliers/{SUPPLIER_B}/bank/2026-11-21/approve", who, {"call_back_note": "ok"}))
    assert must(api("POST", f"/payment-runs/{run['run_id']}/approve", PAYAPPROVER, {}))["status"] == "approved"


def test_pur52_the_larger_of_percent_and_amount_is_the_allowance_whichever_it_is():
    from fractions import Fraction

    from brick_purchasing.domain.matching import Tol, allowance, lower_allowance
    t = Tol(None, "5.00", None, "0.1")                                   # 0.1% of 1000 = 1.00 < 5.00: the amount is larger
    assert allowance(t, Fraction(1000)) == 5
    assert allowance(Tol(None, "5.00", None, "2"), Fraction(1000)) == 20  # 2% = 20.00 > 5.00: the percent is larger
    assert allowance(Tol(None, "0.00", None, "2"), Fraction(1000)) == 20  # "2% or 0.00 per line, whichever is larger"
    assert allowance(Tol(), Fraction(1000)) == 0
    assert lower_allowance(Tol("3.00", None, "1", None), Fraction(1000)) == 10


def test_pur19_two_runs_proposed_at_once_put_a_bill_in_one_of_them(monkeypatch):
    """PUR-19: a bill already in an open run is left out. Two clerks proposing at the same moment both read the bill as free (the
    second's snapshot predates the first's commit); the run claims each bill it takes, so exactly one commits it and the other,
    re-run on what is there, has nothing due. Before the claim both runs held the bill, and the bank would pay it twice
    (found by the procure-to-pay e2e with real threads)."""
    from brick_purchasing.infrastructure import tables as t
    world()
    approved_bill()
    first = must(propose(who=CLERK))
    real, stale = t.RunRepository.open_items, {"n": 1}

    def snapshot(self):                    # the second proposal's FIRST read began before the first run committed: no open run
        if stale["n"]:
            stale["n"] -= 1
            return []
        return real(self)
    monkeypatch.setattr(t.RunRepository, "open_items", snapshot)
    assert "nothing falls due" in refused(propose(who=CLERK2))
    runs = must(api("GET", "/payment-runs", CLERK))["runs"]
    assert [r["run_id"] for r in runs if r["status"] != "cancelled"] == [first["run_id"]]