退款时压缩卖方加价,人工/自动批次仅完结单可入账,号主收入进主站钱包;仲裁后重算释放并支持作废未入账 DS。开通时四种模式按默认生效,对外口径合并为 35/40/25,不再单独展示通道费。 Co-authored-by: Cursor <cursoragent@cursor.com>
314 lines
12 KiB
Go
314 lines
12 KiB
Go
package e2e
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"testing"
|
||
"time"
|
||
|
||
"gorm.io/datatypes"
|
||
"gorm.io/gorm"
|
||
|
||
"hfb_sys/backend/internal/database"
|
||
"hfb_sys/backend/internal/model"
|
||
"hfb_sys/backend/internal/modules/distribution"
|
||
listingpkg "hfb_sys/backend/internal/modules/listing"
|
||
orderpkg "hfb_sys/backend/internal/modules/order"
|
||
)
|
||
|
||
func openDistributionE2EDB(t *testing.T) *gorm.DB {
|
||
t.Helper()
|
||
return database.NewTestDB()
|
||
}
|
||
|
||
// TestE2EScenario1_MainSiteReceive_SubSiteMarkupSell_Settlement
|
||
// 场景 1 完整端到端测试:
|
||
// 1. 主站收号(号主底价 80 元,主站加价定价 100 元,100M 币,比例 100);
|
||
// 2. 分站 A 同步商品,设置自主提价(调减 20 比例,售价自动升为 125 元,分站加价 25 元);
|
||
// 3. 租客在分站前台浏览并下单,实付售价 125 元;
|
||
// 4. 支付成功自动触发快照:
|
||
// - 号主成本 80 元保底;
|
||
// - 分站自主加价 25 元 100% 归卖出分站;
|
||
// - 主站提价利润 20 元(通道费 0.63 元由主站承担)按 35% / 40% / 25% 严丝合缝切分;
|
||
// - 卖出分站总获得 33.00 元;
|
||
//
|
||
// 5. 完单后创建结算批次并执行结算,分站金库钱包精确入账 33.00 元,财务状态置为 settled。
|
||
func TestE2EScenario1_MainSiteReceive_SubSiteMarkupSell_Settlement(t *testing.T) {
|
||
db := openDistributionE2EDB(t)
|
||
ctx := context.Background()
|
||
now := time.Now()
|
||
|
||
// ----------------------------------------------------
|
||
// 1. 基础主体初始化
|
||
// ----------------------------------------------------
|
||
adminUser := model.AdminUser{Username: "super_admin", Status: "active"}
|
||
if err := db.Create(&adminUser).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
subSiteAdmin := model.AdminUser{Username: "site_a_manager", Status: "active"}
|
||
if err := db.Create(&subSiteAdmin).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
siteA := model.DistributionSite{
|
||
SiteCode: "branch_alpha",
|
||
SiteName: "分销甲站",
|
||
Domain: "alpha.example.com",
|
||
Status: "active",
|
||
DisplayAccountEnabled: true,
|
||
}
|
||
if err := db.Create(&siteA).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
siteMember := model.DistributionSiteMember{
|
||
SiteID: siteA.ID,
|
||
AdminUserID: subSiteAdmin.ID,
|
||
MemberType: "operator",
|
||
Status: "active",
|
||
DataScope: "site",
|
||
}
|
||
if err := db.Create(&siteMember).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 规则版本:主站收号、分站卖出(两方-平台收号模式)
|
||
// 收号 35%(归平台),卖出 40%(归分站),服务费 25%(归平台),通道费 50 BPS (0.5%)
|
||
ruleVersion := model.DistributionRuleVersion{
|
||
SiteID: &siteA.ID,
|
||
Mode: distribution.ModeTwoPartyPlatformReceive,
|
||
Version: 1,
|
||
Status: "active",
|
||
PlatformReceiveRatio: 35,
|
||
DistributorSellRatio: 40,
|
||
ServiceFeeValue: 25,
|
||
ServiceFeeUnit: distribution.ServiceFeeUnitPercent,
|
||
PaymentFeeBPS: 50,
|
||
EffectiveFrom: now.Add(-2 * time.Hour),
|
||
}
|
||
if err := db.Create(&ruleVersion).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
owner := model.User{Phone: "13800000001", Status: "active"}
|
||
renter := model.User{Phone: "13900000002", Status: "active"}
|
||
if err := db.Create(&owner).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := db.Create(&renter).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 2. 主站收号:100M 币,号主底价 80 元,主站定价 100 元
|
||
// ----------------------------------------------------
|
||
assetSummaryJSON, _ := json.Marshal(map[string]any{
|
||
"publish_ratio": 100.0,
|
||
"price_breakdown": map[string]any{
|
||
"seller_ratio": 125.0,
|
||
"seller_total_price": 80.0, // 号主底价 80 元
|
||
"buyer_ratio": 100.0,
|
||
"buyer_total_price": 100.0, // 主站标价 100 元
|
||
"platform_markup_amount": 20.0, // 主站提价 20 元
|
||
},
|
||
})
|
||
|
||
account := model.GameAccount{
|
||
Title: "王者账号-100M",
|
||
HafCoinAmount: 100_000_000, // 10000 万币
|
||
AssetSummary: datatypes.JSON(assetSummaryJSON),
|
||
Status: "active",
|
||
}
|
||
if err := db.Create(&account).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
listing := model.RentalListing{
|
||
ListingNo: "L-E2E-101",
|
||
AccountID: account.ID,
|
||
OwnerID: owner.ID,
|
||
PriceCent: 10000, // 主站挂牌基准价 100 元 (10000 分)
|
||
DepositAmountCent: 0,
|
||
Status: "published",
|
||
ReviewStatus: "approved",
|
||
DistributionSiteID: nil, // 主站自营收号
|
||
}
|
||
if err := db.Create(&listing).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 3. 分站货架选品并自主提价(调减 20 比例,售价变为 125 元)
|
||
// ----------------------------------------------------
|
||
shelfRelation := model.DistributionSiteAccount{
|
||
SiteID: siteA.ID,
|
||
AccountID: account.ID,
|
||
ListingID: listing.ID,
|
||
DisplayStatus: "enabled",
|
||
ShelfRatioDelta: 20, // 提价减 20 比例点 (100 -> 80)
|
||
}
|
||
if err := db.Create(&shelfRelation).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 验证货架公共查询计算价格
|
||
sitePriceCent, baseRatio, siteRatio := listingpkg.ApplyShelfRatio(account.HafCoinAmount, listing.PriceCent, shelfRelation.ShelfRatioDelta)
|
||
if baseRatio != 100 || siteRatio != 80 {
|
||
t.Fatalf("shelf ratio calculation error: base=%v, site=%v", baseRatio, siteRatio)
|
||
}
|
||
if sitePriceCent != 12500 {
|
||
t.Fatalf("shelf price calculation error: got %d, want 12500", sitePriceCent)
|
||
}
|
||
shelfMarkupCent := sitePriceCent - listing.PriceCent
|
||
if shelfMarkupCent != 2500 {
|
||
t.Fatalf("shelf markup error: got %d, want 2500", shelfMarkupCent)
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 4. 租客在分站下单并生成快照
|
||
// ----------------------------------------------------
|
||
orderRepo := orderpkg.NewRepository(db, orderpkg.Dependencies{})
|
||
orderDTO, err := orderRepo.Create(ctx, renter.ID, orderpkg.CreateRequest{
|
||
ListingID: listing.ID,
|
||
DistributionSiteCode: siteA.SiteCode,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("order create failed: %v", err)
|
||
}
|
||
|
||
// 校验订单快照
|
||
if orderDTO.RentAmountCent == nil || *orderDTO.RentAmountCent != 12500 {
|
||
t.Fatalf("order rent amount = %v, want 12500", orderDTO.RentAmountCent)
|
||
}
|
||
if orderDTO.BasePriceCent != 10000 || orderDTO.ShelfMarkupCent != 2500 || orderDTO.MarkupSiteID != siteA.ID {
|
||
t.Fatalf("order snapshot error: base=%d, markup=%d, site=%d", orderDTO.BasePriceCent, orderDTO.ShelfMarkupCent, orderDTO.MarkupSiteID)
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 5. 支付成功并触发分销财务快照 SnapshotPaidOrderTx
|
||
// ----------------------------------------------------
|
||
grossPaidCent := int64(12500)
|
||
if err := distribution.SnapshotPaidOrderTx(db, orderDTO.ID, grossPaidCent); err != nil {
|
||
t.Fatalf("SnapshotPaidOrderTx failed: %v", err)
|
||
}
|
||
|
||
// 验证财务快照
|
||
var financial model.DistributionOrderFinancial
|
||
if err := db.Where("order_id = ?", orderDTO.ID).First(&financial).Error; err != nil {
|
||
t.Fatalf("find financial snapshot failed: %v", err)
|
||
}
|
||
if financial.GrossPaidAmountCent != 12500 || financial.MarkupAmountCent != 2500 {
|
||
t.Fatalf("financial snapshot error: gross=%d, markup=%d", financial.GrossPaidAmountCent, financial.MarkupAmountCent)
|
||
}
|
||
// 支付通道费 0.5%:12500 * 50 / 10000 = 63 分,由主站承担,不参与分成基数
|
||
if financial.PaymentFeeCent != 63 {
|
||
t.Fatalf("payment fee = %d, want 63", financial.PaymentFeeCent)
|
||
}
|
||
// 主站提价总额 10000 - 8000 = 2000 分,通道费不扣减,利润基数 = 2000 分
|
||
if financial.SplitBaseAmountCent != 2000 {
|
||
t.Fatalf("split base amount = %d, want 2000", financial.SplitBaseAmountCent)
|
||
}
|
||
// 平台服务费 25%:2000 * 25% = 500 分
|
||
if financial.ServiceFeeCent != 500 {
|
||
t.Fatalf("service fee = %d, want 500", financial.ServiceFeeCent)
|
||
}
|
||
|
||
// 验证各参与方收益快照
|
||
var parties []model.DistributionOrderParty
|
||
if err := db.Where("order_id = ?", orderDTO.ID).Order("id ASC").Find(&parties).Error; err != nil || len(parties) != 2 {
|
||
t.Fatalf("parties error: len=%d, err=%v", len(parties), err)
|
||
}
|
||
|
||
// 参与方 0:distributor_sell(分站 A,卖方)
|
||
// 收益 = 分站自主加价 2500 分 + 提价分成 40% (2000 * 40% = 800) = 3300 分
|
||
var distributorParty, platformParty model.DistributionOrderParty
|
||
for _, p := range parties {
|
||
if p.PartyRole == "distributor_sell" {
|
||
distributorParty = p
|
||
} else if p.PartyRole == "platform_receive" {
|
||
platformParty = p
|
||
}
|
||
}
|
||
if distributorParty.IncomeAmountCent != 3300 || distributorParty.PartyID != siteA.ID {
|
||
t.Fatalf("distributor_sell party error: income=%d (want 3300), partyID=%d", distributorParty.IncomeAmountCent, distributorParty.PartyID)
|
||
}
|
||
// 参与方 1:platform_receive(平台收号方,35% 分成:2000 * 35% = 700 分)
|
||
if platformParty.IncomeAmountCent != 700 {
|
||
t.Fatalf("platform_receive party error: income=%d (want 700)", platformParty.IncomeAmountCent)
|
||
}
|
||
|
||
// 尾差校验:2000 - (800 + 700) - 500 = 0
|
||
if financial.RoundingAdjustmentCent != 0 {
|
||
t.Fatalf("rounding adjustment = %d, want 0", financial.RoundingAdjustmentCent)
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 6. 批次结算与分站金库到账 (Settlement Batch & Wallet Credit)
|
||
// ----------------------------------------------------
|
||
if err := db.Model(&model.RentalOrder{}).Where("id = ?", orderDTO.ID).Update("status", "completed").Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
distRepo := distribution.NewRepository(db)
|
||
|
||
// 创建结算批次
|
||
batch, err := distRepo.CreateSettlementBatch(ctx, subSiteAdmin.ID, distribution.CreateSettlementBatchRequest{
|
||
SiteID: siteA.ID,
|
||
PeriodStart: now.Add(-24 * time.Hour),
|
||
PeriodEnd: now.Add(24 * time.Hour),
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("CreateSettlementBatch failed: %v", err)
|
||
}
|
||
if batch.TotalAmountCent != 3300 {
|
||
t.Fatalf("batch total amount = %d, want 3300", batch.TotalAmountCent)
|
||
}
|
||
|
||
// 确认结算批次入账
|
||
settledBatch, err := distRepo.MarkSettlementBatch(ctx, subSiteAdmin.ID, batch.ID)
|
||
if err != nil {
|
||
t.Fatalf("MarkSettlementBatch failed: %v", err)
|
||
}
|
||
if settledBatch.Status != "settled" {
|
||
t.Fatalf("settled batch status = %s, want settled", settledBatch.Status)
|
||
}
|
||
|
||
// 验证财务快照状态更新为 settled
|
||
var updatedFinancial model.DistributionOrderFinancial
|
||
if err := db.Where("order_id = ?", orderDTO.ID).First(&updatedFinancial).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if updatedFinancial.SettlementStatus != "settled" {
|
||
t.Fatalf("financial settlement status = %s, want settled", updatedFinancial.SettlementStatus)
|
||
}
|
||
|
||
// 验证分站金库钱包账户余额
|
||
var siteWallet model.WalletAccount
|
||
if err := db.Where("user_id = 0 AND distribution_site_id = ?", siteA.ID).First(&siteWallet).Error; err != nil {
|
||
t.Fatalf("find site wallet failed: %v", err)
|
||
}
|
||
if siteWallet.AvailableBalanceCent != 3300 {
|
||
t.Fatalf("site wallet balance = %d, want 3300", siteWallet.AvailableBalanceCent)
|
||
}
|
||
|
||
// 验证钱包流水记录
|
||
var ledger model.WalletLedger
|
||
if err := db.Where("user_id = 0 AND distribution_site_id = ? AND biz_type = 'distribution_settlement'", siteA.ID).First(&ledger).Error; err != nil {
|
||
t.Fatalf("find wallet ledger failed: %v", err)
|
||
}
|
||
if ledger.AmountCent != 3300 || ledger.Direction != "in" {
|
||
t.Fatalf("wallet ledger error: amount=%d, direction=%s", ledger.AmountCent, ledger.Direction)
|
||
}
|
||
|
||
t.Logf("=== 场景 1 全链路端到端验证通过!===")
|
||
t.Logf("实付总额: %d 分 (125.00元)", grossPaidCent)
|
||
t.Logf("号主保底成本: %d 分 (80.00元)", 8000)
|
||
t.Logf("分站自主加价(独享): %d 分 (25.00元)", shelfMarkupCent)
|
||
t.Logf("支付通道手续费(0.5%%): %d 分 (0.63元)", financial.PaymentFeeCent)
|
||
t.Logf("主站提价净利润(主站承担通道费): %d 分 (20.00元)", financial.SplitBaseAmountCent)
|
||
t.Logf(" ├─ 收号方(主站 35%%): %d 分 (7.00元)", platformParty.IncomeAmountCent)
|
||
t.Logf(" ├─ 卖出方提价分成(分站 40%%): %d 分 (8.00元)", distributorParty.IncomeAmountCent-shelfMarkupCent)
|
||
t.Logf(" └─ 平台服务费(25%%): %d 分 (5.00元)", financial.ServiceFeeCent)
|
||
t.Logf("分站 A 最终结算到账: %d 分 (33.00元) [加价25元 + 分成8元]", siteWallet.AvailableBalanceCent)
|
||
}
|