dodil/erp-brick-sales-order

DODIL ERP brick: sales order

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 orders or lines. Counted at the engine, one request at a time."""
from datetime import date

import pytest
from conftest import BILLER, REP, api, as_role, confirm, hand, must
from erp_platform.testing import count_statements
from test_slice_b import DAY1, DAY2, SYSTEM, D, on_day, partner_rows, warehouse_set

COLA = {"product_id": "COLA-075", "quantity": "6", "unit_code": "C62"}


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


def _orders(n):
    return [confirm(hand(REP, lines=[COLA])) for _ in range(n)]


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


def test_reschedule_statements_do_not_grow_with_the_number_of_open_orders(neighbours, inventory, counted):
    warehouse_set("WH1")
    _orders(3)
    with counted() as few:
        must(api("POST", "/sales-orders/reschedule", SYSTEM, {}))
    _orders(10)
    with counted() as many:
        must(api("POST", "/sales-orders/reschedule", SYSTEM, {"trigger": "cancelled", "cancelled_sales_order_id": "other"}))
    _show("reschedule 3 unchanged", few)
    _show("reschedule 13 unchanged", many)
    assert few.statements <= 24 and few.round_trips <= 24, few          # was 65 for 3 orders: thirteen reads per order; 22 now
    assert many.statements <= few.statements + 2, (few.statements, many.statements)     # was 245 for 13


def test_reschedule_that_changes_twelve_orders_stays_within_its_budget(neighbours, inventory, counted):
    neighbours.partners.customers[7312] = True
    partner_rows()
    warehouse_set()
    inventory.supply["JUICE-075"] = [(date(2026, 11, 10), D("720"), None)]
    with on_day(DAY1):
        ids = [confirm(hand(REP, lines=[{"product_id": "JUICE-075", "quantity": "10", "unit_code": "C62"}], requested_delivery_on="2026-11-04"))["sales_order_id"]
               for _ in range(12)]
    inventory.supply["JUICE-075"] = [(date(2026, 11, 17), D("720"), None)]      # the purchase order slips a week: every promise moves later
    with on_day(DAY2), counted() as c:
        res = must(api("POST", "/sales-orders/reschedule", SYSTEM, {}))
    _show("reschedule 12 changed", c)
    assert sorted(res["re_promised"]) == sorted(ids)
    assert c.statements <= 330 and c.round_trips <= 330, c          # 706 before the snapshot and load_many: 27 an order, not 59


def test_cancel_with_many_open_orders_stays_within_its_budget(neighbours, inventory, counted, monkeypatch):
    from brick_sales_order.application import use_cases
    kicked = []
    monkeypatch.setattr(use_cases, "after_response", lambda fn, *a, conn=None, **kw: kicked.append((fn, a)))      # hold the post-response job: the response itself is what is counted
    warehouse_set("WH1")
    _orders(10)
    o = confirm(hand(REP, lines=[COLA]))
    with counted() as c:
        must(api("POST", f"/sales-orders/{o['sales_order_id']}/cancel", as_role("order_admin"), {"reason": "withdrawn"}))
    _show("cancel (request)", c)
    assert c.statements <= 20 and c.round_trips <= 20, c               # was 45: the re-promise never runs inside the cancel
    assert len(kicked) == 1 and not [x for x in inventory.calls if x[0] == "reserve" and x[1] != o["sales_order_id"] and x[2] > 1]     # not run inside the cancel
    with counted() as run:
        kicked[0][0](*kicked[0][1])                                  # the after-response job: the freed stock is re-promised without any explicit reschedule call
    _show("cancel (after-response job)", run)
    assert run.statements <= 26 and run.round_trips <= 26, run          # was 40
    with use_cases.repository() as repo:
        assert repo.postings.pending_repromises() == []             # and it settled the durable row


def test_cancel_runs_the_re_promise_after_the_response_through_the_platform(neighbours, inventory):
    warehouse_set("WH1")
    o = confirm(hand(REP, lines=[COLA]))
    must(api("POST", f"/sales-orders/{o['sales_order_id']}/cancel", as_role("order_admin"), {"reason": "withdrawn"}))
    from brick_sales_order.application import use_cases
    with use_cases.repository() as repo:
        assert repo.postings.pending_repromises() == []


def test_confirm_and_request_invoice_stay_within_their_budgets(neighbours, inventory, counted):
    o = hand(REP, lines=[COLA])
    with counted() as c:
        o = must(api("POST", f"/sales-orders/{o['sales_order_id']}/confirm", REP, {}))
    _show("confirm", c)
    assert c.statements <= 32 and c.round_trips <= 32, c            # was 55 (44 measured)
    with counted() as c:
        must(api("POST", f"/sales-orders/{o['sales_order_id']}/request-invoice", BILLER, {}))
    _show("request-invoice", c)
    assert c.statements <= 41 and c.round_trips <= 41, c            # was 80 (71 measured)


def test_get_order_statements(neighbours, inventory, counted):
    o = confirm(hand(REP, lines=[COLA]))
    with counted() as c:
        must(api("GET", f"/sales-orders/{o['sales_order_id']}", REP))
    _show("get order", c)
    assert c.statements <= 14 and c.round_trips <= 14, c


def test_the_sale_steps_of_an_order_are_flat_in_lines_and_in_other_orders(neighbours, inventory, counted):
    """SO's own statements per sale step (the neighbours are fakes here; l2c-e2e counts theirs): draft_by_hand, confirm, inventory's two reads
    (GET the order, its fulfilment), the delivery inventory records, request_invoice - for 2 vs 20 lines, and 2 lines after 50 other orders."""
    from conftest import INVENTORY

    def sale(n):
        lines = [COLA for _ in range(n)]
        with counted() as draft:
            o = hand(REP, lines=lines)
        sid = o["sales_order_id"]
        with counted() as conf:
            confirm(o)
        with counted() as read:
            must(api("GET", f"/sales-orders/{sid}", INVENTORY))
        with counted() as ful:
            must(api("GET", f"/sales-orders/{sid}/fulfilment", INVENTORY))
        with counted() as dlv:
            must(api("POST", f"/sales-orders/{sid}/deliveries", INVENTORY, {"line_no": 1, "quantity": "6", "unit_code": "C62",
                                                                              "delivery_ref": f"inv:{sid}", "source": "inventory"}))
        with counted() as inv:
            must(api("POST", f"/sales-orders/{sid}/request-invoice", BILLER, {}))
        return tuple(c.statements for c in (draft, conf, read, ful, dlv, inv))
    sale(1)
    two, twenty = sale(2), sale(20)
    for _ in range(48):
        sale(1)
    later = sale(2)
    print("BUDGET draft, confirm, get, fulfilment, delivery, request-invoice: 2 lines", two, "20 lines", twenty, "2 lines after 50 orders", later)
    assert two == twenty == later, (two, twenty, later)
    # was (29, 30, 13, 17, 22, 39): a new order re-read its 13 tables' keys before its first save, the number series its key; confirm asked the
    # partner register once per partner function (sold-to, ship-to, bill-to and payer are often one partner). The order's 13-table load is ONE
    # round trip on the pg wire (pipelined); request-invoice loads it twice by design (claim, ask invoicing, mark: three transactions)
    assert all(n <= cap for n, cap in zip(two, (16, 29, 13, 17, 22, 39))), two