Files
descrybe/apps/api/internal/platformsettings/resolve.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

217 lines
6.3 KiB
Go

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
}