package services import ( "database/sql" "errors" "testing" "quyun/v2/app/commands/testx" "quyun/v2/app/errorx" order_dto "quyun/v2/app/http/v1/dto" "quyun/v2/database" "quyun/v2/database/models" "quyun/v2/pkg/consts" . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/suite" "go.ipao.vip/atom/contracts" "go.uber.org/dig" ) type CouponTestSuiteInjectParams struct { dig.In DB *sql.DB Initials []contracts.Initial `group:"initials"` } type CouponTestSuite struct { suite.Suite CouponTestSuiteInjectParams } func Test_Coupon(t *testing.T) { providers := testx.Default().With(Provide) testx.Serve(providers, t, func(p CouponTestSuiteInjectParams) { suite.Run(t, &CouponTestSuite{CouponTestSuiteInjectParams: p}) }) } func (s *CouponTestSuite) Test_CouponFlow() { Convey("Coupon Flow", s.T(), func() { ctx := s.T().Context() tenantID := int64(1) database.Truncate( ctx, s.DB, models.TableNameCoupon, models.TableNameUserCoupon, models.TableNameOrder, models.TableNameUser, models.TableNameContent, models.TableNameContentPrice, ) user := &models.User{Username: "coupon_user", Phone: "13800000001"} models.UserQuery.WithContext(ctx).Create(user) // 1. Create Coupon (Fixed 5.00 CNY, Min 10.00 CNY) cp := &models.Coupon{ TenantID: tenantID, Title: "Save 5", Type: consts.CouponTypeFixAmount, Value: 500, MinOrderAmount: 1000, } models.CouponQuery.WithContext(ctx).Create(cp) // 2. Give to User uc := &models.UserCoupon{ UserID: user.ID, CouponID: cp.ID, Status: consts.UserCouponStatusUnused, } models.UserCouponQuery.WithContext(ctx).Create(uc) Convey("should validate coupon successfully", func() { discount, err := Coupon.Validate(ctx, tenantID, user.ID, uc.ID, 1500) So(err, ShouldBeNil) So(discount, ShouldEqual, 500) }) Convey("should fail if below min amount", func() { _, err := Coupon.Validate(ctx, tenantID, user.ID, uc.ID, 800) So(err, ShouldNotBeNil) }) Convey("should apply in Order.Create", func() { // Setup Content c := &models.Content{TenantID: tenantID, UserID: 99, Title: "Test", Status: consts.ContentStatusPublished} models.ContentQuery.WithContext(ctx).Create(c) models.ContentPriceQuery.WithContext(ctx).Create(&models.ContentPrice{ TenantID: tenantID, ContentID: c.ID, PriceAmount: 2000, // 20.00 CNY Currency: consts.CurrencyCNY, }) form := &order_dto.OrderCreateForm{ ContentID: c.ID, UserCouponID: uc.ID, } // Simulate Auth context for Order service res, err := Order.Create(ctx, tenantID, user.ID, form) So(err, ShouldBeNil) // Verify Order oid := res.OrderID o, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(oid)).First() So(o.AmountOriginal, ShouldEqual, 2000) So(o.AmountDiscount, ShouldEqual, 500) So(o.AmountPaid, ShouldEqual, 1500) So(o.CouponID, ShouldEqual, cp.ID) // Verify Coupon Status ucReload, _ := models.UserCouponQuery.WithContext(ctx).Where(models.UserCouponQuery.ID.Eq(uc.ID)).First() So(ucReload.Status, ShouldEqual, consts.UserCouponStatusUsed) So(ucReload.OrderID, ShouldEqual, oid) }) }) } func (s *CouponTestSuite) Test_Receive() { Convey("Receive", s.T(), func() { ctx := s.T().Context() tenantID := int64(2) database.Truncate( ctx, s.DB, models.TableNameCoupon, models.TableNameUserCoupon, models.TableNameUser, ) user := &models.User{Username: "coupon_receive", Phone: "13800000002"} So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil) cp := &models.Coupon{ TenantID: tenantID, Title: "Receive Test", Type: consts.CouponTypeFixAmount, Value: 300, MinOrderAmount: 0, TotalQuantity: 1, } So(models.CouponQuery.WithContext(ctx).Create(cp), ShouldBeNil) item, err := Coupon.Receive(ctx, tenantID, user.ID, cp.ID) So(err, ShouldBeNil) So(item, ShouldNotBeNil) So(item.CouponID, ShouldEqual, cp.ID) // second receive should return existing item2, err := Coupon.Receive(ctx, tenantID, user.ID, cp.ID) So(err, ShouldBeNil) So(item2.CouponID, ShouldEqual, cp.ID) }) } func (s *CouponTestSuite) Test_ListAvailable() { Convey("ListAvailable", s.T(), func() { ctx := s.T().Context() tenantID := int64(3) database.Truncate( ctx, s.DB, models.TableNameCoupon, models.TableNameUserCoupon, models.TableNameUser, ) user := &models.User{Username: "coupon_available", Phone: "13800000003"} So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil) cp := &models.Coupon{ TenantID: tenantID, Title: "Available Test", Type: consts.CouponTypeFixAmount, Value: 500, MinOrderAmount: 1000, } So(models.CouponQuery.WithContext(ctx).Create(cp), ShouldBeNil) uc := &models.UserCoupon{ UserID: user.ID, CouponID: cp.ID, Status: consts.UserCouponStatusUnused, } So(models.UserCouponQuery.WithContext(ctx).Create(uc), ShouldBeNil) list, err := Coupon.ListAvailable(ctx, tenantID, user.ID, 500) So(err, ShouldBeNil) So(len(list), ShouldEqual, 0) list, err = Coupon.ListAvailable(ctx, tenantID, user.ID, 1200) So(err, ShouldBeNil) So(len(list), ShouldEqual, 1) So(list[0].CouponID, ShouldEqual, cp.ID) }) } func (s *CouponTestSuite) Test_Validate_DenyCrossTenantCoupon() { Convey("Validate should deny cross-tenant coupon", s.T(), func() { ctx := s.T().Context() database.Truncate( ctx, s.DB, models.TableNameCoupon, models.TableNameUserCoupon, models.TableNameUser, ) user := &models.User{Username: "coupon_cross_validate", Phone: "13800000011"} So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil) tenantA := int64(11) tenantB := int64(22) coupon := &models.Coupon{ TenantID: tenantA, Title: "Tenant A Coupon", Type: consts.CouponTypeFixAmount, Value: 200, MinOrderAmount: 0, } So(models.CouponQuery.WithContext(ctx).Create(coupon), ShouldBeNil) userCoupon := &models.UserCoupon{ UserID: user.ID, CouponID: coupon.ID, Status: consts.UserCouponStatusUnused, } So(models.UserCouponQuery.WithContext(ctx).Create(userCoupon), ShouldBeNil) _, err := Coupon.Validate(ctx, tenantB, user.ID, userCoupon.ID, 1000) So(err, ShouldNotBeNil) var appErr *errorx.AppError So(errors.As(err, &appErr), ShouldBeTrue) So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code) }) } func (s *CouponTestSuite) Test_MarkUsed_DenyCrossTenantCoupon() { Convey("MarkUsed should deny cross-tenant coupon", s.T(), func() { ctx := s.T().Context() database.Truncate( ctx, s.DB, models.TableNameCoupon, models.TableNameUserCoupon, models.TableNameOrder, models.TableNameUser, ) user := &models.User{Username: "coupon_cross_mark", Phone: "13800000012"} So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil) tenantA := int64(33) tenantB := int64(44) coupon := &models.Coupon{ TenantID: tenantA, Title: "Tenant A Coupon", Type: consts.CouponTypeFixAmount, Value: 200, MinOrderAmount: 0, } So(models.CouponQuery.WithContext(ctx).Create(coupon), ShouldBeNil) userCoupon := &models.UserCoupon{ UserID: user.ID, CouponID: coupon.ID, Status: consts.UserCouponStatusUnused, } So(models.UserCouponQuery.WithContext(ctx).Create(userCoupon), ShouldBeNil) order := &models.Order{ TenantID: tenantA, UserID: user.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusCreated, } So(models.OrderQuery.WithContext(ctx).Create(order), ShouldBeNil) err := models.Q.Transaction(func(tx *models.Query) error { return Coupon.MarkUsed(ctx, tx, tenantB, userCoupon.ID, order.ID) }) So(err, ShouldNotBeNil) var appErr *errorx.AppError So(errors.As(err, &appErr), ShouldBeTrue) So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code) }) } func (s *CouponTestSuite) Test_Grant_DenyCrossTenantCoupon() { Convey("Grant should reject coupon from another tenant", s.T(), func() { ctx := s.T().Context() database.Truncate( ctx, s.DB, models.TableNameCoupon, models.TableNameUserCoupon, models.TableNameUser, ) user := &models.User{Username: "coupon_cross_grant", Phone: "13800000013"} So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil) tenantA := int64(55) tenantB := int64(66) coupon := &models.Coupon{ TenantID: tenantA, Title: "Tenant A Coupon", Type: consts.CouponTypeFixAmount, Value: 200, MinOrderAmount: 0, } So(models.CouponQuery.WithContext(ctx).Create(coupon), ShouldBeNil) granted, err := Coupon.Grant(ctx, tenantB, coupon.ID, []int64{user.ID}) So(err, ShouldNotBeNil) So(granted, ShouldEqual, 0) var appErr *errorx.AppError So(errors.As(err, &appErr), ShouldBeTrue) So(appErr.Code, ShouldEqual, errorx.ErrRecordNotFound.Code) exists, err := models.UserCouponQuery.WithContext(ctx). Where(models.UserCouponQuery.UserID.Eq(user.ID), models.UserCouponQuery.CouponID.Eq(coupon.ID)). Exists() So(err, ShouldBeNil) So(exists, ShouldBeFalse) }) }