91 lines
3.0 KiB
Python
91 lines
3.0 KiB
Python
"""Product-line discovery and ranking (pure, no network)."""
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
from dashboard.line import LINE_SORTS, rank
|
||
from pricing_agent.cosmos.models import LineProduct
|
||
|
||
|
||
def _make(sku, inv=0.0, avg30=0.0, cover=None, minpo=0.0) -> LineProduct:
|
||
p = LineProduct.model_validate({
|
||
"sku": sku, "inventory": inv, "averageSale30Days": avg30,
|
||
"minPoQuantity": minpo})
|
||
p.cover_days = cover
|
||
return p
|
||
|
||
|
||
# ---------------------------------------------------------------- size parsing
|
||
@pytest.mark.parametrize("sku,expected", [
|
||
("UBMICROFIBERDUVETQUEENWHITE", "Queen"),
|
||
("UBMICROFIBERDUVETKINGWHITE2", "King"),
|
||
("UBMICROFIBERDUVETTWINGREY", "Twin"),
|
||
("UBMICROFIBERDUVETFULLNAVY", "Full"),
|
||
("UBMICROFIBERDUVETCALKINGWHITE", "Calking"),
|
||
("UBSOMETHINGELSE", "Other"),
|
||
])
|
||
def test_size_is_read_from_the_sku(sku, expected):
|
||
assert _make(sku).size == expected
|
||
|
||
|
||
def test_calking_is_not_mistaken_for_king():
|
||
"""CALKING contains KING, so the order tokens are matched in matters."""
|
||
assert _make("UBMICROFIBERDUVETCALKINGGREY").size == "Calking"
|
||
|
||
|
||
def test_line_product_maps_the_cosmos_aliases():
|
||
p = LineProduct.model_validate({
|
||
"sku": "UBMICROFIBERDUVETQUEENWHITE", "asin": "B00NWPUKMS",
|
||
"inventory": 6623, "averageSale30Days": 188, "averageSale6Months": 193,
|
||
"minPoQuantity": 11280, "parentAsin": "B0DMGS61XY"})
|
||
assert (p.inventory, p.avg_30d, p.avg_6m, p.min_po_quantity) == (
|
||
6623, 188, 193, 11280)
|
||
assert p.asin == "B00NWPUKMS" and p.parent_asin == "B0DMGS61XY"
|
||
|
||
|
||
# ---------------------------------------------------------------- ranking
|
||
ROWS = [
|
||
{"sku": "B_MID", "inventory": 500.0, "avg_30d": 20.0, "cover_days": 90,
|
||
"min_po_quantity": 0.0},
|
||
{"sku": "A_TOP", "inventory": 100.0, "avg_30d": 50.0, "cover_days": 5,
|
||
"min_po_quantity": 900.0},
|
||
{"sku": "C_DEAD", "inventory": 0.0, "avg_30d": 0.0, "cover_days": None,
|
||
"min_po_quantity": 0.0},
|
||
]
|
||
|
||
|
||
def _order(label):
|
||
return [r["sku"] for r in rank(ROWS, label)]
|
||
|
||
|
||
def test_rank_by_velocity_puts_the_biggest_seller_first():
|
||
assert _order("Sales velocity (30d)")[0] == "A_TOP"
|
||
|
||
|
||
def test_rank_by_inventory_puts_the_deepest_stock_first():
|
||
assert _order("Inventory on hand")[0] == "B_MID"
|
||
|
||
|
||
def test_rank_by_cover_puts_the_most_urgent_first():
|
||
assert _order("Cover days — lowest first")[0] == "A_TOP"
|
||
|
||
|
||
def test_missing_values_sort_last_not_first():
|
||
"""A SKU with no cover reading must not be presented as the most urgent."""
|
||
assert _order("Cover days — lowest first")[-1] == "C_DEAD"
|
||
|
||
|
||
def test_alphabetical_sorts_text_without_blowing_up():
|
||
"""A purely numeric sort key compares str against int and raises here."""
|
||
assert _order("A–Z") == ["A_TOP", "B_MID", "C_DEAD"]
|
||
|
||
|
||
def test_every_offered_sort_is_usable():
|
||
"""Each option in the picker must order the pool without raising."""
|
||
for label in LINE_SORTS:
|
||
assert len(_order(label)) == len(ROWS)
|
||
|
||
|
||
def test_unknown_sort_label_falls_back_instead_of_raising():
|
||
assert len(rank(ROWS, "not a real sort")) == len(ROWS)
|