513 lines
16 KiB
Go
513 lines
16 KiB
Go
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,
|
|
)
|
|
|
|
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)
|
|
|
|
models.ContentQuery.WithContext(ctx).Create(&models.Content{
|
|
TenantID: tenant.ID,
|
|
UserID: owner.ID,
|
|
Title: "Content A",
|
|
Status: consts.ContentStatusPublished,
|
|
Views: 100,
|
|
})
|
|
|
|
now := time.Now()
|
|
inRangePaidAt := now.Add(-12 * time.Hour)
|
|
outRangePaidAt := now.Add(-10 * 24 * 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),
|
|
},
|
|
)
|
|
|
|
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.PaidOrders, ShouldEqual, 1)
|
|
So(report.Summary.PaidAmount, ShouldEqual, 10.0)
|
|
So(report.Summary.RefundOrders, ShouldEqual, 1)
|
|
So(report.Summary.RefundAmount, 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,
|
|
)
|
|
|
|
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")
|
|
})
|
|
}
|