simikkk's picture
Upload 36 files
16e1aa7 verified
Raw
History Blame Contribute Delete
4.71 kB
"""
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)