283 lines
9.0 KiB
Go
283 lines
9.0 KiB
Go
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 := calculateProviderAmountMinor(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
|
|
}
|
|
|
|
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
|
|
}
|
|
amount, err := strconv.ParseInt(strings.TrimSpace(data.Amount), 10, 64)
|
|
return err == nil && amount == order.ProviderAmountMinor
|
|
}
|
|
|
|
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
|
|
}
|
|
amount, err := strconv.ParseInt(strings.TrimSpace(data.Amount), 10, 64)
|
|
return err == nil && amount == order.ProviderAmountMinor
|
|
}
|
|
|
|
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)
|
|
}
|