""" 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 # -1 = unlimited title_variants: int history_days: int # -1 = unlimited, 0 = none brand_voice_profiles: int # -1 = unlimited category_hints: bool csv_export: bool bulk_generation: bool multi_shop: bool stripe_price_env_var: str | None # env var name holding the Stripe Price ID 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 # Prices are read from env vars first so they can be tuned without redeploying. # Fall back to the defaults from the product brief if not set. 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] # --------------------------------------------------------------------------- # Generation / validation constants # --------------------------------------------------------------------------- TITLE_MAX_CHARS = 140 TAG_MAX_CHARS = 20 TAG_COUNT = 13 MAX_VALIDATION_RETRIES = 2 # re-prompts after the first attempt # Per-account hard rate limit on /generate, independent of the monthly quota. # Prevents a single account from hammering the LLM provider in a burst. GENERATE_RATE_LIMIT_PER_MINUTE = _int_env("GENERATE_RATE_LIMIT_PER_MINUTE", 6) # Per-IP limit on auth endpoints (login/signup/password-reset). AUTH_RATE_LIMIT_PER_MINUTE = _int_env("AUTH_RATE_LIMIT_PER_MINUTE", 10) # --------------------------------------------------------------------------- # Runtime / demo-mode flags # --------------------------------------------------------------------------- # If real credentials aren't configured yet, the app falls back to safe # in-memory / mock implementations so it can still boot and be demoed. # This is ONLY for local development and sandbox testing - never rely on # this in production. See DEPLOY.md. 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)