hyapp-server/services/wallet-service/internal/service/wallet/external_recharge_payment_sync.go
2026-06-25 21:30:26 +08:00

208 lines
7.5 KiB
Go

package wallet
import (
"context"
"encoding/json"
"strings"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/domain/ledger"
)
type v5PayProductSyncCountry struct {
CountryCode string
CountryName string
CurrencyCode string
SortBase int32
}
var v5PayProductSyncCountries = []v5PayProductSyncCountry{
{CountryCode: "PH", CountryName: "Philippines", CurrencyCode: "PHP", SortBase: 2000},
{CountryCode: "SA", CountryName: "Saudi Arabia", CurrencyCode: "SAR", SortBase: 2300},
{CountryCode: "EG", CountryName: "Egypt", CurrencyCode: "EGP", SortBase: 2400},
{CountryCode: "AE", CountryName: "United Arab Emirates", CurrencyCode: "AED", SortBase: 2500},
{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", SortBase: 2600},
{CountryCode: "IN", CountryName: "India", CurrencyCode: "INR", SortBase: 2800},
{CountryCode: "PK", CountryName: "Pakistan", CurrencyCode: "PKR", SortBase: 2900},
{CountryCode: "TR", CountryName: "Turkey", CurrencyCode: "TRY", SortBase: 3000},
{CountryCode: "MX", CountryName: "Mexico", CurrencyCode: "MXN", SortBase: 3100},
{CountryCode: "VN", CountryName: "Vietnam", CurrencyCode: "VND", SortBase: 3200},
{CountryCode: "BR", CountryName: "Brazil", CurrencyCode: "BRL", SortBase: 3300},
}
// SyncThirdPartyPaymentMethods 从支付商实时拉取已开通方式并落本地配置表;当前只有 V5Pay 提供 productTypes 能力。
func (s *Service) SyncThirdPartyPaymentMethods(ctx context.Context, command ledger.ThirdPartyPaymentMethodSyncCommand) (ledger.ThirdPartyPaymentMethodSyncResult, error) {
command.AppCode = appcode.Normalize(command.AppCode)
command.ProviderCode = ledger.NormalizePaymentProvider(command.ProviderCode)
result := ledger.ThirdPartyPaymentMethodSyncResult{ProviderCode: command.ProviderCode}
if command.ProviderCode == "" {
command.ProviderCode = ledger.PaymentProviderV5Pay
result.ProviderCode = command.ProviderCode
}
if command.OperatorUserID <= 0 {
return result, xerr.New(xerr.InvalidArgument, "payment method sync operator is required")
}
if command.ProviderCode != ledger.PaymentProviderV5Pay {
return result, xerr.New(xerr.InvalidArgument, "payment provider does not support method sync")
}
if s.repository == nil {
return result, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
if s.v5Pay == nil {
return result, xerr.New(xerr.Unavailable, "v5pay client is not configured")
}
ctx = appcode.WithContext(ctx, command.AppCode)
methods, skipped, issues := s.fetchV5PayPaymentMethodsForSync(ctx)
result.ScannedCountryCount = len(v5PayProductSyncCountries)
result.FetchedCountryCount = len(v5PayProductSyncCountries) - len(issues)
result.SkippedCount = skipped
result.Issues = issues
if len(methods) == 0 {
return result, nil
}
created, updated, err := s.repository.UpsertThirdPartyPaymentMethods(ctx, command.AppCode, ledger.PaymentProviderV5Pay, methods)
if err != nil {
return result, err
}
result.CreatedCount = created
result.UpdatedCount = updated
return result, nil
}
func (s *Service) fetchV5PayPaymentMethodsForSync(ctx context.Context) ([]ledger.ThirdPartyPaymentMethod, int, []ledger.ThirdPartyPaymentMethodSyncIssue) {
methods := make([]ledger.ThirdPartyPaymentMethod, 0)
issues := make([]ledger.ThirdPartyPaymentMethodSyncIssue, 0)
skipped := 0
seen := map[string]struct{}{}
for _, country := range v5PayProductSyncCountries {
// V5Pay 的 productTypes 接口必须逐国家/币种查询,没有“全部国家”接口;候选集覆盖当前种子国家和后台已开通截图里的新增国家。
products, err := s.v5Pay.ListProductTypes(ctx, V5PayProductTypesRequest{
CountryCode: country.CountryCode,
CurrencyCode: country.CurrencyCode,
})
if err != nil {
issues = append(issues, v5PayProductSyncIssue(country, err))
continue
}
for index, product := range products.PayinList {
method, ok := v5PayProductToPaymentMethod(country, product, index)
if !ok {
skipped++
continue
}
key := thirdPartyPaymentMethodSyncKey(method)
if _, exists := seen[key]; exists {
skipped++
continue
}
seen[key] = struct{}{}
methods = append(methods, method)
}
}
return methods, skipped, issues
}
func v5PayProductToPaymentMethod(country v5PayProductSyncCountry, product V5PayProductType, index int) (ledger.ThirdPartyPaymentMethod, bool) {
productType := strings.ToUpper(strings.TrimSpace(product.ProductType))
currency := strings.ToUpper(strings.TrimSpace(product.Currency))
if currency == "" {
currency = country.CurrencyCode
}
if productType == "" || product.SupportCashierMode == 0 || !strings.EqualFold(currency, country.CurrencyCode) {
return ledger.ThirdPartyPaymentMethod{}, false
}
methodName := strings.TrimSpace(product.ProductName)
if methodName == "" {
methodName = productType
}
return ledger.ThirdPartyPaymentMethod{
ProviderCode: ledger.PaymentProviderV5Pay,
CountryCode: country.CountryCode,
CountryName: country.CountryName,
CurrencyCode: currency,
PayWay: v5PayProductPayWay(product),
PayType: productType,
MethodName: methodName,
LogoURL: strings.TrimSpace(product.Logo),
Status: ledger.ThirdPartyPaymentStatusActive,
USDToCurrencyRate: "0",
SortOrder: country.SortBase + int32(index+1)*10,
}, true
}
func v5PayProductPayWay(product V5PayProductType) string {
if value := normalizeV5PayProductMode(product.ProductMode); value != "" {
return value
}
value := strings.ToUpper(strings.TrimSpace(product.ProductType + " " + product.ProductName + " " + product.Description))
switch {
case strings.Contains(value, "CARD"):
return "Card"
case strings.Contains(value, "QR"):
return "QR"
case strings.Contains(value, "BANK") || strings.Contains(value, "TRANSFER") || strings.Contains(value, "_VA") || strings.Contains(value, "_ONLINE"):
return "BankTransfer"
case strings.Contains(value, "UPI"):
return "UPI"
case strings.Contains(value, "BILL"):
return "Billing"
default:
return "Ewallet"
}
}
func normalizeV5PayProductMode(value string) string {
mode := strings.ReplaceAll(strings.TrimSpace(value), "_", " ")
mode = strings.ReplaceAll(mode, "-", " ")
switch strings.ToLower(strings.ReplaceAll(mode, " ", "")) {
case "":
return ""
case "card":
return "Card"
case "bank", "banktransfer", "bankonline", "onlinebanking":
return "BankTransfer"
case "ewallet", "wallet":
return "Ewallet"
case "qr":
return "QR"
case "upi":
return "UPI"
case "billing", "bill":
return "Billing"
default:
return strings.TrimSpace(value)
}
}
func v5PayProductSyncIssue(country v5PayProductSyncCountry, err error) ledger.ThirdPartyPaymentMethodSyncIssue {
issue := ledger.ThirdPartyPaymentMethodSyncIssue{
CountryCode: country.CountryCode,
CurrencyCode: country.CurrencyCode,
Code: "REQUEST_FAILED",
Message: err.Error(),
}
if raw := providerPayloadJSON(err); raw != "" {
var payload struct {
Code string `json:"code"`
Message string `json:"message"`
}
if json.Unmarshal([]byte(raw), &payload) == nil {
if strings.TrimSpace(payload.Code) != "" {
issue.Code = strings.TrimSpace(payload.Code)
}
if strings.TrimSpace(payload.Message) != "" {
issue.Message = strings.TrimSpace(payload.Message)
}
}
}
return issue
}
func thirdPartyPaymentMethodSyncKey(method ledger.ThirdPartyPaymentMethod) string {
return strings.ToUpper(strings.TrimSpace(method.CountryCode)) + "|" +
strings.TrimSpace(method.PayWay) + "|" +
strings.ToUpper(strings.TrimSpace(method.PayType))
}