fix(invitation): 邀请返利改用实际结算利润并修正提号基准

This commit is contained in:
yml2213
2026-10-29 11:49:08 +08:00
parent e57c9df6f4
commit 86fbc05e9b
7 changed files with 277 additions and 13 deletions
@@ -62,7 +62,13 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
return nil, err
}
}
_ = invitation.ProcessOrderInvitationRebateTx(tx, order.ID)
// 返利必须按本次实际结算的平台利润计算,不能用订单创建时的预收毛利快照。
actualPlatformProfitCent := settlement.PlatformFeeCent
_ = invitation.ProcessOrderInvitationRebateWithOptionsTx(tx, order.ID, invitation.OrderRebateOptions{
GrossProfitCent: &actualPlatformProfitCent,
RateReferenceAt: now,
SettledAt: now,
})
return refund, nil
}
@@ -2754,3 +2754,149 @@ func TestHideForOwnerRemovesSellerViewOnly(t *testing.T) {
t.Fatalf("隐藏只写时间,订单状态应保持完成,got hidden=%v status=%s", saved.OwnerHiddenAt, saved.Status)
}
}
// 邀请返利必须以实际结账落定的平台利润为基数:
// 租客申报 0 哈夫币损耗、0 额外物品时,实际平台利润为 0,不得再按订单预收毛利返利。
func TestCheckoutInvitationRebateUsesActualPlatformProfit(t *testing.T) {
db := setupOrderTestDB(t)
if err := database.AutoMigrateIfMissing(db,
&model.UserInvitation{},
&model.InvitationRebateConfig{},
&model.InvitationRebateLedger{},
); err != nil {
t.Fatalf("迁移邀请返利表失败: %v", err)
}
repo := NewRepository(db, Dependencies{
RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) {
return "refunding", nil
}),
})
snapshot := `{"haf_coin_amount":1000000,"asset_summary":{"price_breakdown":{"buyer_ratio":1,"seller_ratio":1.5,"buyer_coin_base_price":100,"seller_coin_base_price":80}}}`
cases := []struct {
name string
coinConsumedM float64
wantPlatformCent int64
wantBasisCent int64
wantSingleCent int64
wantRebateCent int64
}{
{name: "零消耗不产生返利", coinConsumedM: 0, wantPlatformCent: 0},
{name: "部分消耗按实际利润", coinConsumedM: 0.1, wantPlatformCent: 333, wantBasisCent: 333, wantSingleCent: 166, wantRebateCent: 100},
}
for index, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
inviter := model.User{
Phone: fmt.Sprintf("1391000%04d", index),
Nickname: "结算返利邀请人",
RealnameStatus: "verified",
Status: "active",
}
owner := model.User{Phone: fmt.Sprintf("1392000%04d", index), Nickname: "结算返利号主", Status: "active"}
renter := model.User{Phone: fmt.Sprintf("1393000%04d", index), Nickname: "结算返利租客", Status: "active"}
if err := db.Create(&inviter).Error; err != nil {
t.Fatalf("创建邀请人失败: %v", err)
}
if err := db.Create(&owner).Error; err != nil {
t.Fatalf("创建号主失败: %v", err)
}
if err := db.Create(&renter).Error; err != nil {
t.Fatalf("创建租客失败: %v", err)
}
verifiedAt := time.Now().AddDate(0, 0, -2).Truncate(time.Second)
if err := db.Create(&model.UserInvitation{
InviterID: inviter.ID,
InviteeID: owner.ID,
InviteCode: fmt.Sprintf("actual%02d", index),
Status: "active",
VerifiedAt: &verifiedAt,
}).Error; err != nil {
t.Fatalf("创建邀请关系失败: %v", err)
}
account := model.GameAccount{
OwnerID: owner.ID,
ServerRegion: "国服",
LoginPlatform: "steam",
Title: "结算返利测试账号",
Status: "rented",
}
if err := db.Create(&account).Error; err != nil {
t.Fatalf("创建账号失败: %v", err)
}
listing := model.RentalListing{
ListingNo: fmt.Sprintf("LST-REBATE-ACTUAL-%02d", index),
AccountID: account.ID,
OwnerID: owner.ID,
PriceCent: 10000,
Status: "rented",
ReviewStatus: "approved",
InTransaction: true,
}
if err := db.Create(&listing).Error; err != nil {
t.Fatalf("创建上架记录失败: %v", err)
}
order := model.RentalOrder{
OrderNo: fmt.Sprintf("ORD-REBATE-ACTUAL-%d-%02d", time.Now().UnixNano(), index),
ListingID: listing.ID,
AccountID: account.ID,
OwnerID: owner.ID,
RenterID: renter.ID,
Status: orderStatusRenting,
HandoffStatus: handoffStatusReceived,
EstimatedDurationHours: 24,
RentAmountCent: 10000,
RentOriginalAmountCent: 10000,
OwnerRentAmountCent: 8000,
PlatformFeeCent: 2000,
PureCoinOriginalAmountCent: 10000,
AccountSnapshot: datatypes.JSON([]byte(snapshot)),
}
if err := db.Create(&order).Error; err != nil {
t.Fatalf("创建订单失败: %v", err)
}
if _, err := repo.SubmitCheckout(t.Context(), renter.ID, order.ID, SubmitCheckoutRequest{
Content: "按实际消耗结账",
CoinConsumedM: tc.coinConsumedM,
}); err != nil {
t.Fatalf("提交结账失败: %v", err)
}
if err := repo.ConfirmCheckout(t.Context(), owner.ID, order.ID); err != nil {
t.Fatalf("确认结账失败: %v", err)
}
var checkout model.OrderCheckout
if err := db.Where("order_id = ?", order.ID).Order("id DESC").First(&checkout).Error; err != nil {
t.Fatalf("读取结账单失败: %v", err)
}
if checkout.PlatformFeeCent != tc.wantPlatformCent {
t.Fatalf("实际平台利润 = %d, want %d", checkout.PlatformFeeCent, tc.wantPlatformCent)
}
var ledgers []model.InvitationRebateLedger
if err := db.Where("source = ? AND order_id = ?", "rental_order", order.ID).Find(&ledgers).Error; err != nil {
t.Fatalf("读取返利流水失败: %v", err)
}
if tc.wantRebateCent == 0 {
if len(ledgers) != 0 {
t.Fatalf("零消耗结账不应发返利,实际 %d 条", len(ledgers))
}
return
}
if len(ledgers) != 1 {
t.Fatalf("返利流水条数 = %d, want 1", len(ledgers))
}
ledger := ledgers[0]
if ledger.OrderProfitBasisCent != tc.wantBasisCent ||
ledger.SingleSideBasisCent != tc.wantSingleCent ||
ledger.RebateAmountCent != tc.wantRebateCent {
t.Fatalf("返利基数/单边/金额 = %d/%d/%d, want %d/%d/%d",
ledger.OrderProfitBasisCent, ledger.SingleSideBasisCent, ledger.RebateAmountCent,
tc.wantBasisCent, tc.wantSingleCent, tc.wantRebateCent)
}
})
}
}