fix(wallet): share lalu payment country configs

This commit is contained in:
zhx 2026-07-14 17:57:08 +08:00
parent d66799b312
commit bb5efebabd
5 changed files with 100 additions and 6 deletions

View File

@ -55,7 +55,7 @@ func (s *Service) createMifaPayRechargeOrder(ctx context.Context, command ledger
if s.mifaPay == nil { if s.mifaPay == nil {
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "mifapay client is not configured") return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "mifapay client is not configured")
} }
method, err := s.repository.GetThirdPartyPaymentMethod(ctx, command.AppCode, command.PaymentMethodID) method, err := s.sharedPaymentMethod(ctx, command.AppCode, ledger.PaymentProviderMifaPay, command.PaymentMethodID)
if err != nil { if err != nil {
return ledger.ExternalRechargeOrder{}, err return ledger.ExternalRechargeOrder{}, err
} }
@ -106,7 +106,7 @@ func (s *Service) createV5PayRechargeOrder(ctx context.Context, command ledger.C
if s.v5Pay == nil { if s.v5Pay == nil {
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "v5pay client is not configured") return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "v5pay client is not configured")
} }
method, err := s.repository.GetThirdPartyPaymentMethod(ctx, command.AppCode, command.PaymentMethodID) method, err := s.sharedPaymentMethod(ctx, command.AppCode, ledger.PaymentProviderV5Pay, command.PaymentMethodID)
if err != nil { if err != nil {
return ledger.ExternalRechargeOrder{}, err return ledger.ExternalRechargeOrder{}, err
} }

View File

@ -8,6 +8,20 @@ import (
"strings" "strings"
) )
// sharedPaymentConfigAppCode 是三方收单方式的唯一国家/币种配置 owner。
// Yumi、Aslan 等 App 可以有自己的商品和用户区域,但不能复制一套 V5Pay/MiFaPay
// 国家、币种或汇率配置,否则人工收款与 H5 下单会因配置漂移得到不同的支付结果。
const sharedPaymentConfigAppCode = appcode.Default
func usesSharedPaymentConfig(providerCode string) bool {
switch ledger.NormalizePaymentProvider(providerCode) {
case ledger.PaymentProviderMifaPay, ledger.PaymentProviderV5Pay:
return true
default:
return false
}
}
// ListThirdPartyPaymentChannels 返回后台三方支付管理页需要的渠道、国家和方式树。 // ListThirdPartyPaymentChannels 返回后台三方支付管理页需要的渠道、国家和方式树。
func (s *Service) ListThirdPartyPaymentChannels(ctx context.Context, query ledger.ListThirdPartyPaymentChannelsQuery) ([]ledger.ThirdPartyPaymentChannel, error) { func (s *Service) ListThirdPartyPaymentChannels(ctx context.Context, query ledger.ListThirdPartyPaymentChannelsQuery) ([]ledger.ThirdPartyPaymentChannel, error) {
if s.repository == nil { if s.repository == nil {
@ -20,8 +34,49 @@ func (s *Service) ListThirdPartyPaymentChannels(ctx context.Context, query ledge
return nil, xerr.New(xerr.InvalidArgument, "status is invalid") return nil, xerr.New(xerr.InvalidArgument, "status is invalid")
} }
} }
// 指定渠道时V5Pay/MiFaPay 的国家和币种统一由 Lalu 维护。读取端始终切到
// 该配置 owner避免某个 App 没有复制支付方式时,临时链接和人工查单退化为无候选可用。
if usesSharedPaymentConfig(query.ProviderCode) {
query.AppCode = sharedPaymentConfigAppCode
ctx = appcode.WithContext(ctx, query.AppCode)
return s.repository.ListThirdPartyPaymentChannels(ctx, query)
}
ctx = appcode.WithContext(ctx, query.AppCode) ctx = appcode.WithContext(ctx, query.AppCode)
return s.repository.ListThirdPartyPaymentChannels(ctx, query) channels, err := s.repository.ListThirdPartyPaymentChannels(ctx, query)
if err != nil || query.AppCode == sharedPaymentConfigAppCode || query.ProviderCode != "" {
return channels, err
}
// 未指定渠道的临时收款页面仍要同时展示两个共享渠道。保留当前 App 的非共享渠道,
// 但移除其历史副本,确保 UI 不会把同一国家/币种配置展示为两套来源。
filtered := make([]ledger.ThirdPartyPaymentChannel, 0, len(channels)+2)
for _, channel := range channels {
if !usesSharedPaymentConfig(channel.ProviderCode) {
filtered = append(filtered, channel)
}
}
for _, providerCode := range []string{ledger.PaymentProviderMifaPay, ledger.PaymentProviderV5Pay} {
sharedQuery := query
sharedQuery.AppCode = sharedPaymentConfigAppCode
sharedQuery.ProviderCode = providerCode
sharedCtx := appcode.WithContext(ctx, sharedPaymentConfigAppCode)
sharedChannels, err := s.repository.ListThirdPartyPaymentChannels(sharedCtx, sharedQuery)
if err != nil {
return nil, err
}
filtered = append(filtered, sharedChannels...)
}
return filtered, nil
}
// sharedPaymentMethod 返回支付配置 owner 的方式。订单本身仍按 command.AppCode 落库,
// 因此只共享国家/币种、产品类型和汇率,不会把 Yumi/Aslan 的账务事实写入 Lalu。
func (s *Service) sharedPaymentMethod(ctx context.Context, appCode string, providerCode string, methodID int64) (ledger.ThirdPartyPaymentMethod, error) {
configAppCode := appcode.Normalize(appCode)
if usesSharedPaymentConfig(providerCode) {
configAppCode = sharedPaymentConfigAppCode
}
return s.repository.GetThirdPartyPaymentMethod(ctx, configAppCode, methodID)
} }
func (s *Service) SetThirdPartyPaymentMethodStatus(ctx context.Context, command ledger.ThirdPartyPaymentMethodStatusCommand) (ledger.ThirdPartyPaymentMethod, error) { func (s *Service) SetThirdPartyPaymentMethodStatus(ctx context.Context, command ledger.ThirdPartyPaymentMethodStatusCommand) (ledger.ThirdPartyPaymentMethod, error) {

View File

@ -282,7 +282,7 @@ func (s *Service) v5PayReceiptQueryCandidates(ctx context.Context, command ledge
if s.repository == nil { if s.repository == nil {
return candidates, nil return candidates, nil
} }
channels, err := s.repository.ListThirdPartyPaymentChannels(ctx, ledger.ListThirdPartyPaymentChannelsQuery{ channels, err := s.ListThirdPartyPaymentChannels(ctx, ledger.ListThirdPartyPaymentChannelsQuery{
AppCode: command.AppCode, AppCode: command.AppCode,
ProviderCode: ledger.PaymentProviderV5Pay, ProviderCode: ledger.PaymentProviderV5Pay,
Status: ledger.ThirdPartyPaymentStatusActive, Status: ledger.ThirdPartyPaymentStatusActive,

View File

@ -427,6 +427,45 @@ func TestVerifyCoinSellerRechargeReceiptV5PayWithoutCountryCurrencyUsesEnabledCa
} }
} }
func TestVerifyCoinSellerRechargeReceiptV5PayUsesLaluCountryCurrencyConfig(t *testing.T) {
repository := &fakeReceiptRepository{channels: []ledger.ThirdPartyPaymentChannel{{
AppCode: "lalu",
ProviderCode: ledger.PaymentProviderV5Pay,
Status: ledger.ThirdPartyPaymentStatusActive,
Methods: []ledger.ThirdPartyPaymentMethod{{
ProviderCode: ledger.PaymentProviderV5Pay,
CountryCode: "TR",
CurrencyCode: "TRY",
Status: ledger.ThirdPartyPaymentStatusActive,
}},
}}}
v5Pay := &fakeV5PayClient{queryResp: walletservice.V5PayQueryOrderResponse{
OrderID: "receipt-v5-yumi-shared-config",
ProviderOrderID: "tx_receipt_v5_yumi_shared_config",
Status: "2",
Amount: "48.00",
Currency: "TRY",
ProductType: "BANK_TRANSFER",
RawJSON: `{"status":"2"}`,
}}
svc := walletservice.New(repository)
svc.SetV5PayClient(v5Pay)
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
AppCode: "yumi",
ProviderCode: ledger.PaymentProviderV5Pay,
ExternalOrderNo: "receipt-v5-yumi-shared-config",
})
if err != nil {
t.Fatalf("VerifyCoinSellerRechargeReceipt v5pay shared config failed: %v", err)
}
if !receipt.Verified || repository.query.AppCode != "lalu" {
t.Fatalf("V5Pay receipt must query Lalu country/currency config, receipt=%+v query=%+v", receipt, repository.query)
}
if len(v5Pay.queryReqs) != 1 || v5Pay.queryReqs[0].CountryCode != "TR" || v5Pay.queryReqs[0].CurrencyCode != "TRY" {
t.Fatalf("V5Pay receipt must use the Lalu candidate, got %+v", v5Pay.queryReqs)
}
}
func TestVerifyCoinSellerRechargeReceiptV5PayWithoutCountryCurrencyNotFound(t *testing.T) { func TestVerifyCoinSellerRechargeReceiptV5PayWithoutCountryCurrencyNotFound(t *testing.T) {
repository := &fakeReceiptRepository{channels: []ledger.ThirdPartyPaymentChannel{{ repository := &fakeReceiptRepository{channels: []ledger.ThirdPartyPaymentChannel{{
AppCode: "lalu", AppCode: "lalu",

View File

@ -35,7 +35,7 @@ func (s *Service) createTemporaryMifaPayRechargeOrder(ctx context.Context, comma
if s.mifaPay == nil { if s.mifaPay == nil {
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "mifapay client is not configured") return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "mifapay client is not configured")
} }
method, err := s.repository.GetThirdPartyPaymentMethod(ctx, command.AppCode, command.PaymentMethodID) method, err := s.sharedPaymentMethod(ctx, command.AppCode, ledger.PaymentProviderMifaPay, command.PaymentMethodID)
if err != nil { if err != nil {
return ledger.ExternalRechargeOrder{}, err return ledger.ExternalRechargeOrder{}, err
} }
@ -89,7 +89,7 @@ func (s *Service) createTemporaryV5PayRechargeOrder(ctx context.Context, command
if s.v5Pay == nil { if s.v5Pay == nil {
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "v5pay client is not configured") return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "v5pay client is not configured")
} }
method, err := s.repository.GetThirdPartyPaymentMethod(ctx, command.AppCode, command.PaymentMethodID) method, err := s.sharedPaymentMethod(ctx, command.AppCode, ledger.PaymentProviderV5Pay, command.PaymentMethodID)
if err != nil { if err != nil {
return ledger.ExternalRechargeOrder{}, err return ledger.ExternalRechargeOrder{}, err
} }