From 27ec14b40ff03b98beabf959d9bd9030bfa50be4 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 30 Mar 2026 22:50:35 -0700 Subject: [PATCH] feat: tiers -- leftover_mode rate-limited free, style_picker paid+, staple_library free --- app/tiers.py | 12 ++++++++++-- tests/test_tiers.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 tests/test_tiers.py diff --git a/app/tiers.py b/app/tiers.py index 133eb45..975aaea 100644 --- a/app/tiers.py +++ b/app/tiers.py @@ -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}." diff --git a/tests/test_tiers.py b/tests/test_tiers.py new file mode 100644 index 0000000..c30ac98 --- /dev/null +++ b/tests/test_tiers.py @@ -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