summaryrefslogtreecommitdiff
path: root/internal/config/config.go
diff options
context:
space:
mode:
authorChia <Chia@93.nz>2026-08-05 22:01:29 +1200
committerChia <Chia@93.nz>2026-08-05 22:07:50 +1200
commiteadb2ffe85c43cf6fc741c9823cd28eedb4a844c (patch)
tree1aba2536d57360da403aa35c9ced58b615c7064e /internal/config/config.go
parentcd0dd91ab93653631904f2ea0e574ccde6d60339 (diff)
feat: harden prepaid billing and commercial operations
Diffstat (limited to 'internal/config/config.go')
-rw-r--r--internal/config/config.go289
1 files changed, 238 insertions, 51 deletions
diff --git a/internal/config/config.go b/internal/config/config.go
index 376cf77..a67f221 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
+ "net"
"net/url"
"os"
"strings"
@@ -26,12 +27,27 @@ type Config struct {
}
type ServerConfig struct {
- Address string `json:"-"`
- AddressEnv string `json:"address_env"`
- MaxBodyBytes int64 `json:"max_body_bytes"`
- ReadHeaderTimeoutSecs int `json:"read_header_timeout_seconds"`
- IdleTimeoutSecs int `json:"idle_timeout_seconds"`
- ShutdownTimeoutSecs int `json:"shutdown_timeout_seconds"`
+ Address string `json:"-"`
+ AddressEnv string `json:"address_env"`
+ SplitListeners bool `json:"split_listeners"`
+ PublicAddressEnv string `json:"public_address_env"`
+ AdminAddressEnv string `json:"admin_address_env"`
+ WebhookAddressEnv string `json:"webhook_address_env"`
+ OperationsAddressEnv string `json:"operations_address_env"`
+ TrustedProxyCIDRsEnv string `json:"trusted_proxy_cidrs_env"`
+ RequireHTTPSEnv string `json:"require_https_env"`
+ DeploymentRegionEnv string `json:"deployment_region_env"`
+ PublicAddress string `json:"-"`
+ AdminAddress string `json:"-"`
+ WebhookAddress string `json:"-"`
+ OperationsAddress string `json:"-"`
+ TrustedProxyCIDRs []string `json:"-"`
+ RequireHTTPS bool `json:"-"`
+ DeploymentRegion string `json:"-"`
+ MaxBodyBytes int64 `json:"max_body_bytes"`
+ ReadHeaderTimeoutSecs int `json:"read_header_timeout_seconds"`
+ IdleTimeoutSecs int `json:"idle_timeout_seconds"`
+ ShutdownTimeoutSecs int `json:"shutdown_timeout_seconds"`
}
type AuthConfig struct {
@@ -40,45 +56,55 @@ type AuthConfig struct {
}
type ControlPlaneConfig struct {
- Enabled bool `json:"enabled"`
- DatabaseURLEnv string `json:"database_url_env"`
- RedisURLEnv string `json:"redis_url_env"`
- CredentialKeyEnv string `json:"credential_key_env"`
- RedisChannel string `json:"redis_channel"`
- SnapshotCacheKey string `json:"snapshot_cache_key"`
- ReloadIntervalSeconds int `json:"reload_interval_seconds"`
- AutoMigrate bool `json:"auto_migrate"`
- DatabaseURL string `json:"-"`
- RedisURL string `json:"-"`
- CredentialKey string `json:"-"`
+ Enabled bool `json:"enabled"`
+ DatabaseURLEnv string `json:"database_url_env"`
+ RedisURLEnv string `json:"redis_url_env"`
+ CredentialKeyEnv string `json:"credential_key_env"`
+ PreviousCredentialKeysEnv string `json:"previous_credential_keys_env"`
+ RedisChannel string `json:"redis_channel"`
+ SnapshotCacheKey string `json:"snapshot_cache_key"`
+ ReloadIntervalSeconds int `json:"reload_interval_seconds"`
+ AutoMigrate bool `json:"auto_migrate"`
+ DatabaseURL string `json:"-"`
+ RedisURL string `json:"-"`
+ CredentialKey string `json:"-"`
+ PreviousCredentialKeys []string `json:"-"`
}
type AdminConfig struct {
- Enabled bool `json:"enabled"`
- TokenEnv string `json:"token_env"`
- BasePath string `json:"base_path"`
- RegistrationEnabled bool `json:"registration_enabled"`
- SessionTTLHours int `json:"session_ttl_hours"`
- PublicURL string `json:"-"`
- PublicURLEnv string `json:"public_url_env"`
- Mail MailConfig `json:"mail"`
- WebAuthn WebAuthnConfig `json:"webauthn"`
- Token string `json:"-"`
+ Enabled bool `json:"enabled"`
+ TokenEnv string `json:"token_env"`
+ BasePath string `json:"base_path"`
+ RegistrationEnabled bool `json:"registration_enabled"`
+ SessionTTLHours int `json:"session_ttl_hours"`
+ AuditRetentionDays int `json:"audit_retention_days"`
+ SecurityRetentionDays int `json:"security_retention_days"`
+ PublicURL string `json:"-"`
+ PublicURLEnv string `json:"public_url_env"`
+ Mail MailConfig `json:"mail"`
+ WebAuthn WebAuthnConfig `json:"webauthn"`
+ Token string `json:"-"`
}
type MailConfig struct {
- Enabled bool `json:"enabled"`
- FromName string `json:"from_name"`
- TLSMode string `json:"tls_mode"`
- FromAddressEnv string `json:"from_address_env"`
- SMTPAddressEnv string `json:"smtp_address_env"`
- SMTPUsernameEnv string `json:"smtp_username_env"`
- SMTPPasswordEnv string `json:"smtp_password_env"`
- SMTPImplicitTLS bool `json:"smtp_implicit_tls"`
- FromAddress string `json:"-"`
- SMTPAddress string `json:"-"`
- SMTPUsername string `json:"-"`
- SMTPPassword string `json:"-"`
+ Enabled bool `json:"enabled"`
+ FromName string `json:"from_name"`
+ TLSMode string `json:"tls_mode"`
+ FromAddressEnv string `json:"from_address_env"`
+ SMTPAddressEnv string `json:"smtp_address_env"`
+ SMTPUsernameEnv string `json:"smtp_username_env"`
+ SMTPPasswordEnv string `json:"smtp_password_env"`
+ FeedbackSecretEnv string `json:"feedback_secret_env"`
+ LowBalanceMicros int64 `json:"low_balance_micros"`
+ SpendAnomalyMultiplier int64 `json:"spend_anomaly_multiplier"`
+ SpendAnomalyMinMicros int64 `json:"spend_anomaly_min_micros"`
+ NotificationIntervalSeconds int `json:"notification_interval_seconds"`
+ SMTPImplicitTLS bool `json:"smtp_implicit_tls"`
+ FromAddress string `json:"-"`
+ SMTPAddress string `json:"-"`
+ SMTPUsername string `json:"-"`
+ SMTPPassword string `json:"-"`
+ FeedbackSecret string `json:"-"`
}
type WebAuthnConfig struct {
@@ -134,19 +160,29 @@ type BillingConfig struct {
DefaultMaxOutputTokens int64 `json:"default_max_output_tokens"`
MinTopUpMinor int64 `json:"min_top_up_minor"`
MaxTopUpMinor int64 `json:"max_top_up_minor"`
+ SettlementSpoolPathEnv string `json:"settlement_spool_path_env"`
+ SettlementSpoolPath string `json:"-"`
Stripe StripeConfig `json:"stripe"`
}
type StripeConfig struct {
- Enabled bool `json:"enabled"`
- APIKeyEnv string `json:"api_key_env"`
- WebhookSecretEnv string `json:"webhook_secret_env"`
- SuccessURL string `json:"-"`
- CancelURL string `json:"-"`
- SuccessURLEnv string `json:"success_url_env"`
- CancelURLEnv string `json:"cancel_url_env"`
- APIKey string `json:"-"`
- WebhookSecret string `json:"-"`
+ Enabled bool `json:"enabled"`
+ APIKeyEnv string `json:"api_key_env"`
+ WebhookSecretEnv string `json:"webhook_secret_env"`
+ SuccessURLEnv string `json:"success_url_env"`
+ CancelURLEnv string `json:"cancel_url_env"`
+ PortalReturnURLEnv string `json:"portal_return_url_env"`
+ AutomaticTaxEnabledEnv string `json:"automatic_tax_enabled_env"`
+ TaxRegistrationConfirmedEnv string `json:"tax_registration_confirmed_env"`
+ ProductTaxCodeEnv string `json:"product_tax_code_env"`
+ SuccessURL string `json:"-"`
+ CancelURL string `json:"-"`
+ PortalReturnURL string `json:"-"`
+ APIKey string `json:"-"`
+ WebhookSecret string `json:"-"`
+ AutomaticTaxEnabled bool `json:"-"`
+ TaxRegistrationConfirmed bool `json:"-"`
+ ProductTaxCode string `json:"-"`
}
func Load(path string) (Config, error) {
@@ -186,6 +222,27 @@ func applyDefaults(cfg *Config) {
if cfg.Server.Address == "" {
cfg.Server.Address = ":8080"
}
+ if cfg.Server.PublicAddressEnv == "" {
+ cfg.Server.PublicAddressEnv = "AIGW_PUBLIC_ADDRESS"
+ }
+ if cfg.Server.AdminAddressEnv == "" {
+ cfg.Server.AdminAddressEnv = "AIGW_ADMIN_ADDRESS"
+ }
+ if cfg.Server.WebhookAddressEnv == "" {
+ cfg.Server.WebhookAddressEnv = "AIGW_WEBHOOK_ADDRESS"
+ }
+ if cfg.Server.OperationsAddressEnv == "" {
+ cfg.Server.OperationsAddressEnv = "AIGW_OPERATIONS_ADDRESS"
+ }
+ if cfg.Server.TrustedProxyCIDRsEnv == "" {
+ cfg.Server.TrustedProxyCIDRsEnv = "AIGW_TRUSTED_PROXY_CIDRS"
+ }
+ if cfg.Server.RequireHTTPSEnv == "" {
+ cfg.Server.RequireHTTPSEnv = "AIGW_REQUIRE_HTTPS"
+ }
+ if cfg.Server.DeploymentRegionEnv == "" {
+ cfg.Server.DeploymentRegionEnv = "AIGW_DEPLOYMENT_REGION"
+ }
if cfg.Server.MaxBodyBytes == 0 {
cfg.Server.MaxBodyBytes = 16 << 20
}
@@ -210,6 +267,9 @@ func applyDefaults(cfg *Config) {
if cfg.ControlPlane.CredentialKeyEnv == "" {
cfg.ControlPlane.CredentialKeyEnv = "AIGW_CREDENTIAL_KEY"
}
+ if cfg.ControlPlane.PreviousCredentialKeysEnv == "" {
+ cfg.ControlPlane.PreviousCredentialKeysEnv = "AIGW_CREDENTIAL_PREVIOUS_KEYS"
+ }
if cfg.ControlPlane.RedisChannel == "" {
cfg.ControlPlane.RedisChannel = "aigw:control:changed"
}
@@ -228,6 +288,12 @@ func applyDefaults(cfg *Config) {
if cfg.Admin.SessionTTLHours == 0 {
cfg.Admin.SessionTTLHours = 12
}
+ if cfg.Admin.AuditRetentionDays == 0 {
+ cfg.Admin.AuditRetentionDays = 2555
+ }
+ if cfg.Admin.SecurityRetentionDays == 0 {
+ cfg.Admin.SecurityRetentionDays = 30
+ }
if cfg.Admin.PublicURLEnv == "" {
cfg.Admin.PublicURLEnv = "AIGW_PUBLIC_URL"
}
@@ -249,6 +315,21 @@ func applyDefaults(cfg *Config) {
if cfg.Admin.Mail.SMTPPasswordEnv == "" {
cfg.Admin.Mail.SMTPPasswordEnv = "AIGW_SMTP_PASSWORD"
}
+ if cfg.Admin.Mail.FeedbackSecretEnv == "" {
+ cfg.Admin.Mail.FeedbackSecretEnv = "AIGW_MAIL_FEEDBACK_SECRET"
+ }
+ if cfg.Admin.Mail.LowBalanceMicros == 0 {
+ cfg.Admin.Mail.LowBalanceMicros = 5_000_000
+ }
+ if cfg.Admin.Mail.SpendAnomalyMultiplier == 0 {
+ cfg.Admin.Mail.SpendAnomalyMultiplier = 3
+ }
+ if cfg.Admin.Mail.SpendAnomalyMinMicros == 0 {
+ cfg.Admin.Mail.SpendAnomalyMinMicros = 10_000_000
+ }
+ if cfg.Admin.Mail.NotificationIntervalSeconds == 0 {
+ cfg.Admin.Mail.NotificationIntervalSeconds = 300
+ }
if cfg.Admin.WebAuthn.RPDisplayName == "" {
cfg.Admin.WebAuthn.RPDisplayName = "AIGW Console"
}
@@ -285,6 +366,9 @@ func applyDefaults(cfg *Config) {
if cfg.Billing.MinTopUpMinor == 0 {
cfg.Billing.MinTopUpMinor = 500
}
+ if cfg.Billing.SettlementSpoolPathEnv == "" {
+ cfg.Billing.SettlementSpoolPathEnv = "AIGW_SETTLEMENT_SPOOL_PATH"
+ }
if cfg.Billing.Stripe.APIKeyEnv == "" {
cfg.Billing.Stripe.APIKeyEnv = "AIGW_STRIPE_API_KEY"
}
@@ -297,6 +381,18 @@ func applyDefaults(cfg *Config) {
if cfg.Billing.Stripe.CancelURLEnv == "" {
cfg.Billing.Stripe.CancelURLEnv = "AIGW_STRIPE_CANCEL_URL"
}
+ if cfg.Billing.Stripe.PortalReturnURLEnv == "" {
+ cfg.Billing.Stripe.PortalReturnURLEnv = "AIGW_STRIPE_PORTAL_RETURN_URL"
+ }
+ if cfg.Billing.Stripe.AutomaticTaxEnabledEnv == "" {
+ cfg.Billing.Stripe.AutomaticTaxEnabledEnv = "AIGW_STRIPE_AUTOMATIC_TAX_ENABLED"
+ }
+ if cfg.Billing.Stripe.TaxRegistrationConfirmedEnv == "" {
+ cfg.Billing.Stripe.TaxRegistrationConfirmedEnv = "AIGW_STRIPE_TAX_REGISTRATION_CONFIRMED"
+ }
+ if cfg.Billing.Stripe.ProductTaxCodeEnv == "" {
+ cfg.Billing.Stripe.ProductTaxCodeEnv = "AIGW_STRIPE_PRODUCT_TAX_CODE"
+ }
for i := range cfg.Models {
for j := range cfg.Models[i].Routes {
if cfg.Models[i].Routes[j].Weight == 0 {
@@ -310,10 +406,40 @@ func resolveSecrets(cfg *Config) error {
if value := strings.TrimSpace(os.Getenv(cfg.Server.AddressEnv)); value != "" {
cfg.Server.Address = value
}
+ if cfg.Server.SplitListeners {
+ if err := resolveRequiredEnv(&cfg.Server.PublicAddress, cfg.Server.PublicAddressEnv, "server.public_address"); err != nil {
+ return err
+ }
+ if err := resolveRequiredEnv(&cfg.Server.AdminAddress, cfg.Server.AdminAddressEnv, "server.admin_address"); err != nil {
+ return err
+ }
+ if err := resolveRequiredEnv(&cfg.Server.WebhookAddress, cfg.Server.WebhookAddressEnv, "server.webhook_address"); err != nil {
+ return err
+ }
+ if err := resolveRequiredEnv(&cfg.Server.OperationsAddress, cfg.Server.OperationsAddressEnv, "server.operations_address"); err != nil {
+ return err
+ }
+ }
+ for _, cidr := range strings.Split(os.Getenv(cfg.Server.TrustedProxyCIDRsEnv), ",") {
+ if cidr = strings.TrimSpace(cidr); cidr != "" {
+ cfg.Server.TrustedProxyCIDRs = append(cfg.Server.TrustedProxyCIDRs, cidr)
+ }
+ }
+ var err error
+ cfg.Server.RequireHTTPS, err = envBool(cfg.Server.RequireHTTPSEnv)
+ if err != nil {
+ return err
+ }
+ cfg.Server.DeploymentRegion = strings.ToLower(strings.TrimSpace(os.Getenv(cfg.Server.DeploymentRegionEnv)))
if cfg.ControlPlane.Enabled {
cfg.ControlPlane.DatabaseURL = os.Getenv(cfg.ControlPlane.DatabaseURLEnv)
cfg.ControlPlane.RedisURL = os.Getenv(cfg.ControlPlane.RedisURLEnv)
cfg.ControlPlane.CredentialKey = os.Getenv(cfg.ControlPlane.CredentialKeyEnv)
+ for _, value := range strings.Split(os.Getenv(cfg.ControlPlane.PreviousCredentialKeysEnv), ",") {
+ if value = strings.TrimSpace(value); value != "" {
+ cfg.ControlPlane.PreviousCredentialKeys = append(cfg.ControlPlane.PreviousCredentialKeys, value)
+ }
+ }
}
if cfg.Admin.Enabled {
cfg.Admin.Token = os.Getenv(cfg.Admin.TokenEnv)
@@ -325,6 +451,7 @@ func resolveSecrets(cfg *Config) error {
cfg.Admin.Mail.SMTPAddress = strings.TrimSpace(os.Getenv(cfg.Admin.Mail.SMTPAddressEnv))
cfg.Admin.Mail.SMTPUsername = os.Getenv(cfg.Admin.Mail.SMTPUsernameEnv)
cfg.Admin.Mail.SMTPPassword = os.Getenv(cfg.Admin.Mail.SMTPPasswordEnv)
+ cfg.Admin.Mail.FeedbackSecret = os.Getenv(cfg.Admin.Mail.FeedbackSecretEnv)
}
if cfg.Admin.WebAuthn.Enabled {
cfg.Admin.WebAuthn.RPID = strings.TrimSpace(os.Getenv(cfg.Admin.WebAuthn.RPIDEnv))
@@ -344,6 +471,22 @@ func resolveSecrets(cfg *Config) error {
if err := resolveRequiredEnv(&cfg.Billing.Stripe.CancelURL, cfg.Billing.Stripe.CancelURLEnv, "billing.stripe.cancel_url"); err != nil {
return err
}
+ if err := resolveRequiredEnv(&cfg.Billing.Stripe.PortalReturnURL, cfg.Billing.Stripe.PortalReturnURLEnv, "billing.stripe.portal_return_url"); err != nil {
+ return err
+ }
+ var err error
+ cfg.Billing.Stripe.AutomaticTaxEnabled, err = envBool(cfg.Billing.Stripe.AutomaticTaxEnabledEnv)
+ if err != nil {
+ return err
+ }
+ cfg.Billing.Stripe.TaxRegistrationConfirmed, err = envBool(cfg.Billing.Stripe.TaxRegistrationConfirmedEnv)
+ if err != nil {
+ return err
+ }
+ cfg.Billing.Stripe.ProductTaxCode = strings.TrimSpace(os.Getenv(cfg.Billing.Stripe.ProductTaxCodeEnv))
+ }
+ if cfg.Billing.Enabled {
+ cfg.Billing.SettlementSpoolPath = strings.TrimSpace(os.Getenv(cfg.Billing.SettlementSpoolPathEnv))
}
for i := range cfg.Providers {
provider := &cfg.Providers[i]
@@ -364,6 +507,17 @@ func resolveSecrets(cfg *Config) error {
return nil
}
+func envBool(name string) (bool, error) {
+ value := strings.TrimSpace(os.Getenv(name))
+ if value == "" || strings.EqualFold(value, "false") || value == "0" {
+ return false, nil
+ }
+ if strings.EqualFold(value, "true") || value == "1" {
+ return true, nil
+ }
+ return false, fmt.Errorf("environment variable %s must be true/false or 1/0", name)
+}
+
func resolveRequiredEnv(target *string, environment, field string) error {
if environment == "" {
return nil
@@ -383,6 +537,23 @@ func Validate(cfg Config) error {
if cfg.Observability.UsageBuffer < 1 {
return errors.New("observability.usage_buffer must be positive")
}
+ if cfg.Server.SplitListeners {
+ seen := map[string]string{}
+ for name, address := range map[string]string{"public": cfg.Server.PublicAddress, "admin": cfg.Server.AdminAddress, "webhook": cfg.Server.WebhookAddress, "operations": cfg.Server.OperationsAddress} {
+ if strings.TrimSpace(address) == "" {
+ return fmt.Errorf("server %s listener address is empty", name)
+ }
+ if previous, ok := seen[address]; ok {
+ return fmt.Errorf("server %s and %s listeners must use different addresses", previous, name)
+ }
+ seen[address] = name
+ }
+ }
+ for _, cidr := range cfg.Server.TrustedProxyCIDRs {
+ if _, _, err := net.ParseCIDR(cidr); err != nil {
+ return fmt.Errorf("invalid trusted proxy CIDR %q", cidr)
+ }
+ }
if cfg.ControlPlane.Enabled {
if cfg.ControlPlane.DatabaseURL == "" {
@@ -408,6 +579,9 @@ func Validate(cfg Config) error {
if cfg.Admin.SessionTTLHours < 1 || cfg.Admin.SessionTTLHours > 720 {
return errors.New("admin.session_ttl_hours must be between 1 and 720")
}
+ if cfg.Admin.AuditRetentionDays < 30 || cfg.Admin.SecurityRetentionDays < 1 {
+ return errors.New("admin audit retention must be at least 30 days and security retention at least 1 day")
+ }
publicURL, err := url.Parse(cfg.Admin.PublicURL)
if err != nil || publicURL.Host == "" || (publicURL.Scheme != "http" && publicURL.Scheme != "https") {
return errors.New("admin.public_url must resolve from an environment variable to an absolute http(s) URL")
@@ -419,6 +593,13 @@ func Validate(cfg Config) error {
if (cfg.Admin.Mail.SMTPUsername == "") != (cfg.Admin.Mail.SMTPPassword == "") {
return errors.New("admin.mail SMTP username and password must both be set or both be empty")
}
+ if cfg.Admin.Mail.FeedbackSecret != "" && len(cfg.Admin.Mail.FeedbackSecret) < 32 {
+ return errors.New("admin.mail feedback secret must contain at least 32 characters")
+ }
+ if cfg.Admin.Mail.LowBalanceMicros < 0 || cfg.Admin.Mail.SpendAnomalyMultiplier < 2 ||
+ cfg.Admin.Mail.SpendAnomalyMinMicros < 0 || cfg.Admin.Mail.NotificationIntervalSeconds < 30 {
+ return errors.New("admin.mail notification thresholds are invalid")
+ }
switch cfg.Admin.Mail.TLSMode {
case "starttls", "tls", "none":
default:
@@ -453,6 +634,9 @@ func Validate(cfg Config) error {
if cfg.Billing.MinTopUpMinor < 1 || cfg.Billing.MaxTopUpMinor < cfg.Billing.MinTopUpMinor {
return errors.New("billing top-up bounds are invalid")
}
+ if strings.TrimSpace(cfg.Billing.SettlementSpoolPath) == "" {
+ return fmt.Errorf("billing: environment variable %s is empty; durable settlement fallback is required", cfg.Billing.SettlementSpoolPathEnv)
+ }
if cfg.Billing.Stripe.Enabled {
if cfg.Billing.Stripe.APIKey == "" {
return fmt.Errorf("billing.stripe: environment variable %s is empty", cfg.Billing.Stripe.APIKeyEnv)
@@ -460,14 +644,17 @@ func Validate(cfg Config) error {
if cfg.Billing.Stripe.WebhookSecret == "" {
return fmt.Errorf("billing.stripe: environment variable %s is empty", cfg.Billing.Stripe.WebhookSecretEnv)
}
- if strings.TrimSpace(cfg.Billing.Stripe.SuccessURL) == "" || strings.TrimSpace(cfg.Billing.Stripe.CancelURL) == "" {
- return errors.New("billing.stripe.success_url and cancel_url are required")
+ if strings.TrimSpace(cfg.Billing.Stripe.SuccessURL) == "" || strings.TrimSpace(cfg.Billing.Stripe.CancelURL) == "" || strings.TrimSpace(cfg.Billing.Stripe.PortalReturnURL) == "" {
+ return errors.New("billing.stripe success, cancel, and portal return URLs are required")
}
- for name, value := range map[string]string{"success_url": cfg.Billing.Stripe.SuccessURL, "cancel_url": cfg.Billing.Stripe.CancelURL} {
+ for name, value := range map[string]string{"success_url": cfg.Billing.Stripe.SuccessURL, "cancel_url": cfg.Billing.Stripe.CancelURL, "portal_return_url": cfg.Billing.Stripe.PortalReturnURL} {
parsed, err := url.Parse(value)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return fmt.Errorf("billing.stripe.%s must be an absolute http(s) URL", name)
}
+ if cfg.Billing.Stripe.AutomaticTaxEnabled && (!cfg.Billing.Stripe.TaxRegistrationConfirmed || cfg.Billing.Stripe.ProductTaxCode == "") {
+ return errors.New("Stripe automatic tax requires an explicit confirmed registration and product tax code")
+ }
}
}
}