Files
zt-closet-backend/internal/service/refund_apply_test.go
T
zhengchiliu b94dd6dffb feat(order): 重构退款全流程并新增申请退款中状态
- 订单状态新增「6 申请退款中」,用户申请退款原子流转并落待审核退款流水
- 退款失败回退订单至已支付并恢复被取消的权益发放任务
- 新增虚拟支付退款查单兜底任务,反查超时订单并自动订正
- 会员包来源额度不叠加,扣减/回退/回收口径统一为只取生效一份
- 退款回收流水按吊销前后有效剩余差值记账,避免无效回收记录
- 同步更新 README 与 Stitch 设计文档
2026-09-16 03:44:09 +08:00

97 lines
4.1 KiB
Go

package service
import (
"context"
"testing"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/DATA-DOG/go-sqlmock"
)
// TestRefundApplyMarksOrderRefunding 申请退款受理成功:返回待处理退款流水信息,订单 2 -> 6 由 DAO 事务内完成。
func TestRefundApplyMarksOrderRefunding(t *testing.T) {
mock := setupOrderQuotaTest(t)
mock.ExpectQuery("SELECT .* FROM `orders` WHERE order_no = \\?").
WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status", "pay_amount", "pay_channel"}).
AddRow(8, "ORDER-1", 7, 2, 990, 1))
mock.ExpectBegin()
mock.ExpectQuery("SELECT .* FROM `orders` .*FOR UPDATE").
WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status", "pay_amount"}).
AddRow(8, "ORDER-1", 7, 2, 990))
mock.ExpectQuery("SELECT .* FROM `refund_flow_logs`").
WillReturnRows(sqlmock.NewRows([]string{"id"}))
mock.ExpectExec("INSERT INTO `refund_flow_logs`").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE `orders` SET").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
resp, e := NewOrderService().RefundApply(context.Background(), 7,
&dto.RefundApplyRequest{OrderNo: "ORDER-1", Reason: "不想要了"})
if e != nil {
t.Fatalf("unexpected error: %v", e)
}
if resp == nil || resp.RefundAmount != 990 || resp.Status != RefundStatusPending || resp.RefundNo == "" {
t.Fatalf("unexpected response: %+v", resp)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
// TestRefundApplyRejectsRepeatApply 重复/并发申请被 DAO 拒绝时映射为「当前状态不允许退款」。
func TestRefundApplyRejectsRepeatApply(t *testing.T) {
mock := setupOrderQuotaTest(t)
mock.ExpectQuery("SELECT .* FROM `orders` WHERE order_no = \\?").
WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status", "pay_amount", "pay_channel"}).
AddRow(8, "ORDER-1", 7, 2, 990, 1))
// 行锁内读到订单已被并发请求置为 6 申请退款中:拒绝且不产生任何写入。
mock.ExpectBegin()
mock.ExpectQuery("SELECT .* FROM `orders` .*FOR UPDATE").
WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status"}).
AddRow(8, "ORDER-1", 7, OrderStatusRefunding))
mock.ExpectCommit()
resp, e := NewOrderService().RefundApply(context.Background(), 7,
&dto.RefundApplyRequest{OrderNo: "ORDER-1", Reason: "重复申请"})
if resp != nil || e != errcode.RefundNotAllowed {
t.Fatalf("resp=%v err=%v, want RefundNotAllowed", resp, e)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
// TestRefundApplyRejectsIOSVirtualOrder iOS 虚拟支付订单不走本接口,引导用户走 App Store。
func TestRefundApplyRejectsIOSVirtualOrder(t *testing.T) {
mock := setupOrderQuotaTest(t)
mock.ExpectQuery("SELECT .* FROM `orders` WHERE order_no = \\?").
WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status", "pay_amount", "pay_channel", "xpay_platform"}).
AddRow(8, "ORDER-1", 7, 2, 990, 2, "ios"))
resp, e := NewOrderService().RefundApply(context.Background(), 7,
&dto.RefundApplyRequest{OrderNo: "ORDER-1", Reason: "不想要了"})
if resp != nil || e != errcode.IOSRefundViaAppStore {
t.Fatalf("resp=%v err=%v, want IOSRefundViaAppStore", resp, e)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
// TestRefundApplyRejectsOtherUserOrder 非本人订单不允许申请退款(归属校验先于状态校验)。
func TestRefundApplyRejectsOtherUserOrder(t *testing.T) {
mock := setupOrderQuotaTest(t)
mock.ExpectQuery("SELECT .* FROM `orders` WHERE order_no = \\?").
WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status", "pay_amount", "pay_channel"}).
AddRow(8, "ORDER-1", 9, 2, 990, 1))
resp, e := NewOrderService().RefundApply(context.Background(), 7,
&dto.RefundApplyRequest{OrderNo: "ORDER-1", Reason: "越权申请"})
if resp != nil || e != errcode.Forbidden {
t.Fatalf("resp=%v err=%v, want Forbidden", resp, e)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}