mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dc538c0e1 | |||
| b4d1a4d8d4 |
@@ -26,9 +26,9 @@ import (
|
|||||||
// rate.okx_c2c_enabled (bool) — use OKX C2C rate feed
|
// rate.okx_c2c_enabled (bool) — use OKX C2C rate feed
|
||||||
//
|
//
|
||||||
// - group=epay:
|
// - group=epay:
|
||||||
// epay.default_token (string) — default token for EPAY submit.php, e.g. "usdt"; empty allows status=4 placeholders when request token/network are also absent
|
// epay.default_token (string) — default token for EPAY submit.php, e.g. "usdt"; ignored when a supported type=token.network selector is supplied; empty allows status=4 placeholders when request token/network are also absent
|
||||||
// epay.default_currency (string) — default fiat currency for EPAY submit.php, e.g. "cny"; empty falls back to cny
|
// epay.default_currency (string) — default fiat currency for EPAY submit.php, e.g. "cny"; still applies when a supported type selector is supplied; empty falls back to cny
|
||||||
// epay.default_network (string) — default network for EPAY submit.php, e.g. "tron" or "ton"; empty allows status=4 placeholders when request token/network are also absent
|
// epay.default_network (string) — default network for EPAY submit.php, e.g. "tron" or "ton"; ignored when a supported type=token.network selector is supplied; empty allows status=4 placeholders when request token/network are also absent
|
||||||
//
|
//
|
||||||
// - group=okpay:
|
// - group=okpay:
|
||||||
// okpay.enabled (bool) — enable OkPay as a switch-network payment option
|
// okpay.enabled (bool) — enable OkPay as a switch-network payment option
|
||||||
@@ -95,7 +95,7 @@ func (c *BaseAdminController) ListSettings(ctx echo.Context) error {
|
|||||||
// @Summary Upsert settings
|
// @Summary Upsert settings
|
||||||
// @Description Batch insert/update settings. Returns per-key status; failed items include error_code for frontend i18n.
|
// @Description Batch insert/update settings. Returns per-key status; failed items include error_code for frontend i18n.
|
||||||
// @Description Supported groups: brand, rate, system, epay, okpay.
|
// @Description Supported groups: brand, rate, system, epay, okpay.
|
||||||
// @Description epay group keys: epay.default_token (e.g. "usdt" or "ton", empty allows status=4 placeholders), epay.default_currency (e.g. "cny", empty falls back to cny), epay.default_network (e.g. "tron" or "ton", empty allows status=4 placeholders).
|
// @Description epay group keys: epay.default_token (e.g. "usdt" or "ton", ignored when a supported type=token.network selector is supplied, empty allows status=4 placeholders), epay.default_currency (e.g. "cny", still applies when a supported type selector is supplied, empty falls back to cny), epay.default_network (e.g. "tron" or "ton", ignored when a supported type=token.network selector is supplied, empty allows status=4 placeholders).
|
||||||
// @Description okpay group keys: okpay.enabled, okpay.shop_id, okpay.shop_token, okpay.api_url, okpay.callback_url, okpay.return_url, okpay.timeout_seconds, okpay.allow_tokens.
|
// @Description okpay group keys: okpay.enabled, okpay.shop_id, okpay.shop_token, okpay.api_url, okpay.callback_url, okpay.return_url, okpay.timeout_seconds, okpay.allow_tokens.
|
||||||
// @Description rate group keys: rate.forced_rate_list (JSON map, e.g. {"cny":{"usdt":0.14635,"ton":0.5}}; base/coin keys are normalized to lowercase), rate.api_url, rate.adjust_percent, rate.okx_c2c_enabled.
|
// @Description rate group keys: rate.forced_rate_list (JSON map, e.g. {"cny":{"usdt":0.14635,"ton":0.5}}; base/coin keys are normalized to lowercase), rate.api_url, rate.adjust_percent, rate.okx_c2c_enabled.
|
||||||
// @Description brand group keys: brand.checkout_name, brand.logo_url, brand.site_title, brand.success_copy, brand.support_url, brand.background_color, brand.background_image_url. Legacy aliases brand.site_name, brand.page_title and brand.pay_success_text are also supported.
|
// @Description brand group keys: brand.checkout_name, brand.logo_url, brand.site_title, brand.success_copy, brand.support_url, brand.background_color, brand.background_image_url. Legacy aliases brand.site_name, brand.page_title and brand.pay_success_text are also supported.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package comm
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/GMWalletApp/epusdt/middleware"
|
"github.com/GMWalletApp/epusdt/middleware"
|
||||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
@@ -13,6 +14,8 @@ import (
|
|||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const EPayTypeContextKey = "epay_type"
|
||||||
|
|
||||||
// apiKeyFromContext returns the api_keys row stamped by CheckApiSign.
|
// apiKeyFromContext returns the api_keys row stamped by CheckApiSign.
|
||||||
// Returns nil when the middleware didn't run (should not happen on authed routes).
|
// Returns nil when the middleware didn't run (should not happen on authed routes).
|
||||||
func apiKeyFromContext(ctx echo.Context) *mdb.ApiKey {
|
func apiKeyFromContext(ctx echo.Context) *mdb.ApiKey {
|
||||||
@@ -101,7 +104,10 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
|
|||||||
// POST (form) per the legacy EPAY protocol; swagger documents POST as
|
// POST (form) per the legacy EPAY protocol; swagger documents POST as
|
||||||
// the canonical form — the GET variant is identical save the transport.
|
// the canonical form — the GET variant is identical save the transport.
|
||||||
// @Summary Create transaction and redirect (EPAY compat)
|
// @Summary Create transaction and redirect (EPAY compat)
|
||||||
// @Description Legacy EPAY-style endpoint. Accepts GET (querystring) and POST (form). On success, 302 redirects to /pay/checkout-counter/{trade_id}. Signature uses MD5 of sorted params + secret_key of the api_keys row matching the submitted pid. Optional request token/network/currency override database defaults and must be included in the EPay signature when sent. The server injects internal payment_type=Epay after EPay signature verification; merchants do not send GMPay payment_type to this endpoint.
|
// @Description Legacy EPAY-style endpoint. Accepts GET (querystring) and POST (form). On success, 302 redirects to /pay/checkout-counter/{trade_id}. Signature uses MD5 of sorted params + secret_key of the api_keys row matching the submitted pid.
|
||||||
|
// @Description After signature verification, token/network resolution is: supported type=token.network selector (for example usdt.tron) first; otherwise request token/network; otherwise epay.default_token / epay.default_network. If token and network are still both empty, the order is created as status=4 placeholder. Supplying only one of token/network remains invalid.
|
||||||
|
// @Description Currency resolution is unchanged: request currency -> epay.default_currency -> cny. Supported type selectors bypass only token/network defaults, not currency fallback.
|
||||||
|
// @Description Success return/notify currently use the stored request type when present, and fall back to alipay only when type was missing. The server injects internal payment_type=Epay after EPay signature verification; merchants do not send GMPay payment_type to this endpoint.
|
||||||
// @Tags Payment
|
// @Tags Payment
|
||||||
// @Accept x-www-form-urlencoded
|
// @Accept x-www-form-urlencoded
|
||||||
// @Produce html
|
// @Produce html
|
||||||
@@ -111,7 +117,7 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
|
|||||||
// @Param notify_url query string false "Callback URL (GET query)"
|
// @Param notify_url query string false "Callback URL (GET query)"
|
||||||
// @Param return_url query string false "Redirect URL after payment (GET query)"
|
// @Param return_url query string false "Redirect URL after payment (GET query)"
|
||||||
// @Param name query string false "Order name (GET query)"
|
// @Param name query string false "Order name (GET query)"
|
||||||
// @Param type query string false "Payment type (e.g. alipay, GET query)"
|
// @Param type query string false "Opaque merchant type or supported token.network selector such as alipay or usdt.tron (GET query)"
|
||||||
// @Param sign query string false "MD5 signature (GET query)"
|
// @Param sign query string false "MD5 signature (GET query)"
|
||||||
// @Param sign_type query string false "Signature type (MD5, GET query)"
|
// @Param sign_type query string false "Signature type (MD5, GET query)"
|
||||||
// @Param pid formData integer true "API key PID"
|
// @Param pid formData integer true "API key PID"
|
||||||
@@ -120,7 +126,7 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
|
|||||||
// @Param notify_url formData string true "Callback URL"
|
// @Param notify_url formData string true "Callback URL"
|
||||||
// @Param return_url formData string false "Redirect URL after payment"
|
// @Param return_url formData string false "Redirect URL after payment"
|
||||||
// @Param name formData string false "Order name"
|
// @Param name formData string false "Order name"
|
||||||
// @Param type formData string false "Payment type (e.g. alipay)"
|
// @Param type formData string false "Opaque merchant type or supported token.network selector such as alipay or usdt.tron"
|
||||||
// @Param sign formData string true "MD5 signature"
|
// @Param sign formData string true "MD5 signature"
|
||||||
// @Param sign_type formData string false "Signature type (MD5)"
|
// @Param sign_type formData string false "Signature type (MD5)"
|
||||||
// @Success 302 "Redirect to checkout counter"
|
// @Success 302 "Redirect to checkout counter"
|
||||||
@@ -133,6 +139,9 @@ func (c *BaseCommController) CreateTransactionAndRedirect(ctx echo.Context) (err
|
|||||||
log.Sugar.Errorf("bind request error: %v", err)
|
log.Sugar.Errorf("bind request error: %v", err)
|
||||||
return c.FailJson(ctx, constant.ParamsMarshalErr)
|
return c.FailJson(ctx, constant.ParamsMarshalErr)
|
||||||
}
|
}
|
||||||
|
if raw, ok := ctx.Get(EPayTypeContextKey).(string); ok {
|
||||||
|
req.EpayType = strings.TrimSpace(raw)
|
||||||
|
}
|
||||||
if err = c.ValidateStruct(ctx, req); err != nil {
|
if err = c.ValidateStruct(ctx, req); err != nil {
|
||||||
log.Sugar.Errorf("validate request error: %v", err)
|
log.Sugar.Errorf("validate request error: %v", err)
|
||||||
return c.FailJson(ctx, err)
|
return c.FailJson(ctx, err)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// CheckoutCounter 收银台
|
// CheckoutCounter 收银台
|
||||||
// @Summary Checkout counter page
|
// @Summary Checkout counter page
|
||||||
// @Description Return checkout initialization data when the order exists. This endpoint only confirms order existence and returns base order data; call /pay/check-status/{trade_id} for the current order status (1=waiting payment, 2=paid, 3=expired, 4=waiting token/network selection).
|
// @Description Return checkout initialization data when the order exists. This endpoint only confirms order existence and returns base order data; call /pay/check-status/{trade_id} for the current order status (1=waiting payment, 2=paid, 3=expired, 4=waiting token/network selection).
|
||||||
// @Description When status=4, actual_amount is 0 and token/network/receive_address are empty; this state is produced by GMPay placeholders or EPay submit.php when no token/network request values or database defaults exist. The cashier should guide the payer to choose an on-chain token/network or OkPay and then call /pay/switch-network.
|
// @Description When status=4, actual_amount is 0 and token/network/receive_address are empty; this state is produced by GMPay placeholders or EPay submit.php when no token/network request values or defaults resolve to a concrete payment asset. The cashier should guide the payer to choose an on-chain token/network or OkPay and then call /pay/switch-network.
|
||||||
// @Description For EPay orders with a merchant return_url, the response redirect_url is rewritten to the internal /pay/return/{trade_id} hop; the database still stores the merchant's raw return_url.
|
// @Description For EPay orders with a merchant return_url, the response redirect_url is rewritten to the internal /pay/return/{trade_id} hop; the database still stores the merchant's raw return_url.
|
||||||
// @Tags Payment
|
// @Tags Payment
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@@ -36,6 +36,7 @@ func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
|
|||||||
// Non-EPay or not-yet-paid orders are sent back to the checkout counter.
|
// Non-EPay or not-yet-paid orders are sent back to the checkout counter.
|
||||||
// @Summary Return to merchant (EPAY compat)
|
// @Summary Return to merchant (EPAY compat)
|
||||||
// @Description Browser-facing success return hop for EPay orders. Paid EPay orders are redirected to the merchant return_url with signed legacy EPay query params. Orders that are not EPay or not yet paid are redirected back to the checkout counter.
|
// @Description Browser-facing success return hop for EPay orders. Paid EPay orders are redirected to the merchant return_url with signed legacy EPay query params. Orders that are not EPay or not yet paid are redirected back to the checkout counter.
|
||||||
|
// @Description The signed query params currently use the stored request type when present; otherwise type=alipay is returned for compatibility.
|
||||||
// @Description This route also returns explicit business errors when the merchant return_url is missing, the order API key is unavailable, or EPay signature construction fails.
|
// @Description This route also returns explicit business errors when the merchant return_url is missing, the order API key is unavailable, or EPay signature construction fails.
|
||||||
// @Tags Payment
|
// @Tags Payment
|
||||||
// @Produce html
|
// @Produce html
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ type Orders struct {
|
|||||||
NotifyUrl string `gorm:"column:notify_url" json:"notify_url" example:"https://example.com/notify"`
|
NotifyUrl string `gorm:"column:notify_url" json:"notify_url" example:"https://example.com/notify"`
|
||||||
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url" example:"https://example.com/success"`
|
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url" example:"https://example.com/success"`
|
||||||
Name string `gorm:"column:name" json:"name" example:"VIP月卡"`
|
Name string `gorm:"column:name" json:"name" example:"VIP月卡"`
|
||||||
|
EpayType string `gorm:"column:epay_type;size:64;default:''" json:"-"`
|
||||||
CallbackNum int `gorm:"column:callback_num;default:0" json:"callback_num" example:"0"`
|
CallbackNum int `gorm:"column:callback_num;default:0" json:"callback_num" example:"0"`
|
||||||
// 回调确认状态 1=回调成功 2=未回调/回调失败
|
// 回调确认状态 1=回调成功 2=未回调/回调失败
|
||||||
CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm" enums:"1,2" example:"2"`
|
CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm" enums:"1,2" example:"2"`
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type CreateTransactionRequest struct {
|
|||||||
// empty or any other value is stored as "Gmpay" and uses GMPay JSON.
|
// empty or any other value is stored as "Gmpay" and uses GMPay JSON.
|
||||||
// It is optional for GMPay, but must be included in the signature when sent.
|
// It is optional for GMPay, but must be included in the signature when sent.
|
||||||
PaymentType string `json:"payment_type" form:"payment_type" example:"Epay"`
|
PaymentType string `json:"payment_type" form:"payment_type" example:"Epay"`
|
||||||
|
EpayType string `json:"-" form:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r CreateTransactionRequest) Translates() map[string]string {
|
func (r CreateTransactionRequest) Translates() map[string]string {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ type NetworkTokenSupport struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type EpayPublicConfig struct {
|
type EpayPublicConfig struct {
|
||||||
DefaultToken string `json:"default_token" example:""` // EPay default token; empty means submit.php can create a status=4 placeholder when request token/network are also absent.
|
DefaultToken string `json:"default_token" example:""` // EPay default token; ignored when a supported type=token.network selector is supplied. Empty means submit.php can create a status=4 placeholder when request token/network are also absent.
|
||||||
DefaultCurrency string `json:"default_currency" example:"cny"` // EPay default fiat currency; falls back to cny when unset.
|
DefaultCurrency string `json:"default_currency" example:"cny"` // EPay default fiat currency; still applies when a supported type selector is supplied and falls back to cny when unset.
|
||||||
DefaultNetwork string `json:"default_network" example:""` // EPay default network; empty means submit.php can create a status=4 placeholder when request token/network are also absent.
|
DefaultNetwork string `json:"default_network" example:""` // EPay default network; ignored when a supported type=token.network selector is supplied. Empty means submit.php can create a status=4 placeholder when request token/network are also absent.
|
||||||
}
|
}
|
||||||
|
|
||||||
type SitePublicConfig struct {
|
type SitePublicConfig struct {
|
||||||
|
|||||||
@@ -71,6 +71,16 @@ func ResolveOrderApiKey(order *mdb.Orders) (*mdb.ApiKey, error) {
|
|||||||
return row, nil
|
return row, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func epayResultType(order *mdb.Orders) string {
|
||||||
|
if order == nil {
|
||||||
|
return "alipay"
|
||||||
|
}
|
||||||
|
if epayType := strings.TrimSpace(order.EpayType); epayType != "" {
|
||||||
|
return epayType
|
||||||
|
}
|
||||||
|
return "alipay"
|
||||||
|
}
|
||||||
|
|
||||||
func BuildEPayResultParams(order *mdb.Orders, apiKeyRow *mdb.ApiKey) (map[string]string, error) {
|
func BuildEPayResultParams(order *mdb.Orders, apiKeyRow *mdb.ApiKey) (map[string]string, error) {
|
||||||
if order == nil || apiKeyRow == nil {
|
if order == nil || apiKeyRow == nil {
|
||||||
return nil, constant.EPayReturnSignatureErr
|
return nil, constant.EPayReturnSignatureErr
|
||||||
@@ -85,7 +95,7 @@ func BuildEPayResultParams(order *mdb.Orders, apiKeyRow *mdb.ApiKey) (map[string
|
|||||||
PID: pidInt,
|
PID: pidInt,
|
||||||
TradeNo: order.TradeId,
|
TradeNo: order.TradeId,
|
||||||
OutTradeNo: order.OrderId,
|
OutTradeNo: order.OrderId,
|
||||||
Type: "alipay",
|
Type: epayResultType(order),
|
||||||
Name: order.Name,
|
Name: order.Name,
|
||||||
Money: fmt.Sprintf("%.4f", order.Amount),
|
Money: fmt.Sprintf("%.4f", order.Amount),
|
||||||
TradeStatus: "TRADE_SUCCESS",
|
TradeStatus: "TRADE_SUCCESS",
|
||||||
|
|||||||
@@ -40,12 +40,13 @@ func TestBuildPublicRedirectURLRewritesOnlyEpayOrders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildEPayResultParamsMatchesLegacyFields(t *testing.T) {
|
func TestBuildEPayResultParamsUsesOriginalType(t *testing.T) {
|
||||||
params, err := BuildEPayResultParams(&mdb.Orders{
|
params, err := BuildEPayResultParams(&mdb.Orders{
|
||||||
TradeId: "trade_epay_params",
|
TradeId: "trade_epay_params",
|
||||||
OrderId: "order_epay_params",
|
OrderId: "order_epay_params",
|
||||||
Name: "VIP",
|
Name: "VIP",
|
||||||
Amount: 1,
|
Amount: 1,
|
||||||
|
EpayType: "usdt.tron",
|
||||||
}, &mdb.ApiKey{
|
}, &mdb.ApiKey{
|
||||||
Pid: "1001",
|
Pid: "1001",
|
||||||
SecretKey: "epay-secret",
|
SecretKey: "epay-secret",
|
||||||
@@ -58,7 +59,7 @@ func TestBuildEPayResultParamsMatchesLegacyFields(t *testing.T) {
|
|||||||
"pid": "1001",
|
"pid": "1001",
|
||||||
"trade_no": "trade_epay_params",
|
"trade_no": "trade_epay_params",
|
||||||
"out_trade_no": "order_epay_params",
|
"out_trade_no": "order_epay_params",
|
||||||
"type": "alipay",
|
"type": "usdt.tron",
|
||||||
"name": "VIP",
|
"name": "VIP",
|
||||||
"money": "1.0000",
|
"money": "1.0000",
|
||||||
"trade_status": "TRADE_SUCCESS",
|
"trade_status": "TRADE_SUCCESS",
|
||||||
@@ -88,6 +89,24 @@ func TestBuildEPayResultParamsMatchesLegacyFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildEPayResultParamsFallsBackToAlipayWhenTypeMissing(t *testing.T) {
|
||||||
|
params, err := BuildEPayResultParams(&mdb.Orders{
|
||||||
|
TradeId: "trade_epay_fallback",
|
||||||
|
OrderId: "order_epay_fallback",
|
||||||
|
Name: "VIP",
|
||||||
|
Amount: 1,
|
||||||
|
}, &mdb.ApiKey{
|
||||||
|
Pid: "1001",
|
||||||
|
SecretKey: "epay-secret",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildEPayResultParams(): %v", err)
|
||||||
|
}
|
||||||
|
if got := params["type"]; got != "alipay" {
|
||||||
|
t.Fatalf("type = %q, want alipay", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildEPayResultParamsRejectsNonNumericPid(t *testing.T) {
|
func TestBuildEPayResultParamsRejectsNonNumericPid(t *testing.T) {
|
||||||
_, err := BuildEPayResultParams(&mdb.Orders{
|
_, err := BuildEPayResultParams(&mdb.Orders{
|
||||||
TradeId: "trade_bad_pid",
|
TradeId: "trade_bad_pid",
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
|
|||||||
if strings.EqualFold(req.PaymentType, mdb.PaymentTypeEpay) {
|
if strings.EqualFold(req.PaymentType, mdb.PaymentTypeEpay) {
|
||||||
paymentType = mdb.PaymentTypeEpay
|
paymentType = mdb.PaymentTypeEpay
|
||||||
}
|
}
|
||||||
|
epayType := ""
|
||||||
|
if paymentType == mdb.PaymentTypeEpay {
|
||||||
|
epayType = strings.TrimSpace(req.EpayType)
|
||||||
|
}
|
||||||
|
|
||||||
gCreateTransactionLock.Lock()
|
gCreateTransactionLock.Lock()
|
||||||
defer gCreateTransactionLock.Unlock()
|
defer gCreateTransactionLock.Unlock()
|
||||||
@@ -146,6 +150,7 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
|
|||||||
NotifyUrl: notifyURL,
|
NotifyUrl: notifyURL,
|
||||||
RedirectUrl: req.RedirectUrl,
|
RedirectUrl: req.RedirectUrl,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
|
EpayType: epayType,
|
||||||
PaymentType: paymentType,
|
PaymentType: paymentType,
|
||||||
PayProvider: mdb.PaymentProviderOnChain,
|
PayProvider: mdb.PaymentProviderOnChain,
|
||||||
ApiKeyID: apiKeyID(apiKey),
|
ApiKeyID: apiKeyID(apiKey),
|
||||||
@@ -205,6 +210,7 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
|
|||||||
NotifyUrl: notifyURL,
|
NotifyUrl: notifyURL,
|
||||||
RedirectUrl: req.RedirectUrl,
|
RedirectUrl: req.RedirectUrl,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
|
EpayType: epayType,
|
||||||
PaymentType: paymentType,
|
PaymentType: paymentType,
|
||||||
PayProvider: mdb.PaymentProviderOnChain,
|
PayProvider: mdb.PaymentProviderOnChain,
|
||||||
ApiKeyID: apiKeyID(apiKey),
|
ApiKeyID: apiKeyID(apiKey),
|
||||||
@@ -637,6 +643,7 @@ func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounter
|
|||||||
NotifyUrl: "",
|
NotifyUrl: "",
|
||||||
RedirectUrl: parent.RedirectUrl,
|
RedirectUrl: parent.RedirectUrl,
|
||||||
Name: parent.Name,
|
Name: parent.Name,
|
||||||
|
EpayType: parent.EpayType,
|
||||||
CallBackConfirm: mdb.CallBackConfirmOk, // don't trigger callback on sub-order
|
CallBackConfirm: mdb.CallBackConfirmOk, // don't trigger callback on sub-order
|
||||||
PaymentType: parent.PaymentType,
|
PaymentType: parent.PaymentType,
|
||||||
PayProvider: mdb.PaymentProviderOnChain,
|
PayProvider: mdb.PaymentProviderOnChain,
|
||||||
@@ -916,6 +923,7 @@ func switchToOkPay(parent *mdb.Orders, token string) (*response.CheckoutCounterR
|
|||||||
NotifyUrl: "",
|
NotifyUrl: "",
|
||||||
RedirectUrl: parent.RedirectUrl,
|
RedirectUrl: parent.RedirectUrl,
|
||||||
Name: parent.Name,
|
Name: parent.Name,
|
||||||
|
EpayType: parent.EpayType,
|
||||||
CallBackConfirm: mdb.CallBackConfirmOk,
|
CallBackConfirm: mdb.CallBackConfirmOk,
|
||||||
PaymentType: parent.PaymentType,
|
PaymentType: parent.PaymentType,
|
||||||
PayProvider: mdb.PaymentProviderOkPay,
|
PayProvider: mdb.PaymentProviderOkPay,
|
||||||
|
|||||||
+88
-2
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -573,6 +574,7 @@ func TestSendOrderCallbackEpayUsesApiKeySecretByPid(t *testing.T) {
|
|||||||
Status: mdb.StatusPaySuccess,
|
Status: mdb.StatusPaySuccess,
|
||||||
NotifyUrl: server.URL,
|
NotifyUrl: server.URL,
|
||||||
BlockTransactionId: "block_epay_sign",
|
BlockTransactionId: "block_epay_sign",
|
||||||
|
EpayType: "usdt.tron",
|
||||||
PaymentType: "epay",
|
PaymentType: "epay",
|
||||||
ApiKeyID: key.ID,
|
ApiKeyID: key.ID,
|
||||||
}
|
}
|
||||||
@@ -587,8 +589,8 @@ func TestSendOrderCallbackEpayUsesApiKeySecretByPid(t *testing.T) {
|
|||||||
if formPayload["sign_type"] != "MD5" {
|
if formPayload["sign_type"] != "MD5" {
|
||||||
t.Fatalf("sign_type = %q, want MD5", formPayload["sign_type"])
|
t.Fatalf("sign_type = %q, want MD5", formPayload["sign_type"])
|
||||||
}
|
}
|
||||||
if formPayload["type"] != "alipay" {
|
if formPayload["type"] != "usdt.tron" {
|
||||||
t.Fatalf("type = %q, want alipay", formPayload["type"])
|
t.Fatalf("type = %q, want usdt.tron", formPayload["type"])
|
||||||
}
|
}
|
||||||
if formPayload["trade_status"] != "TRADE_SUCCESS" {
|
if formPayload["trade_status"] != "TRADE_SUCCESS" {
|
||||||
t.Fatalf("trade_status = %q, want TRADE_SUCCESS", formPayload["trade_status"])
|
t.Fatalf("trade_status = %q, want TRADE_SUCCESS", formPayload["trade_status"])
|
||||||
@@ -629,6 +631,90 @@ func TestSendOrderCallbackEpayUsesApiKeySecretByPid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSendOrderCallbackEpayPreservesStoredTypeValues(t *testing.T) {
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
key := &mdb.ApiKey{
|
||||||
|
Name: "epay-key-stored-type",
|
||||||
|
Pid: "9201",
|
||||||
|
SecretKey: "epay-secret-9201",
|
||||||
|
Status: mdb.ApiKeyStatusEnable,
|
||||||
|
}
|
||||||
|
if err := dao.Mdb.Create(key).Error; err != nil {
|
||||||
|
t.Fatalf("create epay api key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
epayType string
|
||||||
|
}{
|
||||||
|
{name: "alipay", epayType: "alipay"},
|
||||||
|
{name: "non_selector", epayType: "usdt-tron"},
|
||||||
|
{name: "unsupported_selector", epayType: "usdc.tron"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
formPayload := map[string]string{}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for k, v := range r.Form {
|
||||||
|
if len(v) > 0 {
|
||||||
|
formPayload[k] = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = io.WriteString(w, "ok")
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
order := &mdb.Orders{
|
||||||
|
TradeId: "trade_epay_type_case_" + strconv.Itoa(idx),
|
||||||
|
OrderId: "order_epay_type_case_" + strconv.Itoa(idx),
|
||||||
|
Amount: 1,
|
||||||
|
Currency: "CNY",
|
||||||
|
ActualAmount: 1,
|
||||||
|
ReceiveAddress: "wallet_epay_type_case",
|
||||||
|
Token: "USDT",
|
||||||
|
Name: "VIP",
|
||||||
|
Status: mdb.StatusPaySuccess,
|
||||||
|
NotifyUrl: server.URL,
|
||||||
|
BlockTransactionId: "block_epay_type_case_" + strconv.Itoa(idx),
|
||||||
|
EpayType: tc.epayType,
|
||||||
|
PaymentType: "epay",
|
||||||
|
ApiKeyID: key.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sendOrderCallback(order); err != nil {
|
||||||
|
t.Fatalf("send epay callback: %v", err)
|
||||||
|
}
|
||||||
|
if got := formPayload["type"]; got != tc.epayType {
|
||||||
|
t.Fatalf("type = %q, want %q", got, tc.epayType)
|
||||||
|
}
|
||||||
|
|
||||||
|
signParams := map[string]interface{}{
|
||||||
|
"pid": formPayload["pid"],
|
||||||
|
"trade_no": formPayload["trade_no"],
|
||||||
|
"out_trade_no": formPayload["out_trade_no"],
|
||||||
|
"type": formPayload["type"],
|
||||||
|
"name": formPayload["name"],
|
||||||
|
"money": formPayload["money"],
|
||||||
|
"trade_status": formPayload["trade_status"],
|
||||||
|
}
|
||||||
|
calcSig, err := sign.Get(signParams, key.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("calc epay signature: %v", err)
|
||||||
|
}
|
||||||
|
if got := formPayload["sign"]; got != calcSig {
|
||||||
|
t.Fatalf("sign = %q, want %q", got, calcSig)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDispatchPendingCallbacksEpayAcceptsSuccessAck(t *testing.T) {
|
func TestDispatchPendingCallbacksEpayAcceptsSuccessAck(t *testing.T) {
|
||||||
cleanup := testutil.SetupTestDatabases(t)
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|||||||
+42
-1
@@ -15,12 +15,42 @@ import (
|
|||||||
"github.com/GMWalletApp/epusdt/middleware"
|
"github.com/GMWalletApp/epusdt/middleware"
|
||||||
"github.com/GMWalletApp/epusdt/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/GMWalletApp/epusdt/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
"github.com/GMWalletApp/epusdt/util/sign"
|
"github.com/GMWalletApp/epusdt/util/sign"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
echoMiddleware "github.com/labstack/echo/v4/middleware"
|
echoMiddleware "github.com/labstack/echo/v4/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// resolveEPayTypeSelector only claims the type as a selector when it maps to a
|
||||||
|
// currently supported payment asset; otherwise the caller should keep the
|
||||||
|
// legacy token/network/default resolution and treat type as opaque.
|
||||||
|
func resolveEPayTypeSelector(rawType string) (string, string, bool, error) {
|
||||||
|
rawType = strings.TrimSpace(rawType)
|
||||||
|
if rawType == "" || strings.Count(rawType, ".") != 1 {
|
||||||
|
return "", "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(rawType, ".", 2)
|
||||||
|
token := strings.TrimSpace(parts[0])
|
||||||
|
network := strings.ToLower(strings.TrimSpace(parts[1]))
|
||||||
|
if token == "" || network == "" {
|
||||||
|
return "", "", false, nil
|
||||||
|
}
|
||||||
|
if !data.IsChainEnabled(network) {
|
||||||
|
return "", "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRow, err := data.GetEnabledChainTokenBySymbol(network, token)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false, err
|
||||||
|
}
|
||||||
|
if tokenRow == nil || tokenRow.ID == 0 || !service.ChainTokenReadyForPayment(*tokenRow) {
|
||||||
|
return "", "", false, nil
|
||||||
|
}
|
||||||
|
return token, network, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterRoute 路由注册
|
// RegisterRoute 路由注册
|
||||||
func RegisterRoute(e *echo.Echo) {
|
func RegisterRoute(e *echo.Echo) {
|
||||||
e.POST("/", func(c echo.Context) error {
|
e.POST("/", func(c echo.Context) error {
|
||||||
@@ -130,17 +160,27 @@ func RegisterRoute(e *echo.Echo) {
|
|||||||
|
|
||||||
money := getString(params, "money")
|
money := getString(params, "money")
|
||||||
name := getString(params, "name")
|
name := getString(params, "name")
|
||||||
|
epayType := strings.TrimSpace(getString(params, "type"))
|
||||||
notifyURL := getString(params, "notify_url")
|
notifyURL := getString(params, "notify_url")
|
||||||
outTradeNo := getString(params, "out_trade_no")
|
outTradeNo := getString(params, "out_trade_no")
|
||||||
returnURL := getString(params, "return_url")
|
returnURL := getString(params, "return_url")
|
||||||
|
selectorToken, selectorNetwork, selectorMatched, err := resolveEPayTypeSelector(epayType)
|
||||||
|
if err != nil {
|
||||||
|
return comm.Ctrl.FailJson(ctx, constant.SystemErr)
|
||||||
|
}
|
||||||
token := strings.TrimSpace(getString(params, "token"))
|
token := strings.TrimSpace(getString(params, "token"))
|
||||||
|
network := strings.TrimSpace(getString(params, "network"))
|
||||||
|
if selectorMatched {
|
||||||
|
token = selectorToken
|
||||||
|
network = selectorNetwork
|
||||||
|
} else {
|
||||||
if token == "" {
|
if token == "" {
|
||||||
token = data.GetSettingString(mdb.SettingKeyEpayDefaultToken, "")
|
token = data.GetSettingString(mdb.SettingKeyEpayDefaultToken, "")
|
||||||
}
|
}
|
||||||
network := strings.TrimSpace(getString(params, "network"))
|
|
||||||
if network == "" {
|
if network == "" {
|
||||||
network = data.GetSettingString(mdb.SettingKeyEpayDefaultNetwork, "")
|
network = data.GetSettingString(mdb.SettingKeyEpayDefaultNetwork, "")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
currency := strings.TrimSpace(getString(params, "currency"))
|
currency := strings.TrimSpace(getString(params, "currency"))
|
||||||
if currency == "" {
|
if currency == "" {
|
||||||
currency = data.GetSettingString(mdb.SettingKeyEpayDefaultCurrency, "")
|
currency = data.GetSettingString(mdb.SettingKeyEpayDefaultCurrency, "")
|
||||||
@@ -168,6 +208,7 @@ func RegisterRoute(e *echo.Echo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.Set("request_body", body)
|
ctx.Set("request_body", body)
|
||||||
|
ctx.Set(comm.EPayTypeContextKey, epayType)
|
||||||
ctx.Set(middleware.ApiKeyIDKey, apiKeyRow.ID)
|
ctx.Set(middleware.ApiKeyIDKey, apiKeyRow.ID)
|
||||||
ctx.Set(middleware.ApiKeyRowKey, apiKeyRow)
|
ctx.Set(middleware.ApiKeyRowKey, apiKeyRow)
|
||||||
|
|
||||||
|
|||||||
@@ -1272,6 +1272,9 @@ func TestEpaySubmitPhpGetCompatible(t *testing.T) {
|
|||||||
if order.PaymentType != mdb.PaymentTypeEpay {
|
if order.PaymentType != mdb.PaymentTypeEpay {
|
||||||
t.Fatalf("epay payment_type = %q, want %q", order.PaymentType, mdb.PaymentTypeEpay)
|
t.Fatalf("epay payment_type = %q, want %q", order.PaymentType, mdb.PaymentTypeEpay)
|
||||||
}
|
}
|
||||||
|
if order.EpayType != "alipay" {
|
||||||
|
t.Fatalf("epay_type = %q, want alipay", order.EpayType)
|
||||||
|
}
|
||||||
checkoutData := getCheckoutCounterRespData(t, e, tradeID)
|
checkoutData := getCheckoutCounterRespData(t, e, tradeID)
|
||||||
if checkoutData["payment_type"] != "epay" {
|
if checkoutData["payment_type"] != "epay" {
|
||||||
t.Fatalf("epay checkout payment_type = %v, want epay", checkoutData["payment_type"])
|
t.Fatalf("epay checkout payment_type = %v, want epay", checkoutData["payment_type"])
|
||||||
@@ -1327,6 +1330,100 @@ func TestEpaySubmitPhpRequestTokenNetworkOverrideDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEpaySubmitPhpTypeSelectorOverridesRequestAndDefaults(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdc", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_token: %v", err)
|
||||||
|
}
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultNetwork, "solana", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_network: %v", err)
|
||||||
|
}
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultCurrency, "", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("clear epay.default_currency: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := signEpayValues(url.Values{
|
||||||
|
"pid": {"1"},
|
||||||
|
"name": {"epay-type-selector-001"},
|
||||||
|
"type": {"usdt.tron"},
|
||||||
|
"money": {"1.00"},
|
||||||
|
"out_trade_no": {"epay-type-selector-001"},
|
||||||
|
"notify_url": {"https://93.184.216.34/notify"},
|
||||||
|
"return_url": {"http://localhost/return"},
|
||||||
|
"token": {"usdc"},
|
||||||
|
"network": {"solana"},
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/payments/epay/v1/order/create-transaction/submit.php?"+values.Encode(), nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusFound {
|
||||||
|
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reload epay selector order: %v", err)
|
||||||
|
}
|
||||||
|
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
||||||
|
t.Fatalf("selector order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
||||||
|
}
|
||||||
|
if order.Currency != "CNY" {
|
||||||
|
t.Fatalf("currency = %q, want CNY", order.Currency)
|
||||||
|
}
|
||||||
|
if order.EpayType != "usdt.tron" {
|
||||||
|
t.Fatalf("epay_type = %q, want usdt.tron", order.EpayType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEpaySubmitPhpTypeSelectorUsesDefaultCurrencyWhenMissing(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdc", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_token: %v", err)
|
||||||
|
}
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultNetwork, "solana", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_network: %v", err)
|
||||||
|
}
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultCurrency, "usd", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_currency: %v", err)
|
||||||
|
}
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupRate, "rate.forced_rate_list", `{"cny":{"usdt":0.14285714285714285},"usd":{"usdt":1}}`, mdb.SettingTypeJSON); err != nil {
|
||||||
|
t.Fatalf("seed rate.forced_rate_list for usd: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := signEpayValues(url.Values{
|
||||||
|
"pid": {"1"},
|
||||||
|
"name": {"epay-type-default-cur-001"},
|
||||||
|
"type": {"usdt.tron"},
|
||||||
|
"money": {"1.00"},
|
||||||
|
"out_trade_no": {"epay-type-default-cur-001"},
|
||||||
|
"notify_url": {"https://93.184.216.34/notify"},
|
||||||
|
"return_url": {"http://localhost/return"},
|
||||||
|
"token": {"usdc"},
|
||||||
|
"network": {"solana"},
|
||||||
|
})
|
||||||
|
|
||||||
|
rec := doFormPost(e, "/payments/epay/v1/order/create-transaction/submit.php", values)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusFound {
|
||||||
|
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reload epay selector order: %v", err)
|
||||||
|
}
|
||||||
|
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
||||||
|
t.Fatalf("selector order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
||||||
|
}
|
||||||
|
if order.Currency != "USD" {
|
||||||
|
t.Fatalf("currency = %q, want USD", order.Currency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEpaySubmitPhpWithoutTokenNetworkDefaultsCreatesPlaceholder(t *testing.T) {
|
func TestEpaySubmitPhpWithoutTokenNetworkDefaultsCreatesPlaceholder(t *testing.T) {
|
||||||
e := setupTestEnv(t)
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
@@ -1365,12 +1462,95 @@ func TestEpaySubmitPhpWithoutTokenNetworkDefaultsCreatesPlaceholder(t *testing.T
|
|||||||
if order.PaymentType != mdb.PaymentTypeEpay {
|
if order.PaymentType != mdb.PaymentTypeEpay {
|
||||||
t.Fatalf("payment_type = %q, want %q", order.PaymentType, mdb.PaymentTypeEpay)
|
t.Fatalf("payment_type = %q, want %q", order.PaymentType, mdb.PaymentTypeEpay)
|
||||||
}
|
}
|
||||||
|
if order.EpayType != "alipay" {
|
||||||
|
t.Fatalf("epay_type = %q, want alipay", order.EpayType)
|
||||||
|
}
|
||||||
checkoutData := getCheckoutCounterRespData(t, e, tradeID)
|
checkoutData := getCheckoutCounterRespData(t, e, tradeID)
|
||||||
if checkoutData["payment_type"] != "epay" || int(checkoutData["status"].(float64)) != mdb.StatusWaitSelect {
|
if checkoutData["payment_type"] != "epay" || int(checkoutData["status"].(float64)) != mdb.StatusWaitSelect {
|
||||||
t.Fatalf("checkout placeholder data = %#v", checkoutData)
|
t.Fatalf("checkout placeholder data = %#v", checkoutData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEpaySubmitPhpNonSelectorTypeKeepsOldLogic(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdt", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_token: %v", err)
|
||||||
|
}
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultNetwork, "tron", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_network: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := signEpayValues(url.Values{
|
||||||
|
"pid": {"1"},
|
||||||
|
"name": {"epay-non-selector-001"},
|
||||||
|
"type": {"usdt-tron"},
|
||||||
|
"money": {"1.00"},
|
||||||
|
"out_trade_no": {"epay-non-selector-001"},
|
||||||
|
"notify_url": {"https://93.184.216.34/notify"},
|
||||||
|
"return_url": {"http://localhost/return"},
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/payments/epay/v1/order/create-transaction/submit.php?"+values.Encode(), nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusFound {
|
||||||
|
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reload epay non-selector order: %v", err)
|
||||||
|
}
|
||||||
|
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
||||||
|
t.Fatalf("non-selector order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
||||||
|
}
|
||||||
|
if order.EpayType != "usdt-tron" {
|
||||||
|
t.Fatalf("epay_type = %q, want usdt-tron", order.EpayType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEpaySubmitPhpUnsupportedSelectorFallsBackToOldLogic(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdt", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_token: %v", err)
|
||||||
|
}
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultNetwork, "tron", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_network: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := signEpayValues(url.Values{
|
||||||
|
"pid": {"1"},
|
||||||
|
"name": {"epay-unsupported-selector-001"},
|
||||||
|
"type": {"usdc.tron"},
|
||||||
|
"money": {"1.00"},
|
||||||
|
"out_trade_no": {"epay-unsupported-selector-001"},
|
||||||
|
"notify_url": {"https://93.184.216.34/notify"},
|
||||||
|
"return_url": {"http://localhost/return"},
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/payments/epay/v1/order/create-transaction/submit.php?"+values.Encode(), nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusFound {
|
||||||
|
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reload epay unsupported selector order: %v", err)
|
||||||
|
}
|
||||||
|
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
||||||
|
t.Fatalf("unsupported selector order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
||||||
|
}
|
||||||
|
if order.EpayType != "usdc.tron" {
|
||||||
|
t.Fatalf("epay_type = %q, want usdc.tron", order.EpayType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEpaySubmitPhpRejectsPartialResolvedTokenNetwork(t *testing.T) {
|
func TestEpaySubmitPhpRejectsPartialResolvedTokenNetwork(t *testing.T) {
|
||||||
e := setupTestEnv(t)
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
@@ -1428,6 +1608,40 @@ func TestEpaySubmitPhpPostFormCompatible(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEpaySubmitPhpTypeSelectorPostFormCompatible(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultNetwork, "solana", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_network: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := signEpayValues(url.Values{
|
||||||
|
"pid": {"1"},
|
||||||
|
"name": {"epay-post-selector-001"},
|
||||||
|
"type": {"usdt.tron"},
|
||||||
|
"money": {"1.00"},
|
||||||
|
"out_trade_no": {"epay-post-selector-001"},
|
||||||
|
"notify_url": {"https://93.184.216.34/notify"},
|
||||||
|
"return_url": {"http://localhost/return"},
|
||||||
|
})
|
||||||
|
|
||||||
|
rec := doFormPost(e, "/payments/epay/v1/order/create-transaction/submit.php", values)
|
||||||
|
if rec.Code != http.StatusFound {
|
||||||
|
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reload epay post selector order: %v", err)
|
||||||
|
}
|
||||||
|
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
||||||
|
t.Fatalf("post selector order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
||||||
|
}
|
||||||
|
if order.EpayType != "usdt.tron" {
|
||||||
|
t.Fatalf("epay_type = %q, want usdt.tron", order.EpayType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestCheckStatus_NotFound verifies that /pay/check-status/:trade_id returns a
|
// TestCheckStatus_NotFound verifies that /pay/check-status/:trade_id returns a
|
||||||
// graceful JSON error (not 500) when the trade_id doesn't exist.
|
// graceful JSON error (not 500) when the trade_id doesn't exist.
|
||||||
func TestCheckStatus_NotFound(t *testing.T) {
|
func TestCheckStatus_NotFound(t *testing.T) {
|
||||||
@@ -1656,6 +1870,93 @@ func TestPayReturn_PaidEpayRedirectsToMerchantWithSignedParams(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPayReturn_PaidEpayRedirectsWithOriginalType(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
tradeID := mustCreateEPayOrder(t, e, "epay-return-type-001", "https://merchant.example/return", url.Values{
|
||||||
|
"type": {"usdt.tron"},
|
||||||
|
})
|
||||||
|
if err := dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("trade_id = ?", tradeID).
|
||||||
|
Update("status", mdb.StatusPaySuccess).Error; err != nil {
|
||||||
|
t.Fatalf("mark order paid: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/pay/return/"+tradeID, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusFound {
|
||||||
|
t.Fatalf("expected 302, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
targetURL, err := url.Parse(rec.Header().Get("Location"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse redirect location: %v", err)
|
||||||
|
}
|
||||||
|
if got := targetURL.Query().Get("type"); got != "usdt.tron" {
|
||||||
|
t.Fatalf("type = %q, want usdt.tron", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayReturn_PaidEpayRedirectsWithNonSelectorType(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
tradeID := mustCreateEPayOrder(t, e, "epay-return-non-selector-001", "https://merchant.example/return", url.Values{
|
||||||
|
"type": {"usdt-tron"},
|
||||||
|
"token": {"usdt"},
|
||||||
|
"network": {"tron"},
|
||||||
|
"currency": {"cny"},
|
||||||
|
})
|
||||||
|
if err := dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("trade_id = ?", tradeID).
|
||||||
|
Update("status", mdb.StatusPaySuccess).Error; err != nil {
|
||||||
|
t.Fatalf("mark order paid: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/pay/return/"+tradeID, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusFound {
|
||||||
|
t.Fatalf("expected 302, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
targetURL, err := url.Parse(rec.Header().Get("Location"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse redirect location: %v", err)
|
||||||
|
}
|
||||||
|
if got := targetURL.Query().Get("type"); got != "usdt-tron" {
|
||||||
|
t.Fatalf("type = %q, want usdt-tron", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayReturn_PaidEpayRedirectsWithUnsupportedSelectorType(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
tradeID := mustCreateEPayOrder(t, e, "epay-return-unsel-001", "https://merchant.example/return", url.Values{
|
||||||
|
"type": {"usdc.tron"},
|
||||||
|
"token": {"usdt"},
|
||||||
|
"network": {"tron"},
|
||||||
|
"currency": {"cny"},
|
||||||
|
})
|
||||||
|
if err := dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("trade_id = ?", tradeID).
|
||||||
|
Update("status", mdb.StatusPaySuccess).Error; err != nil {
|
||||||
|
t.Fatalf("mark order paid: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/pay/return/"+tradeID, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusFound {
|
||||||
|
t.Fatalf("expected 302, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
targetURL, err := url.Parse(rec.Header().Get("Location"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse redirect location: %v", err)
|
||||||
|
}
|
||||||
|
if got := targetURL.Query().Get("type"); got != "usdc.tron" {
|
||||||
|
t.Fatalf("type = %q, want usdc.tron", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPayReturn_UnpaidEpayRedirectsBackToCheckout(t *testing.T) {
|
func TestPayReturn_UnpaidEpayRedirectsBackToCheckout(t *testing.T) {
|
||||||
e := setupTestEnv(t)
|
e := setupTestEnv(t)
|
||||||
tradeID := mustCreateEPayOrder(t, e, "epay-return-unpaid-001", "https://merchant.example/return", url.Values{
|
tradeID := mustCreateEPayOrder(t, e, "epay-return-unpaid-001", "https://merchant.example/return", url.Values{
|
||||||
@@ -2501,6 +2802,9 @@ func TestSwitchNetwork_EpayPlaceholderCompletesChainInPlace(t *testing.T) {
|
|||||||
if parent.Status != mdb.StatusWaitPay || parent.PaymentType != mdb.PaymentTypeEpay || parent.Network != mdb.NetworkTron || parent.Token != "USDT" || parent.ReceiveAddress == "" {
|
if parent.Status != mdb.StatusWaitPay || parent.PaymentType != mdb.PaymentTypeEpay || parent.Network != mdb.NetworkTron || parent.Token != "USDT" || parent.ReceiveAddress == "" {
|
||||||
t.Fatalf("parent fields = status %d payment_type %q network %q token %q address %q", parent.Status, parent.PaymentType, parent.Network, parent.Token, parent.ReceiveAddress)
|
t.Fatalf("parent fields = status %d payment_type %q network %q token %q address %q", parent.Status, parent.PaymentType, parent.Network, parent.Token, parent.ReceiveAddress)
|
||||||
}
|
}
|
||||||
|
if parent.EpayType != "alipay" {
|
||||||
|
t.Fatalf("parent epay_type = %q, want alipay", parent.EpayType)
|
||||||
|
}
|
||||||
if parent.RedirectUrl != "http://localhost/return" {
|
if parent.RedirectUrl != "http://localhost/return" {
|
||||||
t.Fatalf("stored redirect_url = %q, want merchant raw return_url", parent.RedirectUrl)
|
t.Fatalf("stored redirect_url = %q, want merchant raw return_url", parent.RedirectUrl)
|
||||||
}
|
}
|
||||||
@@ -2616,6 +2920,47 @@ func TestSwitchNetwork_OkPayFromEpayWaitSelectPlaceholder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSwitchNetwork_EpayChildInheritsOriginalType(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
tradeID := mustCreateEPayOrder(t, e, "epay-switch-child-type-001", "http://localhost/return", url.Values{
|
||||||
|
"type": {"usdt.tron"},
|
||||||
|
})
|
||||||
|
|
||||||
|
rec := doPost(e, "/pay/switch-network", map[string]interface{}{
|
||||||
|
"trade_id": tradeID,
|
||||||
|
"token": "USDT",
|
||||||
|
"network": "solana",
|
||||||
|
})
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("switch epay parent to solana failed: %d %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
resp := parseResp(t, rec)
|
||||||
|
respData, _ := resp["data"].(map[string]interface{})
|
||||||
|
subTradeID, _ := respData["trade_id"].(string)
|
||||||
|
if subTradeID == "" || subTradeID == tradeID {
|
||||||
|
t.Fatalf("expected child trade_id, got %q", subTradeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
subOrder, err := data.GetOrderInfoByTradeId(subTradeID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load sub-order: %v", err)
|
||||||
|
}
|
||||||
|
if subOrder.EpayType != "usdt.tron" {
|
||||||
|
t.Fatalf("sub-order epay_type = %q, want usdt.tron", subOrder.EpayType)
|
||||||
|
}
|
||||||
|
apiKeyRow, err := data.GetApiKeyByID(subOrder.ApiKeyID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load api key: %v", err)
|
||||||
|
}
|
||||||
|
params, err := service.BuildEPayResultParams(subOrder, apiKeyRow)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildEPayResultParams(): %v", err)
|
||||||
|
}
|
||||||
|
if params["type"] != "usdt.tron" {
|
||||||
|
t.Fatalf("callback type = %q, want usdt.tron", params["type"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSwitchNetwork_OkPayIntegration(t *testing.T) {
|
func TestSwitchNetwork_OkPayIntegration(t *testing.T) {
|
||||||
shopID := strings.TrimSpace(os.Getenv("EPUSDT_OKPAY_ID"))
|
shopID := strings.TrimSpace(os.Getenv("EPUSDT_OKPAY_ID"))
|
||||||
if shopID == "" {
|
if shopID == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user