mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 10:16:15 +00:00
feat: add epay return relay and simplify multi-arch builds
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dist/
|
dist/
|
||||||
*.log
|
*.log
|
||||||
|
bin/
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
APP := epusdt
|
||||||
|
TARGET_DIR := bin
|
||||||
|
BUILD_TAG ?= 0.0.0-dev
|
||||||
|
VERSION_PKG := github.com/GMWalletApp/epusdt/config
|
||||||
|
LDFLAGS := -s -w -X $(VERSION_PKG).BuildVersion=$(BUILD_TAG)
|
||||||
|
|
||||||
|
.PHONY: build release build-all swagger \
|
||||||
|
linux-amd64 linux-arm64 linux-armv7 \
|
||||||
|
darwin-amd64 darwin-arm64 \
|
||||||
|
windows-amd64 windows-arm64
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build --trimpath -ldflags="$(LDFLAGS)" -o $(TARGET_DIR)/$(APP) .
|
||||||
|
|
||||||
|
release: build
|
||||||
|
|
||||||
|
linux-amd64:
|
||||||
|
GOOS=linux GOARCH=amd64 go build --trimpath -ldflags="$(LDFLAGS)" -o $(TARGET_DIR)/$(APP)-linux-amd64 .
|
||||||
|
|
||||||
|
linux-arm64:
|
||||||
|
GOOS=linux GOARCH=arm64 go build --trimpath -ldflags="$(LDFLAGS)" -o $(TARGET_DIR)/$(APP)-linux-arm64 .
|
||||||
|
|
||||||
|
linux-armv7:
|
||||||
|
GOOS=linux GOARCH=arm GOARM=7 go build --trimpath -ldflags="$(LDFLAGS)" -o $(TARGET_DIR)/$(APP)-linux-armv7 .
|
||||||
|
|
||||||
|
darwin-amd64:
|
||||||
|
GOOS=darwin GOARCH=amd64 go build --trimpath -ldflags="$(LDFLAGS)" -o $(TARGET_DIR)/$(APP)-darwin-amd64 .
|
||||||
|
|
||||||
|
darwin-arm64:
|
||||||
|
GOOS=darwin GOARCH=arm64 go build --trimpath -ldflags="$(LDFLAGS)" -o $(TARGET_DIR)/$(APP)-darwin-arm64 .
|
||||||
|
|
||||||
|
windows-amd64:
|
||||||
|
GOOS=windows GOARCH=amd64 go build --trimpath -ldflags="$(LDFLAGS)" -o $(TARGET_DIR)/$(APP)-windows-amd64.exe .
|
||||||
|
|
||||||
|
windows-arm64:
|
||||||
|
GOOS=windows GOARCH=arm64 go build --trimpath -ldflags="$(LDFLAGS)" -o $(TARGET_DIR)/$(APP)-windows-arm64.exe .
|
||||||
|
|
||||||
|
build-all: linux-amd64 linux-arm64 linux-armv7 darwin-amd64 darwin-arm64 windows-amd64 windows-arm64
|
||||||
|
|
||||||
|
swagger:
|
||||||
|
swag init --outputTypes json,yaml --parseInternal --parseDependency -o docs
|
||||||
@@ -65,6 +65,7 @@ func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) {
|
|||||||
// @Description Switch to a different payment target. A status=4 placeholder is completed in place and returns the same parent trade_id with is_selected=false; an already concrete status=1 parent creates or returns the only sub-order when switching to a different target.
|
// @Description Switch to a different payment target. A status=4 placeholder is completed in place and returns the same parent trade_id with is_selected=false; an already concrete status=1 parent creates or returns the only sub-order when switching to a different target.
|
||||||
// @Description Normal values such as ton/tron/solana/ethereum select on-chain payment; the special value okpay selects OkPay hosted payment.
|
// @Description Normal values such as ton/tron/solana/ethereum select on-chain payment; the special value okpay selects OkPay hosted payment.
|
||||||
// @Description For status=4 placeholders from GMPay or EPay submit.php, both on-chain targets and okpay complete the parent in place without creating a child order. Sub-orders cannot be switched again.
|
// @Description For status=4 placeholders from GMPay or EPay submit.php, both on-chain targets and okpay complete the parent in place without creating a child order. Sub-orders cannot be switched again.
|
||||||
|
// @Description For EPay orders with a merchant return_url, the returned redirect_url is the internal /pay/return/{trade_id} hop rather than the raw merchant return_url.
|
||||||
// @Tags Payment
|
// @Tags Payment
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package comm
|
package comm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/GMWalletApp/epusdt/model/request"
|
"github.com/GMWalletApp/epusdt/model/request"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
// @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 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 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
|
||||||
// @Param trade_id path string true "Trade ID"
|
// @Param trade_id path string true "Trade ID"
|
||||||
@@ -30,6 +32,29 @@ func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
|
|||||||
return c.SucJson(ctx, resp)
|
return c.SucJson(ctx, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReturnToMerchant performs the browser-facing EPay success return hop.
|
||||||
|
// Non-EPay or not-yet-paid orders are sent back to the checkout counter.
|
||||||
|
// @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 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
|
||||||
|
// @Produce html
|
||||||
|
// @Param trade_id path string true "Trade ID"
|
||||||
|
// @Success 302 "Redirect to merchant return_url or checkout counter"
|
||||||
|
// @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10008 order not found, 10044 invalid order redirect url, 10045 order api key unavailable, 10046 failed to build epay return signature"
|
||||||
|
// @Router /pay/return/{trade_id} [get]
|
||||||
|
func (c *BaseCommController) ReturnToMerchant(ctx echo.Context) error {
|
||||||
|
tradeID := ctx.Param("trade_id")
|
||||||
|
redirect, err := service.ResolveEPayReturnRedirect(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
if redirect.IsMerchantRedirect {
|
||||||
|
ctx.Response().Header().Set("Cache-Control", "no-store")
|
||||||
|
}
|
||||||
|
return ctx.Redirect(http.StatusFound, redirect.TargetURL)
|
||||||
|
}
|
||||||
|
|
||||||
// CheckStatus 支付状态检测
|
// CheckStatus 支付状态检测
|
||||||
// @Summary Check payment status
|
// @Summary Check payment status
|
||||||
// @Description Return the current order status by trade ID. Status: 1=waiting payment, 2=paid, 3=expired, 4=waiting token/network selection.
|
// @Description Return the current order status by trade ID. Status: 1=waiting payment, 2=paid, 3=expired, 4=waiting token/network selection.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type CheckoutCounterResponse struct {
|
|||||||
Status int `json:"status" enums:"1,2,3,4" example:"1"` // 订单状态 1=等待支付 2=支付成功 3=已过期 4=等待选择支付网络/币种;status=4 时前端应引导选择链上 token/network 或 OkPay
|
Status int `json:"status" enums:"1,2,3,4" example:"1"` // 订单状态 1=等待支付 2=支付成功 3=已过期 4=等待选择支付网络/币种;status=4 时前端应引导选择链上 token/network 或 OkPay
|
||||||
PaymentType string `json:"payment_type" enums:"gmpay,epay" example:"gmpay"` // 支付接入类型;底层 Epay/Gmpay 转为小写 epay/gmpay 返回
|
PaymentType string `json:"payment_type" enums:"gmpay,epay" example:"gmpay"` // 支付接入类型;底层 Epay/Gmpay 转为小写 epay/gmpay 返回
|
||||||
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
|
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
|
||||||
RedirectUrl string `json:"redirect_url" example:"https://example.com/success"`
|
RedirectUrl string `json:"redirect_url" example:"https://example.com/success"` // 非 EPay 时为商户原始回跳地址;EPay 时为内部中转地址 /pay/return/{trade_id}
|
||||||
PaymentUrl string `json:"payment_url" example:"https://pay.example.com/checkout/3nQ9pL2xV7sK1mR8cT4yB_aZ"` // 支付链接;链上订单为空,OkPay 订单为第三方 payLink
|
PaymentUrl string `json:"payment_url" example:"https://pay.example.com/checkout/3nQ9pL2xV7sK1mR8cT4yB_aZ"` // 支付链接;链上订单为空,OkPay 订单为第三方 payLink
|
||||||
CreatedAt int64 `json:"created_at" example:"1713264000"` // 订单创建时间 时间戳
|
CreatedAt int64 `json:"created_at" example:"1713264000"` // 订单创建时间 时间戳
|
||||||
IsSelected bool `json:"is_selected" example:"false"` // 是否已选择当前支付方式;status=4 占位订单和刚补全的占位父单为 false
|
IsSelected bool `json:"is_selected" example:"false"` // 是否已选择当前支付方式;status=4 占位订单和刚补全的占位父单为 false
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/response"
|
||||||
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
|
"github.com/GMWalletApp/epusdt/util/sign"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EPayReturnRedirect struct {
|
||||||
|
TargetURL string
|
||||||
|
IsMerchantRedirect bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEPayOrder(order *mdb.Orders) bool {
|
||||||
|
return order != nil && strings.EqualFold(order.PaymentType, mdb.PaymentTypeEpay)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCheckoutCounterPath(tradeID string) string {
|
||||||
|
return "/pay/checkout-counter/" + tradeID
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAbsoluteAppURL(path string) string {
|
||||||
|
path = strings.TrimSpace(path)
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
base := strings.TrimRight(strings.TrimSpace(config.GetAppUri()), "/")
|
||||||
|
if base == "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return base + path
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildEPayReturnPath(tradeID string) string {
|
||||||
|
return "/pay/return/" + tradeID
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPublicRedirectURL(order *mdb.Orders) string {
|
||||||
|
if order == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
raw := strings.TrimSpace(order.RedirectUrl)
|
||||||
|
if raw == "" || !isEPayOrder(order) {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
return buildAbsoluteAppURL(buildEPayReturnPath(order.TradeId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveOrderApiKey(order *mdb.Orders) (*mdb.ApiKey, error) {
|
||||||
|
if order == nil || order.ApiKeyID == 0 {
|
||||||
|
return nil, constant.OrderApiKeyUnavailableErr
|
||||||
|
}
|
||||||
|
row, err := data.GetApiKeyByID(order.ApiKeyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if row == nil || row.ID == 0 || row.Status != mdb.ApiKeyStatusEnable {
|
||||||
|
return nil, constant.OrderApiKeyUnavailableErr
|
||||||
|
}
|
||||||
|
return row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildEPayResultParams(order *mdb.Orders, apiKeyRow *mdb.ApiKey) (map[string]string, error) {
|
||||||
|
if order == nil || apiKeyRow == nil {
|
||||||
|
return nil, constant.EPayReturnSignatureErr
|
||||||
|
}
|
||||||
|
|
||||||
|
pidInt, err := strconv.Atoi(strings.TrimSpace(apiKeyRow.Pid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, constant.EPayReturnSignatureErr
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyData := response.OrderNotifyResponseEpay{
|
||||||
|
PID: pidInt,
|
||||||
|
TradeNo: order.TradeId,
|
||||||
|
OutTradeNo: order.OrderId,
|
||||||
|
Type: "alipay",
|
||||||
|
Name: order.Name,
|
||||||
|
Money: fmt.Sprintf("%.4f", order.Amount),
|
||||||
|
TradeStatus: "TRADE_SUCCESS",
|
||||||
|
}
|
||||||
|
|
||||||
|
signstr, err := sign.Get(notifyData, apiKeyRow.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, constant.EPayReturnSignatureErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]string{
|
||||||
|
"pid": strconv.Itoa(pidInt),
|
||||||
|
"trade_no": notifyData.TradeNo,
|
||||||
|
"out_trade_no": notifyData.OutTradeNo,
|
||||||
|
"type": notifyData.Type,
|
||||||
|
"name": notifyData.Name,
|
||||||
|
"money": notifyData.Money,
|
||||||
|
"trade_status": notifyData.TradeStatus,
|
||||||
|
"sign": signstr,
|
||||||
|
"sign_type": "MD5",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendQueryParams(rawURL string, params map[string]string) (string, error) {
|
||||||
|
rawURL = strings.TrimSpace(rawURL)
|
||||||
|
if rawURL == "" {
|
||||||
|
return "", constant.OrderRedirectURLErr
|
||||||
|
}
|
||||||
|
|
||||||
|
targetURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", constant.OrderRedirectURLErr
|
||||||
|
}
|
||||||
|
|
||||||
|
query := targetURL.Query()
|
||||||
|
for key, value := range params {
|
||||||
|
query.Set(key, value)
|
||||||
|
}
|
||||||
|
targetURL.RawQuery = query.Encode()
|
||||||
|
return targetURL.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveEPayReturnRedirect(tradeID string) (*EPayReturnRedirect, error) {
|
||||||
|
order, err := GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isEPayOrder(order) || order.Status != mdb.StatusPaySuccess {
|
||||||
|
return &EPayReturnRedirect{
|
||||||
|
TargetURL: buildCheckoutCounterPath(order.TradeId),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMerchantURL := strings.TrimSpace(order.RedirectUrl)
|
||||||
|
if rawMerchantURL == "" {
|
||||||
|
return nil, constant.OrderRedirectURLErr
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKeyRow, err := ResolveOrderApiKey(order)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := BuildEPayResultParams(order, apiKeyRow)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetURL, err := appendQueryParams(rawMerchantURL, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EPayReturnRedirect{
|
||||||
|
TargetURL: targetURL,
|
||||||
|
IsMerchantRedirect: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
|
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
|
"github.com/GMWalletApp/epusdt/util/sign"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildPublicRedirectURLRewritesOnlyEpayOrders(t *testing.T) {
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
epayOrder := &mdb.Orders{
|
||||||
|
TradeId: "trade_epay_redirect",
|
||||||
|
PaymentType: mdb.PaymentTypeEpay,
|
||||||
|
RedirectUrl: "https://merchant.example/return",
|
||||||
|
}
|
||||||
|
wantRedirect := config.GetAppUri() + "/pay/return/trade_epay_redirect"
|
||||||
|
if got := buildPublicRedirectURL(epayOrder); got != wantRedirect {
|
||||||
|
t.Fatalf("epay redirect_url = %q, want internal return route", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
gmpayOrder := &mdb.Orders{
|
||||||
|
TradeId: "trade_gmpay_redirect",
|
||||||
|
PaymentType: mdb.PaymentTypeGmpay,
|
||||||
|
RedirectUrl: "https://merchant.example/return",
|
||||||
|
}
|
||||||
|
if got := buildPublicRedirectURL(gmpayOrder); got != "https://merchant.example/return" {
|
||||||
|
t.Fatalf("gmpay redirect_url = %q, want merchant raw redirect_url", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := buildPublicRedirectURL(&mdb.Orders{TradeId: "trade_empty", PaymentType: mdb.PaymentTypeEpay}); got != "" {
|
||||||
|
t.Fatalf("empty redirect_url = %q, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildEPayResultParamsMatchesLegacyFields(t *testing.T) {
|
||||||
|
params, err := BuildEPayResultParams(&mdb.Orders{
|
||||||
|
TradeId: "trade_epay_params",
|
||||||
|
OrderId: "order_epay_params",
|
||||||
|
Name: "VIP",
|
||||||
|
Amount: 1,
|
||||||
|
}, &mdb.ApiKey{
|
||||||
|
Pid: "1001",
|
||||||
|
SecretKey: "epay-secret",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildEPayResultParams(): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := map[string]string{
|
||||||
|
"pid": "1001",
|
||||||
|
"trade_no": "trade_epay_params",
|
||||||
|
"out_trade_no": "order_epay_params",
|
||||||
|
"type": "alipay",
|
||||||
|
"name": "VIP",
|
||||||
|
"money": "1.0000",
|
||||||
|
"trade_status": "TRADE_SUCCESS",
|
||||||
|
"sign_type": "MD5",
|
||||||
|
}
|
||||||
|
for key, want := range expected {
|
||||||
|
if got := params[key]; got != want {
|
||||||
|
t.Fatalf("%s = %q, want %q", key, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signParams := map[string]interface{}{
|
||||||
|
"pid": params["pid"],
|
||||||
|
"trade_no": params["trade_no"],
|
||||||
|
"out_trade_no": params["out_trade_no"],
|
||||||
|
"type": params["type"],
|
||||||
|
"name": params["name"],
|
||||||
|
"money": params["money"],
|
||||||
|
"trade_status": params["trade_status"],
|
||||||
|
}
|
||||||
|
wantSign, err := sign.Get(signParams, "epay-secret")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sign.Get(): %v", err)
|
||||||
|
}
|
||||||
|
if got := params["sign"]; got != wantSign {
|
||||||
|
t.Fatalf("sign = %q, want %q", got, wantSign)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildEPayResultParamsRejectsNonNumericPid(t *testing.T) {
|
||||||
|
_, err := BuildEPayResultParams(&mdb.Orders{
|
||||||
|
TradeId: "trade_bad_pid",
|
||||||
|
OrderId: "order_bad_pid",
|
||||||
|
Name: "VIP",
|
||||||
|
Amount: 1,
|
||||||
|
}, &mdb.ApiKey{
|
||||||
|
Pid: "not-a-number",
|
||||||
|
SecretKey: "epay-secret",
|
||||||
|
})
|
||||||
|
if err != constant.EPayReturnSignatureErr {
|
||||||
|
t.Fatalf("error = %v, want %v", err, constant.EPayReturnSignatureErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveOrderApiKeyRejectsUnavailableRows(t *testing.T) {
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
if _, err := ResolveOrderApiKey(&mdb.Orders{TradeId: "missing_api_key_id"}); err != constant.OrderApiKeyUnavailableErr {
|
||||||
|
t.Fatalf("missing api_key_id error = %v, want %v", err, constant.OrderApiKeyUnavailableErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := &mdb.ApiKey{
|
||||||
|
Name: "disabled",
|
||||||
|
Pid: "1201",
|
||||||
|
SecretKey: "disabled-secret",
|
||||||
|
Status: mdb.ApiKeyStatusDisable,
|
||||||
|
}
|
||||||
|
if err := dao.Mdb.Create(row).Error; err != nil {
|
||||||
|
t.Fatalf("create disabled api key: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := ResolveOrderApiKey(&mdb.Orders{TradeId: "disabled_api_key", ApiKeyID: row.ID}); err != constant.OrderApiKeyUnavailableErr {
|
||||||
|
t.Fatalf("disabled api key error = %v, want %v", err, constant.OrderApiKeyUnavailableErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendQueryParamsPreservesMerchantQuery(t *testing.T) {
|
||||||
|
target, err := appendQueryParams("https://merchant.example/return?from=merchant", map[string]string{
|
||||||
|
"pid": "1001",
|
||||||
|
"trade_no": "trade_epay_redirect",
|
||||||
|
"trade_status": "TRADE_SUCCESS",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("appendQueryParams(): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := url.Parse(target)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("url.Parse(): %v", err)
|
||||||
|
}
|
||||||
|
query := parsed.Query()
|
||||||
|
if got := query.Get("from"); got != "merchant" {
|
||||||
|
t.Fatalf("from = %q, want merchant", got)
|
||||||
|
}
|
||||||
|
if got := query.Get("pid"); got != "1001" {
|
||||||
|
t.Fatalf("pid = %q, want 1001", got)
|
||||||
|
}
|
||||||
|
if got := query.Get("trade_no"); got != "trade_epay_redirect" {
|
||||||
|
t.Fatalf("trade_no = %q, want trade_epay_redirect", got)
|
||||||
|
}
|
||||||
|
if got := query.Get("trade_status"); got != "TRADE_SUCCESS" {
|
||||||
|
t.Fatalf("trade_status = %q, want TRADE_SUCCESS", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -706,7 +706,7 @@ func completeWaitSelectOrder(parent *mdb.Orders, token string, network string) (
|
|||||||
|
|
||||||
func buildCheckoutResponse(order *mdb.Orders) *response.CheckoutCounterResponse {
|
func buildCheckoutResponse(order *mdb.Orders) *response.CheckoutCounterResponse {
|
||||||
paymentType := mdb.PaymentTypeGmpay
|
paymentType := mdb.PaymentTypeGmpay
|
||||||
if strings.EqualFold(order.PaymentType, mdb.PaymentTypeEpay) {
|
if isEPayOrder(order) {
|
||||||
paymentType = mdb.PaymentTypeEpay
|
paymentType = mdb.PaymentTypeEpay
|
||||||
}
|
}
|
||||||
return &response.CheckoutCounterResponse{
|
return &response.CheckoutCounterResponse{
|
||||||
@@ -720,7 +720,7 @@ func buildCheckoutResponse(order *mdb.Orders) *response.CheckoutCounterResponse
|
|||||||
Status: order.Status,
|
Status: order.Status,
|
||||||
PaymentType: strings.ToLower(paymentType),
|
PaymentType: strings.ToLower(paymentType),
|
||||||
ExpirationTime: order.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
ExpirationTime: order.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
||||||
RedirectUrl: order.RedirectUrl,
|
RedirectUrl: buildPublicRedirectURL(order),
|
||||||
CreatedAt: order.CreatedAt.TimestampMilli(),
|
CreatedAt: order.CreatedAt.TimestampMilli(),
|
||||||
IsSelected: order.IsSelected,
|
IsSelected: order.IsSelected,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/GMWalletApp/epusdt/config"
|
|
||||||
"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/response"
|
"github.com/GMWalletApp/epusdt/model/response"
|
||||||
@@ -23,26 +20,7 @@ func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterRespo
|
|||||||
if orderInfo.ID <= 0 {
|
if orderInfo.ID <= 0 {
|
||||||
return nil, ErrOrder
|
return nil, ErrOrder
|
||||||
}
|
}
|
||||||
paymentType := mdb.PaymentTypeGmpay
|
resp := buildCheckoutResponse(orderInfo)
|
||||||
if strings.EqualFold(orderInfo.PaymentType, mdb.PaymentTypeEpay) {
|
|
||||||
paymentType = mdb.PaymentTypeEpay
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &response.CheckoutCounterResponse{
|
|
||||||
TradeId: orderInfo.TradeId,
|
|
||||||
Amount: orderInfo.Amount,
|
|
||||||
ActualAmount: orderInfo.ActualAmount,
|
|
||||||
Token: orderInfo.Token,
|
|
||||||
Currency: orderInfo.Currency,
|
|
||||||
ReceiveAddress: orderInfo.ReceiveAddress,
|
|
||||||
Network: orderInfo.Network,
|
|
||||||
Status: orderInfo.Status,
|
|
||||||
PaymentType: strings.ToLower(paymentType),
|
|
||||||
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
|
||||||
RedirectUrl: orderInfo.RedirectUrl,
|
|
||||||
CreatedAt: orderInfo.CreatedAt.TimestampMilli(),
|
|
||||||
IsSelected: orderInfo.IsSelected,
|
|
||||||
}
|
|
||||||
if orderInfo.PayProvider == mdb.PaymentProviderOkPay {
|
if orderInfo.PayProvider == mdb.PaymentProviderOkPay {
|
||||||
providerRow, rowErr := data.GetProviderOrderByTradeIDAndProvider(orderInfo.TradeId, mdb.PaymentProviderOkPay)
|
providerRow, rowErr := data.GetProviderOrderByTradeIDAndProvider(orderInfo.TradeId, mdb.PaymentProviderOkPay)
|
||||||
if rowErr != nil {
|
if rowErr != nil {
|
||||||
|
|||||||
+6
-40
@@ -2,9 +2,7 @@ package mq
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,6 +11,7 @@ import (
|
|||||||
"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/response"
|
"github.com/GMWalletApp/epusdt/model/response"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/GMWalletApp/epusdt/util/http_client"
|
"github.com/GMWalletApp/epusdt/util/http_client"
|
||||||
"github.com/GMWalletApp/epusdt/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/GMWalletApp/epusdt/util/sign"
|
"github.com/GMWalletApp/epusdt/util/sign"
|
||||||
@@ -25,18 +24,12 @@ import (
|
|||||||
// other secret would produce a signature the merchant can't verify.
|
// other secret would produce a signature the merchant can't verify.
|
||||||
// The admin can resend the callback after fixing the key.
|
// The admin can resend the callback after fixing the key.
|
||||||
func resolveOrderApiKey(order *mdb.Orders) (*mdb.ApiKey, error) {
|
func resolveOrderApiKey(order *mdb.Orders) (*mdb.ApiKey, error) {
|
||||||
if order.ApiKeyID == 0 {
|
row, err := service.ResolveOrderApiKey(order)
|
||||||
return nil, fmt.Errorf("order trade_id=%s has no api_key_id", order.TradeId)
|
if err != nil || row == nil || row.ID == 0 {
|
||||||
}
|
|
||||||
row, err := data.GetApiKeyByID(order.ApiKeyID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("lookup api_key_id=%d: %w", order.ApiKeyID, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
if row.ID == 0 {
|
return nil, errors.New("api key unavailable")
|
||||||
return nil, fmt.Errorf("api_key_id=%d not found (deleted?)", order.ApiKeyID)
|
|
||||||
}
|
|
||||||
if row.Status != mdb.ApiKeyStatusEnable {
|
|
||||||
return nil, fmt.Errorf("api_key_id=%d is disabled", order.ApiKeyID)
|
|
||||||
}
|
}
|
||||||
return row, nil
|
return row, nil
|
||||||
}
|
}
|
||||||
@@ -207,38 +200,11 @@ func sendOrderCallback(order *mdb.Orders) error {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.EqualFold(order.PaymentType, mdb.PaymentTypeEpay):
|
case strings.EqualFold(order.PaymentType, mdb.PaymentTypeEpay):
|
||||||
// EPAY uses pid (integer) and secret_key as "key".
|
formData, err := service.BuildEPayResultParams(order, apiKeyRow)
|
||||||
pidInt, convErr := strconv.Atoi(apiKeyRow.Pid)
|
|
||||||
if convErr != nil {
|
|
||||||
return fmt.Errorf("epay pid not numeric: %s", apiKeyRow.Pid)
|
|
||||||
}
|
|
||||||
notifyData := response.OrderNotifyResponseEpay{
|
|
||||||
PID: pidInt,
|
|
||||||
TradeNo: order.TradeId,
|
|
||||||
OutTradeNo: order.OrderId,
|
|
||||||
Type: "alipay",
|
|
||||||
Name: order.Name,
|
|
||||||
Money: fmt.Sprintf("%.4f", order.Amount),
|
|
||||||
TradeStatus: "TRADE_SUCCESS",
|
|
||||||
}
|
|
||||||
|
|
||||||
signstr2, err := sign.Get(notifyData, apiKeyRow.SecretKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
formData := map[string]string{
|
|
||||||
"pid": fmt.Sprintf("%d", notifyData.PID),
|
|
||||||
"trade_no": notifyData.TradeNo,
|
|
||||||
"out_trade_no": notifyData.OutTradeNo,
|
|
||||||
"type": notifyData.Type,
|
|
||||||
"name": notifyData.Name,
|
|
||||||
"money": notifyData.Money,
|
|
||||||
"trade_status": notifyData.TradeStatus,
|
|
||||||
"sign": signstr2,
|
|
||||||
"sign_type": "MD5",
|
|
||||||
}
|
|
||||||
|
|
||||||
epayResp, err := http_client.GetHttpClient().R().SetQueryParams(formData).Get(order.NotifyUrl)
|
epayResp, err := http_client.GetHttpClient().R().SetQueryParams(formData).Get(order.NotifyUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func RegisterRoute(e *echo.Echo) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
payRoute.GET("/checkout-counter-resp/:trade_id", comm.Ctrl.CheckoutCounter)
|
payRoute.GET("/checkout-counter-resp/:trade_id", comm.Ctrl.CheckoutCounter)
|
||||||
|
payRoute.GET("/return/:trade_id", comm.Ctrl.ReturnToMerchant)
|
||||||
payRoute.GET("/check-status/:trade_id", comm.Ctrl.CheckStatus)
|
payRoute.GET("/check-status/:trade_id", comm.Ctrl.CheckStatus)
|
||||||
payRoute.POST("/submit-tx-hash/:trade_id", comm.Ctrl.SubmitTxHash)
|
payRoute.POST("/submit-tx-hash/:trade_id", comm.Ctrl.SubmitTxHash)
|
||||||
payRoute.POST("/switch-network", comm.Ctrl.SwitchNetwork)
|
payRoute.POST("/switch-network", comm.Ctrl.SwitchNetwork)
|
||||||
|
|||||||
@@ -224,6 +224,37 @@ func doFormPost(e *echo.Echo, path string, values url.Values) *httptest.Response
|
|||||||
return rec
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustCreateEPayOrder(t *testing.T, e *echo.Echo, orderID string, returnURL string, extra url.Values) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
values := url.Values{
|
||||||
|
"pid": {"1"},
|
||||||
|
"name": {orderID},
|
||||||
|
"type": {"alipay"},
|
||||||
|
"money": {"1.00"},
|
||||||
|
"out_trade_no": {orderID},
|
||||||
|
"notify_url": {"https://93.184.216.34/notify"},
|
||||||
|
"return_url": {returnURL},
|
||||||
|
}
|
||||||
|
for key, items := range extra {
|
||||||
|
clone := append([]string(nil), items...)
|
||||||
|
values[key] = clone
|
||||||
|
}
|
||||||
|
values = signEpayValues(values)
|
||||||
|
|
||||||
|
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("create epay order failed: %d %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
location := rec.Header().Get("Location")
|
||||||
|
if !strings.HasPrefix(location, "/pay/checkout-counter/") {
|
||||||
|
t.Fatalf("expected checkout redirect, got %q", location)
|
||||||
|
}
|
||||||
|
return strings.TrimPrefix(location, "/pay/checkout-counter/")
|
||||||
|
}
|
||||||
|
|
||||||
func TestRootPostRoute(t *testing.T) {
|
func TestRootPostRoute(t *testing.T) {
|
||||||
e := setupTestEnv(t)
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
@@ -1245,6 +1276,12 @@ func TestEpaySubmitPhpGetCompatible(t *testing.T) {
|
|||||||
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"])
|
||||||
}
|
}
|
||||||
|
if got, _ := checkoutData["redirect_url"].(string); got != "http://localhost:8080/pay/return/"+tradeID {
|
||||||
|
t.Fatalf("epay checkout redirect_url = %q, want internal return route", got)
|
||||||
|
}
|
||||||
|
if order.RedirectUrl != "http://localhost/return" {
|
||||||
|
t.Fatalf("stored redirect_url = %q, want merchant raw return_url", order.RedirectUrl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEpaySubmitPhpRequestTokenNetworkOverrideDefaults(t *testing.T) {
|
func TestEpaySubmitPhpRequestTokenNetworkOverrideDefaults(t *testing.T) {
|
||||||
@@ -1526,6 +1563,234 @@ func TestSubmitTxHash_SuccessUpdatesCheckStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPayReturn_NotFound(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/pay/return/nonexistent-trade-id", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
resp := parseResp(t, rec)
|
||||||
|
if got := int(resp["status_code"].(float64)); got != 10008 {
|
||||||
|
t.Fatalf("status_code = %d, want 10008; response=%v", got, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayReturn_PaidEpayRedirectsToMerchantWithSignedParams(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
tradeID := mustCreateEPayOrder(t, e, "epay-return-paid-001", "https://merchant.example/return?from=merchant", url.Values{
|
||||||
|
"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())
|
||||||
|
}
|
||||||
|
if got := rec.Header().Get("Cache-Control"); got != "no-store" {
|
||||||
|
t.Fatalf("cache-control = %q, want no-store", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
location := rec.Header().Get("Location")
|
||||||
|
targetURL, err := url.Parse(location)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse redirect location: %v", err)
|
||||||
|
}
|
||||||
|
if targetURL.Scheme != "https" || targetURL.Host != "merchant.example" || targetURL.Path != "/return" {
|
||||||
|
t.Fatalf("unexpected redirect target: %q", location)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := targetURL.Query()
|
||||||
|
if query.Get("from") != "merchant" {
|
||||||
|
t.Fatalf("preserved query = %q, want merchant", query.Get("from"))
|
||||||
|
}
|
||||||
|
if query.Get("pid") != "1" {
|
||||||
|
t.Fatalf("pid = %q, want 1", query.Get("pid"))
|
||||||
|
}
|
||||||
|
if query.Get("trade_no") != tradeID {
|
||||||
|
t.Fatalf("trade_no = %q, want %q", query.Get("trade_no"), tradeID)
|
||||||
|
}
|
||||||
|
if query.Get("out_trade_no") != "epay-return-paid-001" {
|
||||||
|
t.Fatalf("out_trade_no = %q", query.Get("out_trade_no"))
|
||||||
|
}
|
||||||
|
if query.Get("type") != "alipay" {
|
||||||
|
t.Fatalf("type = %q, want alipay", query.Get("type"))
|
||||||
|
}
|
||||||
|
if query.Get("name") != "epay-return-paid-001" {
|
||||||
|
t.Fatalf("name = %q", query.Get("name"))
|
||||||
|
}
|
||||||
|
if query.Get("money") != "1.0000" {
|
||||||
|
t.Fatalf("money = %q, want 1.0000", query.Get("money"))
|
||||||
|
}
|
||||||
|
if query.Get("trade_status") != "TRADE_SUCCESS" {
|
||||||
|
t.Fatalf("trade_status = %q, want TRADE_SUCCESS", query.Get("trade_status"))
|
||||||
|
}
|
||||||
|
if query.Get("sign_type") != "MD5" {
|
||||||
|
t.Fatalf("sign_type = %q, want MD5", query.Get("sign_type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
signParams := map[string]interface{}{
|
||||||
|
"pid": query.Get("pid"),
|
||||||
|
"trade_no": query.Get("trade_no"),
|
||||||
|
"out_trade_no": query.Get("out_trade_no"),
|
||||||
|
"type": query.Get("type"),
|
||||||
|
"name": query.Get("name"),
|
||||||
|
"money": query.Get("money"),
|
||||||
|
"trade_status": query.Get("trade_status"),
|
||||||
|
}
|
||||||
|
calcSig, err := sign.Get(signParams, testAPIToken)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("calc epay return signature: %v", err)
|
||||||
|
}
|
||||||
|
if got := query.Get("sign"); got != calcSig {
|
||||||
|
t.Fatalf("sign = %q, want %q", got, calcSig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayReturn_UnpaidEpayRedirectsBackToCheckout(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
tradeID := mustCreateEPayOrder(t, e, "epay-return-unpaid-001", "https://merchant.example/return", url.Values{
|
||||||
|
"token": {"usdt"},
|
||||||
|
"network": {"tron"},
|
||||||
|
"currency": {"cny"},
|
||||||
|
})
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
if got := rec.Header().Get("Location"); got != "/pay/checkout-counter/"+tradeID {
|
||||||
|
t.Fatalf("location = %q, want checkout counter", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayReturn_PaidNonEpayRedirectsBackToCheckout(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
tradeID := createCheckoutCounterRespTestOrder(t, e, "pay-return-gmpay-001")
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
if got := rec.Header().Get("Location"); got != "/pay/checkout-counter/"+tradeID {
|
||||||
|
t.Fatalf("location = %q, want checkout counter", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayReturn_PaidEpayWithEmptyRedirectReturnsError(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
tradeID := mustCreateEPayOrder(t, e, "epay-return-empty-001", "https://merchant.example/return", url.Values{
|
||||||
|
"token": {"usdt"},
|
||||||
|
"network": {"tron"},
|
||||||
|
"currency": {"cny"},
|
||||||
|
})
|
||||||
|
if err := dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("trade_id = ?", tradeID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"status": mdb.StatusPaySuccess,
|
||||||
|
"redirect_url": "",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("update order: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/pay/return/"+tradeID, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
resp := parseResp(t, rec)
|
||||||
|
if got := int(resp["status_code"].(float64)); got != 10044 {
|
||||||
|
t.Fatalf("status_code = %d, want 10044; response=%v", got, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayReturn_PaidEpayWithDisabledApiKeyReturnsError(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
tradeID := mustCreateEPayOrder(t, e, "epay-return-disabled-key-001", "https://merchant.example/return", url.Values{
|
||||||
|
"token": {"usdt"},
|
||||||
|
"network": {"tron"},
|
||||||
|
"currency": {"cny"},
|
||||||
|
})
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load order: %v", err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err := dao.Mdb.Model(&mdb.ApiKey{}).
|
||||||
|
Where("id = ?", order.ApiKeyID).
|
||||||
|
Update("status", mdb.ApiKeyStatusDisable).Error; err != nil {
|
||||||
|
t.Fatalf("disable api key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/pay/return/"+tradeID, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
resp := parseResp(t, rec)
|
||||||
|
if got := int(resp["status_code"].(float64)); got != 10045 {
|
||||||
|
t.Fatalf("status_code = %d, want 10045; response=%v", got, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayReturn_PaidEpayWithNonNumericPidReturnsSignatureError(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
tradeID := mustCreateEPayOrder(t, e, "epay-return-bad-pid-001", "https://merchant.example/return", url.Values{
|
||||||
|
"token": {"usdt"},
|
||||||
|
"network": {"tron"},
|
||||||
|
"currency": {"cny"},
|
||||||
|
})
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load order: %v", err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err := dao.Mdb.Model(&mdb.ApiKey{}).
|
||||||
|
Where("id = ?", order.ApiKeyID).
|
||||||
|
Update("pid", "not-a-number").Error; err != nil {
|
||||||
|
t.Fatalf("break pid: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/pay/return/"+tradeID, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
resp := parseResp(t, rec)
|
||||||
|
if got := int(resp["status_code"].(float64)); got != 10046 {
|
||||||
|
t.Fatalf("status_code = %d, want 10046; response=%v", got, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSubmitTxHash_VerificationFailureAllowsRetrySameHash(t *testing.T) {
|
func TestSubmitTxHash_VerificationFailureAllowsRetrySameHash(t *testing.T) {
|
||||||
e := setupTestEnv(t)
|
e := setupTestEnv(t)
|
||||||
tradeID := createCheckoutCounterRespTestOrder(t, e, "submit-tx-hash-retry-001")
|
tradeID := createCheckoutCounterRespTestOrder(t, e, "submit-tx-hash-retry-001")
|
||||||
@@ -2225,6 +2490,9 @@ func TestSwitchNetwork_EpayPlaceholderCompletesChainInPlace(t *testing.T) {
|
|||||||
if got, _ := respData["trade_id"].(string); got != tradeID {
|
if got, _ := respData["trade_id"].(string); got != tradeID {
|
||||||
t.Fatalf("switch trade_id = %q, want parent %q", got, tradeID)
|
t.Fatalf("switch trade_id = %q, want parent %q", got, tradeID)
|
||||||
}
|
}
|
||||||
|
if got, _ := respData["redirect_url"].(string); got != "http://localhost:8080/pay/return/"+tradeID {
|
||||||
|
t.Fatalf("switch redirect_url = %q, want internal return route", got)
|
||||||
|
}
|
||||||
|
|
||||||
parent, err := data.GetOrderInfoByTradeId(tradeID)
|
parent, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2233,6 +2501,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.RedirectUrl != "http://localhost/return" {
|
||||||
|
t.Fatalf("stored redirect_url = %q, want merchant raw return_url", parent.RedirectUrl)
|
||||||
|
}
|
||||||
count, err := data.CountActiveSubOrders(tradeID)
|
count, err := data.CountActiveSubOrders(tradeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("count active sub-orders: %v", err)
|
t.Fatalf("count active sub-orders: %v", err)
|
||||||
@@ -2315,6 +2586,9 @@ func TestSwitchNetwork_OkPayFromEpayWaitSelectPlaceholder(t *testing.T) {
|
|||||||
if respData["payment_url"] != "https://pay.okaypay.test/epay-placeholder" {
|
if respData["payment_url"] != "https://pay.okaypay.test/epay-placeholder" {
|
||||||
t.Fatalf("payment_url = %v, want okpay url", respData["payment_url"])
|
t.Fatalf("payment_url = %v, want okpay url", respData["payment_url"])
|
||||||
}
|
}
|
||||||
|
if got, _ := respData["redirect_url"].(string); got != "http://localhost:8080/pay/return/"+tradeID {
|
||||||
|
t.Fatalf("switch redirect_url = %q, want internal return route", got)
|
||||||
|
}
|
||||||
|
|
||||||
parent, err := data.GetOrderInfoByTradeId(tradeID)
|
parent, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2323,6 +2597,9 @@ func TestSwitchNetwork_OkPayFromEpayWaitSelectPlaceholder(t *testing.T) {
|
|||||||
if parent.PaymentType != mdb.PaymentTypeEpay || parent.PayProvider != mdb.PaymentProviderOkPay || parent.Network != mdb.PaymentProviderOkPay || parent.ReceiveAddress != "OKPAY" {
|
if parent.PaymentType != mdb.PaymentTypeEpay || parent.PayProvider != mdb.PaymentProviderOkPay || parent.Network != mdb.PaymentProviderOkPay || parent.ReceiveAddress != "OKPAY" {
|
||||||
t.Fatalf("parent fields = payment_type %q provider %q network %q address %q", parent.PaymentType, parent.PayProvider, parent.Network, parent.ReceiveAddress)
|
t.Fatalf("parent fields = payment_type %q provider %q network %q address %q", parent.PaymentType, parent.PayProvider, parent.Network, parent.ReceiveAddress)
|
||||||
}
|
}
|
||||||
|
if parent.RedirectUrl != "http://localhost/return" {
|
||||||
|
t.Fatalf("stored redirect_url = %q, want merchant raw return_url", parent.RedirectUrl)
|
||||||
|
}
|
||||||
count, err := data.CountActiveSubOrders(tradeID)
|
count, err := data.CountActiveSubOrders(tradeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("count active sub-orders: %v", err)
|
t.Fatalf("count active sub-orders: %v", err)
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ var Errno = map[int]string{
|
|||||||
10041: "invalid notify url",
|
10041: "invalid notify url",
|
||||||
10042: "payment provider order creation failed",
|
10042: "payment provider order creation failed",
|
||||||
10043: "invalid setting item",
|
10043: "invalid setting item",
|
||||||
|
10044: "invalid order redirect url",
|
||||||
|
10045: "order api key unavailable",
|
||||||
|
10046: "failed to build epay return signature",
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -94,6 +97,9 @@ var (
|
|||||||
NotifyURLErr = Err(10041)
|
NotifyURLErr = Err(10041)
|
||||||
PaymentProviderCreateErr = Err(10042)
|
PaymentProviderCreateErr = Err(10042)
|
||||||
SettingItemErr = Err(10043)
|
SettingItemErr = Err(10043)
|
||||||
|
OrderRedirectURLErr = Err(10044)
|
||||||
|
OrderApiKeyUnavailableErr = Err(10045)
|
||||||
|
EPayReturnSignatureErr = Err(10046)
|
||||||
)
|
)
|
||||||
|
|
||||||
type RspError struct {
|
type RspError struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user