Files
Bookra/apps/backend/internal/config/config.go
T
Tomas DvorakandDevin f2f7439050 refactor(db): replace sqlc with handwritten pgx repos
- Finish Paddle-to-Stripe billing migration; drop paddle/stripe libs
- Renumber migrations; add 00012 memberships+manage_token,
  00013 location_type_capacity
- Add tests: admin, auth, bookings customer service
- Remove graphify-out artifacts and stale env files

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 20:21:34 +02:00

212 lines
7.4 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
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", "[email protected]"),
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")),
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) 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 {
return "stripe"
}
func (cfg Config) BillingConfigured() bool {
return cfg.StripeConfigured()
}
func (cfg Config) BillingWebhookConfigured() bool {
return cfg.StripeWebhookConfigured()
}
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 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 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
}