File size: 4,713 Bytes
16e1aa7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""
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)