package services import ( "context" "database/sql" "testing" "time" "quyun/v2/app/commands/testx" creator_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 CreatorTestSuiteInjectParams struct { dig.In DB *sql.DB Initials []contracts.Initial `group:"initials"` } type CreatorTestSuite struct { suite.Suite CreatorTestSuiteInjectParams } func Test_Creator(t *testing.T) { providers := testx.Default().With(Provide) testx.Serve(providers, t, func(p CreatorTestSuiteInjectParams) { suite.Run(t, &CreatorTestSuite{CreatorTestSuiteInjectParams: p}) }) } func (s *CreatorTestSuite) Test_Apply() { Convey("Apply", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameTenantUser, models.TableNameUser) u := &models.User{Username: "creator1", Phone: "13700000001"} models.UserQuery.WithContext(ctx).Create(u) ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) Convey("should create tenant", func() { form := &creator_dto.ApplyForm{ Name: "My Channel", } err := Creator.Apply(ctx, tenantID, u.ID, form) So(err, ShouldBeNil) t, _ := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(u.ID)).First() So(t, ShouldNotBeNil) So(t.Name, ShouldEqual, "My Channel") So(t.Status, ShouldEqual, consts.TenantStatusPendingVerify) // Check admin role tu, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(t.ID)).First() So(tu, ShouldNotBeNil) // Role is array, check contains? Or first element? // types.Array is likely []T. So(len(tu.Role), ShouldEqual, 1) So(tu.Role[0], ShouldEqual, consts.TenantUserRoleTenantAdmin) }) }) } func (s *CreatorTestSuite) Test_CreateContent() { Convey("CreateContent", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate( ctx, s.DB, models.TableNameTenant, models.TableNameContent, models.TableNameContentAsset, models.TableNameContentPrice, models.TableNameUser, ) u := &models.User{Username: "creator2", Phone: "13700000002"} models.UserQuery.WithContext(ctx).Create(u) ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) // Create Tenant manually t := &models.Tenant{UserID: u.ID, Name: "Channel 2", Code: "123", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(t) tenantID = t.ID Convey("should create content and assets", func() { form := &creator_dto.ContentCreateForm{ Title: "New Song", Genre: "audio", Price: 9.99, // MediaIDs: ... need media asset } err := Creator.CreateContent(ctx, tenantID, u.ID, form) So(err, ShouldBeNil) c, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.Title.Eq("New Song")).First() So(c, ShouldNotBeNil) So(c.UserID, ShouldEqual, u.ID) So(c.TenantID, ShouldEqual, t.ID) // Check Price p, _ := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(c.ID)).First() So(p, ShouldNotBeNil) So(p.PriceAmount, ShouldEqual, 999) }) }) } func (s *CreatorTestSuite) Test_UpdateContent() { Convey("UpdateContent", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate( ctx, s.DB, models.TableNameTenant, models.TableNameContent, models.TableNameContentAsset, models.TableNameContentPrice, models.TableNameUser, ) u := &models.User{Username: "creator3", Phone: "13700000003"} models.UserQuery.WithContext(ctx).Create(u) ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) t := &models.Tenant{UserID: u.ID, Name: "Channel 3", Code: "124", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(t) tenantID = t.ID c := &models.Content{TenantID: t.ID, UserID: u.ID, Title: "Old Title", Genre: "audio"} models.ContentQuery.WithContext(ctx).Create(c) models.ContentPriceQuery.WithContext(ctx). Create(&models.ContentPrice{TenantID: t.ID, UserID: u.ID, ContentID: c.ID, PriceAmount: 100}) Convey("should update content", func() { price := 20.00 form := &creator_dto.ContentUpdateForm{ Title: "New Title", Genre: "video", Price: &price, } err := Creator.UpdateContent(ctx, tenantID, u.ID, c.ID, form) So(err, ShouldBeNil) // Verify cReload, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(c.ID)).First() So(cReload.Title, ShouldEqual, "New Title") So(cReload.Genre, ShouldEqual, "video") p, _ := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(c.ID)).First() So(p.PriceAmount, ShouldEqual, 2000) }) }) } func (s *CreatorTestSuite) Test_Dashboard() { Convey("Dashboard", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate( ctx, s.DB, models.TableNameTenant, models.TableNameTenantUser, models.TableNameTenantLedger, models.TableNameUser, models.TableNameOrder, ) u := &models.User{Username: "creator4", Phone: "13700000004"} models.UserQuery.WithContext(ctx).Create(u) ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) t := &models.Tenant{UserID: u.ID, Name: "Channel 4", Code: "125", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(t) tenantID = t.ID // Mock Data // 1. Followers models.TenantUserQuery.WithContext(ctx).Create( &models.TenantUser{TenantID: t.ID, UserID: 100}, &models.TenantUser{TenantID: t.ID, UserID: 101}, ) // 2. Revenue (Ledgers) models.TenantLedgerQuery.WithContext(ctx).Create( &models.TenantLedger{TenantID: t.ID, Type: consts.TenantLedgerTypeDebitPurchase, Amount: 1000}, // 10.00 &models.TenantLedger{TenantID: t.ID, Type: consts.TenantLedgerTypeDebitPurchase, Amount: 2000}, // 20.00 &models.TenantLedger{ TenantID: t.ID, Type: consts.TenantLedgerTypeCreditRefund, Amount: 500, }, // -5.00 (Refund, currently Dashboard sums DebitPurchase only, ideally should subtract refunds, but let's stick to implementation) ) Convey("should get stats", func() { stats, err := Creator.Dashboard(ctx, tenantID, u.ID) So(err, ShouldBeNil) So(stats.TotalFollowers.Value, ShouldEqual, 2) // Implementation sums 'debit_purchase' only based on my code So(stats.TotalRevenue.Value, ShouldEqual, 30.00) }) }) } func (s *CreatorTestSuite) Test_PayoutAccount() { Convey("PayoutAccount", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNamePayoutAccount, models.TableNameUser) u := &models.User{Username: "creator5", Phone: "13700000005"} models.UserQuery.WithContext(ctx).Create(u) ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) t := &models.Tenant{UserID: u.ID, Name: "Channel 5", Code: "126", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(t) tenantID = t.ID Convey("should CRUD payout account", func() { // Add form := &creator_dto.PayoutAccount{ Type: string(consts.PayoutAccountTypeAlipay), Name: "Alipay", Account: "user@example.com", Realname: "John Doe", } err := Creator.AddPayoutAccount(ctx, tenantID, u.ID, form) So(err, ShouldBeNil) // List list, err := Creator.ListPayoutAccounts(ctx, tenantID, u.ID) So(err, ShouldBeNil) So(len(list), ShouldEqual, 1) So(list[0].Account, ShouldEqual, "user@example.com") // Remove err = Creator.RemovePayoutAccount(ctx, tenantID, u.ID, list[0].ID) So(err, ShouldBeNil) // Verify Empty list, err = Creator.ListPayoutAccounts(ctx, tenantID, u.ID) So(err, ShouldBeNil) So(len(list), ShouldEqual, 0) }) }) } func (s *CreatorTestSuite) Test_Withdraw() { Convey("Withdraw", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate( ctx, s.DB, models.TableNameTenant, models.TableNamePayoutAccount, models.TableNameUser, models.TableNameOrder, models.TableNameTenantLedger, ) u := &models.User{Username: "creator6", Phone: "13700000006", Balance: 5000, IsRealNameVerified: true} // 50.00 models.UserQuery.WithContext(ctx).Create(u) ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) t := &models.Tenant{UserID: u.ID, Name: "Channel 6", Code: "127", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(t) tenantID = t.ID pa := &models.PayoutAccount{ TenantID: t.ID, UserID: u.ID, Type: "bank", Name: "Bank", Account: "123", Realname: "Creator", } models.PayoutAccountQuery.WithContext(ctx).Create(pa) Convey("should withdraw successfully", func() { form := &creator_dto.WithdrawForm{ Amount: 20.00, AccountID: pa.ID, } err := Creator.Withdraw(ctx, tenantID, u.ID, form) So(err, ShouldBeNil) // Verify Balance Deducted uReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(u.ID)).First() So(uReload.Balance, ShouldEqual, 3000) // Verify Order Created o, _ := models.OrderQuery.WithContext(ctx). Where(models.OrderQuery.TenantID.Eq(t.ID), models.OrderQuery.Type.Eq(consts.OrderTypeWithdrawal)). First() So(o, ShouldNotBeNil) So(o.AmountPaid, ShouldEqual, 2000) // Verify Ledger l, _ := models.TenantLedgerQuery.WithContext(ctx).Where(models.TenantLedgerQuery.OrderID.Eq(o.ID)).First() So(l, ShouldNotBeNil) So(l.Type, ShouldEqual, consts.TenantLedgerTypeCreditWithdrawal) }) Convey("should fail if insufficient balance", func() { form := &creator_dto.WithdrawForm{ Amount: 100.00, AccountID: pa.ID, } err := Creator.Withdraw(ctx, tenantID, u.ID, form) So(err, ShouldNotBeNil) }) }) } func (s *CreatorTestSuite) Test_Refund() { Convey("Refund", s.T(), func() { ctx := s.T().Context() tenantID := int64(0) database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameUser, models.TableNameOrder, models.TableNameOrderItem, models.TableNameContentAccess, models.TableNameTenantLedger, ) // Creator creator := &models.User{Username: "creator7", Phone: "13700000007", Balance: 5000} // Has funds models.UserQuery.WithContext(ctx).Create(creator) // creatorCtx := context.WithValue(ctx, consts.CtxKeyUser, creator.ID) // Tenant t := &models.Tenant{UserID: creator.ID, Name: "Channel 7", Code: "128", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(t) tenantID = t.ID // Buyer buyer := &models.User{Username: "buyer7", Phone: "13900000007", Balance: 0} models.UserQuery.WithContext(ctx).Create(buyer) // Order (Paid -> Refunding) o := &models.Order{ TenantID: t.ID, UserID: buyer.ID, AmountPaid: 1000, // 10.00 Status: consts.OrderStatusRefunding, } models.OrderQuery.WithContext(ctx).Create(o) models.OrderItemQuery.WithContext(ctx).Create(&models.OrderItem{OrderID: o.ID, ContentID: 100}) // Fake content models.ContentAccessQuery.WithContext(ctx). Create(&models.ContentAccess{UserID: buyer.ID, ContentID: 100, Status: consts.ContentAccessStatusActive}) Convey("should accept refund", func() { form := &creator_dto.RefundForm{Action: "accept", Reason: "Defective"} err := Creator.ProcessRefund(ctx, tenantID, creator.ID, o.ID, form) So(err, ShouldBeNil) // Verify Order oReload, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First() So(oReload.Status, ShouldEqual, consts.OrderStatusRefunded) // Verify Balances cReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(creator.ID)).First() So(cReload.Balance, ShouldEqual, 4000) // 5000 - 1000 bReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(buyer.ID)).First() So(bReload.Balance, ShouldEqual, 1000) // 0 + 1000 // Verify Access acc, _ := models.ContentAccessQuery.WithContext(ctx). Where(models.ContentAccessQuery.UserID.Eq(buyer.ID)). First() So(acc.Status, ShouldEqual, consts.ContentAccessStatusRevoked) }) }) } func (s *CreatorTestSuite) Test_ReportOverview() { Convey("ReportOverview", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameUser, models.TableNameContent, models.TableNameOrder, models.TableNameUserContentAction, models.TableNameComment, ) owner := &models.User{Username: "owner_r", Phone: "13900001011"} models.UserQuery.WithContext(ctx).Create(owner) tenant := &models.Tenant{ Name: "Tenant Report", UserID: owner.ID, Status: consts.TenantStatusVerified, } models.TenantQuery.WithContext(ctx).Create(tenant) content := &models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Content A", Status: consts.ContentStatusPublished, Views: 100, } models.ContentQuery.WithContext(ctx).Create(content) now := time.Now() inRangePaidAt := now.Add(-12 * time.Hour) outRangePaidAt := now.Add(-10 * 24 * time.Hour) likeAt := now.Add(-2 * time.Hour) favoriteAt := now.Add(-3 * time.Hour) commentAt := now.Add(-4 * time.Hour) models.OrderQuery.WithContext(ctx).Create( &models.Order{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountPaid: 1000, PaidAt: inRangePaidAt, }, &models.Order{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountPaid: 2000, PaidAt: outRangePaidAt, }, &models.Order{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusRefunded, AmountPaid: 500, UpdatedAt: now.Add(-6 * time.Hour), }, &models.Order{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.OrderTypeWithdrawal, Status: consts.OrderStatusCreated, AmountPaid: 300, CreatedAt: now.Add(-5 * time.Hour), }, &models.Order{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.OrderTypeWithdrawal, Status: consts.OrderStatusPaid, AmountPaid: 800, PaidAt: now.Add(-3 * time.Hour), }, &models.Order{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.OrderTypeWithdrawal, Status: consts.OrderStatusFailed, AmountPaid: 500, UpdatedAt: now.Add(-2 * time.Hour), }, ) models.UserContentActionQuery.WithContext(ctx).Create( &models.UserContentAction{ UserID: owner.ID, ContentID: content.ID, Type: string(consts.UserContentActionTypeLike), CreatedAt: likeAt, }, &models.UserContentAction{ UserID: owner.ID, ContentID: content.ID, Type: string(consts.UserContentActionTypeFavorite), CreatedAt: favoriteAt, }, ) models.CommentQuery.WithContext(ctx).Create(&models.Comment{ TenantID: tenant.ID, UserID: owner.ID, ContentID: content.ID, Content: "Nice", CreatedAt: commentAt, }) start := now.Add(-24 * time.Hour).Format(time.RFC3339) end := now.Format(time.RFC3339) report, err := Creator.ReportOverview(ctx, tenant.ID, owner.ID, &creator_dto.ReportOverviewFilter{ StartAt: &start, EndAt: &end, }) So(err, ShouldBeNil) So(report.Summary.TotalViews, ShouldEqual, 100) So(report.Summary.ContentCount, ShouldEqual, 1) So(report.Summary.ContentCreated, ShouldEqual, 1) So(report.Summary.LikeActions, ShouldEqual, 1) So(report.Summary.FavoriteActions, ShouldEqual, 1) So(report.Summary.CommentCount, ShouldEqual, 1) So(report.Summary.PaidOrders, ShouldEqual, 1) So(report.Summary.PaidAmount, ShouldEqual, 10.0) So(report.Summary.RefundOrders, ShouldEqual, 1) So(report.Summary.RefundAmount, ShouldEqual, 5.0) So(report.Summary.WithdrawalApplyOrders, ShouldEqual, 1) So(report.Summary.WithdrawalApplyAmount, ShouldEqual, 3.0) So(report.Summary.WithdrawalPaidOrders, ShouldEqual, 1) So(report.Summary.WithdrawalPaidAmount, ShouldEqual, 8.0) So(report.Summary.WithdrawalFailedOrders, ShouldEqual, 1) So(report.Summary.WithdrawalFailedAmount, ShouldEqual, 5.0) var paidSum, refundSum int64 for _, item := range report.Items { paidSum += item.PaidOrders refundSum += item.RefundOrders } So(paidSum, ShouldEqual, report.Summary.PaidOrders) So(refundSum, ShouldEqual, report.Summary.RefundOrders) }) } func (s *CreatorTestSuite) Test_ExportReport() { Convey("ExportReport", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameUser, models.TableNameContent, models.TableNameOrder, models.TableNameUserContentAction, models.TableNameComment, ) owner := &models.User{Username: "owner_e", Phone: "13900001012"} models.UserQuery.WithContext(ctx).Create(owner) tenant := &models.Tenant{ Name: "Tenant Export", UserID: owner.ID, Status: consts.TenantStatusVerified, } models.TenantQuery.WithContext(ctx).Create(tenant) models.ContentQuery.WithContext(ctx).Create(&models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Content Export", Status: consts.ContentStatusPublished, Views: 10, }) models.OrderQuery.WithContext(ctx).Create(&models.Order{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountPaid: 1200, PaidAt: time.Now().Add(-2 * time.Hour), }) form := &creator_dto.ReportExportForm{Format: "csv"} resp, err := Creator.ExportReport(ctx, tenant.ID, owner.ID, form) So(err, ShouldBeNil) So(resp.Filename, ShouldNotBeBlank) So(resp.Content, ShouldContainSubstring, "date,paid_orders,paid_amount,refund_orders,refund_amount,withdrawal_apply_orders,withdrawal_apply_amount,withdrawal_paid_orders,withdrawal_paid_amount,withdrawal_failed_orders,withdrawal_failed_amount,content_created,like_actions,favorite_actions,comment_count") }) }