322 lines
10 KiB
Go
322 lines
10 KiB
Go
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()
|
|
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)
|
|
// 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, 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, 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, buyer.ID, form)
|
|
So(err, ShouldBeNil)
|
|
|
|
payForm := &order_dto.OrderPayForm{Method: "balance"}
|
|
_, err = Order.Pay(ctx, 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()
|
|
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)
|
|
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,
|
|
buyer.ID,
|
|
&order_dto.OrderCreateForm{ContentID: content.ID},
|
|
)
|
|
Order.Pay(ctx, buyer.ID, createRes.OrderID, &order_dto.OrderPayForm{Method: "balance"})
|
|
|
|
// Get Detail
|
|
detail, err := Order.GetUserOrder(ctx, 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()
|
|
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)
|
|
// 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, 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()
|
|
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)
|
|
// 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, 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)
|
|
})
|
|
})
|
|
}
|