Files
Bookra/apps/backend/internal/config/config.go
T

259 lines
9.1 KiB
Go

package config
import (
"errors"
"fmt"
"os"
"strings"
"bookra/apps/backend/internal/shared"
)
type Config struct {
Environment string
Port string
APIURL string
FrontendURL string
DatabaseURL string
DatabaseDirectURL string
NeonAuthURL string
AuthJWTSecret string
JobRunnerKey string
EmailFrom string
SMTPHost string
SMTPPort string
SMTPUsername string
SMTPPassword string
PaddleEnvironment string
PaddleAPIKey string
PaddleWebhookKey string
PaddlePriceMatrix map[string]map[string]string
StripeAPIKey string
StripeWebhookKey string
StripePriceMatrix map[string]map[string]string
AdminEmail string
AdminKey string
UmamiAPIURL string
UmamiAPIKey string
SentryDSN string
DemoMode bool
SMSManagerAPIKey string
SMSManagerBaseURL string
StripeSMSPriceMatrix map[string]string // currency -> price ID (czk, usd, eur, gbp, ...)
}
func Load() (Config, error) {
cfg := Config{
Environment: valueOrDefault("BOOKRA_APP_ENV", "development"),
Port: valueOrDefault("BOOKRA_API_PORT", "8080"),
APIURL: valueOrDefault("BOOKRA_API_URL", "http://localhost:8080"),
FrontendURL: valueOrDefault("BOOKRA_FRONTEND_URL", "http://localhost:3000"),
DatabaseURL: strings.TrimSpace(os.Getenv("BOOKRA_DATABASE_URL")),
DatabaseDirectURL: strings.TrimSpace(os.Getenv("BOOKRA_DATABASE_DIRECT_URL")),
NeonAuthURL: strings.TrimSpace(os.Getenv("BOOKRA_NEON_AUTH_URL")),
AuthJWTSecret: strings.TrimSpace(os.Getenv("BOOKRA_AUTH_JWT_SECRET")),
JobRunnerKey: strings.TrimSpace(os.Getenv("BOOKRA_JOB_RUNNER_KEY")),
EmailFrom: valueOrDefault("BOOKRA_EMAIL_FROM", "noreply@bookra.dev"),
SMTPHost: strings.TrimSpace(os.Getenv("BOOKRA_SMTP_HOST")),
SMTPPort: valueOrDefault("BOOKRA_SMTP_PORT", "587"),
SMTPUsername: strings.TrimSpace(os.Getenv("BOOKRA_SMTP_USERNAME")),
SMTPPassword: strings.TrimSpace(os.Getenv("BOOKRA_SMTP_PASSWORD")),
PaddleEnvironment: normalizePaddleEnvironment(os.Getenv("BOOKRA_PADDLE_ENV")),
PaddleAPIKey: strings.TrimSpace(os.Getenv("BOOKRA_PADDLE_API_KEY")),
PaddleWebhookKey: strings.TrimSpace(os.Getenv("BOOKRA_PADDLE_WEBHOOK_SECRET")),
PaddlePriceMatrix: paddlePriceMatrixFromEnv(),
StripeAPIKey: strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_API_KEY")),
StripeWebhookKey: strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_WEBHOOK_SECRET")),
StripePriceMatrix: stripePriceMatrixFromEnv(),
AdminEmail: strings.TrimSpace(os.Getenv("BOOKRA_ADMIN_EMAIL")),
AdminKey: strings.TrimSpace(os.Getenv("BOOKRA_ADMIN_KEY")),
UmamiAPIURL: strings.TrimSpace(os.Getenv("BOOKRA_UMAMI_API_URL")),
UmamiAPIKey: strings.TrimSpace(os.Getenv("BOOKRA_UMAMI_API_KEY")),
SentryDSN: strings.TrimSpace(os.Getenv("BOOKRA_SENTRY_DSN")),
DemoMode: boolFromEnv("BOOKRA_DEMO_MODE", false),
SMSManagerAPIKey: strings.TrimSpace(os.Getenv("BOOKRA_SMSMANAGER_API_KEY")),
SMSManagerBaseURL: valueOrDefault("BOOKRA_SMSMANAGER_BASE_URL", "https://api.smsmngr.com/v2"),
StripeSMSPriceMatrix: smsPriceMatrixFromEnv(),
}
if cfg.FrontendURL == "" {
return Config{}, errors.New("BOOKRA_FRONTEND_URL is required")
}
if err := cfg.validateRuntimeRequirements(); err != nil {
return Config{}, err
}
return cfg, nil
}
func (cfg Config) validateRuntimeRequirements() error {
if cfg.Environment == "development" || cfg.Environment == "test" {
return nil
}
missing := make([]string, 0, 3)
if cfg.DatabaseURL == "" {
missing = append(missing, "BOOKRA_DATABASE_URL")
}
if cfg.NeonAuthURL == "" {
missing = append(missing, "BOOKRA_NEON_AUTH_URL")
}
if cfg.JobRunnerKey == "" {
missing = append(missing, "BOOKRA_JOB_RUNNER_KEY")
}
if cfg.SMTPHost == "" {
missing = append(missing, "BOOKRA_SMTP_HOST")
}
if len(missing) > 0 {
return fmt.Errorf("%s required when BOOKRA_APP_ENV=%s", strings.Join(uniqueStrings(missing), ", "), cfg.Environment)
}
return nil
}
func (cfg Config) PaddleConfigured() bool {
return strings.TrimSpace(cfg.PaddleAPIKey) != ""
}
func (cfg Config) PaddleWebhookConfigured() bool {
return strings.TrimSpace(cfg.PaddleWebhookKey) != ""
}
func (cfg Config) PaddleCheckoutConfigured(planCode string) bool {
planCode = shared.NormalizePlanCode(planCode)
return cfg.PaddleConfigured() && cfg.PaddleWebhookConfigured() && cfg.PaddlePriceMatrix[planCode]["czk"] != "" && cfg.PaddlePriceMatrix[planCode]["usd"] != ""
}
func (cfg Config) StripeConfigured() bool {
return strings.TrimSpace(cfg.StripeAPIKey) != ""
}
func (cfg Config) StripeWebhookConfigured() bool {
return strings.TrimSpace(cfg.StripeWebhookKey) != ""
}
func (cfg Config) StripeCheckoutConfigured(planCode string) bool {
planCode = shared.NormalizePlanCode(planCode)
return cfg.StripeConfigured() && cfg.StripeWebhookConfigured() && cfg.StripePriceMatrix[planCode]["czk"] != "" && cfg.StripePriceMatrix[planCode]["usd"] != ""
}
func (cfg Config) BillingProvider() string {
if cfg.StripeConfigured() {
return "stripe"
}
return "paddle"
}
func (cfg Config) BillingConfigured() bool {
return cfg.StripeConfigured() || cfg.PaddleConfigured()
}
func (cfg Config) BillingWebhookConfigured() bool {
return cfg.StripeWebhookConfigured() || cfg.PaddleWebhookConfigured()
}
func (cfg Config) SMSConfigured() bool {
return strings.TrimSpace(cfg.SMSManagerAPIKey) != ""
}
func (cfg Config) StripeSMSConfigured() bool {
return cfg.StripeConfigured() && cfg.StripeSMSPriceMatrix["czk"] != ""
}
func (cfg Config) StripeSMSPriceID(currency string) string {
c := strings.ToLower(strings.TrimSpace(currency))
if c == "" {
c = "czk"
}
if id := cfg.StripeSMSPriceMatrix[c]; id != "" {
return id
}
return cfg.StripeSMSPriceMatrix["czk"]
}
func paddlePriceMatrixFromEnv() map[string]map[string]string {
matrix := map[string]map[string]string{
"starter": {},
"pro": {},
"business": {},
}
for _, planCode := range []string{"starter", "pro", "business"} {
envPlan := strings.ToUpper(strings.ReplaceAll(planCode, "-", "_"))
matrix[planCode]["czk"] = strings.TrimSpace(os.Getenv("BOOKRA_PADDLE_" + envPlan + "_CZK_PRICE_ID"))
matrix[planCode]["usd"] = strings.TrimSpace(os.Getenv("BOOKRA_PADDLE_" + envPlan + "_USD_PRICE_ID"))
}
return matrix
}
func stripePriceMatrixFromEnv() map[string]map[string]string {
matrix := map[string]map[string]string{
"starter": {},
"pro": {},
"business": {},
}
for _, planCode := range []string{"starter", "pro", "business"} {
envPlan := strings.ToUpper(strings.ReplaceAll(planCode, "-", "_"))
// Monthly prices
matrix[planCode][planCode+":czk:monthly"] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_" + envPlan + "_CZK_MONTHLY_PRICE_ID"))
matrix[planCode][planCode+":usd:monthly"] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_" + envPlan + "_USD_MONTHLY_PRICE_ID"))
matrix[planCode][planCode+":czk"] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_" + envPlan + "_CZK_PRICE_ID"))
matrix[planCode][planCode+":usd"] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_" + envPlan + "_USD_PRICE_ID"))
matrix[planCode]["czk"] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_" + envPlan + "_CZK_PRICE_ID"))
matrix[planCode]["usd"] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_" + envPlan + "_USD_PRICE_ID"))
// Yearly prices
matrix[planCode][planCode+":czk:yearly"] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_" + envPlan + "_CZK_YEARLY_PRICE_ID"))
matrix[planCode][planCode+":usd:yearly"] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_" + envPlan + "_USD_YEARLY_PRICE_ID"))
matrix[planCode]["yearly:czk"] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_" + envPlan + "_CZK_YEARLY_PRICE_ID"))
matrix[planCode]["yearly:usd"] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_" + envPlan + "_USD_YEARLY_PRICE_ID"))
}
return matrix
}
func normalizePaddleEnvironment(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "live", "production":
return "live"
default:
return "sandbox"
}
}
func valueOrDefault(key string, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
return fallback
}
func boolFromEnv(key string, fallback bool) bool {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
value = strings.ToLower(value)
return value == "true" || value == "1" || value == "yes" || value == "on"
}
func smsPriceMatrixFromEnv() map[string]string {
matrix := map[string]string{}
for _, currency := range []string{"czk", "usd", "eur", "gbp", "pln"} {
upper := strings.ToUpper(currency)
matrix[currency] = strings.TrimSpace(os.Getenv("BOOKRA_STRIPE_SMS_" + upper + "_PRICE_ID"))
}
return matrix
}
func uniqueStrings(values []string) []string {
seen := map[string]struct{}{}
out := make([]string, 0, len(values))
for _, value := range values {
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
return out
}