dodil/erp-brick-product

tests/test_batch_price.py

"""PRD-41 (product v7, DRAFT): POST /prices/batch — many lines in, the one-line routes' answers out, in one read."""
from conftest import CATALOGUE, DEFAULT, SELLING, api, mk_list, mk_product, must, row
from erp_platform.testing import count_statements


def _setup():
    mk_product("HOST-M")
    mk_product("HOST-L", name="Large hosting, per month")
    mk_list("LIST-GBP", rows=[row("HOST-M", "2026-11-01", "120.00"), row("HOST-L", "2026-11-01", "300.00")], default_from=DEFAULT)
    must(api("POST", "/products/HOST-L/set-rounding", CATALOGUE, {"role": "delivery", "rounding_multiple": "6", "unit_code": "MON"}))


ON = "2027-01-04"                                   # after the rounding rule's start (a rule starts tomorrow at the earliest)


def _batch(lines, who=SELLING, **kw):
    return api("POST", "/prices/batch", who, {"currency": "GBP", "bp_id": "7", "priced_on": ON, "lines": lines, **kw})


def test_prd_41_each_line_is_what_the_one_line_routes_answer_and_a_refusal_stays_on_its_line(engine):
    _setup()
    status, got = _batch([{"line_no": 1, "product_id": "HOST-M", "quantity": "3"},
                          {"line_no": 2, "product_id": "HOST-L", "quantity": "4", "rounding_role": "delivery", "convert": True},
                          {"line_no": 3, "product_id": "NOPE", "quantity": "1"}])
    assert status == 200, got
    one, two, three = got["lines"]
    on = got["priced_on"]
    assert [x["line_no"] for x in got["lines"]] == [1, 2, 3]
    assert one["price"] == must(api("GET", "/products/HOST-M/price", SELLING, {"currency": "GBP", "bp_id": "7", "quantity": "3", "priced_on": on}))
    assert one["rounding"] is None and one["conversion"] is None
    assert two["price"] == must(api("GET", "/products/HOST-L/price", SELLING, {"currency": "GBP", "bp_id": "7", "quantity": "4", "priced_on": on}))
    assert two["rounding"] == must(api("GET", "/products/HOST-L/rounding", SELLING,
                                       {"role": "delivery", "quantity": "4", "unit_code": "MON", "on": on}))
    assert two["rounding"]["met"] is False and two["rounding"]["next_up"] == "6"
    assert two["conversion"] == must(api("GET", "/products/HOST-L/convert", SELLING, {"quantity": "4", "from_unit": "MON", "to_unit": "MON"}))
    assert three["price"] == {"status": 404, "reason": "no product NOPE"}


def test_prd_41_the_whole_batch_is_refused_only_for_its_own_shape(engine):
    _setup()
    assert _batch([])[0] == 422
    assert _batch([{"line_no": i, "product_id": "HOST-M", "quantity": "1"} for i in range(201)])[0] == 422
    assert _batch([{"line_no": 1, "product_id": "HOST-M", "quantity": "1"}], currency="GB1")[0] == 422


def test_prd_41_a_part_the_caller_may_not_read_answers_403(engine):
    """The rounding and conversion parts need product:product:read beside the route's product:price_list:read."""
    from conftest import person
    _setup()
    lister = person("lister@shop.example", ["product:price_list:read"])
    got = must(_batch([{"line_no": 1, "product_id": "HOST-L", "quantity": "6", "rounding_role": "delivery"}], who=lister))
    assert got["lines"][0]["rounding"]["status"] == 403 and got["lines"][0]["price"]["unit_price"]


def test_prd_41_reads_do_not_grow_with_the_lines(engine):
    _setup()

    def n(k):
        lines = [{"line_no": i, "product_id": ("HOST-M", "HOST-L")[i % 2], "quantity": "6", "rounding_role": "delivery"} for i in range(k)]
        with count_statements(engine) as c:
            must(_batch(lines))
        return c.statements
    two, twenty = n(2), n(20)
    print("BUDGET product batch", two, twenty)
    assert two == twenty, (two, twenty)
    assert two <= 20, two                                     # measured 18 (2026-10-25) + 10%