Supplier price breaks
Price breaks (volume discounts) make the unit price depend on the quantity ordered. This creates incentives that are not visible in a flat unit price: the total cost can jump or even drop at a breakpoint, depending on how the supplier applies the discount. Envision models those incentives explicitly with zedfuncs so the economics can be reasoned about, not guessed.
Table of contents
Merchant vs fiscal price breaks
Two interpretations exist:
- Merchant: once a breakpoint is reached, the lower unit price applies to all units in the order. This is easy to compute but can make a larger order cheaper than a smaller one.
- Fiscal: the lower price applies only to units beyond the breakpoint. This is monotonic but harder to compute by hand.
The difference shows up in the marginal price curve, which is exactly what
pricebrk.m() and pricebrk.f() return.
table Items[id] = with
[| as id, as StartPrice |]
[| "spoon", 10 |]
table Breaks = with
[| as Id, as Quantity, as Price |]
[| "spoon", 5, 9 |]
[| "spoon", 10, 8 |]
expect Breaks.id = Breaks.Id
Items.ZM = pricebrk.m(Items.StartPrice, Breaks.Quantity, Breaks.Price)
Items.ZF = pricebrk.f(Items.StartPrice, Breaks.Quantity, Breaks.Price)
table Q = extend.range(12 into Items)
Q.UnitM = valueAt(Items.ZM, Q.N)
Q.UnitF = valueAt(Items.ZF, Q.N)
Q.TotalM = int(Items.ZM, 1, Q.N)
Q.TotalF = int(Items.ZF, 1, Q.N)
show table "Breakpoints compared" with
Q.N as "Qty"
Q.UnitM
Q.UnitF
Q.TotalM
Q.TotalF
Example output:
| Qty | UnitM | UnitF | TotalM | TotalF |
|---|---|---|---|---|
| 1 | 10 | 10 | 10 | 10 |
| 2 | 10 | 10 | 20 | 20 |
| 3 | 10 | 10 | 30 | 30 |
| 4 | 10 | 10 | 40 | 40 |
| 5 | 5 | 9 | 45 | 49 |
| 6 | 9 | 9 | 54 | 58 |
| 7 | 9 | 9 | 63 | 67 |
| 8 | 9 | 9 | 72 | 76 |
| 9 | 9 | 9 | 81 | 85 |
| 10 | -1 | 8 | 80 | 93 |
| 11 | 8 | 8 | 88 | 101 |
| 12 | 8 | 8 | 96 | 109 |
The merchant curve shows negative or very low marginal prices at the breakpoint: those units compensate for the retroactive discount on earlier units. This is why buying 10 units can be cheaper than buying 9.
Ordering space vs stocking space
Price breaks are defined in the ordering space: the index is “units ordered in this purchase.” Inventory optimization is often in stocking space: the index is “total stock position after the purchase.” Shifting the zedfunc aligns the two.
table Items[id] = with
[| as id, as StartPrice, as StockOnHand |]
[| "spoon", 10, 6 |]
table Breaks = with
[| as Id, as Quantity, as Price |]
[| "spoon", 5, 9 |]
[| "spoon", 10, 8 |]
expect Breaks.id = Breaks.Id
Items.ZM = pricebrk.m(Items.StartPrice, Breaks.Quantity, Breaks.Price)
Items.ZX = shift(Items.ZM, Items.StockOnHand)
table Pos = extend.range(12 into Items)
Pos.OrderingMarginal = valueAt(Items.ZM, Pos.N)
Pos.StockingMarginal = valueAt(Items.ZX, Pos.N)
show table "Ordering vs stocking" with
Pos.N as "StockPos"
Pos.OrderingMarginal
Pos.StockingMarginal
Example output:
| StockPos | OrderingMarginal | StockingMarginal |
|---|---|---|
| 1 | 10 | 0 |
| 2 | 10 | 0 |
| 3 | 10 | 0 |
| 4 | 10 | 0 |
| 5 | 5 | 0 |
| 6 | 9 | 0 |
| 7 | 9 | 10 |
| 8 | 9 | 10 |
| 9 | 9 | 10 |
| 10 | -1 | 10 |
| 11 | 8 | 5 |
| 12 | 8 | 9 |
The stocking curve is zero for already-held units, then follows the ordering curve once new units start to be purchased. This makes price breaks compatible with reward functions that operate on stock positions.
Implications for economic optimization
Price breaks can invert the usual “one more unit costs more” intuition. The merchant curve can make the marginal cost of a breakpoint unit negative, while the fiscal curve keeps marginal costs non-negative. This is why price breaks should be modeled as zedfuncs and composed with reward or cost curves rather than flattened into a single unit price.