package service import ( "context" "fmt" "strconv" "strings" dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance" "github.com/Wei-Shaw/sub2api/internal/payment" ) const ( SettingPaymentEnabled = "payment_enabled" SettingMinRechargeAmount = "MIN_RECHARGE_AMOUNT" SettingMaxRechargeAmount = "MAX_RECHARGE_AMOUNT" SettingDailyRechargeLimit = "DAILY_RECHARGE_LIMIT" SettingOrderTimeoutMinutes = "ORDER_TIMEOUT_MINUTES" SettingMaxPendingOrders = "MAX_PENDING_ORDERS" SettingEnabledPaymentTypes = "ENABLED_PAYMENT_TYPES" SettingLoadBalanceStrategy = "LOAD_BALANCE_STRATEGY" SettingBalancePayDisabled = "BALANCE_PAYMENT_DISABLED" SettingProductNamePrefix = "PRODUCT_NAME_PREFIX" SettingProductNameSuffix = "PRODUCT_NAME_SUFFIX" SettingHelpImageURL = "PAYMENT_HELP_IMAGE_URL" SettingHelpText = "PAYMENT_HELP_TEXT" SettingCancelRateLimitOn = "CANCEL_RATE_LIMIT_ENABLED" SettingCancelRateLimitMax = "CANCEL_RATE_LIMIT_MAX" SettingCancelWindowSize = "CANCEL_RATE_LIMIT_WINDOW" SettingCancelWindowUnit = "CANCEL_RATE_LIMIT_UNIT" SettingCancelWindowMode = "CANCEL_RATE_LIMIT_WINDOW_MODE" ) // Default values for payment configuration settings. const ( defaultOrderTimeoutMin = 30 defaultMaxPendingOrders = 3 ) // PaymentConfig holds the payment system configuration. type PaymentConfig struct { Enabled bool `json:"enabled"` MinAmount float64 `json:"min_amount"` MaxAmount float64 `json:"max_amount"` DailyLimit float64 `json:"daily_limit"` OrderTimeoutMin int `json:"order_timeout_minutes"` MaxPendingOrders int `json:"max_pending_orders"` EnabledTypes []string `json:"enabled_payment_types"` BalanceDisabled bool `json:"balance_disabled"` LoadBalanceStrategy string `json:"load_balance_strategy"` ProductNamePrefix string `json:"product_name_prefix"` ProductNameSuffix string `json:"product_name_suffix"` HelpImageURL string `json:"help_image_url"` HelpText string `json:"help_text"` StripePublishableKey string `json:"stripe_publishable_key,omitempty"` // Cancel rate limit settings CancelRateLimitEnabled bool `json:"cancel_rate_limit_enabled"` CancelRateLimitMax int `json:"cancel_rate_limit_max"` CancelRateLimitWindow int `json:"cancel_rate_limit_window"` CancelRateLimitUnit string `json:"cancel_rate_limit_unit"` CancelRateLimitMode string `json:"cancel_rate_limit_window_mode"` } // UpdatePaymentConfigRequest contains fields to update payment configuration. type UpdatePaymentConfigRequest struct { Enabled *bool `json:"enabled"` MinAmount *float64 `json:"min_amount"` MaxAmount *float64 `json:"max_amount"` DailyLimit *float64 `json:"daily_limit"` OrderTimeoutMin *int `json:"order_timeout_minutes"` MaxPendingOrders *int `json:"max_pending_orders"` EnabledTypes []string `json:"enabled_payment_types"` BalanceDisabled *bool `json:"balance_disabled"` LoadBalanceStrategy *string `json:"load_balance_strategy"` ProductNamePrefix *string `json:"product_name_prefix"` ProductNameSuffix *string `json:"product_name_suffix"` HelpImageURL *string `json:"help_image_url"` HelpText *string `json:"help_text"` // Cancel rate limit settings CancelRateLimitEnabled *bool `json:"cancel_rate_limit_enabled"` CancelRateLimitMax *int `json:"cancel_rate_limit_max"` CancelRateLimitWindow *int `json:"cancel_rate_limit_window"` CancelRateLimitUnit *string `json:"cancel_rate_limit_unit"` CancelRateLimitMode *string `json:"cancel_rate_limit_window_mode"` } // MethodLimits holds per-payment-type limits. type MethodLimits struct { PaymentType string `json:"payment_type"` FeeRate float64 `json:"fee_rate"` DailyLimit float64 `json:"daily_limit"` SingleMin float64 `json:"single_min"` SingleMax float64 `json:"single_max"` } // MethodLimitsResponse is the full response for the user-facing /limits API. // It includes per-method limits and the global widest range (union of all methods). type MethodLimitsResponse struct { Methods map[string]MethodLimits `json:"methods"` GlobalMin float64 `json:"global_min"` // 0 = no minimum GlobalMax float64 `json:"global_max"` // 0 = no maximum } type CreateProviderInstanceRequest struct { ProviderKey string `json:"provider_key"` Name string `json:"name"` Config map[string]string `json:"config"` SupportedTypes []string `json:"supported_types"` Enabled bool `json:"enabled"` PaymentMode string `json:"payment_mode"` SortOrder int `json:"sort_order"` Limits string `json:"limits"` RefundEnabled bool `json:"refund_enabled"` } type UpdateProviderInstanceRequest struct { Name *string `json:"name"` Config map[string]string `json:"config"` SupportedTypes []string `json:"supported_types"` Enabled *bool `json:"enabled"` PaymentMode *string `json:"payment_mode"` SortOrder *int `json:"sort_order"` Limits *string `json:"limits"` RefundEnabled *bool `json:"refund_enabled"` } type CreatePlanRequest struct { GroupID int64 `json:"group_id"` Name string `json:"name"` Description string `json:"description"` Price float64 `json:"price"` OriginalPrice *float64 `json:"original_price"` ValidityDays int `json:"validity_days"` ValidityUnit string `json:"validity_unit"` Features string `json:"features"` ProductName string `json:"product_name"` ForSale bool `json:"for_sale"` SortOrder int `json:"sort_order"` } type UpdatePlanRequest struct { GroupID *int64 `json:"group_id"` Name *string `json:"name"` Description *string `json:"description"` Price *float64 `json:"price"` OriginalPrice *float64 `json:"original_price"` ValidityDays *int `json:"validity_days"` ValidityUnit *string `json:"validity_unit"` Features *string `json:"features"` ProductName *string `json:"product_name"` ForSale *bool `json:"for_sale"` SortOrder *int `json:"sort_order"` } // PaymentConfigService manages payment configuration and CRUD for // provider instances, channels, and subscription plans. type PaymentConfigService struct { entClient *dbent.Client settingRepo SettingRepository encryptionKey []byte } // NewPaymentConfigService creates a new PaymentConfigService. func NewPaymentConfigService(entClient *dbent.Client, settingRepo SettingRepository, encryptionKey []byte) *PaymentConfigService { return &PaymentConfigService{entClient: entClient, settingRepo: settingRepo, encryptionKey: encryptionKey} } // IsPaymentEnabled returns whether the payment system is enabled. func (s *PaymentConfigService) IsPaymentEnabled(ctx context.Context) bool { val, err := s.settingRepo.GetValue(ctx, SettingPaymentEnabled) if err != nil { return false } return val == "true" } // GetPaymentConfig returns the full payment configuration. func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentConfig, error) { keys := []string{ SettingPaymentEnabled, SettingMinRechargeAmount, SettingMaxRechargeAmount, SettingDailyRechargeLimit, SettingOrderTimeoutMinutes, SettingMaxPendingOrders, SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingLoadBalanceStrategy, SettingProductNamePrefix, SettingProductNameSuffix, SettingHelpImageURL, SettingHelpText, SettingCancelRateLimitOn, SettingCancelRateLimitMax, SettingCancelWindowSize, SettingCancelWindowUnit, SettingCancelWindowMode, } vals, err := s.settingRepo.GetMultiple(ctx, keys) if err != nil { return nil, fmt.Errorf("get payment config settings: %w", err) } cfg := s.parsePaymentConfig(vals) // Load Stripe publishable key from the first enabled Stripe provider instance cfg.StripePublishableKey = s.getStripePublishableKey(ctx) return cfg, nil } func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *PaymentConfig { cfg := &PaymentConfig{ Enabled: vals[SettingPaymentEnabled] == "true", MinAmount: pcParseFloat(vals[SettingMinRechargeAmount], 1), MaxAmount: pcParseFloat(vals[SettingMaxRechargeAmount], 0), DailyLimit: pcParseFloat(vals[SettingDailyRechargeLimit], 0), OrderTimeoutMin: pcParseInt(vals[SettingOrderTimeoutMinutes], defaultOrderTimeoutMin), MaxPendingOrders: pcParseInt(vals[SettingMaxPendingOrders], defaultMaxPendingOrders), BalanceDisabled: vals[SettingBalancePayDisabled] == "true", LoadBalanceStrategy: vals[SettingLoadBalanceStrategy], ProductNamePrefix: vals[SettingProductNamePrefix], ProductNameSuffix: vals[SettingProductNameSuffix], HelpImageURL: vals[SettingHelpImageURL], HelpText: vals[SettingHelpText], CancelRateLimitEnabled: vals[SettingCancelRateLimitOn] == "true", CancelRateLimitMax: pcParseInt(vals[SettingCancelRateLimitMax], 10), CancelRateLimitWindow: pcParseInt(vals[SettingCancelWindowSize], 1), CancelRateLimitUnit: vals[SettingCancelWindowUnit], CancelRateLimitMode: vals[SettingCancelWindowMode], } if cfg.LoadBalanceStrategy == "" { cfg.LoadBalanceStrategy = payment.DefaultLoadBalanceStrategy } if raw := vals[SettingEnabledPaymentTypes]; raw != "" { for _, t := range strings.Split(raw, ",") { t = strings.TrimSpace(t) if t != "" { cfg.EnabledTypes = append(cfg.EnabledTypes, t) } } } return cfg } // getStripePublishableKey finds the publishable key from the first enabled Stripe provider instance. func (s *PaymentConfigService) getStripePublishableKey(ctx context.Context) string { instances, err := s.entClient.PaymentProviderInstance.Query(). Where( paymentproviderinstance.EnabledEQ(true), paymentproviderinstance.ProviderKeyEQ(payment.TypeStripe), ).Limit(1).All(ctx) if err != nil || len(instances) == 0 { return "" } cfg, err := s.decryptConfig(instances[0].Config) if err != nil || cfg == nil { return "" } return cfg[payment.ConfigKeyPublishableKey] } // UpdatePaymentConfig updates the payment configuration settings. // NOTE: This function exceeds 30 lines because each field requires an independent // nil-check before serialisation — this is inherent to patch-style update patterns // and cannot be meaningfully decomposed without introducing unnecessary abstraction. func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req UpdatePaymentConfigRequest) error { m := map[string]string{ SettingPaymentEnabled: formatBoolOrEmpty(req.Enabled), SettingMinRechargeAmount: formatPositiveFloat(req.MinAmount), SettingMaxRechargeAmount: formatPositiveFloat(req.MaxAmount), SettingDailyRechargeLimit: formatPositiveFloat(req.DailyLimit), SettingOrderTimeoutMinutes: formatPositiveInt(req.OrderTimeoutMin), SettingMaxPendingOrders: formatPositiveInt(req.MaxPendingOrders), SettingBalancePayDisabled: formatBoolOrEmpty(req.BalanceDisabled), SettingLoadBalanceStrategy: derefStr(req.LoadBalanceStrategy), SettingProductNamePrefix: derefStr(req.ProductNamePrefix), SettingProductNameSuffix: derefStr(req.ProductNameSuffix), SettingHelpImageURL: derefStr(req.HelpImageURL), SettingHelpText: derefStr(req.HelpText), SettingCancelRateLimitOn: formatBoolOrEmpty(req.CancelRateLimitEnabled), SettingCancelRateLimitMax: formatPositiveInt(req.CancelRateLimitMax), SettingCancelWindowSize: formatPositiveInt(req.CancelRateLimitWindow), SettingCancelWindowUnit: derefStr(req.CancelRateLimitUnit), SettingCancelWindowMode: derefStr(req.CancelRateLimitMode), } if req.EnabledTypes != nil { m[SettingEnabledPaymentTypes] = strings.Join(req.EnabledTypes, ",") } else { m[SettingEnabledPaymentTypes] = "" } return s.settingRepo.SetMultiple(ctx, m) } func formatBoolOrEmpty(v *bool) string { if v == nil { return "" } return strconv.FormatBool(*v) } func formatPositiveFloat(v *float64) string { if v == nil || *v <= 0 { return "" // empty → parsePaymentConfig uses default } return strconv.FormatFloat(*v, 'f', 2, 64) } func formatPositiveInt(v *int) string { if v == nil || *v <= 0 { return "" } return strconv.Itoa(*v) } func derefStr(v *string) string { if v == nil { return "" } return *v } func splitTypes(s string) []string { if s == "" { return nil } parts := strings.Split(s, ",") result := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { result = append(result, p) } } return result } func joinTypes(types []string) string { return strings.Join(types, ",") } func pcParseFloat(s string, defaultVal float64) float64 { if s == "" { return defaultVal } v, err := strconv.ParseFloat(s, 64) if err != nil { return defaultVal } return v } func pcParseInt(s string, defaultVal int) int { if s == "" { return defaultVal } v, err := strconv.Atoi(s) if err != nil { return defaultVal } return v }