| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- package service
- import (
- "context"
- "testing"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/internal/payment"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/stretchr/testify/require"
- )
- func TestUnionFloat(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- agg float64
- limited bool
- val float64
- wantMin bool
- wantAgg float64
- wantLimited bool
- }{
- {"first non-zero value", 0, true, 5, true, 5, true},
- {"lower min replaces", 10, true, 3, true, 3, true},
- {"higher min does not replace", 3, true, 10, true, 3, true},
- {"higher max replaces", 10, true, 20, false, 20, true},
- {"lower max does not replace", 20, true, 10, false, 20, true},
- {"zero value makes unlimited", 5, true, 0, true, 5, false},
- {"already unlimited stays unlimited", 5, false, 10, true, 5, false},
- {"zero on first call", 0, true, 0, true, 0, false},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- gotAgg, gotLimited := unionFloat(tt.agg, tt.limited, tt.val, tt.wantMin)
- if gotAgg != tt.wantAgg || gotLimited != tt.wantLimited {
- t.Fatalf("unionFloat(%v, %v, %v, %v) = (%v, %v), want (%v, %v)",
- tt.agg, tt.limited, tt.val, tt.wantMin,
- gotAgg, gotLimited, tt.wantAgg, tt.wantLimited)
- }
- })
- }
- }
- func makeInstance(id int64, providerKey, supportedTypes, limits string) *dbent.PaymentProviderInstance {
- return &dbent.PaymentProviderInstance{
- ID: id,
- ProviderKey: providerKey,
- SupportedTypes: supportedTypes,
- Limits: limits,
- Enabled: true,
- }
- }
- func TestPcAggregateMethodLimits(t *testing.T) {
- t.Parallel()
- t.Run("single instance with limits", func(t *testing.T) {
- t.Parallel()
- inst := makeInstance(1, "easypay", "alipay,wxpay",
- `{"alipay":{"singleMin":2,"singleMax":14},"wxpay":{"singleMin":1,"singleMax":12}}`)
- ml := pcAggregateMethodLimits("alipay", []*dbent.PaymentProviderInstance{inst})
- if ml.SingleMin != 2 || ml.SingleMax != 14 {
- t.Fatalf("alipay limits = min:%v max:%v, want min:2 max:14", ml.SingleMin, ml.SingleMax)
- }
- })
- t.Run("two instances union takes widest range", func(t *testing.T) {
- t.Parallel()
- inst1 := makeInstance(1, "easypay", "alipay,wxpay",
- `{"alipay":{"singleMin":5,"singleMax":100}}`)
- inst2 := makeInstance(2, "easypay", "alipay,wxpay",
- `{"alipay":{"singleMin":2,"singleMax":200}}`)
- ml := pcAggregateMethodLimits("alipay", []*dbent.PaymentProviderInstance{inst1, inst2})
- if ml.SingleMin != 2 {
- t.Fatalf("SingleMin = %v, want 2 (lowest floor)", ml.SingleMin)
- }
- if ml.SingleMax != 200 {
- t.Fatalf("SingleMax = %v, want 200 (highest ceiling)", ml.SingleMax)
- }
- })
- t.Run("one instance unlimited makes aggregate unlimited", func(t *testing.T) {
- t.Parallel()
- inst1 := makeInstance(1, "easypay", "wxpay",
- `{"wxpay":{"singleMin":3,"singleMax":10}}`)
- inst2 := makeInstance(2, "easypay", "wxpay", "") // no limits = unlimited
- ml := pcAggregateMethodLimits("wxpay", []*dbent.PaymentProviderInstance{inst1, inst2})
- if ml.SingleMin != 0 || ml.SingleMax != 0 {
- t.Fatalf("limits = min:%v max:%v, want min:0 max:0 (unlimited)", ml.SingleMin, ml.SingleMax)
- }
- })
- t.Run("one field unlimited others limited", func(t *testing.T) {
- t.Parallel()
- inst1 := makeInstance(1, "easypay", "alipay",
- `{"alipay":{"singleMin":5,"singleMax":100}}`)
- inst2 := makeInstance(2, "easypay", "alipay",
- `{"alipay":{"singleMin":3,"singleMax":0}}`) // singleMax=0 = unlimited
- ml := pcAggregateMethodLimits("alipay", []*dbent.PaymentProviderInstance{inst1, inst2})
- if ml.SingleMin != 3 {
- t.Fatalf("SingleMin = %v, want 3 (lowest floor)", ml.SingleMin)
- }
- if ml.SingleMax != 0 {
- t.Fatalf("SingleMax = %v, want 0 (unlimited)", ml.SingleMax)
- }
- })
- t.Run("empty instances returns zeros", func(t *testing.T) {
- t.Parallel()
- ml := pcAggregateMethodLimits("alipay", nil)
- if ml.SingleMin != 0 || ml.SingleMax != 0 || ml.DailyLimit != 0 {
- t.Fatalf("empty instances should return all zeros, got %+v", ml)
- }
- })
- t.Run("invalid JSON treated as unlimited", func(t *testing.T) {
- t.Parallel()
- inst := makeInstance(1, "easypay", "alipay", `{invalid json}`)
- ml := pcAggregateMethodLimits("alipay", []*dbent.PaymentProviderInstance{inst})
- if ml.SingleMin != 0 || ml.SingleMax != 0 {
- t.Fatalf("invalid JSON should be treated as unlimited, got %+v", ml)
- }
- })
- t.Run("type not in limits JSON treated as unlimited", func(t *testing.T) {
- t.Parallel()
- inst := makeInstance(1, "easypay", "alipay,wxpay",
- `{"wxpay":{"singleMin":1,"singleMax":10}}`) // only wxpay, no alipay
- ml := pcAggregateMethodLimits("alipay", []*dbent.PaymentProviderInstance{inst})
- if ml.SingleMin != 0 || ml.SingleMax != 0 {
- t.Fatalf("missing type should be treated as unlimited, got %+v", ml)
- }
- })
- t.Run("daily limit aggregation", func(t *testing.T) {
- t.Parallel()
- inst1 := makeInstance(1, "easypay", "alipay",
- `{"alipay":{"singleMin":1,"singleMax":100,"dailyLimit":500}}`)
- inst2 := makeInstance(2, "easypay", "alipay",
- `{"alipay":{"singleMin":2,"singleMax":200,"dailyLimit":1000}}`)
- ml := pcAggregateMethodLimits("alipay", []*dbent.PaymentProviderInstance{inst1, inst2})
- if ml.DailyLimit != 1000 {
- t.Fatalf("DailyLimit = %v, want 1000 (highest cap)", ml.DailyLimit)
- }
- })
- }
- func TestPcGroupByPaymentType(t *testing.T) {
- t.Parallel()
- t.Run("stripe instance maps all types to stripe group", func(t *testing.T) {
- t.Parallel()
- stripe := makeInstance(1, payment.TypeStripe, "card,alipay,link,wxpay", "")
- easypay := makeInstance(2, payment.TypeEasyPay, "alipay,wxpay", "")
- groups := pcGroupByPaymentType([]*dbent.PaymentProviderInstance{stripe, easypay})
- // Stripe instance should only be in "stripe" group
- if len(groups[payment.TypeStripe]) != 1 || groups[payment.TypeStripe][0].ID != 1 {
- t.Fatalf("stripe group should contain only stripe instance, got %v", groups[payment.TypeStripe])
- }
- // alipay group should only contain easypay, NOT stripe
- if len(groups[payment.TypeAlipay]) != 1 || groups[payment.TypeAlipay][0].ID != 2 {
- t.Fatalf("alipay group should contain only easypay instance, got %v", groups[payment.TypeAlipay])
- }
- // wxpay group should only contain easypay, NOT stripe
- if len(groups[payment.TypeWxpay]) != 1 || groups[payment.TypeWxpay][0].ID != 2 {
- t.Fatalf("wxpay group should contain only easypay instance, got %v", groups[payment.TypeWxpay])
- }
- })
- t.Run("multiple easypay instances in same groups", func(t *testing.T) {
- t.Parallel()
- ep1 := makeInstance(1, payment.TypeEasyPay, "alipay,wxpay", "")
- ep2 := makeInstance(2, payment.TypeEasyPay, "alipay,wxpay", "")
- groups := pcGroupByPaymentType([]*dbent.PaymentProviderInstance{ep1, ep2})
- if len(groups[payment.TypeAlipay]) != 2 {
- t.Fatalf("alipay group should have 2 instances, got %d", len(groups[payment.TypeAlipay]))
- }
- if len(groups[payment.TypeWxpay]) != 2 {
- t.Fatalf("wxpay group should have 2 instances, got %d", len(groups[payment.TypeWxpay]))
- }
- })
- t.Run("stripe with no supported types still in stripe group", func(t *testing.T) {
- t.Parallel()
- stripe := makeInstance(1, payment.TypeStripe, "", "")
- groups := pcGroupByPaymentType([]*dbent.PaymentProviderInstance{stripe})
- if len(groups[payment.TypeStripe]) != 1 {
- t.Fatalf("stripe with empty types should still be in stripe group, got %v", groups)
- }
- })
- }
- func TestPcAggregateMethodCurrency(t *testing.T) {
- t.Parallel()
- svc := &PaymentConfigService{}
- stripe := makeInstance(1, payment.TypeStripe, payment.TypeStripe, "")
- stripe.Config = `{"currency":"hkd"}`
- currency, ok := svc.pcAggregateMethodCurrency([]*dbent.PaymentProviderInstance{stripe})
- require.True(t, ok)
- require.Equal(t, "HKD", currency)
- airwallex := makeInstance(2, payment.TypeAirwallex, payment.TypeAirwallex, "")
- airwallex.Config = `{"currency":"usd"}`
- currency, ok = svc.pcAggregateMethodCurrency([]*dbent.PaymentProviderInstance{stripe, airwallex})
- require.False(t, ok)
- require.Empty(t, currency)
- easypay := makeInstance(3, payment.TypeEasyPay, payment.TypeAlipay, "")
- currency, ok = svc.pcAggregateMethodCurrency([]*dbent.PaymentProviderInstance{easypay})
- require.True(t, ok)
- require.Equal(t, payment.DefaultPaymentCurrency, currency)
- }
- func TestGetAvailableMethodLimitsOmitsMixedCurrencyMethod(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeStripe).
- SetName("Stripe HKD").
- SetConfig(`{"currency":"HKD"}`).
- SetSupportedTypes("card,link").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeStripe).
- SetName("Stripe USD").
- SetConfig(`{"currency":"USD"}`).
- SetSupportedTypes("card,link").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- svc := &PaymentConfigService{entClient: client}
- resp, err := svc.GetAvailableMethodLimits(ctx)
- require.NoError(t, err)
- require.NotContains(t, resp.Methods, payment.TypeStripe)
- _, err = svc.ValidateMethodCurrencyConsistency(ctx, payment.TypeStripe)
- require.Error(t, err)
- appErr := infraerrors.FromError(err)
- require.Equal(t, "PAYMENT_METHOD_CURRENCY_CONFLICT", appErr.Reason)
- }
- func TestGetAvailableMethodLimitsIncludesEasyPayCustomMethodDisplayName(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeEasyPay).
- SetName("EasyPay Custom").
- SetConfig(`{"customMethods":"[{\"type\":\"ldc\",\"upstreamType\":\"ldc\",\"displayName\":\"LDC Pay\"}]"}`).
- SetSupportedTypes("alipay,wxpay,ldc").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- svc := &PaymentConfigService{entClient: client}
- resp, err := svc.GetAvailableMethodLimits(ctx)
- require.NoError(t, err)
- limits, ok := resp.Methods["ldc"]
- require.True(t, ok, "expected custom EasyPay method limits to be visible")
- require.Equal(t, "LDC Pay", limits.DisplayName)
- }
- func TestPcComputeGlobalRange(t *testing.T) {
- t.Parallel()
- t.Run("all methods have limits", func(t *testing.T) {
- t.Parallel()
- methods := map[string]MethodLimits{
- "alipay": {SingleMin: 2, SingleMax: 14},
- "wxpay": {SingleMin: 1, SingleMax: 12},
- "stripe": {SingleMin: 5, SingleMax: 100},
- }
- gMin, gMax := pcComputeGlobalRange(methods)
- if gMin != 1 {
- t.Fatalf("global min = %v, want 1 (lowest floor)", gMin)
- }
- if gMax != 100 {
- t.Fatalf("global max = %v, want 100 (highest ceiling)", gMax)
- }
- })
- t.Run("one method unlimited makes global unlimited", func(t *testing.T) {
- t.Parallel()
- methods := map[string]MethodLimits{
- "alipay": {SingleMin: 2, SingleMax: 14},
- "stripe": {SingleMin: 0, SingleMax: 0}, // unlimited
- }
- gMin, gMax := pcComputeGlobalRange(methods)
- if gMin != 0 {
- t.Fatalf("global min = %v, want 0 (unlimited)", gMin)
- }
- if gMax != 0 {
- t.Fatalf("global max = %v, want 0 (unlimited)", gMax)
- }
- })
- t.Run("empty methods returns zeros", func(t *testing.T) {
- t.Parallel()
- gMin, gMax := pcComputeGlobalRange(map[string]MethodLimits{})
- if gMin != 0 || gMax != 0 {
- t.Fatalf("empty methods should return (0, 0), got (%v, %v)", gMin, gMax)
- }
- })
- t.Run("only min unlimited", func(t *testing.T) {
- t.Parallel()
- methods := map[string]MethodLimits{
- "alipay": {SingleMin: 0, SingleMax: 100},
- "wxpay": {SingleMin: 5, SingleMax: 50},
- }
- gMin, gMax := pcComputeGlobalRange(methods)
- if gMin != 0 {
- t.Fatalf("global min = %v, want 0 (unlimited)", gMin)
- }
- if gMax != 100 {
- t.Fatalf("global max = %v, want 100", gMax)
- }
- })
- }
- func TestPcInstanceTypeLimits(t *testing.T) {
- t.Parallel()
- t.Run("empty limits string returns false", func(t *testing.T) {
- t.Parallel()
- inst := makeInstance(1, "easypay", "alipay", "")
- _, ok := pcInstanceTypeLimits(inst, "alipay")
- if ok {
- t.Fatal("expected ok=false for empty limits")
- }
- })
- t.Run("type found returns correct values", func(t *testing.T) {
- t.Parallel()
- inst := makeInstance(1, "easypay", "alipay",
- `{"alipay":{"singleMin":2,"singleMax":14,"dailyLimit":500}}`)
- cl, ok := pcInstanceTypeLimits(inst, "alipay")
- if !ok {
- t.Fatal("expected ok=true")
- }
- if cl.SingleMin != 2 || cl.SingleMax != 14 || cl.DailyLimit != 500 {
- t.Fatalf("limits = %+v, want min:2 max:14 daily:500", cl)
- }
- })
- t.Run("type not found returns false", func(t *testing.T) {
- t.Parallel()
- inst := makeInstance(1, "easypay", "alipay",
- `{"wxpay":{"singleMin":1}}`)
- _, ok := pcInstanceTypeLimits(inst, "alipay")
- if ok {
- t.Fatal("expected ok=false for missing type")
- }
- })
- t.Run("invalid JSON returns false", func(t *testing.T) {
- t.Parallel()
- inst := makeInstance(1, "easypay", "alipay", `{bad json}`)
- _, ok := pcInstanceTypeLimits(inst, "alipay")
- if ok {
- t.Fatal("expected ok=false for invalid JSON")
- }
- })
- }
- func TestGetAvailableMethodLimitsUsesConfiguredVisibleMethodSource(t *testing.T) {
- tests := []struct {
- name string
- sourceSetting string
- wantAlipaySingleMin float64
- wantAlipaySingleMax float64
- wantGlobalMin float64
- wantGlobalMax float64
- }{
- {
- name: "official source",
- sourceSetting: VisibleMethodSourceOfficialAlipay,
- wantAlipaySingleMin: 10,
- wantAlipaySingleMax: 100,
- wantGlobalMin: 10,
- wantGlobalMax: 300,
- },
- {
- name: "easypay source",
- sourceSetting: VisibleMethodSourceEasyPayAlipay,
- wantAlipaySingleMin: 20,
- wantAlipaySingleMax: 200,
- wantGlobalMin: 20,
- wantGlobalMax: 300,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeAlipay).
- SetName("Official Alipay").
- SetConfig("{}").
- SetSupportedTypes("alipay").
- SetLimits(`{"alipay":{"singleMin":10,"singleMax":100}}`).
- SetEnabled(true).
- Save(ctx)
- if err != nil {
- t.Fatalf("create official alipay instance: %v", err)
- }
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeEasyPay).
- SetName("EasyPay Alipay").
- SetConfig("{}").
- SetSupportedTypes("alipay").
- SetLimits(`{"alipay":{"singleMin":20,"singleMax":200}}`).
- SetEnabled(true).
- Save(ctx)
- if err != nil {
- t.Fatalf("create easypay alipay instance: %v", err)
- }
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeWxpay).
- SetName("Official WeChat").
- SetConfig("{}").
- SetSupportedTypes("wxpay").
- SetLimits(`{"wxpay":{"singleMin":30,"singleMax":300}}`).
- SetEnabled(true).
- Save(ctx)
- if err != nil {
- t.Fatalf("create official wxpay instance: %v", err)
- }
- svc := &PaymentConfigService{
- entClient: client,
- settingRepo: &paymentConfigSettingRepoStub{
- values: map[string]string{
- SettingPaymentVisibleMethodAlipaySource: tt.sourceSetting,
- },
- },
- }
- resp, err := svc.GetAvailableMethodLimits(ctx)
- if err != nil {
- t.Fatalf("GetAvailableMethodLimits returned error: %v", err)
- }
- alipayLimits, ok := resp.Methods[payment.TypeAlipay]
- if !ok {
- t.Fatalf("expected alipay limits to remain visible, got %v", resp.Methods)
- }
- if alipayLimits.SingleMin != tt.wantAlipaySingleMin || alipayLimits.SingleMax != tt.wantAlipaySingleMax {
- t.Fatalf("alipay limits = %+v, want min=%v max=%v", alipayLimits, tt.wantAlipaySingleMin, tt.wantAlipaySingleMax)
- }
- wxpayLimits, ok := resp.Methods[payment.TypeWxpay]
- if !ok {
- t.Fatalf("expected wxpay limits to remain visible, got %v", resp.Methods)
- }
- if wxpayLimits.SingleMin != 30 || wxpayLimits.SingleMax != 300 {
- t.Fatalf("wxpay limits = %+v, want official-only min=30 max=300", wxpayLimits)
- }
- if resp.GlobalMin != tt.wantGlobalMin || resp.GlobalMax != tt.wantGlobalMax {
- t.Fatalf("global range = (%v, %v), want (%v, %v)", resp.GlobalMin, resp.GlobalMax, tt.wantGlobalMin, tt.wantGlobalMax)
- }
- })
- }
- }
- func TestGetAvailableMethodLimitsPreservesLegacyCrossProviderBehaviorWhenVisibleMethodSourceMissing(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeAlipay).
- SetName("Official Alipay").
- SetConfig("{}").
- SetSupportedTypes("alipay").
- SetLimits(`{"alipay":{"singleMin":10,"singleMax":100}}`).
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeEasyPay).
- SetName("EasyPay Mixed").
- SetConfig("{}").
- SetSupportedTypes("alipay,wxpay").
- SetLimits(`{"alipay":{"singleMin":20,"singleMax":200},"wxpay":{"singleMin":40,"singleMax":400}}`).
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeWxpay).
- SetName("Official WeChat").
- SetConfig("{}").
- SetSupportedTypes("wxpay").
- SetLimits(`{"wxpay":{"singleMin":30,"singleMax":300}}`).
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- svc := &PaymentConfigService{
- entClient: client,
- settingRepo: &paymentConfigSettingRepoStub{values: map[string]string{}},
- }
- resp, err := svc.GetAvailableMethodLimits(ctx)
- require.NoError(t, err)
- alipayLimits, ok := resp.Methods[payment.TypeAlipay]
- require.True(t, ok, "expected alipay limits to remain visible")
- require.Equal(t, 10.0, alipayLimits.SingleMin)
- require.Equal(t, 200.0, alipayLimits.SingleMax)
- wxpayLimits, ok := resp.Methods[payment.TypeWxpay]
- require.True(t, ok, "expected wxpay limits to remain visible")
- require.Equal(t, 30.0, wxpayLimits.SingleMin)
- require.Equal(t, 400.0, wxpayLimits.SingleMax)
- require.Equal(t, 10.0, resp.GlobalMin)
- require.Equal(t, 400.0, resp.GlobalMax)
- }
|