Files
zarinpal-analytics-backend/internal/config/config.go

170 lines
6.0 KiB
Go

package config
import (
"errors"
"fmt"
"os"
"strconv"
)
type Config struct {
AppEnv string
HTTPAddr string
DatabaseURL string
RedisAddr string
RedisPassword string
ImportPath string
DemoUsername string
DemoPassword string
SessionSecret string
CursorSecret string
FrontendOrigin string
CookieSecure bool
AIGrowthEnabled bool
AICoachEnabled bool
AIPageInsightsEnabled bool
AvalAIAPIKey string
AvalAIBaseURL string
AvalAIModel string
AIDailyRequestBudget int64
AIDailyTokenBudget int64
AIMaxConcurrent int
AICreatePerMinute int64
AIUncachedScopesPerDay int64
AICoachDailyRequestBudget int64
AICoachDailyTokenBudget int64
AIPageInsightsDailyRequestBudget int64
AIPageInsightsDailyTokenBudget int64
}
func Load() (Config, error) {
cfg := Config{
AppEnv: value("APP_ENV", "development"),
HTTPAddr: value("HTTP_ADDR", "127.0.0.1:8080"),
DatabaseURL: os.Getenv("DATABASE_URL"),
RedisAddr: value("REDIS_ADDR", "localhost:6379"),
RedisPassword: os.Getenv("REDIS_PASSWORD"),
ImportPath: value("IMPORT_PATH", "data/incoming/challenge_data.csv.gz"),
DemoUsername: value("DEMO_USERNAME", "merchant-0001"),
DemoPassword: os.Getenv("DEMO_PASSWORD"),
SessionSecret: os.Getenv("SESSION_SECRET"),
CursorSecret: os.Getenv("CURSOR_SECRET"),
FrontendOrigin: value("FRONTEND_ORIGIN", "http://localhost:5173"),
AvalAIAPIKey: os.Getenv("AVALAI_API_KEY"),
AvalAIBaseURL: value("AVALAI_BASE_URL", "https://api.avalai.ir"),
AvalAIModel: value("AVALAI_MODEL", "gpt-5-mini-2025-08-07"),
}
var err error
cfg.CookieSecure, err = strconv.ParseBool(value("COOKIE_SECURE", "false"))
if err != nil {
return Config{}, fmt.Errorf("parsing cookie secure: %w", err)
}
cfg.AIGrowthEnabled, err = strconv.ParseBool(value("AI_GROWTH_ENABLED", "false"))
if err != nil {
return Config{}, fmt.Errorf("parsing AI growth enabled: %w", err)
}
cfg.AICoachEnabled, err = strconv.ParseBool(value("AI_COACH_ENABLED", strconv.FormatBool(cfg.AIGrowthEnabled)))
if err != nil {
return Config{}, fmt.Errorf("parsing AI coach enabled: %w", err)
}
cfg.AIPageInsightsEnabled, err = strconv.ParseBool(value("AI_PAGE_INSIGHTS_ENABLED", strconv.FormatBool(cfg.AIGrowthEnabled)))
if err != nil {
return Config{}, fmt.Errorf("parsing AI page insights enabled: %w", err)
}
cfg.AIDailyRequestBudget, err = parseInt64("AI_DAILY_REQUEST_BUDGET", 0)
if err != nil {
return Config{}, err
}
cfg.AIDailyTokenBudget, err = parseInt64("AI_DAILY_TOKEN_BUDGET", 0)
if err != nil {
return Config{}, err
}
cfg.AICoachDailyRequestBudget, err = parseInt64("AI_COACH_DAILY_REQUEST_BUDGET", cfg.AIDailyRequestBudget)
if err != nil {
return Config{}, err
}
cfg.AICoachDailyTokenBudget, err = parseInt64("AI_COACH_DAILY_TOKEN_BUDGET", cfg.AIDailyTokenBudget)
if err != nil {
return Config{}, err
}
cfg.AIPageInsightsDailyRequestBudget, err = parseInt64("AI_PAGE_INSIGHTS_DAILY_REQUEST_BUDGET", cfg.AIDailyRequestBudget)
if err != nil {
return Config{}, err
}
cfg.AIPageInsightsDailyTokenBudget, err = parseInt64("AI_PAGE_INSIGHTS_DAILY_TOKEN_BUDGET", cfg.AIDailyTokenBudget)
if err != nil {
return Config{}, err
}
maxConcurrent, err := parseInt64("AI_MAX_CONCURRENT", 4)
if err != nil {
return Config{}, err
}
cfg.AIMaxConcurrent = int(maxConcurrent)
cfg.AICreatePerMinute, err = parseInt64("AI_CREATE_PER_MINUTE", 10)
if err != nil {
return Config{}, err
}
cfg.AIUncachedScopesPerDay, err = parseInt64("AI_UNCACHED_SCOPES_PER_DAY", 5)
if err != nil {
return Config{}, err
}
if cfg.DatabaseURL == "" {
return Config{}, errors.New("config: DATABASE_URL is required")
}
return cfg, nil
}
func (c Config) ValidateAPI() error {
if len(c.SessionSecret) < 32 {
return errors.New("config: SESSION_SECRET must contain at least 32 characters")
}
if len(c.CursorSecret) < 32 {
return errors.New("config: CURSOR_SECRET must contain at least 32 characters")
}
if c.CursorSecret == c.SessionSecret {
return errors.New("config: CURSOR_SECRET must differ from SESSION_SECRET")
}
if c.AIMaxConcurrent < 1 || c.AICreatePerMinute < 1 || c.AIUncachedScopesPerDay < 1 {
return errors.New("config: AI limits must be positive")
}
if c.AppEnv == "production" && (c.AIGrowthEnabled || c.AICoachEnabled || c.AIPageInsightsEnabled) {
if c.AvalAIAPIKey == "" || c.AvalAIModel == "" {
return errors.New("config: enabled production AI requires AVALAI_API_KEY and AVALAI_MODEL")
}
if c.AIGrowthEnabled && (c.AIDailyRequestBudget < 1 || c.AIDailyTokenBudget < 1) {
return errors.New("config: enabled production AI requires positive daily budgets")
}
if c.AICoachEnabled && (c.AICoachDailyRequestBudget < 1 || c.AICoachDailyTokenBudget < 1) {
return errors.New("config: enabled production coach requires positive coach budgets")
}
if c.AIPageInsightsEnabled && (c.AIPageInsightsDailyRequestBudget < 1 || c.AIPageInsightsDailyTokenBudget < 1) {
return errors.New("config: enabled production page insights require positive page-insight budgets")
}
}
return nil
}
func (c Config) ValidateSeed() error {
if len(c.DemoPassword) < 12 {
return errors.New("config: DEMO_PASSWORD must contain at least 12 characters")
}
return nil
}
func value(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
func parseInt64(name string, fallback int64) (int64, error) {
raw := value(name, strconv.FormatInt(fallback, 10))
parsed, err := strconv.ParseInt(raw, 10, 64)
if err != nil || parsed < 0 {
return 0, fmt.Errorf("config: %s must be a non-negative integer", name)
}
return parsed, nil
}