package platformsettings import ( "context" "os" "strconv" "strings" "time" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/descrybe/descrybe-v2/apps/api/internal/eprel" ) // ResolveStripe merges platform Values over env/base StripeConfig. // Non-empty DB values win; price IDs use "starter:monthly" keys. func (s *Service) ResolveStripe(ctx context.Context, base billing.StripeConfig) (billing.StripeConfig, error) { out := base if out.PriceIDs == nil { out.PriceIDs = map[string]string{} } else { cp := make(map[string]string, len(out.PriceIDs)) for k, v := range out.PriceIDs { cp[k] = v } out.PriceIDs = cp } if v, ok, err := s.GetKV(ctx, KeyStripeSecretKey); err != nil { return out, err } else if ok && strings.TrimSpace(v) != "" { out.SecretKey = v } if v, ok, err := s.GetKV(ctx, KeyStripeWebhookSecret); err != nil { return out, err } else if ok && strings.TrimSpace(v) != "" { out.WebhookSecret = v } if v, ok, err := s.GetKV(ctx, KeyStripeMock); err != nil { return out, err } else if ok { out.ForceMock = parseTruthy(v) } priceKeys := []struct { setting string price string }{ {KeyStripePriceStarterMo, "starter:monthly"}, {KeyStripePriceStarterYr, "starter:yearly"}, {KeyStripePricePlusMo, "plus:monthly"}, {KeyStripePricePlusYr, "plus:yearly"}, {KeyStripePriceGrowthMo, "growth:monthly"}, {KeyStripePriceGrowthYr, "growth:yearly"}, {KeyStripePriceBizMo, "business:monthly"}, {KeyStripePriceBizYr, "business:yearly"}, {KeyStripePriceScaleMo, "scale:monthly"}, {KeyStripePriceScaleYr, "scale:yearly"}, } for _, p := range priceKeys { if v, ok, err := s.GetKV(ctx, p.setting); err != nil { return out, err } else if ok && strings.TrimSpace(v) != "" { out.PriceIDs[p.price] = strings.TrimSpace(v) } } for _, pack := range billing.DefaultCreditPacks() { setting := billing.CreditPackSettingsKey(pack.ID) if v, ok, err := s.GetKV(ctx, setting); err != nil { return out, err } else if ok && strings.TrimSpace(v) != "" { out.PriceIDs[billing.CreditPackPriceKey(pack.ID)] = strings.TrimSpace(v) } } return out, nil } // EPRELOptions is the runtime EPREL client config after settings merge. type EPRELOptions struct { Enabled bool BaseURL string Timeout time.Duration FicheLanguage string APIKey string } // ResolveEPREL merges Values over EnvConfig EPREL fields (when set) and defaults. // Enrichment defaults on (EPREL_ENABLED env default true); admin eprel.enabled overrides when set. // API key is optional — the public EU EPREL API does not require authentication. func (s *Service) ResolveEPREL(ctx context.Context) (EPRELOptions, error) { out := EPRELOptions{ Enabled: s.Env.EPRELEnabled, BaseURL: s.Env.EPRELBaseURL, Timeout: s.Env.EPRELTimeout, FicheLanguage: s.Env.EPRELFicheLanguage, APIKey: s.Env.EPRELAPIKey, } if out.BaseURL == "" { out.BaseURL = "https://eprel.ec.europa.eu/api" } if out.Timeout <= 0 { out.Timeout = 10 * time.Second } if out.FicheLanguage == "" { out.FicheLanguage = "EN" } if v, ok, err := s.GetKV(ctx, KeyEPRELEnabled); err != nil { return out, err } else if ok { out.Enabled = parseTruthy(v) } if v, ok, err := s.GetKV(ctx, KeyEPRELBaseURL); err != nil { return out, err } else if ok && strings.TrimSpace(v) != "" { out.BaseURL = strings.TrimSpace(v) } if v, ok, err := s.GetKV(ctx, KeyEPRELTimeout); err != nil { return out, err } else if ok { if d, okDur := parseDuration(v); okDur { out.Timeout = d } } if v, ok, err := s.GetKV(ctx, KeyEPRELFicheLanguage); err != nil { return out, err } else if ok && strings.TrimSpace(v) != "" { if code, nerr := NormalizeEPRELFicheLanguage(v); nerr == nil { out.FicheLanguage = code } } if v, ok, err := s.GetKV(ctx, KeyEPRELAPIKey); err != nil { return out, err } else if ok { out.APIKey = strings.TrimSpace(v) } return out, nil } // NewEPRELClient builds an EPREL client from ResolveEPREL. func (s *Service) NewEPRELClient(ctx context.Context) (*eprel.Client, error) { opts, err := s.ResolveEPREL(ctx) if err != nil { return nil, err } return eprel.NewClient(eprel.Options{ Enabled: opts.Enabled, BaseURL: opts.BaseURL, Timeout: opts.Timeout, FicheLanguage: opts.FicheLanguage, APIKey: opts.APIKey, }), nil } // ResolvedPinecone is runtime Pinecone config after settings merge (plaintext key — never log). type ResolvedPinecone struct { APIKey string Host string Namespace string } // Configured reports whether vector features can run (key + host required). func (r ResolvedPinecone) Configured() bool { return strings.TrimSpace(r.APIKey) != "" && strings.TrimSpace(r.Host) != "" } // ResolvePinecone merges Values over EnvConfig Pinecone fields (DB wins when set). func (s *Service) ResolvePinecone(ctx context.Context) (ResolvedPinecone, error) { out := ResolvedPinecone{ APIKey: strings.TrimSpace(s.Env.PineconeAPIKey), Host: strings.TrimSpace(s.Env.PineconeHost), Namespace: strings.TrimSpace(s.Env.PineconeNamespace), } if v, ok, err := s.GetKV(ctx, KeyPineconeAPIKey); err != nil { return out, err } else if ok && strings.TrimSpace(v) != "" { out.APIKey = strings.TrimSpace(v) } if v, ok, err := s.GetKV(ctx, KeyPineconeHost); err != nil { return out, err } else if ok && strings.TrimSpace(v) != "" { out.Host = strings.TrimSpace(v) } if v, ok, err := s.GetKV(ctx, KeyPineconeNamespace); err != nil { return out, err } else if ok { out.Namespace = strings.TrimSpace(v) } return out, nil } // ResolveFeedPrivateAllowlist returns settings CSV, falling back to env. func (s *Service) ResolveFeedPrivateAllowlist(ctx context.Context) (string, error) { if v, ok, err := s.GetKV(ctx, KeyFeedPrivateAllowlist); err != nil { return "", err } else if ok && strings.TrimSpace(v) != "" { return strings.TrimSpace(v), nil } if strings.TrimSpace(s.Env.FeedPrivateAllowlist) != "" { return strings.TrimSpace(s.Env.FeedPrivateAllowlist), nil } return strings.TrimSpace(os.Getenv("FEED_URL_PRIVATE_ALLOWLIST")), nil } func parseDuration(v string) (time.Duration, bool) { v = strings.TrimSpace(v) if v == "" { return 0, false } if d, err := time.ParseDuration(v); err == nil { return d, true } if n, err := strconv.Atoi(v); err == nil && n >= 0 { return time.Duration(n) * time.Second, true } return 0, false }