记录退款失败请求参数
This commit is contained in:
@@ -146,6 +146,7 @@ type QueryRefundResponse struct {
|
|||||||
RefundAmount string
|
RefundAmount string
|
||||||
RefundTime string
|
RefundTime string
|
||||||
Raw map[string]string
|
Raw map[string]string
|
||||||
|
RawRequest map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type VerifyNotifyResult struct {
|
type VerifyNotifyResult struct {
|
||||||
@@ -275,7 +276,14 @@ func (c *Client) CreateRefund(ctx context.Context, req CreateRefundRequest) (*Cr
|
|||||||
rawReq := stringifyMap(reqData)
|
rawReq := stringifyMap(reqData)
|
||||||
raw, err := c.post(ctx, endpointRefund, reqData)
|
raw, err := c.post(ctx, endpointRefund, reqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return &CreateRefundResponse{
|
||||||
|
MerchantID: c.cfg.MerchantID,
|
||||||
|
ThirdOrderID: req.ThirdOrderID,
|
||||||
|
ProviderOrderID: req.ProviderOrderID,
|
||||||
|
MerchantRefundID: req.MerchantRefundID,
|
||||||
|
RefundAmount: strconv.FormatInt(req.RefundAmountCent, 10),
|
||||||
|
RawRequest: rawReq,
|
||||||
|
}, err
|
||||||
}
|
}
|
||||||
return &CreateRefundResponse{
|
return &CreateRefundResponse{
|
||||||
OK: responseOK(raw),
|
OK: responseOK(raw),
|
||||||
@@ -312,9 +320,17 @@ func (c *Client) QueryRefund(ctx context.Context, req QueryRefundRequest) (*Quer
|
|||||||
if c.cfg.TermNo == "" {
|
if c.cfg.TermNo == "" {
|
||||||
delete(reqData, "term_no")
|
delete(reqData, "term_no")
|
||||||
}
|
}
|
||||||
|
rawReq := stringifyMap(reqData)
|
||||||
raw, err := c.post(ctx, endpointRefundQuery, reqData)
|
raw, err := c.post(ctx, endpointRefundQuery, reqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return &QueryRefundResponse{
|
||||||
|
MerchantID: c.cfg.MerchantID,
|
||||||
|
ThirdOrderID: req.ThirdOrderID,
|
||||||
|
ProviderOrderID: req.ProviderOrderID,
|
||||||
|
MerchantRefundID: req.MerchantRefundID,
|
||||||
|
ProviderRefundID: req.ProviderRefundID,
|
||||||
|
RawRequest: rawReq,
|
||||||
|
}, err
|
||||||
}
|
}
|
||||||
return &QueryRefundResponse{
|
return &QueryRefundResponse{
|
||||||
OK: responseOK(raw),
|
OK: responseOK(raw),
|
||||||
@@ -328,6 +344,7 @@ func (c *Client) QueryRefund(ctx context.Context, req QueryRefundRequest) (*Quer
|
|||||||
RefundAmount: raw["refund_amount"],
|
RefundAmount: raw["refund_amount"],
|
||||||
RefundTime: firstNonEmpty(raw["refund_time"], raw["trade_time"], raw["finish_time"]),
|
RefundTime: firstNonEmpty(raw["refund_time"], raw["trade_time"], raw["finish_time"]),
|
||||||
Raw: raw,
|
Raw: raw,
|
||||||
|
RawRequest: rawReq,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,53 @@ func TestCreatePaymentUsesShanghaiTimeWhenLocalIsUTC(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateRefundReturnsRawRequestOnHTTPError(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != endpointRefund {
|
||||||
|
t.Fatalf("path = %s, want %s", r.URL.Path, endpointRefund)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = w.Write([]byte(`{"code":"OP90001","msg":"请求服务失败"}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate key: %v", err)
|
||||||
|
}
|
||||||
|
privatePEM, certPEM := testKeyPairPEM(t, key)
|
||||||
|
client := NewClient(Config{
|
||||||
|
GatewayURL: server.URL,
|
||||||
|
AppID: "app-1",
|
||||||
|
SerialNo: "serial-1",
|
||||||
|
MerchantID: "merchant-1",
|
||||||
|
TermNo: "term-1",
|
||||||
|
PrivateKey: privatePEM,
|
||||||
|
NotifyCert: certPEM,
|
||||||
|
})
|
||||||
|
resp, err := client.CreateRefund(context.Background(), CreateRefundRequest{
|
||||||
|
ThirdOrderID: "PAY123",
|
||||||
|
ProviderOrderID: "TRADE123",
|
||||||
|
MerchantRefundID: "REF123",
|
||||||
|
RefundAmountCent: 99,
|
||||||
|
RefundReason: "结账退款",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("CreateRefund error = nil, want HTTP error")
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("CreateRefund response = nil, want raw request")
|
||||||
|
}
|
||||||
|
if resp.RawRequest["merchant_no"] != "merchant-1" ||
|
||||||
|
resp.RawRequest["term_no"] != "term-1" ||
|
||||||
|
resp.RawRequest["out_trade_no"] != "REF123" ||
|
||||||
|
resp.RawRequest["refund_amount"] != "99" ||
|
||||||
|
resp.RawRequest["origin_out_trade_no"] != "PAY123" ||
|
||||||
|
resp.RawRequest["origin_trade_no"] != "TRADE123" {
|
||||||
|
t.Fatalf("raw request = %#v", resp.RawRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parseLakalaTestTime(t *testing.T, value string) time.Time {
|
func parseLakalaTestTime(t *testing.T, value string) time.Time {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
parsed, err := time.ParseInLocation("20060102150405", value, timeutil.ShanghaiLocation())
|
parsed, err := time.ParseInLocation("20060102150405", value, timeutil.ShanghaiLocation())
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func (r *Repository) StartRefund(ctx context.Context, orderID uint64, refundAmou
|
|||||||
}
|
}
|
||||||
|
|
||||||
if runtimeConfig.Channel == nil {
|
if runtimeConfig.Channel == nil {
|
||||||
if err := r.markRefundFailed(ctx, refundOrder.ID, orderID, refundAmountCent, map[string]string{"error": "payment channel unavailable"}); err != nil {
|
if err := r.markRefundFailed(ctx, refundOrder.ID, orderID, refundAmountCent, map[string]string{"error": "payment channel unavailable"}, nil); err != nil {
|
||||||
log.Printf("[payment] mark refund failed status failed order_id=%d payment_id=%d err=%v", orderID, refundOrder.ID, err)
|
log.Printf("[payment] mark refund failed status failed order_id=%d payment_id=%d err=%v", orderID, refundOrder.ID, err)
|
||||||
}
|
}
|
||||||
return nil, ErrPaymentUnavailable
|
return nil, ErrPaymentUnavailable
|
||||||
@@ -82,7 +82,11 @@ func (r *Repository) StartRefund(ctx context.Context, orderID uint64, refundAmou
|
|||||||
Remark: remark,
|
Remark: remark,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if markErr := r.markRefundFailed(ctx, refundOrder.ID, orderID, refundAmountCent, map[string]string{"error": err.Error()}); markErr != nil {
|
var rawRequest map[string]string
|
||||||
|
if resp != nil {
|
||||||
|
rawRequest = resp.RawRequest
|
||||||
|
}
|
||||||
|
if markErr := r.markRefundFailed(ctx, refundOrder.ID, orderID, refundAmountCent, map[string]string{"error": err.Error()}, rawRequest); markErr != nil {
|
||||||
log.Printf("[payment] mark refund failed status failed order_id=%d payment_id=%d err=%v", orderID, refundOrder.ID, markErr)
|
log.Printf("[payment] mark refund failed status failed order_id=%d payment_id=%d err=%v", orderID, refundOrder.ID, markErr)
|
||||||
}
|
}
|
||||||
log.Printf("[payment] refund request failed order_id=%d payment_id=%d biz_type=%s provider=%s amount_cent=%d err=%v",
|
log.Printf("[payment] refund request failed order_id=%d payment_id=%d biz_type=%s provider=%s amount_cent=%d err=%v",
|
||||||
@@ -90,7 +94,7 @@ func (r *Repository) StartRefund(ctx context.Context, orderID uint64, refundAmou
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !resp.OK {
|
if !resp.OK {
|
||||||
if markErr := r.markRefundFailed(ctx, refundOrder.ID, orderID, refundAmountCent, resp.Raw); markErr != nil {
|
if markErr := r.markRefundFailed(ctx, refundOrder.ID, orderID, refundAmountCent, resp.Raw, resp.RawRequest); markErr != nil {
|
||||||
log.Printf("[payment] mark refund rejected status failed order_id=%d payment_id=%d err=%v", orderID, refundOrder.ID, markErr)
|
log.Printf("[payment] mark refund rejected status failed order_id=%d payment_id=%d err=%v", orderID, refundOrder.ID, markErr)
|
||||||
}
|
}
|
||||||
log.Printf("[payment] refund rejected order_id=%d payment_id=%d biz_type=%s provider=%s amount_cent=%d code=%s message=%s",
|
log.Printf("[payment] refund rejected order_id=%d payment_id=%d biz_type=%s provider=%s amount_cent=%d code=%s message=%s",
|
||||||
@@ -253,6 +257,11 @@ func (r *Repository) syncRefundPayment(ctx context.Context, payment *model.Payme
|
|||||||
}
|
}
|
||||||
resp, err := runtimeConfig.Channel.QueryRefund(ctx, refundQueryRequest(*payment, originalPayment))
|
resp, err := runtimeConfig.Channel.QueryRefund(ctx, refundQueryRequest(*payment, originalPayment))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if resp != nil && resp.RawRequest != nil {
|
||||||
|
if updateErr := r.updateRefundRawRequest(ctx, payment.ID, resp.RawRequest); updateErr != nil {
|
||||||
|
log.Printf("[payment] update refund query raw request failed payment_id=%d err=%v", payment.ID, updateErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !resp.OK {
|
if !resp.OK {
|
||||||
@@ -355,14 +364,24 @@ func (r *Repository) markOrderRefundFailed(ctx context.Context, orderID uint64,
|
|||||||
"refunded_at": nil,
|
"refunded_at": nil,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
func (r *Repository) markRefundFailed(ctx context.Context, paymentID uint64, orderID uint64, refundAmountCent int64, raw map[string]string) error {
|
func (r *Repository) updateRefundRawRequest(ctx context.Context, paymentID uint64, rawRequest map[string]string) error {
|
||||||
|
if rawRequest == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", paymentID).Update("raw_request", jsonMap(rawRequest)).Error
|
||||||
|
}
|
||||||
|
func (r *Repository) markRefundFailed(ctx context.Context, paymentID uint64, orderID uint64, refundAmountCent int64, raw map[string]string, rawRequest map[string]string) error {
|
||||||
if raw == nil {
|
if raw == nil {
|
||||||
raw = map[string]string{"error": "refund failed"}
|
raw = map[string]string{"error": "refund failed"}
|
||||||
}
|
}
|
||||||
if err := r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", paymentID).Updates(map[string]any{
|
updates := map[string]any{
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
"raw_response": jsonMap(raw),
|
"raw_response": jsonMap(raw),
|
||||||
}).Error; err != nil {
|
}
|
||||||
|
if rawRequest != nil {
|
||||||
|
updates["raw_request"] = jsonMap(rawRequest)
|
||||||
|
}
|
||||||
|
if err := r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", paymentID).Updates(updates).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return r.markOrderRefundFailed(ctx, orderID, refundAmountCent)
|
return r.markOrderRefundFailed(ctx, orderID, refundAmountCent)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package payment
|
package payment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -114,6 +116,53 @@ func createPayableOrderFixture(t *testing.T, db *gorm.DB, suffix string, createP
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarkRefundFailedPersistsRawRequest(t *testing.T) {
|
||||||
|
db := setupPaymentTestDB(t)
|
||||||
|
f := createPayableOrderFixture(t, db, "RAWREQ", true)
|
||||||
|
refund := model.PaymentOrder{
|
||||||
|
PaymentNo: "PAYREFRAWREQ",
|
||||||
|
OrderID: f.Order.ID,
|
||||||
|
OrderNo: f.Order.OrderNo,
|
||||||
|
UserID: f.Renter.ID,
|
||||||
|
Provider: "lakala",
|
||||||
|
MerchantID: "merchant-1",
|
||||||
|
ThirdOrderID: "REFRAWREQ",
|
||||||
|
ProviderOrderID: "",
|
||||||
|
AmountCent: 99,
|
||||||
|
BizType: "checkout_refund",
|
||||||
|
Status: "refunding",
|
||||||
|
}
|
||||||
|
if err := db.Create(&refund).Error; err != nil {
|
||||||
|
t.Fatalf("create refund payment failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := NewRepository(db, nil, nil)
|
||||||
|
rawRequest := map[string]string{
|
||||||
|
"merchant_no": "merchant-1",
|
||||||
|
"out_trade_no": "REFRAWREQ",
|
||||||
|
"refund_amount": "99",
|
||||||
|
"origin_trade_no": "TRADE123",
|
||||||
|
}
|
||||||
|
err := repo.markRefundFailed(context.Background(), refund.ID, f.Order.ID, refund.AmountCent, map[string]string{"error": "lakala http status 400"}, rawRequest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mark refund failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var saved model.PaymentOrder
|
||||||
|
if err := db.First(&saved, refund.ID).Error; err != nil {
|
||||||
|
t.Fatalf("find refund payment failed: %v", err)
|
||||||
|
}
|
||||||
|
var got map[string]string
|
||||||
|
if err := json.Unmarshal(saved.RawRequest, &got); err != nil {
|
||||||
|
t.Fatalf("decode raw_request: %v", err)
|
||||||
|
}
|
||||||
|
for key, want := range rawRequest {
|
||||||
|
if got[key] != want {
|
||||||
|
t.Fatalf("raw_request[%s] = %q, want %q; full=%v", key, got[key], want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestCanReusePaymentWithSameProvider 测试相同渠道可复用
|
// TestCanReusePaymentWithSameProvider 测试相同渠道可复用
|
||||||
func TestCanReusePaymentWithSameProvider(t *testing.T) {
|
func TestCanReusePaymentWithSameProvider(t *testing.T) {
|
||||||
payment := model.PaymentOrder{
|
payment := model.PaymentOrder{
|
||||||
|
|||||||
Reference in New Issue
Block a user