| """ |
| Central configuration for Etsy Listing Optimizer. |
| |
| IMPORTANT: Tier pricing/limits live here (not scattered through the code) so |
| product changes don't require hunting through business logic. Prices are also |
| overridable via environment variables so pricing can change without a redeploy. |
| """ |
| import os |
| from dataclasses import dataclass |
| from enum import Enum |
|
|
|
|
| class Tier(str, Enum): |
| FREE = "free" |
| STARTER = "starter" |
| PRO = "pro" |
| BUSINESS = "business" |
|
|
|
|
| @dataclass(frozen=True) |
| class TierConfig: |
| tier: Tier |
| display_name: str |
| price_czk: int |
| generations_per_month: int |
| title_variants: int |
| history_days: int |
| brand_voice_profiles: int |
| category_hints: bool |
| csv_export: bool |
| bulk_generation: bool |
| multi_shop: bool |
| stripe_price_env_var: str | None |
|
|
|
|
| def _int_env(name: str, default: int) -> int: |
| val = os.environ.get(name) |
| if val is None or val == "": |
| return default |
| try: |
| return int(val) |
| except ValueError: |
| return default |
|
|
|
|
| |
| |
| TIERS: dict[Tier, TierConfig] = { |
| Tier.FREE: TierConfig( |
| tier=Tier.FREE, |
| display_name="Free", |
| price_czk=0, |
| generations_per_month=_int_env("PRICE_FREE_GENERATIONS", 5), |
| title_variants=1, |
| history_days=0, |
| brand_voice_profiles=1, |
| category_hints=False, |
| csv_export=False, |
| bulk_generation=False, |
| multi_shop=False, |
| stripe_price_env_var=None, |
| ), |
| Tier.STARTER: TierConfig( |
| tier=Tier.STARTER, |
| display_name="Starter", |
| price_czk=_int_env("PRICE_STARTER_CZK", 149), |
| generations_per_month=_int_env("PRICE_STARTER_GENERATIONS", 50), |
| title_variants=3, |
| history_days=30, |
| brand_voice_profiles=1, |
| category_hints=False, |
| csv_export=False, |
| bulk_generation=False, |
| multi_shop=False, |
| stripe_price_env_var="STRIPE_PRICE_STARTER", |
| ), |
| Tier.PRO: TierConfig( |
| tier=Tier.PRO, |
| display_name="Pro", |
| price_czk=_int_env("PRICE_PRO_CZK", 349), |
| generations_per_month=-1, |
| title_variants=3, |
| history_days=-1, |
| brand_voice_profiles=5, |
| category_hints=True, |
| csv_export=True, |
| bulk_generation=False, |
| multi_shop=False, |
| stripe_price_env_var="STRIPE_PRICE_PRO", |
| ), |
| Tier.BUSINESS: TierConfig( |
| tier=Tier.BUSINESS, |
| display_name="Business", |
| price_czk=_int_env("PRICE_BUSINESS_CZK", 799), |
| generations_per_month=-1, |
| title_variants=3, |
| history_days=-1, |
| brand_voice_profiles=-1, |
| category_hints=True, |
| csv_export=True, |
| bulk_generation=True, |
| multi_shop=True, |
| stripe_price_env_var="STRIPE_PRICE_BUSINESS", |
| ), |
| } |
|
|
|
|
| def get_tier_config(tier: str | Tier) -> TierConfig: |
| if isinstance(tier, str): |
| tier = Tier(tier) |
| return TIERS[tier] |
|
|
|
|
| |
| |
| |
| TITLE_MAX_CHARS = 140 |
| TAG_MAX_CHARS = 20 |
| TAG_COUNT = 13 |
| MAX_VALIDATION_RETRIES = 2 |
|
|
| |
| |
| GENERATE_RATE_LIMIT_PER_MINUTE = _int_env("GENERATE_RATE_LIMIT_PER_MINUTE", 6) |
|
|
| |
| AUTH_RATE_LIMIT_PER_MINUTE = _int_env("AUTH_RATE_LIMIT_PER_MINUTE", 10) |
|
|
| |
| |
| |
| |
| |
| |
| |
| def _has(name: str) -> bool: |
| return bool(os.environ.get(name)) |
|
|
|
|
| SUPABASE_CONFIGURED = _has("SUPABASE_URL") and _has("SUPABASE_SERVICE_ROLE_KEY") |
| GROQ_CONFIGURED = _has("GROQ_API_KEY") |
| GEMINI_CONFIGURED = _has("GEMINI_API_KEY") |
| STRIPE_CONFIGURED = _has("STRIPE_SECRET_KEY") |
| RESEND_CONFIGURED = _has("RESEND_API_KEY") |
|
|
| DEMO_MODE = not (SUPABASE_CONFIGURED and (GROQ_CONFIGURED or GEMINI_CONFIGURED) and STRIPE_CONFIGURED) |
|
|