package services import ( "database/sql" "testing" "quyun/v2/app/commands/testx" 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 OrderTestSuiteInjectParams struct { dig.In DB *sql.DB Initials []contracts.Initial `group:"initials"` } type OrderTestSuite struct { suite.Suite OrderTestSuiteInjectParams } func Test_Order(t *testing.T) { providers := testx.Default().With(Provide) testx.Serve(providers, t, func(p OrderTestSuiteInjectParams) { suite.Run(t, &OrderTestSuite{OrderTestSuiteInjectParams: p}) }) } func (s *OrderTestSuite) Test_PurchaseFlow() { Convey("Purchase Flow", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate(ctx, s.DB, models.TableNameOrder, models.TableNameOrderItem, models.TableNameUser, models.TableNameContent, models.TableNameContentPrice, models.TableNameTenant, models.TableNameContentAccess, models.TableNameTenantLedger, ) // 1. Setup Data // Creator creator := &models.User{Username: "creator", Phone: "13800000001"} models.UserQuery.WithContext(ctx).Create(creator) // Tenant tenant := &models.Tenant{ UserID: creator.ID, Name: "Music Shop", Code: "shop1", Status: consts.TenantStatusVerified, } models.TenantQuery.WithContext(ctx).Create(tenant) tenantID = tenant.ID // Content content := &models.Content{ TenantID: tenant.ID, UserID: creator.ID, Title: "Song A", Status: consts.ContentStatusPublished, } models.ContentQuery.WithContext(ctx).Create(content) // Price (10.00 CNY = 1000 cents) price := &models.ContentPrice{ TenantID: tenant.ID, ContentID: content.ID, PriceAmount: 1000, Currency: consts.CurrencyCNY, } models.ContentPriceQuery.WithContext(ctx).Create(price) // Buyer buyer := &models.User{Username: "buyer", Phone: "13900000001", Balance: 2000} // Has 20.00 models.UserQuery.WithContext(ctx).Create(buyer) // buyerCtx := context.WithValue(ctx, consts.CtxKeyUser, buyer.ID) Convey("should create and pay order successfully", func() { // Step 1: Create Order form := &order_dto.OrderCreateForm{ContentID: content.ID} createRes, err := Order.Create(ctx, tenantID, buyer.ID, form) So(err, ShouldBeNil) So(createRes.OrderID, ShouldNotBeEmpty) // Verify created status oid := createRes.OrderID o, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(oid)).First() So(o.Status, ShouldEqual, consts.OrderStatusCreated) So(o.AmountPaid, ShouldEqual, 1000) // Step 2: Pay Order payForm := &order_dto.OrderPayForm{Method: "balance"} _, err = Order.Pay(ctx, tenantID, buyer.ID, createRes.OrderID, payForm) So(err, ShouldBeNil) // Verify Order Paid o, _ = models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(oid)).First() So(o.Status, ShouldEqual, consts.OrderStatusPaid) So(o.PaidAt, ShouldNotBeZeroValue) // Verify Balance Deducted b, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(buyer.ID)).First() So(b.Balance, ShouldEqual, 1000) // 2000 - 1000 // Verify Access Granted access, _ := models.ContentAccessQuery.WithContext(ctx). Where(models.ContentAccessQuery.UserID.Eq(buyer.ID), models.ContentAccessQuery.ContentID.Eq(content.ID)). First() So(access, ShouldNotBeNil) So(access.Status, ShouldEqual, consts.ContentAccessStatusActive) // Verify Ledger Created (Creator received money logic?) // Note: My implementation credits the TENANT OWNER (creator.ID). l, _ := models.TenantLedgerQuery.WithContext(ctx).Where(models.TenantLedgerQuery.OrderID.Eq(o.ID)).First() So(l, ShouldNotBeNil) So(l.UserID, ShouldEqual, creator.ID) So(l.Amount, ShouldEqual, 900) So(l.Type, ShouldEqual, consts.TenantLedgerTypeDebitPurchase) }) Convey("should fail pay if insufficient balance", func() { // Set balance to 5.00 models.UserQuery.WithContext(ctx). Where(models.UserQuery.ID.Eq(buyer.ID)). Update(models.UserQuery.Balance, 500) form := &order_dto.OrderCreateForm{ContentID: content.ID} createRes, err := Order.Create(ctx, tenantID, buyer.ID, form) So(err, ShouldBeNil) payForm := &order_dto.OrderPayForm{Method: "balance"} _, err = Order.Pay(ctx, tenantID, buyer.ID, createRes.OrderID, payForm) So(err, ShouldNotBeNil) // Error should be QuotaExceeded or similar }) }) } func (s *OrderTestSuite) Test_OrderDetails() { Convey("Order Details", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate( ctx, s.DB, models.TableNameOrder, models.TableNameOrderItem, models.TableNameUser, models.TableNameContent, models.TableNameContentPrice, models.TableNameTenant, models.TableNameContentAccess, models.TableNameTenantLedger, models.TableNameMediaAsset, models.TableNameContentAsset, ) // Setup creator := &models.User{Username: "creator2", Phone: "13800000002"} models.UserQuery.WithContext(ctx).Create(creator) tenant := &models.Tenant{UserID: creator.ID, Name: "Best Shop", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(tenant) tenantID = tenant.ID content := &models.Content{ TenantID: tenant.ID, UserID: creator.ID, Title: "Amazing Song", Status: consts.ContentStatusPublished, } models.ContentQuery.WithContext(ctx).Create(content) price := &models.ContentPrice{ TenantID: tenant.ID, ContentID: content.ID, PriceAmount: 500, Currency: consts.CurrencyCNY, } models.ContentPriceQuery.WithContext(ctx).Create(price) // Asset (Cover) asset := &models.MediaAsset{ TenantID: tenant.ID, UserID: creator.ID, Type: consts.MediaAssetTypeImage, ObjectKey: "cover.jpg", } models.MediaAssetQuery.WithContext(ctx).Create(asset) models.ContentAssetQuery.WithContext(ctx). Create(&models.ContentAsset{ContentID: content.ID, AssetID: asset.ID, Role: consts.ContentAssetRoleCover}) // Buyer buyer := &models.User{Username: "buyer2", Phone: "13900000002", Balance: 1000} models.UserQuery.WithContext(ctx).Create(buyer) // buyerCtx := context.WithValue(ctx, consts.CtxKeyUser, buyer.ID) Convey("should get full order details", func() { // Create & Pay createRes, _ := Order.Create( ctx, tenantID, buyer.ID, &order_dto.OrderCreateForm{ContentID: content.ID}, ) Order.Pay(ctx, tenantID, buyer.ID, createRes.OrderID, &order_dto.OrderPayForm{Method: "balance"}) // Get Detail detail, err := Order.GetUserOrder(ctx, tenantID, buyer.ID, createRes.OrderID) So(err, ShouldBeNil) So(detail.TenantName, ShouldEqual, "Best Shop") So(len(detail.Items), ShouldEqual, 1) So(detail.Items[0].Title, ShouldEqual, "Amazing Song") So(detail.Items[0].Cover, ShouldContainSubstring, "cover.jpg") So(detail.Amount, ShouldEqual, 5.00) }) }) } func (s *OrderTestSuite) Test_PlatformCommission() { Convey("Platform Commission", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate( ctx, s.DB, models.TableNameUser, models.TableNameOrder, models.TableNameOrderItem, models.TableNameTenant, models.TableNameTenantLedger, models.TableNameContentAccess, ) // Creator creator := &models.User{Username: "creator_c", Balance: 0} models.UserQuery.WithContext(ctx).Create(creator) // Tenant t := &models.Tenant{UserID: creator.ID, Name: "Shop C", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(t) tenantID = t.ID // Buyer buyer := &models.User{Username: "buyer_c", Balance: 2000} models.UserQuery.WithContext(ctx).Create(buyer) // buyerCtx := context.WithValue(ctx, consts.CtxKeyUser, buyer.ID) // Order (10.00 CNY = 1000) o := &models.Order{ TenantID: t.ID, UserID: buyer.ID, AmountPaid: 1000, Status: consts.OrderStatusCreated, } models.OrderQuery.WithContext(ctx).Create(o) models.OrderItemQuery.WithContext(ctx).Create(&models.OrderItem{OrderID: o.ID, ContentID: 999}) // Fake content Convey("should deduct 10% fee", func() { payForm := &order_dto.OrderPayForm{Method: "balance"} _, err := Order.Pay(ctx, tenantID, buyer.ID, o.ID, payForm) So(err, ShouldBeNil) // Verify Creator Balance (1000 - 10% = 900) cReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(creator.ID)).First() So(cReload.Balance, ShouldEqual, 900) // Verify Ledger l, _ := models.TenantLedgerQuery.WithContext(ctx).Where(models.TenantLedgerQuery.OrderID.Eq(o.ID)).First() So(l.Amount, ShouldEqual, 900) }) }) } func (s *OrderTestSuite) Test_ExternalPayment() { Convey("External Payment", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate( ctx, s.DB, models.TableNameUser, models.TableNameOrder, models.TableNameOrderItem, models.TableNameTenant, models.TableNameTenantLedger, models.TableNameContentAccess, ) // Creator creator := &models.User{Username: "creator_ext", Balance: 0} models.UserQuery.WithContext(ctx).Create(creator) // Tenant t := &models.Tenant{UserID: creator.ID, Name: "Shop Ext", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(t) tenantID = t.ID // Buyer (Balance 0) buyer := &models.User{Username: "buyer_ext", Balance: 0} models.UserQuery.WithContext(ctx).Create(buyer) // Order o := &models.Order{ TenantID: t.ID, UserID: buyer.ID, AmountPaid: 1000, Status: consts.OrderStatusCreated, } models.OrderQuery.WithContext(ctx).Create(o) models.OrderItemQuery.WithContext(ctx).Create(&models.OrderItem{OrderID: o.ID, ContentID: 999}) Convey("should process external payment callback", func() { err := Order.ProcessExternalPayment(ctx, tenantID, o.ID, "ext_tx_id_123") So(err, ShouldBeNil) // Verify Status oReload, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First() So(oReload.Status, ShouldEqual, consts.OrderStatusPaid) // Verify Creator Credited cReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(creator.ID)).First() So(cReload.Balance, ShouldEqual, 900) // 1000 - 10% // Verify Buyer Balance (Should NOT be deducted) bReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(buyer.ID)).First() So(bReload.Balance, ShouldEqual, 0) }) }) }