AI-Pricing-Agent/tests/test_reproject_inventory.py

127 lines
5.3 KiB
Python

"""Re-projecting the shelf at a different price.
The Inventory tab shows COSMOS's forecast at TODAY's price, so a recommendation to raise or
cut is otherwise displayed with no stock consequence at all. On a SKU near a stockout that is
the difference between a sound price move and an expensive one.
The invariants that make this safe to show:
* a RAISE leaves more stock, a CUT leaves less -- the sign must never invert;
* week 0 is today's shelf and is untouched by price;
* arrivals are carried through exactly (a placed PO does not move because we re-priced);
* stock never goes negative;
* both cover columns use OUR velocity, so their difference is the price effect alone;
* bad inputs return [] rather than a fabricated table.
"""
from __future__ import annotations
import pytest
from dashboard.live_data import reproject_inventory
def proj(units, arrivals=None, per_unit=4.45, cover=None):
"""A COSMOS-shaped weekly projection."""
arrivals = arrivals or [0] * len(units)
return [{
"date": f"2026-08-{i + 1:02d}",
"inventory": u,
"inventory_value": u * per_unit,
"cover_days": (cover[i] if cover else None),
"warehouse_arrival": arrivals[i],
} for i, u in enumerate(units)]
# A shelf drawing down 100 units a week from 1,000, on 10 units/day.
STEADY = proj([1000, 930, 860, 790, 720])
def test_a_raise_leaves_more_stock_on_the_shelf():
rows = reproject_inventory(STEADY, units_day_now=10.0, units_day_new=8.0)
assert rows[-1]["units_new"] > rows[-1]["units_now"]
assert rows[-1]["delta_units"] > 0
# ...and monotonically so: every week after the first is at least as good.
assert all(r["delta_units"] >= 0 for r in rows)
def test_a_cut_leaves_less_stock_on_the_shelf():
rows = reproject_inventory(STEADY, units_day_now=10.0, units_day_new=12.0)
assert rows[-1]["units_new"] < rows[-1]["units_now"]
assert all(r["delta_units"] <= 0 for r in rows)
def test_week_zero_is_todays_shelf_and_price_cannot_change_it():
"""We have not sold anything yet at the new price."""
for new in (5.0, 10.0, 25.0):
rows = reproject_inventory(STEADY, 10.0, new)
assert rows[0]["units_new"] == rows[0]["units_now"] == 1000
def test_no_change_in_rate_reproduces_cosmos_exactly():
"""The identity case. If this drifts, every other number here is suspect."""
rows = reproject_inventory(STEADY, 10.0, 10.0)
assert [r["units_new"] for r in rows] == [1000, 930, 860, 790, 720]
assert all(r["delta_units"] == 0 for r in rows)
def test_arrivals_are_carried_through_untouched():
"""A purchase order already placed does not move because we re-priced."""
p = proj([1000, 950, 1400, 1350], arrivals=[0, 0, 500, 0])
rows = reproject_inventory(p, 10.0, 7.0)
assert [r["arrival"] for r in rows] == [0, 0, 500, 0]
# The arrival still lands in full even though the draw around it shrank.
assert rows[2]["units_new"] > rows[1]["units_new"]
def test_stock_is_floored_at_zero_and_the_week_is_flagged():
"""A negative shelf is not a forecast."""
p = proj([300, 200, 100, 0])
rows = reproject_inventory(p, 10.0, 40.0) # 4x the burn
assert all(r["units_new"] >= 0 for r in rows)
assert any(r["stockout"] for r in rows)
def test_a_flat_cosmos_projection_has_no_draw_to_rescale():
"""Real case: COSMOS returns the same 167 units for all 11 weeks on a slow SKU.
There is no depletion to scale, so units cannot move at any price. The UI says so rather
than showing an unexplained column of zeros.
"""
rows = reproject_inventory(proj([167] * 5), 0.5, 2.0)
assert all(r["delta_units"] == 0 for r in rows)
# Cover still responds, because it is computed from our velocity.
assert rows[0]["cover_new"] < rows[0]["cover_now"]
def test_both_cover_columns_use_our_velocity_not_cosmos_own():
"""Mixing bases made a 5% RAISE look like it SHRANK cover, 78 -> 76.
COSMOS's `coverDays` divides by its own 7-day average; the new column divides by the
modelled rate. Side by side that difference is the divisor, not the price.
"""
p = proj([1000, 930], cover=[78, 72]) # COSMOS's own figures
rows = reproject_inventory(p, 10.0, 8.0) # a raise: cover must STRETCH
assert rows[0]["cover_now"] == 100 # 1000 / 10, ours
assert rows[0]["cover_new"] == 125 # 1000 / 8, ours
assert rows[0]["cover_new"] > rows[0]["cover_now"]
# COSMOS's figure is kept alongside, never mixed into the comparison.
assert rows[0]["cover_cosmos"] == 78
@pytest.mark.parametrize("projections,now,new", [
([], 10.0, 8.0), # no projection from COSMOS
(STEADY, 0.0, 8.0), # no current velocity -> ratio undefined
(STEADY, 10.0, 0.0), # no projected velocity
(STEADY, -1.0, 8.0), # nonsense input
])
def test_unusable_inputs_return_nothing_rather_than_a_guess(projections, now, new):
assert reproject_inventory(projections, now, new) == []
def test_value_stays_on_cosmos_cost_basis():
"""Scaled with units at COSMOS's own $/unit, not re-derived from our COGS."""
rows = reproject_inventory(proj([1000, 900], per_unit=4.45), 10.0, 5.0)
assert rows[0]["value_new"] == pytest.approx(4450, abs=1)
# week 1 keeps half the draw, so more units and proportionally more value
assert rows[1]["value_new"] > 900 * 4.45