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)) }