Files
qitongxue-api/internal/logic/coupon_test.go
2026-09-29 10:57:01 +08:00

107 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logic
import (
"testing"
"github.com/gogf/gf/v2/os/gtime"
"tool-api/internal/consts"
"tool-api/internal/model/entity"
)
// ============================================================================
// 优惠券单测(T04.8 / T06)
//
// 覆盖纯函数:券型隔离(展示券不可用于抵扣)、读取态过期判定、已发券快照隔离。
// 全部不依赖 DB。
//
// 注意(T06):expire_at 的「是否过期」已改为 **SQL 侧**判定后以布尔量传入
// (effectiveCouponStatus / couponUsableErr),故这里直接注入该布尔量,不再依赖 Go 侧时间比较。
// ============================================================================
// TestCouponUsableErr 券型隔离(约定 S7):仅免单券可支付;展示券一律不行(fail-closed)。
func TestCouponUsableErr(t *testing.T) {
free := func() *entity.UserCoupons {
return &entity.UserCoupons{Id: 1, CouponType: consts.CouponTypeFreebie, Status: consts.UserCouponStatusUnused}
}
cases := []struct {
name string
c *entity.UserCoupons
expired bool
wantCode int // 0 = 通过
}{
{"免单券未用未过期-可用", free(), false, 0},
{"nil-不可用", nil, false, consts.CodeCouponUnavailable},
{"展示券-不可抵扣", &entity.UserCoupons{CouponType: consts.CouponTypeDisplay, Status: consts.UserCouponStatusUnused}, false, consts.CodeCouponUnavailable},
{"免单券已用", &entity.UserCoupons{CouponType: consts.CouponTypeFreebie, Status: consts.UserCouponStatusUsed}, false, consts.CodeCouponUnavailable},
{"免单券已作废", &entity.UserCoupons{CouponType: consts.CouponTypeFreebie, Status: consts.UserCouponStatusVoid}, false, consts.CodeCouponUnavailable},
{"免单券已过期-状态位", &entity.UserCoupons{CouponType: consts.CouponTypeFreebie, Status: consts.UserCouponStatusExpired}, false, consts.CodeCouponUnavailable},
{"免单券已过期-SQL 侧判定", &entity.UserCoupons{
CouponType: consts.CouponTypeFreebie, Status: consts.UserCouponStatusUnused,
ExpireAt: gtime.New("2000-01-01 00:00:00"),
}, true, consts.CodeCouponUnavailable},
}
for _, c := range cases {
if got := codeOf(couponUsableErr(c.c, c.expired)); got != c.wantCode {
t.Errorf("%s: code=%d, want %d", c.name, got, c.wantCode)
}
}
}
// TestEffectiveCouponStatus 读取态过期判定:未使用且(SQL 侧判定)已过期 → 视为已过期(不落库)。
func TestEffectiveCouponStatus(t *testing.T) {
unused := &entity.UserCoupons{Status: consts.UserCouponStatusUnused}
// 未使用 + 已过期 → 2
if got := effectiveCouponStatus(unused, true); got != consts.UserCouponStatusExpired {
t.Errorf("已过期未使用券应判为 2,got %d", got)
}
// 未使用 + 未过期 → 0
if got := effectiveCouponStatus(unused, false); got != consts.UserCouponStatusUnused {
t.Errorf("未过期未使用券应判为 0,got %d", got)
}
// 已使用 → 保持 1(不因为过期改判)
used := &entity.UserCoupons{Status: consts.UserCouponStatusUsed}
if got := effectiveCouponStatus(used, true); got != consts.UserCouponStatusUsed {
t.Errorf("已使用券应保持 1,got %d", got)
}
}
// TestBuildUserCouponRowSnapshot 快照隔离(约定 S8):
// 发放时把模板字段快照进 user_coupons;之后模板改名/改型/改面额**不影响已发券**。
func TestBuildUserCouponRowSnapshot(t *testing.T) {
now := gtime.Now()
template := &entity.Coupons{
Id: 10, Title: "免单券A", CouponType: consts.CouponTypeFreebie,
Kind: consts.CouponKindFreebie, Value: 500, ThresholdCents: 100,
ValidTo: gtime.New("2999-01-01 00:00:00"),
}
row := buildUserCouponRow(template, 7, "admin", "新客礼", now)
// 发放后立刻改动模板
template.Title = "改名后"
template.CouponType = consts.CouponTypeDisplay
template.Value = 9999
if row["coupon_title"] != "免单券A" {
t.Errorf("标题快照应保持不变,got %v", row["coupon_title"])
}
if row["coupon_type"] != consts.CouponTypeFreebie {
t.Errorf("券型快照应保持不变,got %v", row["coupon_type"])
}
if row["value"] != 500 {
t.Errorf("面额快照应保持不变,got %v", row["value"])
}
if row["user_id"] != int64(7) {
t.Errorf("user_id 错误,got %v", row["user_id"])
}
if row["status"] != consts.UserCouponStatusUnused {
t.Errorf("新发券状态应为未使用 0,got %v", row["status"])
}
if row["source"] != consts.UserCouponSourceAdmin {
t.Errorf("来源应为后台发放,got %v", row["source"])
}
if row["granted_by"] != "admin" {
t.Errorf("发放人错误,got %v", row["granted_by"])
}
}