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