229 lines
7.8 KiB
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
|
|
}
|