feat(order): 重构退款全流程并新增申请退款中状态
- 订单状态新增「6 申请退款中」,用户申请退款原子流转并落待审核退款流水 - 退款失败回退订单至已支付并恢复被取消的权益发放任务 - 新增虚拟支付退款查单兜底任务,反查超时订单并自动订正 - 会员包来源额度不叠加,扣减/回退/回收口径统一为只取生效一份 - 退款回收流水按吊销前后有效剩余差值记账,避免无效回收记录 - 同步更新 README 与 Stitch 设计文档
This commit is contained in:
@@ -252,6 +252,129 @@ func TestXPayOrderInfoUnmarshal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestXPayRefundNotifyOfficialPayload 官方退款推送报文:订单号取 MchOrderId、退款单号取 MchRefundId、
|
||||
// 结果由 RetCode=0 判定为成功(回归防护:早期误用 OutTradeNo/RefundStatus 导致取不到订单号而静默丢单)。
|
||||
func TestXPayRefundNotifyOfficialPayload(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"ToUserName":"gh_test",
|
||||
"FromUserName":"official_openid",
|
||||
"CreateTime":1789500611,
|
||||
"MsgType":"event",
|
||||
"Event":"xpay_refund_notify",
|
||||
"OpenId":"o_user",
|
||||
"WxRefundId":"wx_refund_1",
|
||||
"MchRefundId":"RF20260916033011abcdef",
|
||||
"WxOrderId":"wx_order_1",
|
||||
"MchOrderId":"OD20260916030000fc1cfee91b3748a2",
|
||||
"RefundFee":990,
|
||||
"RetCode":0,
|
||||
"RetMsg":"",
|
||||
"RefundStartTimestamp":1789500600,
|
||||
"RefundSuccTimestamp":1789500611,
|
||||
"WxpayRefundTransactionId":"wxpay_refund_1",
|
||||
"RetryTimes":0
|
||||
}`)
|
||||
|
||||
var refund XPayRefundNotify
|
||||
if err := json.Unmarshal(raw, &refund); err != nil {
|
||||
t.Fatalf("unmarshal refund notify: %v", err)
|
||||
}
|
||||
if got := refund.OrderNo(); got != "OD20260916030000fc1cfee91b3748a2" {
|
||||
t.Fatalf("OrderNo() = %q", got)
|
||||
}
|
||||
if got := refund.RefundNo(); got != "RF20260916033011abcdef" {
|
||||
t.Fatalf("RefundNo() = %q", got)
|
||||
}
|
||||
if refund.RefundFee != 990 {
|
||||
t.Fatalf("RefundFee = %d, want 990", refund.RefundFee)
|
||||
}
|
||||
succ, determinable := refund.Succeeded()
|
||||
if !determinable || !succ {
|
||||
t.Fatalf("Succeeded() = (%v, %v), want (true, true)", succ, determinable)
|
||||
}
|
||||
}
|
||||
|
||||
// TestXPayRefundNotifyFailurePayload 官方失败报文:RetCode 非 0 即退款失败,原因在 RetMsg。
|
||||
func TestXPayRefundNotifyFailurePayload(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"Event":"xpay_refund_notify",
|
||||
"OpenId":"o_user",
|
||||
"MchOrderId":"OD1",
|
||||
"MchRefundId":"RF1",
|
||||
"RefundFee":990,
|
||||
"RetCode":268490005,
|
||||
"RetMsg":"已退款不可再退"
|
||||
}`)
|
||||
|
||||
var refund XPayRefundNotify
|
||||
if err := json.Unmarshal(raw, &refund); err != nil {
|
||||
t.Fatalf("unmarshal refund notify: %v", err)
|
||||
}
|
||||
succ, determinable := refund.Succeeded()
|
||||
if !determinable || succ {
|
||||
t.Fatalf("Succeeded() = (%v, %v), want (false, true)", succ, determinable)
|
||||
}
|
||||
if refund.RetMsg == "" {
|
||||
t.Fatal("RetMsg should be preserved for fail_reason")
|
||||
}
|
||||
}
|
||||
|
||||
// TestXPayRefundNotifyLegacyPayload 旧版式报文(OutTradeNo + RefundStatus)仍可解析并判定(大小写不敏感)。
|
||||
func TestXPayRefundNotifyLegacyPayload(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"Event":"xpay_refund_notify",
|
||||
"OutTradeNo":"OD-LEGACY",
|
||||
"RefundOrderId":"RF-LEGACY",
|
||||
"RefundStatus":"success",
|
||||
"RefundFee":100
|
||||
}`)
|
||||
|
||||
var refund XPayRefundNotify
|
||||
if err := json.Unmarshal(raw, &refund); err != nil {
|
||||
t.Fatalf("unmarshal refund notify: %v", err)
|
||||
}
|
||||
if got := refund.OrderNo(); got != "OD-LEGACY" {
|
||||
t.Fatalf("OrderNo() = %q, want OD-LEGACY", got)
|
||||
}
|
||||
if got := refund.RefundNo(); got != "RF-LEGACY" {
|
||||
t.Fatalf("RefundNo() = %q, want RF-LEGACY", got)
|
||||
}
|
||||
succ, determinable := refund.Succeeded()
|
||||
if !determinable || !succ {
|
||||
t.Fatalf("Succeeded() = (%v, %v), want (true, true)", succ, determinable)
|
||||
}
|
||||
}
|
||||
|
||||
// TestXPayRefundNotifyUndeterminable 报文未携带 RetCode 与 RefundStatus 时必须报「不可判定」,
|
||||
// 调用方据此应答失败触发微信重试(而非静默按成功处理)。
|
||||
func TestXPayRefundNotifyUndeterminable(t *testing.T) {
|
||||
raw := []byte(`{"Event":"xpay_refund_notify","MchOrderId":"OD1","RefundFee":100}`)
|
||||
|
||||
var refund XPayRefundNotify
|
||||
if err := json.Unmarshal(raw, &refund); err != nil {
|
||||
t.Fatalf("unmarshal refund notify: %v", err)
|
||||
}
|
||||
succ, determinable := refund.Succeeded()
|
||||
if determinable || succ {
|
||||
t.Fatalf("Succeeded() = (%v, %v), want (false, false)", succ, determinable)
|
||||
}
|
||||
if got := refund.RefundNo(); got != "" {
|
||||
t.Fatalf("RefundNo() = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestXPayIOSRefundQueryOrderNoFallback iOS 退款问询推送订单号:OutTradeNo 优先,为空回退 MchOrderId。
|
||||
func TestXPayIOSRefundQueryOrderNoFallback(t *testing.T) {
|
||||
withOutTradeNo := XPayIOSRefundQueryNotify{XPayNotifyBase: XPayNotifyBase{OutTradeNo: "OD-A"}, MchOrderID: "OD-B"}
|
||||
if got := withOutTradeNo.OrderNo(); got != "OD-A" {
|
||||
t.Fatalf("OrderNo() = %q, want OD-A (OutTradeNo 优先)", got)
|
||||
}
|
||||
onlyMch := XPayIOSRefundQueryNotify{MchOrderID: "OD-B"}
|
||||
if got := onlyMch.OrderNo(); got != "OD-B" {
|
||||
t.Fatalf("OrderNo() = %q, want OD-B (回退 MchOrderId)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveEnvKey 校验环境自动选择规则:只填现网/只填沙箱/两把都填/两把都空。
|
||||
func TestResolveEnvKey(t *testing.T) {
|
||||
// 只填现网 -> 现网
|
||||
|
||||
Reference in New Issue
Block a user