174 lines
7.3 KiB
Python
174 lines
7.3 KiB
Python
"""COSMOS display bands vs the engine's pricing triggers — two things, one definition each.
|
|
|
|
They are NOT the same thing and the difference is deliberate:
|
|
|
|
* `theme.COVER_BANDS` — COSMOS Inventory Planning's own Alpha/Beta shading. Pink at 70 days
|
|
is a REPLENISHMENT warning: don't reorder yet.
|
|
* `low/high_cover_days` in pricing_rules.yaml — where this engine spends margin, raising 5%
|
|
to slow a burn or cutting 5% to clear stock.
|
|
|
|
Measured on 47 SKUs of the covered line: moving the triggers to the band edges (40/70) would
|
|
put six more SKUs on a discount, all of them in the 70-90 day window. So the bands match COSMOS
|
|
for display parity, the triggers stay put, and the UI says which is which.
|
|
|
|
What these tests protect is that each has exactly ONE definition, and that a change to either
|
|
is visible rather than silent.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from config.settings import get_rules
|
|
from dashboard import theme
|
|
from dashboard.live_data import HIGH_COVER_DAYS, LOW_COVER_DAYS
|
|
|
|
|
|
# ------------------------------------------------------- one definition each
|
|
def test_the_engine_reads_its_thresholds_from_config_not_from_code():
|
|
r = get_rules()
|
|
assert (LOW_COVER_DAYS, HIGH_COVER_DAYS) == (r.low_cover_days, r.high_cover_days)
|
|
|
|
|
|
def test_the_thresholds_are_still_the_backtested_ones():
|
|
"""35/90 is what the impact table in pricing_rules.yaml was measured against.
|
|
|
|
Changing them is allowed — it is a config line — but it invalidates that table, so this
|
|
fails loudly rather than letting the documented evidence quietly describe another policy.
|
|
"""
|
|
assert (LOW_COVER_DAYS, HIGH_COVER_DAYS) == (35, 90), (
|
|
"cover triggers changed — re-measure the impact table in pricing_rules.yaml")
|
|
|
|
|
|
def test_bands_match_the_cosmos_legend_exactly():
|
|
"""Alpha 0-20-40-70-100, Beta 0-15-30-60-90 — straight off the INVP legend."""
|
|
assert [b[0] for b in theme.COVER_BANDS["alpha"]] == [20, 40, 70, 100, None]
|
|
assert [b[0] for b in theme.COVER_BANDS["beta"]] == [15, 30, 60, 90, None]
|
|
|
|
|
|
@pytest.mark.parametrize("days,word", [
|
|
(0, "very low"), (19, "very low"), (20, "low"), (39, "low"),
|
|
(40, "healthy"), (69, "healthy"), (70, "high"), (99, "high"),
|
|
(100, "excess"), (5000, "excess"),
|
|
])
|
|
def test_alpha_band_boundaries(days, word):
|
|
assert theme.cover_band(days, "alpha")[1] == word
|
|
|
|
|
|
@pytest.mark.parametrize("days,word", [
|
|
(14, "very low"), (15, "low"), (29, "low"), (30, "healthy"),
|
|
(59, "healthy"), (60, "high"), (89, "high"), (90, "excess"),
|
|
])
|
|
def test_beta_bands_are_tighter_than_alpha(days, word):
|
|
assert theme.cover_band(days, "beta")[1] == word
|
|
|
|
|
|
def test_an_unknown_cover_gets_no_colour_verdict():
|
|
"""A missing number must not be shaded as though we had judged it."""
|
|
colour, word = theme.cover_band(None)
|
|
assert word == "unknown" and colour == theme.BORDER
|
|
|
|
|
|
def test_an_unknown_class_falls_back_to_alpha_rather_than_raising():
|
|
assert theme.cover_band(50, "gamma") == theme.cover_band(50, "alpha")
|
|
|
|
|
|
# ------------------------------------------------- matching COSMOS's own figure
|
|
def _cover(cosmos, onhand):
|
|
"""The reconciliation `build_live_sku` performs, in one line."""
|
|
return int(cosmos) if cosmos else onhand
|
|
|
|
|
|
def test_cover_matches_cosmos_when_cosmos_has_an_answer():
|
|
"""The dashboard must not quote a different number from the INVP grid for one SKU.
|
|
|
|
Real values for UBMICROFIBERDUVETTWINWHITE: 3,999 on hand + 1,030 inbound = 5,029, over a
|
|
64/day 7-day velocity = 78 days. On-hand only is 63.
|
|
"""
|
|
assert _cover(78, 63) == 78
|
|
|
|
|
|
def test_a_zero_from_cosmos_with_stock_on_hand_falls_back():
|
|
"""`coverDays: 0` on a stocked SKU is a missing answer, not a full shelf.
|
|
|
|
UBMICROFIBERBS4PCFULLGREY: 167 units, 334 real days of cover, COSMOS returned 0. Taken
|
|
literally it satisfies neither inventory rule and silences both.
|
|
"""
|
|
assert _cover(0, 334) == 334
|
|
assert _cover(None, 334) == 334
|
|
|
|
|
|
def test_a_genuine_empty_shelf_stays_zero():
|
|
"""No stock and no velocity must not be inflated into cover by the fallback."""
|
|
assert _cover(0, 0) == 0
|
|
|
|
|
|
def test_matching_cosmos_can_mask_a_thin_shelf_and_that_is_recorded():
|
|
"""The accepted trade-off, pinned so it is a decision rather than a surprise.
|
|
|
|
UBMICROFIBERBS4PCKINGWHITE holds 12 days on the shelf and 84 counting inbound, so matching
|
|
COSMOS stops it triggering LOW_STOCK. If the shipment slips, nothing protects it.
|
|
"""
|
|
onhand, cosmos = 12, 84
|
|
assert 0 < onhand <= LOW_COVER_DAYS # would have raised on shelf stock alone
|
|
matched = _cover(cosmos, onhand)
|
|
assert not (0 < matched <= LOW_COVER_DAYS) # ...and does not, once inbound counts
|
|
|
|
|
|
# ------------------------------------------- the gap between band and trigger
|
|
def test_there_is_a_window_where_the_band_warns_and_the_engine_does_not_act():
|
|
"""The 70-90 day window on Alpha. Six real SKUs sit in it.
|
|
|
|
This is the state that looked like a bug on screen, so it is pinned: the tile must be able
|
|
to detect it in order to explain it.
|
|
"""
|
|
band_warns = theme.cover_band(82, "alpha")[1] == "high"
|
|
engine_acts = 0 < 82 <= LOW_COVER_DAYS or 82 >= HIGH_COVER_DAYS
|
|
assert band_warns and not engine_acts
|
|
|
|
|
|
@pytest.mark.parametrize("days", [12, 30, 95, 140])
|
|
def test_where_the_engine_acts_the_band_is_never_green(days):
|
|
"""The colour may be more cautious than the engine; it must never be LESS.
|
|
|
|
A green tile beside a SKU the engine is discounting would be the genuinely dangerous
|
|
direction of disagreement.
|
|
"""
|
|
acts = 0 < days <= LOW_COVER_DAYS or days >= HIGH_COVER_DAYS
|
|
assert acts
|
|
assert theme.cover_band(days, "alpha")[1] != "healthy"
|
|
|
|
|
|
def test_every_acting_cover_is_outside_the_green_band_for_alpha():
|
|
"""Swept, not sampled — no Alpha cover may read green while a price move fires.
|
|
|
|
Alpha is the only class in use: `inv_class` is hardcoded to "alpha" for every SKU in
|
|
`live_data.build_live_sku`, so this is the invariant that actually holds today.
|
|
"""
|
|
for days in range(0, 400):
|
|
if 0 < days <= LOW_COVER_DAYS or days >= HIGH_COVER_DAYS:
|
|
assert theme.cover_band(days, "alpha")[1] != "healthy", days
|
|
|
|
|
|
def test_beta_would_break_that_invariant_if_it_were_ever_populated():
|
|
"""A latent conflict, recorded rather than hidden.
|
|
|
|
Beta's healthy band starts at 30 days, but the raise trigger is a single global 35 — so a
|
|
Beta SKU at 30-35 days would be repriced while its own tile showed green. Nothing does
|
|
that today because `inv_class` is hardcoded to "alpha"; the moment COSMOS's real
|
|
classification is plumbed through, the cover triggers have to become per-class and Beta's
|
|
raise threshold must drop below 30.
|
|
|
|
This test passes on the CURRENT behaviour and is written to fail the day Beta appears with
|
|
the global thresholds still in force.
|
|
"""
|
|
conflict = [d for d in range(0, 400)
|
|
if (0 < d <= LOW_COVER_DAYS or d >= HIGH_COVER_DAYS)
|
|
and theme.cover_band(d, "beta")[1] == "healthy"]
|
|
assert conflict == list(range(30, LOW_COVER_DAYS + 1)), conflict
|
|
# The guard that keeps it harmless: no SKU is Beta.
|
|
from dashboard import live_data
|
|
import inspect
|
|
src = inspect.getsource(live_data.build_live_sku)
|
|
assert 'inv_class="alpha"' in src, (
|
|
"a SKU can now be Beta — make the cover triggers per-class before shipping this")
|