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"]) } }