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%