hyapp-server/services/wallet-service/internal/service/wallet/external_recharge_validate.go

314 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package wallet
import (
"encoding/json"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/domain/ledger"
"math"
"net/url"
"strconv"
"strings"
)
func normalizeCreateExternalRechargeOrderCommand(command ledger.CreateExternalRechargeOrderCommand) ledger.CreateExternalRechargeOrderCommand {
command.AppCode = appcode.Normalize(command.AppCode)
command.CommandID = strings.TrimSpace(command.CommandID)
command.TargetCountryCode = strings.ToUpper(strings.TrimSpace(command.TargetCountryCode))
command.AudienceType = ledger.NormalizeRechargeAudienceType(command.AudienceType)
command.ProviderCode = ledger.NormalizePaymentProvider(command.ProviderCode)
command.ReturnURL = strings.TrimSpace(command.ReturnURL)
command.NotifyURL = strings.TrimSpace(command.NotifyURL)
command.ClientIP = strings.TrimSpace(command.ClientIP)
command.Language = normalizeMifaPayLanguage(command.Language)
command.PayerName = strings.TrimSpace(command.PayerName)
command.PayerAccount = strings.TrimSpace(command.PayerAccount)
command.PayerEmail = strings.TrimSpace(command.PayerEmail)
return command
}
func normalizeMifaPayLanguage(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "en"
}
if comma := strings.IndexByte(value, ','); comma >= 0 {
value = value[:comma]
}
if semicolon := strings.IndexByte(value, ';'); semicolon >= 0 {
value = value[:semicolon]
}
value = strings.TrimSpace(value)
if !validMifaPayLanguageTag(value) {
return "en"
}
return value
}
func validMifaPayLanguageTag(value string) bool {
if value == "" {
return false
}
parts := strings.Split(value, "-")
if len(parts) == 0 || len(parts) > 4 || len(parts[0]) < 2 || len(parts[0]) > 3 || !allASCIILetters(parts[0]) {
return false
}
for _, part := range parts[1:] {
if len(part) < 2 || len(part) > 8 || !allASCIILettersOrDigits(part) {
return false
}
}
return true
}
func allASCIILetters(value string) bool {
for _, char := range value {
if (char < 'A' || char > 'Z') && (char < 'a' || char > 'z') {
return false
}
}
return value != ""
}
func allASCIILettersOrDigits(value string) bool {
for _, char := range value {
if (char < 'A' || char > 'Z') && (char < 'a' || char > 'z') && (char < '0' || char > '9') {
return false
}
}
return value != ""
}
func validateCreateExternalRechargeOrderCommand(command ledger.CreateExternalRechargeOrderCommand) error {
if command.CommandID == "" || command.TargetUserID <= 0 || command.TargetRegionID <= 0 || command.ProductID <= 0 {
return xerr.New(xerr.InvalidArgument, "external recharge command is incomplete")
}
if command.AudienceType == "" || command.ProviderCode == "" {
return xerr.New(xerr.InvalidArgument, "external recharge command type is invalid")
}
if len(command.CommandID) > 128 {
return xerr.New(xerr.InvalidArgument, "command_id is too long")
}
return nil
}
func validateExternalRechargeProduct(product ledger.RechargeProduct, command ledger.CreateExternalRechargeOrderCommand) error {
if product.Status != ledger.RechargeProductStatusActive || !product.Enabled {
return xerr.New(xerr.Conflict, "recharge product is not active")
}
if product.Platform != ledger.RechargeProductPlatformWeb {
return xerr.New(xerr.InvalidArgument, "recharge product is not h5 product")
}
if product.AudienceType != command.AudienceType {
return xerr.New(xerr.Conflict, "recharge product audience does not match")
}
if !rechargeProductSupportsRegion(product, command.TargetRegionID) {
return xerr.New(xerr.Conflict, "recharge product is not available in user region")
}
return nil
}
func validateMifaPayMethodForOrder(method ledger.ThirdPartyPaymentMethod, command ledger.CreateExternalRechargeOrderCommand) error {
if method.ProviderCode != ledger.PaymentProviderMifaPay || method.Status != ledger.ThirdPartyPaymentStatusActive {
return xerr.New(xerr.Conflict, "mifapay method is not active")
}
if method.CountryCode != "GLOBAL" && !strings.EqualFold(method.CountryCode, command.TargetCountryCode) {
return xerr.New(xerr.Conflict, "mifapay method country does not match")
}
if _, err := calculateMifaPayProviderAmountMinor(100, method.USDToCurrencyRate, method.CurrencyCode); err != nil {
return err
}
return nil
}
func validateV5PayMethodForOrder(method ledger.ThirdPartyPaymentMethod, command ledger.CreateExternalRechargeOrderCommand) error {
if method.ProviderCode != ledger.PaymentProviderV5Pay || method.Status != ledger.ThirdPartyPaymentStatusActive {
return xerr.New(xerr.Conflict, "v5pay method is not active")
}
if method.CountryCode != "GLOBAL" && !strings.EqualFold(method.CountryCode, command.TargetCountryCode) {
return xerr.New(xerr.Conflict, "v5pay method country does not match")
}
if strings.TrimSpace(method.PayType) == "" {
return xerr.New(xerr.InvalidArgument, "v5pay product type is required")
}
if _, err := calculateProviderAmountMinor(100, method.USDToCurrencyRate, method.CurrencyCode); err != nil {
return err
}
return nil
}
func calculateProviderAmountMinor(usdMinor int64, rate string, currencyCode string) (int64, error) {
if usdMinor <= 0 {
return 0, xerr.New(xerr.InvalidArgument, "usd amount is invalid")
}
value, err := strconv.ParseFloat(strings.TrimSpace(rate), 64)
if err != nil || value <= 0 {
return 0, xerr.New(xerr.InvalidArgument, "payment exchange rate is invalid")
}
if providerCurrencyScale(currencyCode) == 0 {
// MiFaPay still receives a fixed two-decimal integer amount, but zero-decimal currencies
// such as IDR must represent a whole major-unit amount after the provider divides by 100.
return int64(math.Round(float64(usdMinor)/100*value)) * 100, nil
}
return int64(math.Round(float64(usdMinor) * value)), nil
}
const mifaPayINRMinimumMajorAmount = int64(100)
func calculateMifaPayProviderAmountMinor(usdMinor int64, rate string, currencyCode string) (int64, error) {
if !mifaPayUsesWholeMajorAmount(currencyCode) {
return calculateProviderAmountMinor(usdMinor, rate, currencyCode)
}
if usdMinor <= 0 {
return 0, xerr.New(xerr.InvalidArgument, "usd amount is invalid")
}
value, err := strconv.ParseFloat(strings.TrimSpace(rate), 64)
if err != nil || value <= 0 {
return 0, xerr.New(xerr.InvalidArgument, "payment exchange rate is invalid")
}
majorAmount := int64(math.Round(float64(usdMinor) / 100 * value))
if strings.EqualFold(currencyCode, "INR") && majorAmount < mifaPayINRMinimumMajorAmount {
// MiFaPay 印度通道要求 amount 是整数卢比,且当前网关最小金额为 100 INR
// 本地仍按 minor 保存 10000回调和账单都能表达实际向三方提交的收款金额。
majorAmount = mifaPayINRMinimumMajorAmount
}
return majorAmount * 100, nil
}
func mifaPayProviderAmountValue(providerAmountMinor int64, currencyCode string) string {
if mifaPayUsesWholeMajorAmount(currencyCode) {
return strconv.FormatInt(providerAmountMinor/100, 10)
}
return strconv.FormatInt(providerAmountMinor, 10)
}
func mifaPayUsesWholeMajorAmount(currencyCode string) bool {
return strings.EqualFold(strings.TrimSpace(currencyCode), "INR")
}
func providerCurrencyScale(currencyCode string) int {
switch strings.ToUpper(strings.TrimSpace(currencyCode)) {
case "BIF", "CLP", "DJF", "GNF", "IDR", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "UGX", "VND", "VUV", "XAF", "XOF", "XPF":
return 0
default:
return 2
}
}
func amountMicroToUSDMinor(amountMicro int64) int64 {
if amountMicro <= 0 {
return 0
}
return amountMicro / 10_000
}
func validNonNegativeDecimal(value string) bool {
parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
return err == nil && parsed >= 0
}
func mifaPayNotifyMatchesOrder(data MifaPayNotifyData, order ledger.ExternalRechargeOrder) bool {
if !strings.EqualFold(data.Currency, order.CurrencyCode) {
return false
}
if data.PayType != "" && !strings.EqualFold(data.PayType, order.PayType) {
return false
}
return strings.TrimSpace(data.Amount) == mifaPayProviderAmountValue(order.ProviderAmountMinor, order.CurrencyCode)
}
func mifaPayQueryMatchesOrder(data MifaPayNotifyData, order ledger.ExternalRechargeOrder) bool {
if data.OrderID != "" && !strings.EqualFold(data.OrderID, order.OrderID) {
return false
}
if data.Currency != "" && !strings.EqualFold(data.Currency, order.CurrencyCode) {
return false
}
if data.PayType != "" && !strings.EqualFold(data.PayType, order.PayType) {
return false
}
return strings.TrimSpace(data.Amount) == mifaPayProviderAmountValue(order.ProviderAmountMinor, order.CurrencyCode)
}
func v5PayOrderDataMatchesOrder(data V5PayNotifyData, order ledger.ExternalRechargeOrder) bool {
if data.OrderID != "" && !strings.EqualFold(data.OrderID, order.OrderID) {
return false
}
if data.Currency != "" && !strings.EqualFold(data.Currency, order.CurrencyCode) {
return false
}
if data.ProductType != "" && !strings.EqualFold(data.ProductType, order.PayType) {
return false
}
amount, err := parseProviderAmountMinor(data.Amount)
return err == nil && amount == order.ProviderAmountMinor
}
func parseProviderAmountMinor(value string) (int64, error) {
value = strings.TrimSpace(value)
if value == "" {
return 0, strconv.ErrSyntax
}
if !strings.Contains(value, ".") {
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return 0, err
}
return parsed * 100, nil
}
parts := strings.SplitN(value, ".", 2)
whole, err := strconv.ParseInt(firstNonEmpty(parts[0], "0"), 10, 64)
if err != nil {
return 0, err
}
fraction := parts[1]
if len(fraction) > 2 {
fraction = fraction[:2]
}
for len(fraction) < 2 {
fraction += "0"
}
minor, err := strconv.ParseInt(fraction, 10, 64)
if err != nil {
return 0, err
}
return whole*100 + minor, nil
}
func mifaPayProviderReturnURL(returnURL string) string {
returnURL = strings.TrimSpace(returnURL)
if returnURL == "" {
return returnURL
}
parsed, err := url.Parse(returnURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return returnURL
}
parsed.RawQuery = ""
parsed.Fragment = ""
return parsed.String()
}
func v5PayProviderReturnURL(returnURL string) string {
return mifaPayProviderReturnURL(returnURL)
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
return ""
}
func mustJSONText(value any) string {
body, err := json.Marshal(value)
if err != nil {
return "{}"
}
return string(body)
}