package config import ( "fmt" "os" "github.com/joho/godotenv" ) // Config holds all configuration for the billing service type Config struct { // Server Port string Environment string // Database DatabaseURL string // JWT (shared with consent-service) JWTSecret string // Stripe StripeSecretKey string StripeWebhookSecret string StripePublishableKey string StripeMockMode bool // If true, Stripe calls are mocked (for dev without Stripe keys) // URLs BillingSuccessURL string BillingCancelURL string FrontendURL string // Trial TrialPeriodDays int // CORS AllowedOrigins []string // Rate Limiting RateLimitRequests int RateLimitWindow int // in seconds // Internal API Key (for service-to-service communication) InternalAPIKey string } // Load loads configuration from environment variables func Load() (*Config, error) { // Load .env file if exists (for development) _ = godotenv.Load() cfg := &Config{ Port: getEnv("PORT", "8083"), Environment: getEnv("ENVIRONMENT", "development"), DatabaseURL: getEnv("DATABASE_URL", ""), JWTSecret: getEnv("JWT_SECRET", ""), // Stripe StripeSecretKey: getEnv("STRIPE_SECRET_KEY", ""), StripeWebhookSecret: getEnv("STRIPE_WEBHOOK_SECRET", ""), StripePublishableKey: getEnv("STRIPE_PUBLISHABLE_KEY", ""), StripeMockMode: getEnvBool("STRIPE_MOCK_MODE", false), // URLs BillingSuccessURL: getEnv("BILLING_SUCCESS_URL", "http://localhost:8000/app/billing/success"), BillingCancelURL: getEnv("BILLING_CANCEL_URL", "http://localhost:8000/app/billing/cancel"), FrontendURL: getEnv("FRONTEND_URL", "http://localhost:8000"), // Trial TrialPeriodDays: getEnvInt("TRIAL_PERIOD_DAYS", 7), // Rate Limiting RateLimitRequests: getEnvInt("RATE_LIMIT_REQUESTS", 100), RateLimitWindow: getEnvInt("RATE_LIMIT_WINDOW", 60), // Internal API InternalAPIKey: getEnv("INTERNAL_API_KEY", ""), } // Parse allowed origins originsStr := getEnv("ALLOWED_ORIGINS", "http://localhost:3000,http://localhost:8000") cfg.AllowedOrigins = parseCommaSeparated(originsStr) // Validate required fields if cfg.DatabaseURL == "" { return nil, fmt.Errorf("DATABASE_URL is required") } if cfg.JWTSecret == "" { return nil, fmt.Errorf("JWT_SECRET is required") } // Stripe key is required unless mock mode is enabled if cfg.StripeSecretKey == "" && !cfg.StripeMockMode { // In development mode, auto-enable mock mode if no Stripe key if cfg.Environment == "development" { cfg.StripeMockMode = true } else { return nil, fmt.Errorf("STRIPE_SECRET_KEY is required (set STRIPE_MOCK_MODE=true to bypass in dev)") } } return cfg, nil } // IsMockMode returns true if Stripe should be mocked func (c *Config) IsMockMode() bool { return c.StripeMockMode } func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func getEnvInt(key string, defaultValue int) int { if value := os.Getenv(key); value != "" { var result int fmt.Sscanf(value, "%d", &result) return result } return defaultValue } func getEnvBool(key string, defaultValue bool) bool { if value := os.Getenv(key); value != "" { return value == "true" || value == "1" || value == "yes" } return defaultValue } func parseCommaSeparated(s string) []string { if s == "" { return []string{} } var result []string start := 0 for i := 0; i <= len(s); i++ { if i == len(s) || s[i] == ',' { item := s[start:i] // Trim whitespace for len(item) > 0 && item[0] == ' ' { item = item[1:] } for len(item) > 0 && item[len(item)-1] == ' ' { item = item[:len(item)-1] } if item != "" { result = append(result, item) } start = i + 1 } } return result }