dodil/erp-brick-sales-order
DODIL ERP brick: sales order
tests/test_slice_a2.py
"""Slice A2 of sales-order v6: SO-27 (a price per 100 through amend and invoicing), SO-28 (delivery multiples, warn or block), SO-29 (order
types and item categories), SO-30 (condition pricing), SO-31 (header discount and freight spread to the lines), SO-32 (rebate agreements)."""
from decimal import Decimal as D
import pytest
from conftest import ADMIN, ADMIN2, BILLER, OWNER, REP, SYSTEM, api, confirm, deliver, hand, must, on_day, refused
from erp_platform.repository import CLAIM_TABLES
from brick_sales_order.application import use_cases
from brick_sales_order.domain.rebate import RebateAccrual
DAY1, DAY2, DAY3 = "2026-10-30T09:00:00+00:00", "2026-11-02T09:00:00+00:00", "2026-11-21T09:00:00+00:00"
COLA2 = [{"product_id": "COLA-075", "quantity": "120", "unit_code": "C62"}, {"product_id": "COLA-150", "quantity": "120", "unit_code": "C62"}]
def ln(o, n):
return next(x for x in o["lines"] if x["line_no"] == n)
def rows(o, n):
return {c["condition_type_code"] + ("*" if c["header_share_of"] else ""): D(c["amount"]) for c in ln(o, n)["conditions"]}
def record(**kw):
return must(api("POST", "/condition-records", ADMIN, kw))
def pricing_setup():
"""SO-30's agreements, set on 2026-10-30 (a record starts AFTER the day it is set)."""
with on_day(DAY1):
must(api("POST", "/customer-order-policies", ADMIN, {"bp_id": 7311, "valid_from": "2026-10-31", "price_group": "TRADE"}))
record(condition_type_code="DISC_PCT", key_fields="customer + product", bp_id=7311, product_id="COLA-075", rate="5", valid_from="2026-11-01")
record(condition_type_code="DISC_PCT", key_fields="customer + category", bp_id=7311, category_code="drinks", rate="3", valid_from="2026-10-31")
record(condition_type_code="DISC_PCT", key_fields="price_group + category", price_group="TRADE", category_code="drinks", rate="2",
scale=[{"from_quantity": "100", "rate": "4"}], valid_from="2026-10-31")
record(condition_type_code="SURCH_PCT", key_fields="category", category_code="drinks", rate="2", valid_from="2026-10-31")
record(condition_type_code="FREIGHT", key_fields="ship_to_country", ship_to_country="GB", rate="0.12", minimum="15.00", valid_from="2026-10-31")
# ── SO-27 ─────────────────────────────────────────────────────────────────────────────────────
def test_SO_27_a_price_per_100_nets_rounded_once_survives_an_amendment_and_reaches_invoicing(neighbours):
must(api("POST", "/condition-records", ADMIN, {"condition_type_code": "DISC_PCT", "key_fields": "customer + product", "bp_id": 7311,
"product_id": "SCREW-M4", "rate": "10", "valid_from": "2026-11-21"}))
with on_day(DAY3):
o = hand(REP, lines=[{"product_id": "SCREW-M4", "quantity": "250", "unit_code": "C62"}])
o = must(api("POST", f"/sales-orders/{o['sales_order_id']}/lines", REP, {"line": {"product_id": "SCREW-M4", "quantity": "3", "unit_code": "BX500"}}))
one, two = ln(o, 1), ln(o, 2)
assert (D(one["unit_price"]), one["pricing_quantity"], one["pricing_unit_code"]) == (D("2.45"), 100, "C62")
assert D(one["net_amount"]) == D("5.51") # 250 x 2.4500 / 100 = 6.125, less 10% = 5.5125, rounded once
assert rows(o, 1) == {"BASE": D("6.13"), "DISC_PCT": D("-0.62")} # the rows add up to the net exactly: nothing is divided down
assert two["unit_code"] == "BX500" and two["pricing_unit_code"] == "C62" and D(two["quantity_priced"]) == D("1500")
assert rows(o, 2)["BASE"] == D("36.75") and D(two["net_amount"]) == D("33.08")
sid = o["sales_order_id"]
must(api("POST", f"/sales-orders/{sid}/confirm", REP, {}))
must(api("POST", f"/sales-orders/{sid}/amend", REP, {"reason": "more", "changes": [{"line_no": 1, "quantity": "400"}]}))
o = must(api("POST", f"/sales-orders/{sid}/revisions/2/approve", ADMIN, {}))
assert rows(o, 1) == {"BASE": D("9.80"), "DISC_PCT": D("-0.98")} and D(ln(o, 1)["net_amount"]) == D("8.82")
assert D(ln(o, 2)["net_amount"]) == D("33.08") and {c["revision"] for c in ln(o, 2)["conditions"]} == {0} # only line 1 was re-priced
must(api("POST", f"/sales-orders/{sid}/request-invoice", BILLER, {}))
sent = {x["so_line_no"] if "so_line_no" in x else x["line_no"]: x for x in neighbours.invoicing.drafts[f"so:{sid}:1"]["lines"]}
assert sent[1]["pricing_quantity"] == 100 and sent[1]["pricing_unit_code"] == "C62" and D(sent[1]["billed_net"]) == D("8.82")
assert sent[2]["pricing_quantity"] == 100 and D(sent[2]["billed_net"]) == D("33.08") # invoicing nets what the order nets
assert D(sent[1]["quantity_priced"]) == D("400") and D(sent[2]["quantity_priced"]) == D("1500") # v8: the quantity in the pricing unit, exact
# ── SO-28 ─────────────────────────────────────────────────────────────────────────────────────
REASON = "COLA-075 is delivered in multiples of 6 C62"
def test_SO_28_warn_confirms_and_records_the_reason_block_refuses_and_the_amendment_is_checked_on_its_date():
first = hand(REP, lines=[{"product_id": "COLA-075", "quantity": "20", "unit_code": "C62"}])
gate = next(g for g in must(api("GET", f"/sales-orders/{first['sales_order_id']}/readiness", REP))["readiness"]["confirm"] if g["gate"] == "delivery_multiple")
assert gate == {"gate": "delivery_multiple", "met": True, "reason": REASON, "fix": "change to 18 or 24, or confirm as it is"}
done = confirm(first)
assert done["lifecycle"] == "confirmed" and done["warnings"] == [gate] # shown and recorded on the order
must(api("POST", "/controls/delivery_multiple_mode", OWNER, {"value": "block", "valid_from": "2026-11-21"}))
second = hand(REP, lines=[{"product_id": "COLA-075", "quantity": "20", "unit_code": "C62"}])
third = hand(REP, lines=[{"product_id": "COLA-075", "quantity": "2", "unit_code": "PK24"}])
quoted = must(api("POST", "/sales-orders/from-quote", SYSTEM, {
"quote_id": "Q-9", "quote_version": 1, "bp_id": 7311, "currency": "GBP", "owner": "sales_rep@shop.example",
"lines": [{"line_no": 1, "product_id": "COLA-075", "quantity": "20", "list_price": "1.20", "unit_price": "1.20", "unit_code": "C62",
"priced_from": {"price_list_id": "LIST-GBP", "rule_seq": 1}}]}))
wid = hand(REP, lines=[{"product_id": "WIDGET", "quantity": "7", "unit_code": "C62"}])
with on_day(DAY3):
for o in (second, quoted): # block, hand order and quote order alike
said = refused(api("POST", f"/sales-orders/{o['sales_order_id']}/confirm", REP, {}))
assert "delivery_multiple" in said and REASON in said
assert confirm(third)["lifecycle"] == "confirmed" # 2 PK24 = 48 C62
assert confirm(wid)["lifecycle"] == "confirmed" # no delivery rule: always passes
sid = first["sales_order_id"]
said = refused(api("POST", f"/sales-orders/{sid}/amend", REP, {"reason": "r", "changes": [{"line_no": 1, "quantity": "22"}]}))
assert "delivery_multiple" in said and "COLA-075 is delivered in multiples of 6 C62" in said
must(api("POST", f"/sales-orders/{sid}/amend", REP, {"reason": "r", "changes": [{"line_no": 1, "quantity": "24"}]}))
def test_SO_28_a_product_that_does_not_answer_is_not_a_pass(neighbours, monkeypatch):
from brick_sales_order.infrastructure.seams import Missing
o = hand(REP, lines=[{"product_id": "WIDGET", "quantity": "7", "unit_code": "C62"}])
def down(*a, **k):
raise Missing("product", "product did not answer", 503)
monkeypatch.setattr(neighbours.product, "rounding", down)
said = refused(api("POST", f"/sales-orders/{o['sales_order_id']}/confirm", REP, {}))
assert "delivery_multiple" in said and "did not answer" in said
# ── SO-29 ─────────────────────────────────────────────────────────────────────────────────────
def test_SO_29_item_categories_follow_the_type_and_the_product_and_a_line_keeps_the_one_it_was_given():
o = hand(REP) # COLA-075 and SETUP
assert [ln(o, 1)["item_category_code"], ln(o, 2)["item_category_code"]] == ["STD_STOCK", "STD_SERVICE"]
drop = hand(REP, lines=[{"product_id": "COLA-075", "quantity": "5", "usage": "drop_ship"}])
assert ln(drop, 1)["item_category_code"] == "THIRD_PARTY"
free = hand(REP, order_type_code="FREE_OF_CHARGE")
assert [ln(free, 1)["item_category_code"], ln(free, 2)["item_category_code"]] == ["FREE", "FREE"]
sid = o["sales_order_id"]
must(api("POST", "/item-category-rules", ADMIN, {"order_type_code": "STANDARD", "product_type_code": "TRADING", "usage": "normal",
"item_category_code": "THIRD_PARTY", "valid_from": "2026-11-21"}))
assert ln(hand(REP), 1)["item_category_code"] == "STD_STOCK" # a rule is a dated row: from tomorrow
done = confirm(o)
with on_day(DAY3):
assert ln(hand(REP), 1)["item_category_code"] == "THIRD_PARTY"
assert ln(must(api("GET", f"/sales-orders/{sid}", REP)), 1)["item_category_code"] == "STD_STOCK" and done["lifecycle"] == "confirmed"
assert api("POST", "/item-category-rules", ADMIN, {"order_type_code": "STANDARD", "product_type_code": "TRADING", "usage": "normal",
"item_category_code": "FREE", "valid_from": "2026-11-20"})[0] == 422 # not today
assert api("POST", "/item-category-rules", ADMIN, {"order_type_code": "STANDARD", "product_type_code": "TRADING", "usage": "normal",
"item_category_code": "FREE", "valid_from": "2026-11-21"})[0] == 409 # a correction is a new row
def test_SO_29_a_return_names_what_it_returns_and_every_type_numbers_from_its_own_series_with_no_gap():
said = refused(api("POST", "/sales-orders", REP, {"bp_id": 7311, "currency": "GBP", "order_type_code": "RETURNS",
"lines": [{"product_id": "COLA-075", "quantity": "1"}]}))
assert "reference_required" in said and "a return names the order line it returns" in said
assert must(api("GET", "/sales-orders", ADMIN))["orders"] == [] # refused before anything is made
standard = confirm(hand(REP)) # SO-2026-0001: 2 PK24 of COLA-075, delivered and invoiced
must(deliver(standard, 1, 2, "DN-1"))
ref = {"ref_sales_order_id": standard["sales_order_id"], "ref_line_no": 1, "ref_delivery_ref": "DN-1", "ref_invoice_id": "INV-1", "ref_invoice_line_no": 1}
bad = hand(REP, bp_id=7312, order_type_code="RETURNS", lines=[{"product_id": "COLA-075", "quantity": "1"}], **ref, return_reason="damaged")
assert "customer_active" in refused(api("POST", f"/sales-orders/{bad['sales_order_id']}/confirm", REP, {})) # a refused confirmation claims nothing
must(api("POST", f"/sales-orders/{bad['sales_order_id']}/cancel-draft", REP, {})) # and cancelling the draft releases its returned quantity
ret = confirm(hand(REP, order_type_code="RETURNS", lines=[{"product_id": "COLA-075", "quantity": "1"}], **ref, return_reason="damaged"))
ret2 = confirm(hand(REP, order_type_code="RETURNS", lines=[{"product_id": "COLA-075", "quantity": "1"}], **ref, return_reason="damaged"))
assert (standard["order_no"], ret["order_no"], ret2["order_no"]) == ("SO-2026-0001", "RE-2026-0001", "RE-2026-0002")
assert "so_order_no_claim" in CLAIM_TABLES and {"so_condition_record", "so_order_type", "so_item_category_rule"} <= CLAIM_TABLES
def test_SO_29_an_orders_type_is_fixed_once_confirmed_and_a_type_row_is_dated():
draft = hand(REP)
changed = must(api("POST", f"/sales-orders/{draft['sales_order_id']}/order-type", REP, {"order_type_code": "FREE_OF_CHARGE"}))
assert changed["order_type_code"] == "FREE_OF_CHARGE" and ln(changed, 1)["item_category_code"] == "FREE"
must(api("POST", f"/sales-orders/{draft['sales_order_id']}/order-type", REP, {"order_type_code": "STANDARD"}))
done = confirm(must(api("GET", f"/sales-orders/{draft['sales_order_id']}", REP)))
said = refused(api("POST", f"/sales-orders/{done['sales_order_id']}/order-type", REP, {"order_type_code": "RUSH"}))
assert "an order's type is fixed once confirmed: cancel it and raise a new one" in said
assert api("POST", "/order-types", ADMIN, {"order_type_code": "RUSH", "valid_from": "2026-11-20", "default_block": "APPROVAL"})[0] == 422
must(api("POST", "/order-types", ADMIN, {"order_type_code": "RUSH", "valid_from": "2026-11-21", "default_block": "APPROVAL"}))
rush = next(t for t in must(api("GET", "/pricing-setup", REP))["order_types"] if t["order_type_code"] == "RUSH")
assert rush["default_block"] == "none" and rush["series_id"] == "RU" # today's row until tomorrow
assert api("POST", "/order-types", REP, {"order_type_code": "RUSH", "valid_from": "2026-11-21"})[0] == 403
# ── SO-30 ─────────────────────────────────────────────────────────────────────────────────────
def test_SO_30_the_procedure_prices_each_line_from_the_first_record_found(neighbours):
# the item conditions alone: the ship-to's country (copied from business-partner, v7) has no FREIGHT record, so SO-31's
# header condition does not ride on these lines (SO-31 ships the same order to GB)
neighbours.partners.countries[7311] = "IE"
pricing_setup()
with on_day(DAY2):
o = hand(REP, lines=COLA2)
sid = o["sales_order_id"]
assert api("POST", f"/sales-orders/{sid}/lines/2/conditions", REP, {"condition_type_code": "DISC_PCT", "rate": "12"})[0] == 409
said = refused(api("POST", f"/sales-orders/{sid}/lines/2/conditions", REP, {"condition_type_code": "DISC_PCT", "rate": "12"}))
assert "manual_within_limit" in said and "DISC_PCT may be changed by hand up to 10%" in said
o = confirm(must(api("GET", f"/sales-orders/{sid}", REP)))
assert rows(o, 1) == {"BASE": D("144.00"), "DISC_PCT": D("-7.20"), "SURCH_PCT": D("2.74")} and D(ln(o, 1)["net_amount"]) == D("139.54")
assert rows(o, 2) == {"BASE": D("216.00"), "DISC_PCT": D("-6.48"), "SURCH_PCT": D("4.19")} and D(ln(o, 2)["net_amount"]) == D("213.71")
by = {c["condition_type_code"]: c for c in ln(o, 1)["conditions"]}
assert by["DISC_PCT"]["source"] == "record DISC_PCT customer + product 7311|COLA-075 from 2026-11-01" # the first hit stops the search
assert D(by["SURCH_PCT"]["basis"]) == D("136.80")
assert {c["condition_type_code"]: c["source"] for c in ln(o, 2)["conditions"]}["DISC_PCT"].startswith("record DISC_PCT customer + category")
assert D(o["subtotal"]) == D("353.25")
assert "frozen" in refused(api("POST", f"/sales-orders/{sid}/price", REP, {})) # confirming froze every condition
must(api("POST", f"/sales-orders/{sid}/amend", REP, {"reason": "more", "changes": [{"line_no": 2, "quantity": "130"}]}))
o = must(api("POST", f"/sales-orders/{sid}/revisions/2/approve", ADMIN, {}))
assert D(ln(o, 2)["net_amount"]) == D("231.52") and {c["revision"] for c in ln(o, 1)["conditions"]} == {0} # only the changed line is re-priced
assert D(ln(o, 1)["net_amount"]) == D("139.54")
def test_SO_30_a_typed_discount_within_the_limit_is_recorded_as_manual_with_who_and_survives_a_new_line():
pricing_setup()
with on_day(DAY2):
o = hand(REP, lines=COLA2)
sid = o["sales_order_id"]
o = must(api("POST", f"/sales-orders/{sid}/lines/2/conditions", REP, {"condition_type_code": "DISC_PCT", "rate": "8"}))
disc = next(c for c in ln(o, 2)["conditions"] if c["condition_type_code"] == "DISC_PCT")
assert disc["source"] == "manual:sales_rep@shop.example" and D(disc["amount"]) == D("-17.28")
o = must(api("POST", f"/sales-orders/{sid}/lines", REP, {"line": {"product_id": "SETUP", "quantity": "1", "unit_code": "HUR"}}))
assert rows(o, 2) == {"BASE": D("216.00"), "DISC_PCT": D("-17.28"), "SURCH_PCT": D("3.97")} # 8% by hand, kept; 202.6944 rounded once
assert api("POST", f"/sales-orders/{sid}/lines/1/conditions", NOBODY_REP(), {"condition_type_code": "DISC_PCT", "rate": "1"})[0] == 404
def NOBODY_REP():
from conftest import REP2
return REP2 # another rep's order is a 404
def test_SO_30_a_record_never_starts_today_and_an_amount_beyond_its_bound_is_a_422_with_the_reason():
r = {"condition_type_code": "SURCH_PCT", "key_fields": "category", "category_code": "x", "rate": "1"}
assert api("POST", "/condition-records", ADMIN, {**r, "valid_from": "2026-11-20"})[0] == 422
assert api("POST", "/condition-records", ADMIN, {**r, "valid_from": "2026-11-21", "rate": "101"})[0] == 422
assert api("POST", "/condition-records", ADMIN, {**r, "valid_from": "2026-11-21", "key_fields": "customer + product"})[0] == 422
assert api("POST", "/condition-records", REP, {**r, "valid_from": "2026-11-21"})[0] == 403
must(api("POST", "/condition-records", ADMIN, {**r, "valid_from": "2026-11-21"}))
assert "already has a row" in refused(api("POST", "/condition-records", ADMIN, {**r, "valid_from": "2026-11-21"})) # never an edit
must(api("POST", "/condition-records", ADMIN, {"condition_type_code": "FREIGHT", "key_fields": "ship_to_country", "ship_to_country": "XX",
"rate": "99999999999999", "valid_from": "2026-11-21"}))
with on_day(DAY3):
said = refused(api("POST", "/sales-orders", REP, {"bp_id": 7311, "currency": "GBP", "ship_to_country": "XX", "lines": COLA2}), 422)
assert "beyond what an order holds" in said
# ── SO-31 ─────────────────────────────────────────────────────────────────────────────────────
def freight_order():
return hand(REP, bp_id=7311, lines=COLA2, ship_to_bp_id=7313, ship_to_country="GB")
def test_SO_31_a_header_discount_and_freight_are_spread_so_the_lines_sum_exactly_and_invoicing_bills_them(neighbours):
pricing_setup()
with on_day(DAY2):
o = freight_order()
sid = o["sales_order_id"]
o = must(api("POST", f"/sales-orders/{sid}/header-conditions", REP, {"condition_type_code": "HDR_DISC", "rate": "2"}))
assert {h["condition_type_code"]: D(h["amount"]) for h in o["header_conditions"]} == {"HDR_DISC": D("-7.07"), "FREIGHT": D("34.42")}
assert rows(o, 1)["HDR_DISC*"] == D("-2.79") and rows(o, 2)["HDR_DISC*"] == D("-4.28") # by net, the remainder on the largest
assert rows(o, 1)["FREIGHT*"] == D("11.38") and rows(o, 2)["FREIGHT*"] == D("23.04") # by gross weight, the remainder on the heaviest
assert (D(ln(o, 1)["net_amount"]), D(ln(o, 2)["net_amount"]), D(o["subtotal"])) == (D("148.13"), D("232.47"), D("380.60"))
assert (ln(o, 1)["gross_weight"], ln(o, 1)["weight_unit_code"], ln(o, 1)["measure_valid_from"]) == ("0.79", "KGM", "2026-10-01")
assert ln(o, 2)["gross_weight"] == "1.6"
assert D(ln(o, 1)["tax_amount"]) == D("29.63") and D(ln(o, 2)["tax_amount"]) == D("46.49") # taxed on the net that is billed
confirm(o)
must(api("POST", f"/sales-orders/{sid}/request-invoice", BILLER, {}))
sent = neighbours.invoicing.drafts[f"so:{sid}:1"]["lines"]
assert [D(x["billed_net"]) for x in sent] == [D("148.13"), D("232.47")] and sum(D(x["net_amount"]) for x in sent) == D("380.60")
def test_SO_31_an_unknown_weight_leaves_freight_uncomputable_and_confirmation_is_refused():
pricing_setup()
with on_day(DAY2):
o = hand(REP, lines=[*COLA2, {"product_id": "SETUP", "quantity": "1", "unit_code": "HUR"}], ship_to_country="GB")
assert o["freight_unknown"] == "3" and not o["header_conditions"]
said = refused(api("POST", f"/sales-orders/{o['sales_order_id']}/confirm", REP, {}))
assert "freight_computable" in said and "record the unit's weight in Products" in said
must(api("DELETE", f"/sales-orders/{o['sales_order_id']}/lines/3", REP))
o = must(api("POST", f"/sales-orders/{o['sales_order_id']}/price", REP, {}))
assert o["freight_unknown"] == "" and confirm(o)["lifecycle"] == "confirmed"
with on_day(DAY2):
assert api("POST", "/sales-orders", REP, {"bp_id": 7311, "currency": "GBP", "lines": COLA2, "ship_to_country": "GBR"})[0] == 422
def test_SO_31_a_header_discount_above_its_limit_is_refused():
o = hand(REP, lines=COLA2)
said = refused(api("POST", f"/sales-orders/{o['sales_order_id']}/header-conditions", REP, {"condition_type_code": "HDR_DISC", "rate": "11"}))
assert "manual_within_limit" in said and "up to 10%" in said
# ── SO-32 ─────────────────────────────────────────────────────────────────────────────────────
AGREEMENT = {"bp_id": 7311, "basis": "categories", "basis_values": ["drinks"], "valid_from": "2027-01-01", "valid_to": "2027-12-31", "accrual_rate_pct": "2",
"currency": "GBP", "scale": [{"from_value": "0", "rate_pct": "1"}, {"from_value": "50000.00", "rate_pct": "2"}, {"from_value": "100000", "rate_pct": "3"}]}
def test_SO_32_an_agreement_is_opened_activated_from_a_later_day_and_its_scale_is_locked_by_the_first_accrual():
with on_day("2026-12-10T09:00:00+00:00"):
a = must(api("POST", "/rebate-agreements", ADMIN, AGREEMENT))
assert a["rebate_agreement_id"] == "RB-2027-7311" and a["lifecycle"] == "draft" and a["basis_values"] == ["drinks"]
aid = a["rebate_agreement_id"]
assert api("POST", f"/rebate-agreements/{aid}/activate", REP, {})[0] == 403
assert must(api("POST", f"/rebate-agreements/{aid}/activate", ADMIN, {}))["lifecycle"] == "active"
assert must(api("POST", f"/rebate-agreements/{aid}/activate", ADMIN, {}))["lifecycle"] == "active" # a repeat changes nothing
back = must(api("POST", "/rebate-agreements", ADMIN, {**AGREEMENT, "valid_from": "2026-12-01", "valid_to": "2026-12-31", "rebate_agreement_id": "RB-2026-7311"}))
said = refused(api("POST", f"/rebate-agreements/{back['rebate_agreement_id']}/activate", ADMIN, {}))
assert "starts_after_today" in said and "open a new agreement from a later date" in said
assert must(api("POST", f"/rebate-agreements/{back['rebate_agreement_id']}/cancel", ADMIN, {}))["lifecycle"] == "cancelled"
again = must(api("POST", f"/rebate-agreements/{aid}/scale", ADMIN, {"scale": [{"from_value": "0", "rate_pct": "1"}, {"from_value": "40000", "rate_pct": "2"}]}))
assert [t["from_value"] for t in again["scale"]] == ["0.00", "40000.00"] # nothing accrued yet: still editable
with use_cases.repository() as repo: # SO-33 will write this; the guard reads it
found, version = repo.rebates.get(aid)
found.accruals.append(RebateAccrual(aid, "INV-1", 1, "invoice", None, D("1000.00"), D("20.00")))
repo.rebates.save(found, version)
said = refused(api("POST", f"/rebate-agreements/{aid}/scale", ADMIN, {"scale": AGREEMENT["scale"]}))
assert "end it and open a new agreement from a later date" in said
got = must(api("GET", f"/rebate-agreements/{aid}", ADMIN))
assert got["accruals"] == 1 and got["lifecycle"] == "active" and [t["rate_pct"] for t in got["scale"]] == ["1", "2"]
def test_SO_32_the_agreement_names_one_customer_or_a_price_group_and_a_valid_scale_and_stays_in_bounds():
assert "customer or a price group" in refused(api("POST", "/rebate-agreements", ADMIN, {**AGREEMENT, "price_group": "TRADE"}))
assert "customer or a price group" in refused(api("POST", "/rebate-agreements", ADMIN, {k: v for k, v in AGREEMENT.items() if k != "bp_id"}))
assert api("POST", "/rebate-agreements", ADMIN, {**AGREEMENT, "basis": "all"})[0] == 409 # all names no products
assert api("POST", "/rebate-agreements", ADMIN, {**AGREEMENT, "valid_to": "2026-12-31"})[0] == 409
assert api("POST", "/rebate-agreements", ADMIN, {**AGREEMENT, "scale": [{"from_value": "0", "rate_pct": "101"}]})[0] == 422
assert "an amount is below" in refused(api("POST", "/rebate-agreements", ADMIN, {**AGREEMENT, "scale": [{"from_value": "10000000000000000", "rate_pct": "1"}]}), 422)
odd = must(api("POST", "/rebate-agreements", ADMIN, {**AGREEMENT, "price_group": "TRADE", "bp_id": None, "scale": [{"from_value": "5", "rate_pct": "2"}, {"from_value": "5", "rate_pct": "3"}]}))
said = refused(api("POST", f"/rebate-agreements/{odd['rebate_agreement_id']}/activate", ADMIN, {}))
assert "scale_valid" in said and "tiers ascending" in said
def test_SO_32_two_writers_of_one_agreement_meet_on_its_version_and_one_agreement_id_is_opened_once():
a = must(api("POST", "/rebate-agreements", ADMIN, AGREEMENT))
assert "already exists" in refused(api("POST", "/rebate-agreements", ADMIN, AGREEMENT))
aid = a["rebate_agreement_id"]
must(api("POST", f"/rebate-agreements/{aid}/scale", ADMIN, {"scale": AGREEMENT["scale"], "version": a["version"]}))
assert "changed since you opened it" in refused(api("POST", f"/rebate-agreements/{aid}/activate", ADMIN2, {"version": a["version"]}))
assert api("GET", "/rebate-agreements/RB-9999-1", ADMIN)[0] == 404
assert [x["rebate_agreement_id"] for x in must(api("GET", "/rebate-agreements", ADMIN, {"bp_id": 7311}))["rebate_agreements"]] == [aid]
@pytest.mark.parametrize("field", ["valid_from", "basis"])
def test_SO_32_a_missing_field_is_a_422_not_a_500(field):
body = {k: v for k, v in AGREEMENT.items() if k != field}
assert api("POST", "/rebate-agreements", ADMIN, body)[0] == 422
def test_SO_31_the_remainder_lands_on_the_largest_line_and_the_shares_add_up_to_the_amount_exactly():
from fractions import Fraction as F
from brick_sales_order.domain import pricing as pr
shares = pr.spread(D("0.10"), {1: F(1), 2: F(1), 3: F(1), 4: F(3)})
assert shares == {1: D("0.02"), 2: D("0.02"), 3: D("0.02"), 4: D("0.04")} and sum(shares.values()) == D("0.10")
tie = pr.spread(D("1.00"), {1: F(1), 2: F(1), 3: F(1)})
assert tie == {1: D("0.34"), 2: D("0.33"), 3: D("0.33")} # a tie: the lowest line number takes the remainder