Files
sub2api/backend/internal/service/payment_config_limits.go

229 lines
7.8 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
"github.com/Wei-Shaw/sub2api/internal/payment"
)
// GetAvailableMethodLimits collects all payment types from enabled provider
// instances and returns limits for each, plus the global widest range.
// Stripe sub-types (card, link) are aggregated under "stripe".
func (s *PaymentConfigService) GetAvailableMethodLimits(ctx context.Context) (*MethodLimitsResponse, error) {
instances, err := s.entClient.PaymentProviderInstance.Query().
Where(paymentproviderinstance.EnabledEQ(true)).All(ctx)
if err != nil {
return nil, fmt.Errorf("query provider instances: %w", err)
}
typeInstances := pcGroupByPaymentType(instances)
typeInstances = pcApplyEnabledVisibleMethodInstances(typeInstances, instances)
resp := &MethodLimitsResponse{
Methods: make(map[string]MethodLimits, len(typeInstances)),
}
for pt, insts := range typeInstances {
ml := pcAggregateMethodLimits(pt, insts)
resp.Methods[ml.PaymentType] = ml
}
resp.GlobalMin, resp.GlobalMax = pcComputeGlobalRange(resp.Methods)
return resp, nil
}
func pcApplyEnabledVisibleMethodInstances(typeInstances map[string][]*dbent.PaymentProviderInstance, instances []*dbent.PaymentProviderInstance) map[string][]*dbent.PaymentProviderInstance {
if len(typeInstances) == 0 {
return typeInstances
}
filtered := make(map[string][]*dbent.PaymentProviderInstance, len(typeInstances))
for paymentType, groupedInstances := range typeInstances {
filtered[paymentType] = groupedInstances
}
for _, method := range []string{payment.TypeAlipay, payment.TypeWxpay} {
matching := filterEnabledVisibleMethodInstances(instances, method)
if len(matching) != 1 {
delete(filtered, method)
continue
}
filtered[method] = []*dbent.PaymentProviderInstance{matching[0]}
}
return filtered
}
func pcApplyVisibleMethodRouting(typeInstances map[string][]*dbent.PaymentProviderInstance, vals map[string]string, available map[string]bool) map[string][]*dbent.PaymentProviderInstance {
if len(typeInstances) == 0 {
return typeInstances
}
filtered := make(map[string][]*dbent.PaymentProviderInstance, len(typeInstances))
for paymentType, instances := range typeInstances {
visibleMethod := NormalizeVisibleMethod(paymentType)
switch visibleMethod {
case payment.TypeAlipay, payment.TypeWxpay:
if !visibleMethodShouldBeExposed(visibleMethod, vals, available) {
continue
}
targetProviderKey, ok := VisibleMethodProviderKeyForSource(visibleMethod, vals[visibleMethodSourceSettingKey(visibleMethod)])
if !ok {
continue
}
matching := make([]*dbent.PaymentProviderInstance, 0, len(instances))
for _, inst := range instances {
if inst.ProviderKey == targetProviderKey {
matching = append(matching, inst)
}
}
if len(matching) == 0 {
continue
}
filtered[paymentType] = matching
default:
filtered[paymentType] = instances
}
}
return filtered
}
// GetMethodLimits returns per-payment-type limits from enabled provider instances.
func (s *PaymentConfigService) GetMethodLimits(ctx context.Context, types []string) ([]MethodLimits, error) {
instances, err := s.entClient.PaymentProviderInstance.Query().
Where(paymentproviderinstance.EnabledEQ(true)).All(ctx)
if err != nil {
return nil, fmt.Errorf("query provider instances: %w", err)
}
result := make([]MethodLimits, 0, len(types))
for _, pt := range types {
var matching []*dbent.PaymentProviderInstance
for _, inst := range instances {
if payment.InstanceSupportsType(inst.SupportedTypes, pt) {
matching = append(matching, inst)
}
}
result = append(result, pcAggregateMethodLimits(pt, matching))
}
return result, nil
}
// pcGroupByPaymentType groups instances by user-facing payment type.
// For Stripe providers, ALL sub-types (card, link, alipay, wxpay) map to "stripe"
// because the user sees a single "Stripe" button, not individual sub-methods.
// Uses a seen set to avoid counting one instance twice.
func pcGroupByPaymentType(instances []*dbent.PaymentProviderInstance) map[string][]*dbent.PaymentProviderInstance {
typeInstances := make(map[string][]*dbent.PaymentProviderInstance)
seen := make(map[string]map[int64]bool)
add := func(key string, inst *dbent.PaymentProviderInstance) {
if seen[key] == nil {
seen[key] = make(map[int64]bool)
}
if !seen[key][int64(inst.ID)] {
seen[key][int64(inst.ID)] = true
typeInstances[key] = append(typeInstances[key], inst)
}
}
for _, inst := range instances {
// Stripe provider: all sub-types → single "stripe" group
if inst.ProviderKey == payment.TypeStripe {
add(payment.TypeStripe, inst)
continue
}
for _, t := range splitTypes(inst.SupportedTypes) {
add(t, inst)
}
}
return typeInstances
}
// pcInstanceTypeLimits extracts per-type limits from a provider instance.
// Returns (limits, true) if configured; (zero, false) if unlimited.
// For Stripe instances, limits are stored under "stripe" key regardless of sub-types.
func pcInstanceTypeLimits(inst *dbent.PaymentProviderInstance, pt string) (payment.ChannelLimits, bool) {
if inst.Limits == "" {
return payment.ChannelLimits{}, false
}
var limits payment.InstanceLimits
if err := json.Unmarshal([]byte(inst.Limits), &limits); err != nil {
return payment.ChannelLimits{}, false
}
cl, ok := limits[pt]
return cl, ok
}
// unionFloat merges a single limit value into the aggregate using UNION semantics.
// - For "min" fields (wantMin=true): keeps the lowest non-zero value
// - For "max"/"cap" fields (wantMin=false): keeps the highest non-zero value
// - If any value is 0 (unlimited), the result is unlimited.
//
// Returns (aggregated value, still limited).
func unionFloat(agg float64, limited bool, val float64, wantMin bool) (float64, bool) {
if val == 0 {
return agg, false
}
if !limited {
return agg, false
}
if agg == 0 {
return val, true
}
if wantMin && val < agg {
return val, true
}
if !wantMin && val > agg {
return val, true
}
return agg, true
}
// pcAggregateMethodLimits computes the UNION (least restrictive) of limits
// across all provider instances for a given payment type.
//
// Since the load balancer can route an order to any available instance,
// the user should see the widest possible range:
// - SingleMin: lowest floor across instances; 0 if any is unlimited
// - SingleMax: highest ceiling across instances; 0 if any is unlimited
// - DailyLimit: highest cap across instances; 0 if any is unlimited
func pcAggregateMethodLimits(pt string, instances []*dbent.PaymentProviderInstance) MethodLimits {
ml := MethodLimits{PaymentType: pt}
minLimited, maxLimited, dailyLimited := true, true, true
for _, inst := range instances {
cl, hasLimits := pcInstanceTypeLimits(inst, pt)
if !hasLimits {
return MethodLimits{PaymentType: pt} // any unlimited instance → all zeros
}
ml.SingleMin, minLimited = unionFloat(ml.SingleMin, minLimited, cl.SingleMin, true)
ml.SingleMax, maxLimited = unionFloat(ml.SingleMax, maxLimited, cl.SingleMax, false)
ml.DailyLimit, dailyLimited = unionFloat(ml.DailyLimit, dailyLimited, cl.DailyLimit, false)
}
if !minLimited {
ml.SingleMin = 0
}
if !maxLimited {
ml.SingleMax = 0
}
if !dailyLimited {
ml.DailyLimit = 0
}
return ml
}
// pcComputeGlobalRange computes the widest [min, max] across all methods.
// Uses the same union logic: lowest min, highest max, 0 if any is unlimited.
func pcComputeGlobalRange(methods map[string]MethodLimits) (globalMin, globalMax float64) {
minLimited, maxLimited := true, true
for _, ml := range methods {
globalMin, minLimited = unionFloat(globalMin, minLimited, ml.SingleMin, true)
globalMax, maxLimited = unionFloat(globalMax, maxLimited, ml.SingleMax, false)
}
if !minLimited {
globalMin = 0
}
if !maxLimited {
globalMax = 0
}
return globalMin, globalMax
}