feat(products): improve replenishment-aware shopping suggestions

Replace product weight and repurchase intent fields with per-package remaining levels and inventory-first restock signals. Enrich shopping suggestions with usage-aware replenishment scoring so the frontend and LLM can prioritize real gaps and near-empty staples more reliably.
This commit is contained in:
Piotr Oleszczyk 2026-03-09 13:37:40 +01:00
parent bb5d402c15
commit d91d06455b
18 changed files with 587 additions and 210 deletions

View file

@ -8,6 +8,8 @@ from innercontext.api.products import (
ProductSuggestion,
ShoppingSuggestionResponse,
_build_shopping_context,
_compute_days_since_last_used,
_compute_replenishment_score,
_extract_requested_product_ids,
build_product_details_tool_handler,
)
@ -68,7 +70,12 @@ def test_build_shopping_context(session: Session):
session.commit()
# Add inventory
inv = ProductInventory(id=uuid.uuid4(), product_id=p.id, is_opened=True)
inv = ProductInventory(
id=uuid.uuid4(),
product_id=p.id,
is_opened=True,
remaining_level="medium",
)
session.add(inv)
session.commit()
@ -87,9 +94,89 @@ def test_build_shopping_context(session: Session):
assert "Soothing Serum" in ctx
assert f"id={p.id}" in ctx
assert "BrandX" in ctx
assert "targets: ['redness']" in ctx
assert "actives: ['Centella']" in ctx
assert "effects: {'soothing': 4}" in ctx
assert "targets=['redness']" in ctx
assert "actives=['Centella']" in ctx
assert "effects={'soothing': 4}" in ctx
assert "stock_state=monitor" in ctx
assert "opened_count=1" in ctx
assert "sealed_backup_count=0" in ctx
assert "lowest_remaining_level=medium" in ctx
assert "replenishment_score=30" in ctx
assert "replenishment_priority_hint=low" in ctx
assert "repurchase_candidate=true" in ctx
def test_build_shopping_context_flags_replenishment_signal(session: Session):
product = Product(
id=uuid.uuid4(),
short_id=str(uuid.uuid4())[:8],
name="Barrier Cleanser",
brand="BrandY",
category="cleanser",
recommended_time="both",
leave_on=False,
product_effect_profile={},
)
session.add(product)
session.commit()
session.add(
ProductInventory(
id=uuid.uuid4(),
product_id=product.id,
is_opened=True,
remaining_level="nearly_empty",
)
)
session.commit()
ctx = _build_shopping_context(session, reference_date=date.today())
assert "lowest_remaining_level=nearly_empty" in ctx
assert "stock_state=urgent" in ctx
assert "replenishment_priority_hint=high" in ctx
def test_compute_replenishment_score_prefers_recent_staples_without_backup():
result = _compute_replenishment_score(
has_stock=True,
sealed_backup_count=0,
lowest_remaining_level="low",
days_since_last_used=2,
category=ProductCategory.CLEANSER,
)
assert result["replenishment_score"] == 95
assert result["replenishment_priority_hint"] == "high"
assert result["repurchase_candidate"] is True
assert result["replenishment_reason_codes"] == [
"low_opened",
"recently_used",
"staple_category",
]
def test_compute_replenishment_score_downranks_sealed_backup_and_stale_usage():
result = _compute_replenishment_score(
has_stock=True,
sealed_backup_count=1,
lowest_remaining_level="nearly_empty",
days_since_last_used=70,
category=ProductCategory.EXFOLIANT,
)
assert result["replenishment_score"] == 0
assert result["replenishment_priority_hint"] == "none"
assert result["repurchase_candidate"] is False
assert result["replenishment_reason_codes"] == [
"has_sealed_backup",
"stale_usage",
"occasional_category",
]
def test_compute_days_since_last_used_returns_none_without_usage():
assert _compute_days_since_last_used(None, date(2026, 3, 9)) is None
assert _compute_days_since_last_used(date(2026, 3, 7), date(2026, 3, 9)) == 2
def test_suggest_shopping(client, session):