mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
feat: integrate okpay switch-network flow and notify handling
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/config"
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
"github.com/gookit/color"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -35,6 +36,7 @@ func MdbTableInit() {
|
||||
{"Chain", &mdb.Chain{}},
|
||||
{"ChainToken", &mdb.ChainToken{}},
|
||||
{"RpcNode", &mdb.RpcNode{}},
|
||||
{"ProviderOrder", &mdb.ProviderOrder{}},
|
||||
}
|
||||
for _, m := range migrations {
|
||||
if err := Mdb.AutoMigrate(m.model); err != nil {
|
||||
@@ -132,10 +134,19 @@ func seedRpcNodes() {
|
||||
// seedDefaultSettings inserts built-in default settings.
|
||||
// Uses ON CONFLICT DO NOTHING so admin edits persist across restarts.
|
||||
func seedDefaultSettings() {
|
||||
okPayCallbackURL := strings.TrimRight(strings.TrimSpace(config.GetAppUri()), "/")
|
||||
if okPayCallbackURL != "" {
|
||||
okPayCallbackURL += "/payments/okpay/v1/notify"
|
||||
}
|
||||
defaults := []mdb.Setting{
|
||||
{Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultToken, Value: "usdt", Type: mdb.SettingTypeString},
|
||||
{Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultCurrency, Value: "cny", Type: mdb.SettingTypeString},
|
||||
{Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultNetwork, Value: "tron", Type: mdb.SettingTypeString},
|
||||
{Group: mdb.SettingGroupOkPay, Key: mdb.SettingKeyOkPayEnabled, Value: "false", Type: mdb.SettingTypeBool},
|
||||
{Group: mdb.SettingGroupOkPay, Key: mdb.SettingKeyOkPayAPIURL, Value: "https://api.okaypay.me/shop/", Type: mdb.SettingTypeString},
|
||||
{Group: mdb.SettingGroupOkPay, Key: mdb.SettingKeyOkPayCallbackURL, Value: okPayCallbackURL, Type: mdb.SettingTypeString},
|
||||
{Group: mdb.SettingGroupOkPay, Key: mdb.SettingKeyOkPayTimeoutSeconds, Value: "10", Type: mdb.SettingTypeInt},
|
||||
{Group: mdb.SettingGroupOkPay, Key: mdb.SettingKeyOkPayAllowTokens, Value: "USDT,TRX", Type: mdb.SettingTypeString},
|
||||
}
|
||||
if err := Mdb.Clauses(clause.OnConflict{DoNothing: true}).Create(&defaults).Error; err != nil {
|
||||
color.Red.Printf("[store_db] seed default settings err=%s\n", err)
|
||||
|
||||
@@ -202,6 +202,18 @@ func MarkOrderSelected(tradeId string) error {
|
||||
Update("is_selected", true).Error
|
||||
}
|
||||
|
||||
// ExpireOrderByTradeID marks a waiting order as expired. Used to retire failed
|
||||
// child-order attempts that should not remain selectable/reusable.
|
||||
func ExpireOrderByTradeID(tradeId string) error {
|
||||
return dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", tradeId).
|
||||
Where("status = ?", mdb.StatusWaitPay).
|
||||
Updates(map[string]interface{}{
|
||||
"status": mdb.StatusExpired,
|
||||
"is_selected": false,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// RefreshOrderExpiration resets created_at to now so the expiration timer restarts.
|
||||
// Called on the parent order when a sub-order is created or returned.
|
||||
func RefreshOrderExpiration(tradeId string) error {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/GMWalletApp/epusdt/model/dao"
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func GetProviderOrderByTradeIDAndProvider(tradeID string, provider string) (*mdb.ProviderOrder, error) {
|
||||
row := new(mdb.ProviderOrder)
|
||||
err := dao.Mdb.Model(row).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Where("provider = ?", provider).
|
||||
Limit(1).
|
||||
Find(row).Error
|
||||
return row, err
|
||||
}
|
||||
|
||||
func CreateProviderOrderWithTransaction(tx *gorm.DB, row *mdb.ProviderOrder) error {
|
||||
return tx.Model(row).Create(row).Error
|
||||
}
|
||||
|
||||
func UpdateProviderOrderCreated(tradeID string, provider string, providerOrderID string, payURL string) error {
|
||||
return dao.Mdb.Model(&mdb.ProviderOrder{}).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Where("provider = ?", provider).
|
||||
Updates(map[string]interface{}{
|
||||
"provider_order_id": providerOrderID,
|
||||
"pay_url": payURL,
|
||||
"status": mdb.ProviderOrderStatusPending,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func MarkProviderOrderFailed(tradeID string, provider string) error {
|
||||
return dao.Mdb.Model(&mdb.ProviderOrder{}).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Where("provider = ?", provider).
|
||||
Update("status", mdb.ProviderOrderStatusFailed).Error
|
||||
}
|
||||
|
||||
func SaveProviderOrderNotify(tradeID string, provider string, notifyRaw string) error {
|
||||
return dao.Mdb.Model(&mdb.ProviderOrder{}).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Where("provider = ?", provider).
|
||||
Update("notify_raw", notifyRaw).Error
|
||||
}
|
||||
|
||||
func MarkProviderOrderPaid(tradeID string, provider string, notifyRaw string) error {
|
||||
return dao.Mdb.Model(&mdb.ProviderOrder{}).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Where("provider = ?", provider).
|
||||
Updates(map[string]interface{}{
|
||||
"status": mdb.ProviderOrderStatusPaid,
|
||||
"notify_raw": notifyRaw,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func MarkProviderOrderExpired(tradeID string, provider string) error {
|
||||
return dao.Mdb.Model(&mdb.ProviderOrder{}).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Where("provider = ?", provider).
|
||||
Where("status IN ?", []string{mdb.ProviderOrderStatusCreating, mdb.ProviderOrderStatusPending}).
|
||||
Update("status", mdb.ProviderOrderStatusExpired).Error
|
||||
}
|
||||
|
||||
func GetSubOrderByTokenPayProvider(parentTradeID string, token string, payProvider string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
err := dao.Mdb.Model(order).
|
||||
Where("parent_trade_id = ?", parentTradeID).
|
||||
Where("token = ?", token).
|
||||
Where("pay_provider = ?", payProvider).
|
||||
Where("status = ?", mdb.StatusWaitPay).
|
||||
Limit(1).
|
||||
Find(order).Error
|
||||
return order, err
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/config"
|
||||
"github.com/GMWalletApp/epusdt/model/dao"
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -157,6 +158,63 @@ var sensitiveSettingKeys = []string{
|
||||
mdb.SettingKeyInitAdminPasswordChanged,
|
||||
}
|
||||
|
||||
// OkPay settings helpers keep the provider-specific defaults in one place so
|
||||
// business logic does not need to repeat raw settings keys.
|
||||
func GetOkPayEnabled() bool {
|
||||
return GetSettingBool(mdb.SettingKeyOkPayEnabled, false)
|
||||
}
|
||||
|
||||
func GetOkPayShopID() string {
|
||||
return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayShopID, ""))
|
||||
}
|
||||
|
||||
func GetOkPayShopToken() string {
|
||||
return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayShopToken, ""))
|
||||
}
|
||||
|
||||
func GetOkPayAPIURL() string {
|
||||
return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayAPIURL, "https://api.okaypay.me/shop/"))
|
||||
}
|
||||
|
||||
func GetOkPayCallbackURL() string {
|
||||
if configured := strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayCallbackURL, "")); configured != "" {
|
||||
return configured
|
||||
}
|
||||
appURI := strings.TrimSpace(config.GetAppUri())
|
||||
if appURI == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimRight(appURI, "/") + "/payments/okpay/v1/notify"
|
||||
}
|
||||
|
||||
func GetOkPayReturnURL() string {
|
||||
return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayReturnURL, ""))
|
||||
}
|
||||
|
||||
func GetOkPayTimeoutSeconds() int {
|
||||
return GetSettingInt(mdb.SettingKeyOkPayTimeoutSeconds, 10)
|
||||
}
|
||||
|
||||
func GetOkPayAllowTokens() []string {
|
||||
raw := GetSettingString(mdb.SettingKeyOkPayAllowTokens, "USDT,TRX")
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return []string{"USDT", "TRX"}
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
token := strings.ToUpper(strings.TrimSpace(part))
|
||||
if token == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, token)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []string{"USDT", "TRX"}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ListSettingsByGroup returns all rows for a given group (empty group = all),
|
||||
// excluding any keys in sensitiveSettingKeys.
|
||||
func ListSettingsByGroup(group string) ([]mdb.Setting, error) {
|
||||
|
||||
@@ -12,6 +12,19 @@ const (
|
||||
PaymentTypeEpay = "Epay"
|
||||
)
|
||||
|
||||
const (
|
||||
// PaymentProviderOnChain means this concrete order record is settled by
|
||||
// sending funds to a directly assigned wallet address on a supported chain.
|
||||
PaymentProviderOnChain = "on_chain"
|
||||
// PaymentProviderOkPay means this concrete order record is settled through
|
||||
// the third-party OkayPay/OkPay hosted checkout flow.
|
||||
//
|
||||
// In the current design this is typically used by a switch-network-created
|
||||
// child order, while the parent order keeps its original merchant-facing
|
||||
// semantics and callback behavior.
|
||||
PaymentProviderOkPay = "okpay"
|
||||
)
|
||||
|
||||
type Orders struct {
|
||||
TradeId string `gorm:"column:trade_id;uniqueIndex:orders_trade_id_uindex" json:"trade_id" example:"T2026041612345678"`
|
||||
OrderId string `gorm:"column:order_id;uniqueIndex:orders_order_id_uindex" json:"order_id" example:"ORD20260416001"`
|
||||
@@ -33,7 +46,17 @@ type Orders struct {
|
||||
CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm" enums:"1,2" example:"2"`
|
||||
IsSelected bool `gorm:"column:is_selected;default:false" json:"is_selected" example:"false"`
|
||||
PaymentType string `gorm:"column:payment_type" json:"payment_type" example:"Epay"`
|
||||
ApiKeyID uint64 `gorm:"column:api_key_id;default:0;index:orders_api_key_id_index" json:"api_key_id" example:"1"`
|
||||
// PayProvider identifies how this specific order row is collected.
|
||||
//
|
||||
// Semantics:
|
||||
// - parent orders and regular chain child orders use on_chain
|
||||
// - third-party hosted checkout child orders use their provider name
|
||||
// (for example okpay)
|
||||
//
|
||||
// Existing rows default to on_chain for backward compatibility so upgrades
|
||||
// can rely on AutoMigrate without rewriting old orders.
|
||||
PayProvider string `gorm:"column:pay_provider;size:32;default:on_chain;index:orders_pay_provider_index" json:"pay_provider" example:"on_chain"`
|
||||
ApiKeyID uint64 `gorm:"column:api_key_id;default:0;index:orders_api_key_id_index" json:"api_key_id" example:"1"`
|
||||
// PayBySubId holds the primary-key ID of the sub-order that settled this parent order.
|
||||
// Zero when the parent order was paid directly (no sub-order involved).
|
||||
PayBySubId uint64 `gorm:"column:pay_by_sub_id;default:0" json:"pay_by_sub_id" example:"0"`
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package mdb
|
||||
|
||||
const (
|
||||
ProviderOrderStatusCreating = "creating"
|
||||
ProviderOrderStatusPending = "pending"
|
||||
ProviderOrderStatusPaid = "paid"
|
||||
ProviderOrderStatusFailed = "failed"
|
||||
ProviderOrderStatusExpired = "expired"
|
||||
)
|
||||
|
||||
// ProviderOrder stores provider-specific checkout data for one internal order
|
||||
// row. For OkPay this is expected to bind to the concrete child order created
|
||||
// by switch-network, not to the parent merchant order.
|
||||
//
|
||||
// Binding rules:
|
||||
// - trade_id always points to orders.trade_id
|
||||
// - the bound orders row should use the same pay_provider value
|
||||
// - provider identifies which downstream checkout created the provider row
|
||||
// - provider_order_id is the provider's own order number (for OkPay this is
|
||||
// the returned order_id from /payLink)
|
||||
// - pay_url is the hosted payment URL returned by the provider
|
||||
//
|
||||
// The main orders table remains the source of truth for merchant-facing state,
|
||||
// while this table keeps provider-specific identifiers and callback payloads.
|
||||
type ProviderOrder struct {
|
||||
TradeId string `gorm:"column:trade_id;size:32;not null;uniqueIndex:provider_orders_trade_id_provider_uindex,priority:1;index:provider_orders_trade_id_index" json:"trade_id" example:"T2026041612345678"`
|
||||
// Provider values should match the bound Orders.PayProvider value.
|
||||
Provider string `gorm:"column:provider;size:32;not null;uniqueIndex:provider_orders_trade_id_provider_uindex,priority:2;index:provider_orders_provider_status_index,priority:1" json:"provider" example:"okpay"`
|
||||
// ProviderOrderID is the downstream provider's own order identifier for the
|
||||
// bound concrete order row.
|
||||
ProviderOrderID string `gorm:"column:provider_order_id;size:128;not null;default:'';index:provider_orders_provider_order_id_index" json:"provider_order_id" example:"ac7b86615fdb137576ae35879f7ed844"`
|
||||
// PayURL is the hosted checkout URL returned by the downstream provider for
|
||||
// the bound concrete order row.
|
||||
PayURL string `gorm:"column:pay_url;size:512;not null;default:''" json:"pay_url" example:"https://pay.example.com/checkout/abc123"`
|
||||
// Amount/Coin mirror the exact payload submitted to the provider, which may
|
||||
// be useful when validating callbacks and troubleshooting mismatches.
|
||||
Amount float64 `gorm:"column:amount" json:"amount" example:"14.2857"`
|
||||
Coin string `gorm:"column:coin;size:16;not null;default:''" json:"coin" example:"USDT"`
|
||||
Status string `gorm:"column:status;size:32;not null;default:pending;index:provider_orders_provider_status_index,priority:2" json:"status" example:"pending"`
|
||||
// NotifyRaw stores the original provider callback payload for auditing and
|
||||
// replay/debug use. It is provider-specific and intentionally opaque here.
|
||||
NotifyRaw string `gorm:"column:notify_raw;type:text" json:"notify_raw"`
|
||||
BaseModel
|
||||
}
|
||||
|
||||
func (p *ProviderOrder) TableName() string {
|
||||
return "provider_orders"
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package mdb
|
||||
|
||||
// Setting stores non-credential runtime configuration as key/value pairs.
|
||||
// Groups: brand, rate, system, epay. Merchant credentials (pid + secret_key)
|
||||
// live in the api_keys table; notification configs live in NotificationChannel.
|
||||
// Default rows for the epay group are seeded on first startup (see
|
||||
// Groups: brand, rate, system, epay, okpay. Merchant credentials (pid +
|
||||
// secret_key) live in the api_keys table; notification configs live in
|
||||
// NotificationChannel.
|
||||
// Default rows for the epay/okpay groups are seeded on first startup (see
|
||||
// model/dao/mdb_table_init.go seedDefaultSettings). All other groups start
|
||||
// empty and fall back to hardcoded defaults until an admin sets them.
|
||||
// Only exception: system.jwt_secret is auto-generated on first startup.
|
||||
@@ -12,6 +13,7 @@ const (
|
||||
SettingGroupRate = "rate"
|
||||
SettingGroupSystem = "system"
|
||||
SettingGroupEpay = "epay"
|
||||
SettingGroupOkPay = "okpay"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -42,10 +44,20 @@ const (
|
||||
SettingKeyEpayDefaultToken = "epay.default_token"
|
||||
SettingKeyEpayDefaultCurrency = "epay.default_currency"
|
||||
SettingKeyEpayDefaultNetwork = "epay.default_network"
|
||||
|
||||
// OkPay hosted-checkout settings.
|
||||
SettingKeyOkPayEnabled = "okpay.enabled"
|
||||
SettingKeyOkPayShopID = "okpay.shop_id"
|
||||
SettingKeyOkPayShopToken = "okpay.shop_token"
|
||||
SettingKeyOkPayAPIURL = "okpay.api_url"
|
||||
SettingKeyOkPayCallbackURL = "okpay.callback_url"
|
||||
SettingKeyOkPayReturnURL = "okpay.return_url"
|
||||
SettingKeyOkPayTimeoutSeconds = "okpay.timeout_seconds"
|
||||
SettingKeyOkPayAllowTokens = "okpay.allow_tokens"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
Group string `gorm:"column:group;size:32;index:settings_group_index" json:"group" enums:"brand,rate,system" example:"rate"`
|
||||
Group string `gorm:"column:group;size:32;index:settings_group_index" json:"group" enums:"brand,rate,system,epay,okpay" example:"rate"`
|
||||
Key string `gorm:"column:key;uniqueIndex:settings_key_uindex;size:128" json:"key" example:"rate.forced_usdt_rate"`
|
||||
Value string `gorm:"column:value;type:text" json:"value" example:"7.2"`
|
||||
Type string `gorm:"column:type;size:16;default:string" json:"type" enums:"string,int,bool,json" example:"string"`
|
||||
|
||||
@@ -43,7 +43,7 @@ type OrderProcessingRequest struct {
|
||||
type SwitchNetworkRequest struct {
|
||||
TradeId string `json:"trade_id" validate:"required" example:"T2026041612345678"`
|
||||
Token string `json:"token" validate:"required" example:"USDT"`
|
||||
Network string `json:"network" validate:"required" example:"ethereum"`
|
||||
Network string `json:"network" validate:"required" example:"okpay,tron,solana,ethereum"`
|
||||
}
|
||||
|
||||
func (r SwitchNetworkRequest) Translates() map[string]string {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package response
|
||||
|
||||
type CheckoutCounterResponse struct {
|
||||
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
|
||||
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,保留4位小数 法币金额
|
||||
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,保留4位小数 加密货币金额
|
||||
Token string `json:"token" example:"USDT"` // 所属币种 TRX USDT......
|
||||
Currency string `json:"currency" example:"CNY"` // 法币币种 CNY USD ...
|
||||
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
|
||||
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,保留4位小数 法币金额
|
||||
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,保留4位小数 加密货币金额
|
||||
Token string `json:"token" example:"USDT"` // 所属币种 TRX USDT......
|
||||
Currency string `json:"currency" example:"CNY"` // 法币币种 CNY USD ...
|
||||
ReceiveAddress string `json:"receive_address" example:"TTestTronAddress001"` // 收款钱包地址
|
||||
Network string `json:"network" example:"tron"` // 网络 TRON ETH ...
|
||||
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
|
||||
Network string `json:"network" example:"tron"` // 网络 TRON ETH ...
|
||||
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
|
||||
RedirectUrl string `json:"redirect_url" example:"https://example.com/success"`
|
||||
CreatedAt int64 `json:"created_at" example:"1713264000"` // 订单创建时间 时间戳
|
||||
PaymentUrl string `json:"payment_url" example:"https://pay.example.com/checkout/T2026041612345678"` // 支付链接;链上订单通常为本地收银台,OkPay 子订单为第三方 payLink
|
||||
CreatedAt int64 `json:"created_at" example:"1713264000"` // 订单创建时间 时间戳
|
||||
IsSelected bool `json:"is_selected" example:"false"`
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,25 @@ type NetworkTokenSupport struct {
|
||||
Tokens []string `json:"tokens" example:"USDT,USDC"`
|
||||
}
|
||||
|
||||
type SupportedAssetsResponse struct {
|
||||
Supports []NetworkTokenSupport `json:"supports"`
|
||||
type EpayPublicConfig struct {
|
||||
DefaultToken string `json:"default_token" example:"usdt"`
|
||||
DefaultCurrency string `json:"default_currency" example:"cny"`
|
||||
DefaultNetwork string `json:"default_network" example:"tron"`
|
||||
}
|
||||
|
||||
type OkPayPublicConfig struct {
|
||||
Enabled bool `json:"enabled" example:"true"`
|
||||
AllowTokens []string `json:"allow_tokens" example:"USDT,TRX"`
|
||||
ShopID string `json:"shop_id,omitempty" example:"okpay-shop-test"`
|
||||
ShopToken string `json:"shop_token,omitempty" example:"secret-token"`
|
||||
APIURL string `json:"api_url,omitempty" example:"https://api.okaypay.me/shop/"`
|
||||
CallbackURL string `json:"callback_url,omitempty" example:"https://pay.example.com/payments/okpay/v1/notify"`
|
||||
ReturnURL string `json:"return_url,omitempty" example:"https://pay.example.com/success"`
|
||||
TimeoutSeconds int `json:"timeout_seconds,omitempty" example:"10"`
|
||||
}
|
||||
|
||||
type PublicConfigResponse struct {
|
||||
SupportedAssets []NetworkTokenSupport `json:"supported_assets"`
|
||||
Epay EpayPublicConfig `json:"epay"`
|
||||
OkPay OkPayPublicConfig `json:"okpay"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/model/data"
|
||||
"github.com/GMWalletApp/epusdt/model/request"
|
||||
"github.com/GMWalletApp/epusdt/util/constant"
|
||||
"github.com/GMWalletApp/epusdt/util/http_client"
|
||||
"github.com/GMWalletApp/epusdt/util/log"
|
||||
)
|
||||
|
||||
type okPayCreateDepositResponse struct {
|
||||
Status string `json:"status"`
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
OrderID string `json:"order_id"`
|
||||
PayURL string `json:"pay_url"`
|
||||
} `json:"data"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type okPayDepositOrder struct {
|
||||
ProviderOrderID string
|
||||
PayURL string
|
||||
}
|
||||
|
||||
type okPayNotifyPayload struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Code string `json:"code"`
|
||||
OrderID string `json:"order_id"`
|
||||
UniqueID string `json:"unique_id"`
|
||||
PayUserID string `json:"pay_user_id"`
|
||||
Amount string `json:"amount"`
|
||||
Coin string `json:"coin"`
|
||||
PayStatus string `json:"pay_status"`
|
||||
NotifyType string `json:"notify_type"`
|
||||
Sign string `json:"sign"`
|
||||
RawFormData string `json:"raw_form_data"`
|
||||
}
|
||||
|
||||
func okPaySign(form map[string]string, shopID string, shopToken string) map[string]string {
|
||||
values := url.Values{}
|
||||
for key, value := range form {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
continue
|
||||
}
|
||||
values.Set(key, value)
|
||||
}
|
||||
values.Set("id", shopID)
|
||||
query, _ := url.QueryUnescape(values.Encode())
|
||||
sum := md5.Sum([]byte(query + "&token=" + shopToken))
|
||||
|
||||
signed := make(map[string]string, len(values)+1)
|
||||
for key, vals := range values {
|
||||
if len(vals) == 0 {
|
||||
continue
|
||||
}
|
||||
signed[key] = vals[0]
|
||||
}
|
||||
signed["sign"] = strings.ToUpper(hex.EncodeToString(sum[:]))
|
||||
return signed
|
||||
}
|
||||
|
||||
func createOkPayDepositOrder(uniqueID string, amount float64, coin string, returnURL string) (*okPayDepositOrder, error) {
|
||||
shopID := data.GetOkPayShopID()
|
||||
shopToken := data.GetOkPayShopToken()
|
||||
apiURL := strings.TrimSpace(data.GetOkPayAPIURL())
|
||||
callbackURL := data.GetOkPayCallbackURL()
|
||||
if shopID == "" || shopToken == "" || apiURL == "" || callbackURL == "" {
|
||||
return nil, fmt.Errorf("okpay config incomplete")
|
||||
}
|
||||
|
||||
if returnURL == "" {
|
||||
returnURL = data.GetOkPayReturnURL()
|
||||
}
|
||||
|
||||
form := map[string]string{
|
||||
"unique_id": uniqueID,
|
||||
"name": uniqueID,
|
||||
"amount": fmt.Sprintf("%.2f", amount),
|
||||
"coin": strings.ToUpper(strings.TrimSpace(coin)),
|
||||
"callback_url": callbackURL,
|
||||
"return_url": returnURL,
|
||||
}
|
||||
form = okPaySign(form, shopID, shopToken)
|
||||
|
||||
base, err := url.Parse(apiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base.Path = path.Join(base.Path, "payLink")
|
||||
|
||||
client := http_client.GetHttpClient()
|
||||
client.SetTimeout(time.Duration(data.GetOkPayTimeoutSeconds()) * time.Second)
|
||||
|
||||
var payload okPayCreateDepositResponse
|
||||
resp, err := client.R().
|
||||
SetFormData(form).
|
||||
SetResult(&payload).
|
||||
Post(base.String())
|
||||
if err != nil {
|
||||
log.Sugar.Warnf("[okpay] create order request failed unique_id=%s api_url=%s callback_url=%s return_url=%s err=%v", uniqueID, base.String(), callbackURL, returnURL, err)
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode() >= 400 {
|
||||
log.Sugar.Warnf("[okpay] create order http error unique_id=%s status=%s api_url=%s callback_url=%s return_url=%s body=%s", uniqueID, resp.Status(), base.String(), callbackURL, returnURL, resp.String())
|
||||
return nil, fmt.Errorf("okpay http status: %s", resp.Status())
|
||||
}
|
||||
if payload.Data.OrderID == "" || payload.Data.PayURL == "" {
|
||||
log.Sugar.Warnf("[okpay] create order rejected unique_id=%s api_url=%s callback_url=%s return_url=%s status=%s code=%d msg=%s", uniqueID, base.String(), callbackURL, returnURL, payload.Status, payload.Code, payload.Msg)
|
||||
if payload.Msg != "" {
|
||||
return nil, fmt.Errorf("okpay create order failed: %s (callback_url=%s)", payload.Msg, callbackURL)
|
||||
}
|
||||
return nil, fmt.Errorf("okpay create order failed (callback_url=%s)", callbackURL)
|
||||
}
|
||||
|
||||
return &okPayDepositOrder{
|
||||
ProviderOrderID: payload.Data.OrderID,
|
||||
PayURL: payload.Data.PayURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func verifyOkPayNotify(form map[string]string) bool {
|
||||
shopID := data.GetOkPayShopID()
|
||||
shopToken := data.GetOkPayShopToken()
|
||||
signature := strings.TrimSpace(form["sign"])
|
||||
if shopID == "" || shopToken == "" || signature == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if expected := okPayNotifySign(form, shopToken); expected != "" && strings.EqualFold(expected, signature) {
|
||||
return true
|
||||
}
|
||||
|
||||
unsigned := make(map[string]string, len(form))
|
||||
for key, value := range form {
|
||||
if strings.EqualFold(key, "sign") {
|
||||
continue
|
||||
}
|
||||
unsigned[key] = value
|
||||
}
|
||||
signed := okPaySign(unsigned, shopID, shopToken)
|
||||
return strings.EqualFold(strings.TrimSpace(signed["sign"]), signature)
|
||||
}
|
||||
|
||||
func okPayNotifySign(form map[string]string, shopToken string) string {
|
||||
orderedKeys := []string{
|
||||
"code",
|
||||
"data[order_id]",
|
||||
"data[unique_id]",
|
||||
"data[pay_user_id]",
|
||||
"data[amount]",
|
||||
"data[coin]",
|
||||
"data[status]",
|
||||
"data[type]",
|
||||
"id",
|
||||
"status",
|
||||
}
|
||||
|
||||
pairs := make([]string, 0, len(orderedKeys))
|
||||
for _, key := range orderedKeys {
|
||||
value := strings.TrimSpace(form[key])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
pairs = append(pairs, url.QueryEscape(key)+"="+url.QueryEscape(value))
|
||||
}
|
||||
if len(pairs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
query, err := url.QueryUnescape(strings.Join(pairs, "&"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
sum := md5.Sum([]byte(query + "&token=" + shopToken))
|
||||
return strings.ToUpper(hex.EncodeToString(sum[:]))
|
||||
}
|
||||
|
||||
func parseOkPayNotify(form map[string]string, rawFormData string) *okPayNotifyPayload {
|
||||
return &okPayNotifyPayload{
|
||||
ID: strings.TrimSpace(form["id"]),
|
||||
Status: strings.TrimSpace(form["status"]),
|
||||
Code: strings.TrimSpace(form["code"]),
|
||||
OrderID: strings.TrimSpace(form["data[order_id]"]),
|
||||
UniqueID: strings.TrimSpace(form["data[unique_id]"]),
|
||||
PayUserID: strings.TrimSpace(form["data[pay_user_id]"]),
|
||||
Amount: strings.TrimSpace(form["data[amount]"]),
|
||||
Coin: strings.TrimSpace(form["data[coin]"]),
|
||||
PayStatus: strings.TrimSpace(form["data[status]"]),
|
||||
NotifyType: strings.TrimSpace(form["data[type]"]),
|
||||
Sign: strings.TrimSpace(form["sign"]),
|
||||
RawFormData: rawFormData,
|
||||
}
|
||||
}
|
||||
|
||||
func HandleOkPayNotify(form map[string]string, rawFormData string) error {
|
||||
if !verifyOkPayNotify(form) {
|
||||
return fmt.Errorf("invalid okpay notify sign")
|
||||
}
|
||||
|
||||
payload := parseOkPayNotify(form, rawFormData)
|
||||
|
||||
if payload.ID == "" || payload.ID != data.GetOkPayShopID() {
|
||||
return fmt.Errorf("okpay notify shop id mismatch: %s", payload.ID)
|
||||
}
|
||||
if payload.UniqueID == "" || payload.OrderID == "" {
|
||||
return fmt.Errorf("missing okpay notify identifiers")
|
||||
}
|
||||
|
||||
order, err := data.GetOrderInfoByTradeId(payload.UniqueID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if order.ID <= 0 {
|
||||
return fmt.Errorf("okpay notify order not found: %s", payload.UniqueID)
|
||||
}
|
||||
if order.PayProvider != "okpay" {
|
||||
return fmt.Errorf("okpay notify order provider mismatch: %s", payload.UniqueID)
|
||||
}
|
||||
|
||||
providerRow, err := data.GetProviderOrderByTradeIDAndProvider(payload.UniqueID, order.PayProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if providerRow.ID <= 0 {
|
||||
return fmt.Errorf("okpay provider order not found: %s", payload.UniqueID)
|
||||
}
|
||||
|
||||
_ = data.SaveProviderOrderNotify(payload.UniqueID, order.PayProvider, payload.RawFormData)
|
||||
|
||||
if !strings.EqualFold(payload.Status, "success") || !strings.EqualFold(payload.NotifyType, "deposit") || payload.PayStatus != "1" {
|
||||
log.Sugar.Infof("[okpay] notify ignored trade_id=%s status=%s pay_status=%s type=%s", payload.UniqueID, payload.Status, payload.PayStatus, payload.NotifyType)
|
||||
return nil
|
||||
}
|
||||
|
||||
if providerRow.ProviderOrderID != "" && providerRow.ProviderOrderID != payload.OrderID {
|
||||
return fmt.Errorf("okpay provider order id mismatch: got=%s want=%s", payload.OrderID, providerRow.ProviderOrderID)
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(order.Token), strings.TrimSpace(payload.Coin)) {
|
||||
return fmt.Errorf("okpay coin mismatch: got=%s want=%s", payload.Coin, order.Token)
|
||||
}
|
||||
|
||||
notifyAmount, err := strconv.ParseFloat(payload.Amount, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid okpay amount: %w", err)
|
||||
}
|
||||
if fmt.Sprintf("%.2f", notifyAmount) != fmt.Sprintf("%.2f", order.ActualAmount) {
|
||||
return fmt.Errorf("okpay amount mismatch: got=%.2f want=%.2f", notifyAmount, order.ActualAmount)
|
||||
}
|
||||
|
||||
err = OrderProcessing(&request.OrderProcessingRequest{
|
||||
ReceiveAddress: order.ReceiveAddress,
|
||||
Currency: order.Currency,
|
||||
Token: order.Token,
|
||||
Network: order.Network,
|
||||
Amount: order.ActualAmount,
|
||||
TradeId: order.TradeId,
|
||||
BlockTransactionId: payload.OrderID,
|
||||
})
|
||||
if err != nil && err != constant.OrderBlockAlreadyProcess {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = data.MarkProviderOrderPaid(payload.UniqueID, order.PayProvider, payload.RawFormData); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -120,6 +120,7 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
|
||||
RedirectUrl: req.RedirectUrl,
|
||||
Name: req.Name,
|
||||
PaymentType: req.PaymentType,
|
||||
PayProvider: mdb.PaymentProviderOnChain,
|
||||
ApiKeyID: apiKeyID(apiKey),
|
||||
}
|
||||
if err = data.CreateOrderWithTransaction(tx, order); err != nil {
|
||||
@@ -199,6 +200,11 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
|
||||
if err = data.ExpireOrderByTradeId(sub.TradeId); err != nil {
|
||||
log.Sugar.Warnf("[order] expire sub-order failed, trade_id=%s, err=%v", sub.TradeId, err)
|
||||
}
|
||||
if sub.PayProvider != "" && sub.PayProvider != mdb.PaymentProviderOnChain {
|
||||
if err = data.MarkProviderOrderExpired(sub.TradeId, sub.PayProvider); err != nil {
|
||||
log.Sugar.Warnf("[order] expire provider order failed, trade_id=%s, provider=%s, err=%v", sub.TradeId, sub.PayProvider, err)
|
||||
}
|
||||
}
|
||||
if err = data.UnLockTransaction(sub.Network, sub.ReceiveAddress, sub.Token, sub.ActualAmount); err != nil {
|
||||
log.Sugar.Warnf("[order] unlock sub-order transaction failed, trade_id=%s, err=%v", sub.TradeId, err)
|
||||
}
|
||||
@@ -257,6 +263,11 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
|
||||
|
||||
// Release sibling locks after their status transitions commit.
|
||||
for _, sib := range siblings {
|
||||
if sib.PayProvider != "" && sib.PayProvider != mdb.PaymentProviderOnChain {
|
||||
if err = data.MarkProviderOrderExpired(sib.TradeId, sib.PayProvider); err != nil {
|
||||
log.Sugar.Warnf("[order] expire sibling provider order failed, trade_id=%s, provider=%s, err=%v", sib.TradeId, sib.PayProvider, err)
|
||||
}
|
||||
}
|
||||
if err = data.UnLockTransaction(sib.Network, sib.ReceiveAddress, sib.Token, sib.ActualAmount); err != nil {
|
||||
log.Sugar.Warnf("[order] unlock sibling transaction failed, trade_id=%s, err=%v", sib.TradeId, err)
|
||||
}
|
||||
@@ -346,6 +357,10 @@ func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounter
|
||||
return nil, constant.OrderNotWaitPay
|
||||
}
|
||||
|
||||
if network == mdb.PaymentProviderOkPay {
|
||||
return switchToOkPay(parent, token)
|
||||
}
|
||||
|
||||
// 2. Same token+network as parent → mark selected and return
|
||||
if strings.EqualFold(parent.Token, token) && strings.EqualFold(parent.Network, network) {
|
||||
_ = data.MarkOrderSelected(parent.TradeId)
|
||||
@@ -427,6 +442,7 @@ func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounter
|
||||
Name: parent.Name,
|
||||
CallBackConfirm: mdb.CallBackConfirmOk, // don't trigger callback on sub-order
|
||||
PaymentType: parent.PaymentType,
|
||||
PayProvider: mdb.PaymentProviderOnChain,
|
||||
ApiKeyID: parent.ApiKeyID, // inherit from parent so resolveOrderApiKey never fails
|
||||
}
|
||||
if err = data.CreateOrderWithTransaction(tx, subOrder); err != nil {
|
||||
@@ -458,7 +474,140 @@ func buildCheckoutResponse(order *mdb.Orders) *response.CheckoutCounterResponse
|
||||
Network: order.Network,
|
||||
ExpirationTime: order.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
||||
RedirectUrl: order.RedirectUrl,
|
||||
PaymentUrl: fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), order.TradeId),
|
||||
CreatedAt: order.CreatedAt.TimestampMilli(),
|
||||
IsSelected: order.IsSelected,
|
||||
}
|
||||
}
|
||||
|
||||
func switchToOkPay(parent *mdb.Orders, token string) (*response.CheckoutCounterResponse, error) {
|
||||
if !data.GetOkPayEnabled() {
|
||||
return nil, constant.PaymentProviderNotEnabled
|
||||
}
|
||||
if data.GetOkPayShopID() == "" || data.GetOkPayShopToken() == "" || data.GetOkPayAPIURL() == "" || data.GetOkPayCallbackURL() == "" {
|
||||
return nil, constant.PaymentProviderConfigErr
|
||||
}
|
||||
if !okPayTokenAllowed(token) {
|
||||
return nil, constant.PaymentProviderNotSupport
|
||||
}
|
||||
|
||||
existing, err := data.GetSubOrderByTokenPayProvider(parent.TradeId, token, mdb.PaymentProviderOkPay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.ID > 0 {
|
||||
providerRow, err := data.GetProviderOrderByTradeIDAndProvider(existing.TradeId, mdb.PaymentProviderOkPay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if providerRow.ID == 0 || strings.TrimSpace(providerRow.PayURL) == "" {
|
||||
return nil, constant.SystemErr
|
||||
}
|
||||
_ = data.MarkOrderSelected(parent.TradeId)
|
||||
_ = data.MarkOrderSelected(existing.TradeId)
|
||||
_ = data.RefreshOrderExpiration(parent.TradeId)
|
||||
existing.IsSelected = true
|
||||
resp := buildCheckoutResponse(existing)
|
||||
resp.PaymentUrl = providerRow.PayURL
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
count, err := data.CountActiveSubOrders(parent.TradeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count >= MaxSubOrders {
|
||||
return nil, constant.SubOrderLimitExceeded
|
||||
}
|
||||
|
||||
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(parent.Currency))
|
||||
if rate <= 0 {
|
||||
return nil, constant.RateAmountErr
|
||||
}
|
||||
decimalPayAmount := decimal.NewFromFloat(parent.Amount)
|
||||
decimalTokenAmount := decimalPayAmount.Mul(decimal.NewFromFloat(rate))
|
||||
if decimalTokenAmount.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
|
||||
return nil, constant.PayAmountErr
|
||||
}
|
||||
|
||||
subTradeID := GenerateCode()
|
||||
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
|
||||
returnURL := strings.TrimSpace(parent.RedirectUrl)
|
||||
if returnURL == "" {
|
||||
returnURL = data.GetOkPayReturnURL()
|
||||
}
|
||||
if returnURL == "" {
|
||||
returnURL = fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), parent.TradeId)
|
||||
}
|
||||
|
||||
tx := dao.Mdb.Begin()
|
||||
subOrder := &mdb.Orders{
|
||||
TradeId: subTradeID,
|
||||
OrderId: subTradeID,
|
||||
ParentTradeId: parent.TradeId,
|
||||
Amount: parent.Amount,
|
||||
Currency: parent.Currency,
|
||||
ActualAmount: amount,
|
||||
ReceiveAddress: "OKPAY",
|
||||
Token: token,
|
||||
Network: mdb.NetworkTron,
|
||||
Status: mdb.StatusWaitPay,
|
||||
IsSelected: true,
|
||||
NotifyUrl: "",
|
||||
RedirectUrl: parent.RedirectUrl,
|
||||
Name: parent.Name,
|
||||
CallBackConfirm: mdb.CallBackConfirmOk,
|
||||
PaymentType: parent.PaymentType,
|
||||
PayProvider: mdb.PaymentProviderOkPay,
|
||||
ApiKeyID: parent.ApiKeyID,
|
||||
}
|
||||
if err = data.CreateOrderWithTransaction(tx, subOrder); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
providerRow := &mdb.ProviderOrder{
|
||||
TradeId: subTradeID,
|
||||
Provider: mdb.PaymentProviderOkPay,
|
||||
ProviderOrderID: "",
|
||||
PayURL: "",
|
||||
Amount: amount,
|
||||
Coin: token,
|
||||
Status: mdb.ProviderOrderStatusCreating,
|
||||
}
|
||||
if err = data.CreateProviderOrderWithTransaction(tx, providerRow); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
okpayOrder, err := createOkPayDepositOrder(subTradeID, amount, token, returnURL)
|
||||
if err != nil {
|
||||
_ = data.MarkProviderOrderFailed(subTradeID, mdb.PaymentProviderOkPay)
|
||||
_ = data.ExpireOrderByTradeID(subTradeID)
|
||||
return nil, err
|
||||
}
|
||||
if err = data.UpdateProviderOrderCreated(subTradeID, mdb.PaymentProviderOkPay, okpayOrder.ProviderOrderID, okpayOrder.PayURL); err != nil {
|
||||
_ = data.MarkProviderOrderFailed(subTradeID, mdb.PaymentProviderOkPay)
|
||||
_ = data.ExpireOrderByTradeID(subTradeID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = data.MarkOrderSelected(parent.TradeId)
|
||||
_ = data.RefreshOrderExpiration(parent.TradeId)
|
||||
|
||||
resp := buildCheckoutResponse(subOrder)
|
||||
resp.PaymentUrl = okpayOrder.PayURL
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func okPayTokenAllowed(token string) bool {
|
||||
for _, item := range data.GetOkPayAllowTokens() {
|
||||
if strings.EqualFold(item, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -612,6 +612,81 @@ func TestOrderProcessingSubOrderPaidParentKeepsOwnFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderProcessingParentDirectPayExpiresOkPayProviderOrder(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := data.AddWalletAddress("TTestTronAddress001"); err != nil {
|
||||
t.Fatalf("add tron wallet: %v", err)
|
||||
}
|
||||
|
||||
parentReq := newCreateTransactionRequest("order_parent_direct_okpay_expire", 1)
|
||||
parentReq.Network = mdb.NetworkTron
|
||||
parentResp, err := CreateTransaction(parentReq, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("create parent order: %v", err)
|
||||
}
|
||||
|
||||
subOrder := &mdb.Orders{
|
||||
TradeId: "okpay_sub_parent_direct_expire",
|
||||
OrderId: "okpay_sub_parent_direct_expire",
|
||||
ParentTradeId: parentResp.TradeId,
|
||||
Amount: parentResp.Amount,
|
||||
Currency: parentResp.Currency,
|
||||
ActualAmount: 0.15,
|
||||
ReceiveAddress: "OKPAY",
|
||||
Token: "USDT",
|
||||
Network: mdb.NetworkTron,
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: "",
|
||||
CallBackConfirm: mdb.CallBackConfirmOk,
|
||||
PayProvider: mdb.PaymentProviderOkPay,
|
||||
}
|
||||
if err := dao.Mdb.Create(subOrder).Error; err != nil {
|
||||
t.Fatalf("create okpay sub-order: %v", err)
|
||||
}
|
||||
providerRow := &mdb.ProviderOrder{
|
||||
TradeId: subOrder.TradeId,
|
||||
Provider: mdb.PaymentProviderOkPay,
|
||||
ProviderOrderID: "okp-parent-direct-expire",
|
||||
PayURL: "https://t.me/ExampleWalletBot?start=shop_deposit--okpay-order-parent-direct-expire",
|
||||
Amount: subOrder.ActualAmount,
|
||||
Coin: subOrder.Token,
|
||||
Status: mdb.ProviderOrderStatusPending,
|
||||
}
|
||||
if err := dao.Mdb.Create(providerRow).Error; err != nil {
|
||||
t.Fatalf("create provider row: %v", err)
|
||||
}
|
||||
|
||||
err = OrderProcessing(&request.OrderProcessingRequest{
|
||||
ReceiveAddress: parentResp.ReceiveAddress,
|
||||
Token: strings.ToUpper(parentResp.Token),
|
||||
Network: mdb.NetworkTron,
|
||||
TradeId: parentResp.TradeId,
|
||||
Amount: parentResp.ActualAmount,
|
||||
BlockTransactionId: "block_parent_direct_okpay_expire",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("order processing parent direct pay: %v", err)
|
||||
}
|
||||
|
||||
expiredSub, err := data.GetOrderInfoByTradeId(subOrder.TradeId)
|
||||
if err != nil {
|
||||
t.Fatalf("reload sub-order: %v", err)
|
||||
}
|
||||
if expiredSub.Status != mdb.StatusExpired {
|
||||
t.Fatalf("sub-order status = %d, want %d", expiredSub.Status, mdb.StatusExpired)
|
||||
}
|
||||
|
||||
expiredProviderRow, err := data.GetProviderOrderByTradeIDAndProvider(subOrder.TradeId, mdb.PaymentProviderOkPay)
|
||||
if err != nil {
|
||||
t.Fatalf("reload provider row: %v", err)
|
||||
}
|
||||
if expiredProviderRow.Status != mdb.ProviderOrderStatusExpired {
|
||||
t.Fatalf("provider row status = %q, want %q", expiredProviderRow.Status, mdb.ProviderOrderStatusExpired)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrderProcessingSubOrderExpiresSiblingsAndReleasesLocks verifies that when one
|
||||
// sub-order is paid all sibling sub-orders are expired and their runtime locks (as
|
||||
// well as the parent's lock) are released.
|
||||
|
||||
Reference in New Issue
Block a user