feat: tiers -- leftover_mode rate-limited free, style_picker paid+, staple_library free

This commit is contained in:
pyr0ball 2026-03-30 22:50:35 -07:00
parent 59b6a8265f
commit 27ec14b40f
2 changed files with 30 additions and 2 deletions

View file

@ -25,6 +25,8 @@ KIWI_FEATURES: dict[str, str] = {
"receipt_upload": "free",
"expiry_alerts": "free",
"export_csv": "free",
"leftover_mode": "free", # Rate-limited at API layer, not tier-gated
"staple_library": "free",
# Paid tier
"receipt_ocr": "paid", # BYOK-unlockable
@ -32,11 +34,11 @@ KIWI_FEATURES: dict[str, str] = {
"expiry_llm_matching": "paid", # BYOK-unlockable
"meal_planning": "paid",
"dietary_profiles": "paid",
"style_picker": "paid",
# Premium tier
"multi_household": "premium",
"background_monitoring": "premium",
"leftover_mode": "premium",
}
@ -47,6 +49,7 @@ def can_use(feature: str, tier: str, has_byok: bool = False) -> bool:
tier,
has_byok=has_byok,
_features=KIWI_FEATURES,
_byok_unlockable=KIWI_BYOK_UNLOCKABLE,
)
@ -54,7 +57,12 @@ def require_feature(feature: str, tier: str, has_byok: bool = False) -> None:
"""Raise ValueError if the tier cannot access the feature."""
if not can_use(feature, tier, has_byok):
from circuitforge_core.tiers.tiers import tier_label
needed = tier_label(feature, has_byok=has_byok, _features=KIWI_FEATURES)
needed = tier_label(
feature,
has_byok=has_byok,
_features=KIWI_FEATURES,
_byok_unlockable=KIWI_BYOK_UNLOCKABLE,
)
raise ValueError(
f"Feature '{feature}' requires {needed} tier. "
f"Current tier: {tier}."

20
tests/test_tiers.py Normal file
View file

@ -0,0 +1,20 @@
from app.tiers import can_use
def test_leftover_mode_free_tier():
"""Leftover mode is now available to free users (rate-limited at API layer, not hard-gated)."""
assert can_use("leftover_mode", "free") is True
def test_style_picker_requires_paid():
assert can_use("style_picker", "free") is False
assert can_use("style_picker", "paid") is True
def test_staple_library_is_free():
assert can_use("staple_library", "free") is True
def test_recipe_suggestions_byok_unlockable():
assert can_use("recipe_suggestions", "free", has_byok=False) is False
assert can_use("recipe_suggestions", "free", has_byok=True) is True