fix(invitation): 线下提号接入邀请返利并补发历史漏发,返利比例文案改读配置
- 线下提号(admin_pickups)不走订单结账流程,此前完全不发邀请返利;现于 Complete() 内按号主利润对半基数发放卖家侧返利 - 迁移 000086:invitation_rebate_ledgers 增加 source 列,唯一键调整为 (source, order_id, trade_role),支持提号来源且保持幂等 - 新增 cmd/rebate-backfill 幂等补发脚本:默认预演,加 -apply 才写库 - 返利阶段文案不再写死 60%/30%/15%,改为按本次实际生效比例生成(钱包流水备注同步修正) - 前台邀请页阶段标签/横幅/空态文案、移动端个人中心入口文案均改读接口配置,并修掉合法 0% 比例被 || 覆盖成 60% 的问题 - 新增单测:提号返利发放与幂等、cycleLabel 跟随后台配置
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/listingstatus"
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/invitation"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
"hfb_sys/backend/internal/timeutil"
|
||||
@@ -305,6 +306,30 @@ func (r *Repository) Complete(ctx context.Context, pickupID uint64, req Complete
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请返利:线下提号不走正常订单结账流程,必须在这里补发号主(卖家)侧返利,
|
||||
// 否则号主邀请人永远拿不到提号产生的分成。基数按提号利润对半(与租赁订单卖家侧口径一致)。
|
||||
if grossBasisCent := pickup.ProfitAmountCent; grossBasisCent > 0 {
|
||||
sellerSiteID := uint64(0)
|
||||
if listing.DistributionSiteID != nil {
|
||||
sellerSiteID = *listing.DistributionSiteID
|
||||
}
|
||||
if _, err := invitation.AwardSellerSideRebateTx(tx, invitation.SellerSideRebateInput{
|
||||
Source: invitation.RebateSourcePickup,
|
||||
SourceID: pickup.ID,
|
||||
SourceNo: pickup.PickupNo,
|
||||
SellerID: pickup.OwnerID,
|
||||
GrossBasisCent: grossBasisCent,
|
||||
BasisCent: grossBasisCent / 2,
|
||||
SiteID: sellerSiteID,
|
||||
RateReferenceAt: now,
|
||||
SettledAt: now,
|
||||
RemarkPrefix: "邀请卖家提号返利",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := appendPickupEvent(tx, pickup, "pickup_completed", adminID, "管理员完成提号。", pickup.CompleteRemark, map[string]any{
|
||||
"settle_amount_cent": pickup.SettleAmountCent, "profit_amount_cent": pickup.ProfitAmountCent,
|
||||
"settlement_mode": pickup.SettlementMode, "offline_settlement_status": pickup.OfflineSettlementStatus,
|
||||
|
||||
@@ -365,6 +365,74 @@ func TestRepositoryCompleteInternalPickupCreditsWallet(t *testing.T) {
|
||||
assertInt64(t, "站内提号钱包入账", ledger.AmountCent, 12000)
|
||||
}
|
||||
|
||||
// 线下提号不走正常订单结账流程,完成时必须同步给号主邀请人发放返利,
|
||||
// 否则邀请人永远拿不到提号产生的分成(历史漏发缺口)。
|
||||
func TestRepositoryCompletePickupAwardsSellerInvitationRebate(t *testing.T) {
|
||||
repo, db := newPickupTestRepo(t)
|
||||
|
||||
inviter := model.User{Phone: "19900000001", Nickname: "提号邀请人", RealnameStatus: "verified", Status: "active"}
|
||||
if err := db.Create(&inviter).Error; err != nil {
|
||||
t.Fatalf("创建邀请人失败: %v", err)
|
||||
}
|
||||
|
||||
listing := seedAvailableListing(t, db, "LST-REBATE-001", "返利提号账号", false)
|
||||
verifiedAt := time.Now().AddDate(0, 0, -2) // 距提号完成 2 天 => 首月 60% 档
|
||||
if err := db.Create(&model.UserInvitation{
|
||||
InviterID: inviter.ID,
|
||||
InviteeID: listing.OwnerID,
|
||||
InviteCode: "rebate01",
|
||||
Status: "active",
|
||||
VerifiedAt: &verifiedAt,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("绑定号主邀请关系失败: %v", err)
|
||||
}
|
||||
|
||||
pickup, err := repo.Create(t.Context(), CreateRequest{
|
||||
ListingID: listing.ID,
|
||||
OrderAmountCent: 20000,
|
||||
ProfitAmountCent: 10000,
|
||||
}, 99, auditlog.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
|
||||
var stored model.AdminPickup
|
||||
if err := db.First(&stored, pickup.ID).Error; err != nil {
|
||||
t.Fatalf("读取提号记录失败: %v", err)
|
||||
}
|
||||
if stored.ProfitAmountCent != 10000 {
|
||||
t.Fatalf("提号利润被改写为 %d,期望 10000(用例前提失效,请同步调整断言)", stored.ProfitAmountCent)
|
||||
}
|
||||
|
||||
if _, err := repo.Complete(t.Context(), pickup.ID, CompleteRequest{SettleAmountCent: 10000}, 99, auditlog.Meta{}); err != nil {
|
||||
t.Fatalf("Complete() error = %v", err)
|
||||
}
|
||||
|
||||
var rebate model.InvitationRebateLedger
|
||||
if err := db.Where("source = ? AND order_id = ? AND trade_role = ?", "pickup", pickup.ID, "seller").
|
||||
First(&rebate).Error; err != nil {
|
||||
t.Fatalf("提号完成应发放号主邀请人返利: %v", err)
|
||||
}
|
||||
if rebate.InviterID != inviter.ID || rebate.InviteeID != listing.OwnerID {
|
||||
t.Fatalf("返利双方 = %d/%d, want %d/%d", rebate.InviterID, rebate.InviteeID, inviter.ID, listing.OwnerID)
|
||||
}
|
||||
if rebate.OrderNo != pickup.PickupNo {
|
||||
t.Fatalf("返利单据号 = %q, want %q", rebate.OrderNo, pickup.PickupNo)
|
||||
}
|
||||
// 基数 = 提号利润 10000 对半 = 5000,首月 60% = 3000
|
||||
assertInt64(t, "提号返利单边基数", rebate.SingleSideBasisCent, 5000)
|
||||
assertInt64(t, "提号返利金额", rebate.RebateAmountCent, 3000)
|
||||
|
||||
var wallet model.WalletLedger
|
||||
if err := db.Where("biz_type = ? AND biz_no = ?", "invite_rebate", pickup.PickupNo).First(&wallet).Error; err != nil {
|
||||
t.Fatalf("提号返利应写钱包流水: %v", err)
|
||||
}
|
||||
if wallet.UserID != inviter.ID {
|
||||
t.Fatalf("返利入账用户 = %d, want %d", wallet.UserID, inviter.ID)
|
||||
}
|
||||
assertInt64(t, "提号返利入账金额", wallet.AmountCent, 3000)
|
||||
}
|
||||
|
||||
func TestRepositoryCompletePlatformManagedPickupUsesOfflineSettlement(t *testing.T) {
|
||||
repo, db := newPickupTestRepo(t)
|
||||
listing := seedAvailableListing(t, db, "LST-CMPL-EXTERNAL", "代管结算账号", true)
|
||||
|
||||
Reference in New Issue
Block a user