dodil/erp-brick-ar

DODIL ERP brick: accounts receivable

tests/test_statement_budget.py

"""Statement budgets: every statement is a round trip (100-300 ms on the live pg wire), so a request's statements must not grow with the number of
unrelated documents. Counted at the engine, one request at a time (erp_platform.testing.count_statements)."""
import pytest
from conftest import BILLER, SYSTEM, account_map, api, billing, hand_draft, issue, line, must, on_day, raise_so
from erp_platform.testing import count_statements


@pytest.fixture
def counted(engine):
    return lambda: count_statements(engine)


def _show(label, c):
    print(f"BUDGET {label}: statements={c.statements} round_trips={c.round_trips}")


def _more_invoices(n):
    for _ in range(n):
        issue(hand_draft()["invoice_id"])


def test_issue_does_not_grow_with_the_number_of_other_invoices(world, counted):
    billing()
    _more_invoices(2)
    d = hand_draft()
    with counted() as few:
        must(api("POST", f"/invoices/{d['invoice_id']}/issue", BILLER, {}))
    _more_invoices(8)
    d = hand_draft()
    with counted() as many:
        must(api("POST", f"/invoices/{d['invoice_id']}/issue", BILLER, {}))
    _show("issue (2 other invoices)", few)
    _show("issue (10 other invoices)", many)
    assert many.statements <= few.statements + 2, (few.statements, many.statements)
    assert few.statements <= 30, few                    # was 36


def test_the_invoice_list_does_not_grow_with_the_number_of_invoices(world, counted):
    billing()
    _more_invoices(2)
    with counted() as few:
        must(api("GET", "/invoices", BILLER, {"lifecycle": "issued"}))
    _more_invoices(8)
    with counted() as many:
        must(api("GET", "/invoices", BILLER, {"lifecycle": "issued"}))
    _show("list (2 issued)", few)
    _show("list (10 issued)", many)
    assert many.statements <= few.statements + 10, (few.statements, many.statements)


def test_the_monthly_export_loads_its_documents_in_one_round_trip_per_table(world, counted):
    world.remove("gl")

    def month(n):
        with on_day("2026-10-10"):
            billing()
            for _ in range(n):
                issue(hand_draft()["invoice_id"])
        with counted() as c:
            must(api("POST", "/exports", BILLER, {"period": "2026-10"}))
        return c

    few = month(2)
    with on_day("2026-10-10"):
        for _ in range(8):
            issue(hand_draft()["invoice_id"])
    many = month(0)          # the first export took the first two; this one exports the next eight
    _show("export (2 invoices)", few)
    _show("export (8 more invoices)", many)
    per_extra = (many.statements - few.statements) / 6          # eight documents against two: only the rows that CHANGE are written (the mark)
    assert few.statements <= 20 and many.statements <= 38, (few.statements, many.statements)       # was 38 and 128
    assert per_extra <= 3.2, (few.statements, many.statements)



def test_the_sale_steps_of_invoicing_are_flat_in_lines_and_in_other_invoices(world, counted):
    """request_invoice's raise, issue_invoice and the ledger pass (post-pending, one document): 2 vs 20 lines, 2 vs 50 other invoices."""
    billing()
    account_map()
    seq = iter(range(10_000))

    def sale(n):
        k = next(seq)
        lines = [line(f"line {i}", "1", "10.00", sales_order_id=f"SO-{k}", so_line_no=i) for i in range(1, n + 1)]
        with counted() as raised:
            inv = must(raise_so(f"so:SO-{k}:1", lines=lines))
        with counted() as issued_:
            issue(inv["invoice_id"], world=world)
        with counted() as posted:
            must(api("POST", "/documents/post-pending", SYSTEM, {}))
        return raised.statements, issued_.statements, posted.statements
    sale(1)
    two, twenty = sale(2), sale(20)
    for _ in range(48):
        sale(1)
    later = sale(2)
    print("BUDGET raise, issue, post-pending: 2 lines", two, "20 lines", twenty, "2 lines after 50 invoices", later)
    assert two == twenty == later, (two, twenty, later)
    # was (20, 29, 29) at 2 lines, post-pending 60 at 20 lines and 649 after 50 invoices: the pass loaded every issued invoice ever to find
    # it posted; settings, profiles, terms and the account map re-read per helper; a new invoice's keys re-read before its first save
    assert two[0] <= 13 and two[1] <= 26 and two[2] <= 13, two