fix: 仲裁实际结算发放邀请返利
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
// rebate-backfill-rental 为「仲裁按实际消耗结算」的租赁订单补发邀请返利。
|
||||
//
|
||||
// 背景:仲裁结案历史路径未触发邀请返利;且订单保留原预收金额快照,
|
||||
// 不能直接用 order.rent_amount_cent - owner_rent_amount_cent 计算。
|
||||
// 本脚本按仲裁后的实际平台毛利(优先取已结算 checkout 的平台费,
|
||||
// 否则取 actual_pure_coin_amount_cent - 仲裁入账给号主的金额)补发。
|
||||
//
|
||||
// 幂等:复用 invitation_rebate_ledgers 的 (source, order_id, trade_role) 唯一键。
|
||||
//
|
||||
// 用法(默认预演):
|
||||
//
|
||||
// go run ./cmd/rebate-backfill-rental -order-no RO20261017002241506,RO20261012185753383
|
||||
// go run ./cmd/rebate-backfill-rental -apply -order-no RO20261017002241506,RO20261012185753383
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/config"
|
||||
"hfb_sys/backend/internal/database"
|
||||
"hfb_sys/backend/internal/logging"
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/invitation"
|
||||
"hfb_sys/backend/pkg/money"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type candidate struct {
|
||||
Order model.RentalOrder
|
||||
ActualProfitCent int64
|
||||
ActualProfitSource string
|
||||
}
|
||||
|
||||
func main() {
|
||||
apply := flag.Bool("apply", false, "实际执行补发(默认仅预演)")
|
||||
orderNos := flag.String("order-no", "", "需要补发的租赁订单号,多个用逗号分隔")
|
||||
flag.Parse()
|
||||
|
||||
orderNoList := splitOrderNos(*orderNos)
|
||||
if len(orderNoList) == 0 {
|
||||
log.Fatal("必须通过 -order-no 指定订单号,避免误补全站历史订单")
|
||||
}
|
||||
|
||||
cfg := config.Load()
|
||||
logger, err := logging.New(cfg.Log)
|
||||
if err != nil {
|
||||
log.Fatalf("初始化日志失败: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = logger.Sync()
|
||||
}()
|
||||
|
||||
db, err := database.OpenMySQLWithOptions(cfg.MySQLDSN, cfg.Log.Level, logger, database.MySQLOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("连接数据库失败: %v", err)
|
||||
}
|
||||
|
||||
rows, err := loadCandidates(db, orderNoList)
|
||||
if err != nil {
|
||||
log.Fatalf("查询待补发订单失败: %v", err)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
fmt.Println("没有可补发的订单:订单不存在、未仲裁完成、无实际毛利或已补发。")
|
||||
return
|
||||
}
|
||||
|
||||
mode := "预演"
|
||||
if *apply {
|
||||
mode = "实际补发"
|
||||
}
|
||||
fmt.Printf("模式=%s,候选订单=%d\n", mode, len(rows))
|
||||
|
||||
var paidCount int
|
||||
var paidCent int64
|
||||
for _, row := range rows {
|
||||
rateAt := time.Now()
|
||||
if row.Order.SettledAt != nil {
|
||||
rateAt = *row.Order.SettledAt
|
||||
}
|
||||
fmt.Printf("[%s] %s 号主=%d 原毛利=%s 实际平台毛利=%s(%s)计提时刻=%s\n",
|
||||
mode, row.Order.OrderNo, row.Order.OwnerID,
|
||||
money.FormatWithSymbol(row.Order.RentAmountCent-row.Order.OwnerRentAmountCent),
|
||||
money.FormatWithSymbol(row.ActualProfitCent), row.ActualProfitSource,
|
||||
rateAt.Format("2006-01-02 15:04:05"))
|
||||
|
||||
if !*apply {
|
||||
continue
|
||||
}
|
||||
amountCent, err := backfillOne(db, row, rateAt)
|
||||
if err != nil {
|
||||
log.Printf("补发失败 %s: %v", row.Order.OrderNo, err)
|
||||
continue
|
||||
}
|
||||
if amountCent <= 0 {
|
||||
fmt.Printf("[跳过] %s 未产生返利(无生效邀请 / 邀请人未实名 / 返利已关闭 / 已存在记录)\n", row.Order.OrderNo)
|
||||
continue
|
||||
}
|
||||
paidCount++
|
||||
paidCent += amountCent
|
||||
fmt.Printf("[补发] %s 返利 %s 已入邀请人钱包\n", row.Order.OrderNo, money.FormatWithSymbol(amountCent))
|
||||
}
|
||||
|
||||
if *apply {
|
||||
fmt.Printf("完成:补发 %d 单,合计 %s。\n", paidCount, money.FormatWithSymbol(paidCent))
|
||||
}
|
||||
}
|
||||
|
||||
func splitOrderNos(raw string) []string {
|
||||
parts := strings.Split(raw, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if value := strings.TrimSpace(part); value != "" {
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func loadCandidates(db *gorm.DB, orderNos []string) ([]candidate, error) {
|
||||
var orders []model.RentalOrder
|
||||
if err := db.Where("order_no IN ?", orderNos).Order("id ASC").Find(&orders).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]candidate, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
if order.Status != "completed" || order.SettlementStatus != "arbitrated" {
|
||||
continue
|
||||
}
|
||||
var rebateCount int64
|
||||
if err := db.Model(&model.InvitationRebateLedger{}).
|
||||
Where("source = ? AND order_id = ? AND trade_role = 'seller'", invitation.RebateSourceRentalOrder, order.ID).
|
||||
Count(&rebateCount).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rebateCount > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
profitCent, source, err := actualProfitCent(db, order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if profitCent <= 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, candidate{
|
||||
Order: order,
|
||||
ActualProfitCent: profitCent,
|
||||
ActualProfitSource: source,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// actualProfitCent 返回仲裁后的实际平台毛利。
|
||||
// 已结算 checkout 的平台费最准确;没有 checkout 时用实际纯币收入减去仲裁入账号主的金额。
|
||||
func actualProfitCent(db *gorm.DB, order model.RentalOrder) (int64, string, error) {
|
||||
var checkout model.OrderCheckout
|
||||
checkoutErr := db.Where("order_id = ? AND status = ?", order.ID, "settled").
|
||||
Order("id DESC").First(&checkout).Error
|
||||
if checkoutErr == nil && checkout.PlatformFeeCent > 0 {
|
||||
return checkout.PlatformFeeCent, "settled_checkout.platform_fee_cent", nil
|
||||
}
|
||||
if checkoutErr != nil && checkoutErr != gorm.ErrRecordNotFound {
|
||||
return 0, "", checkoutErr
|
||||
}
|
||||
|
||||
var ownerIncomeCent int64
|
||||
if err := db.Model(&model.WalletLedger{}).
|
||||
Where("order_id = ? AND biz_type = ? AND direction = 'in' AND balance_type = 'available'", order.ID, "arbitration_owner_income").
|
||||
Select("COALESCE(SUM(amount_cent), 0)").Scan(&ownerIncomeCent).Error; err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if order.ActualPureCoinAmountCent <= 0 || ownerIncomeCent <= 0 {
|
||||
return 0, "", nil
|
||||
}
|
||||
return order.ActualPureCoinAmountCent - ownerIncomeCent, "actual_pure_coin - arbitration_owner_income", nil
|
||||
}
|
||||
|
||||
func backfillOne(db *gorm.DB, row candidate, rateAt time.Time) (int64, error) {
|
||||
var amountCent int64
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
// 事务内重新读取,避免预演后订单状态或金额发生变化。
|
||||
var order model.RentalOrder
|
||||
if err := tx.First(&order, row.Order.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.Status != "completed" || order.SettlementStatus != "arbitrated" {
|
||||
return nil
|
||||
}
|
||||
|
||||
profitCent, _, err := actualProfitCent(tx, order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if profitCent <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
options := invitation.OrderRebateOptions{
|
||||
GrossProfitCent: &profitCent,
|
||||
RateReferenceAt: rateAt,
|
||||
SettledAt: time.Now(),
|
||||
}
|
||||
before := int64(0)
|
||||
_ = tx.Model(&model.InvitationRebateLedger{}).
|
||||
Where("source = ? AND order_id = ? AND trade_role = 'seller'", invitation.RebateSourceRentalOrder, order.ID).
|
||||
Select("COALESCE(SUM(rebate_amount_cent), 0)").Scan(&before).Error
|
||||
if err := invitation.ProcessOrderInvitationRebateWithOptionsTx(tx, order.ID, options); err != nil {
|
||||
return err
|
||||
}
|
||||
var after int64
|
||||
if err := tx.Model(&model.InvitationRebateLedger{}).
|
||||
Where("source = ? AND order_id = ? AND trade_role = 'seller'", invitation.RebateSourceRentalOrder, order.ID).
|
||||
Select("COALESCE(SUM(rebate_amount_cent), 0)").Scan(&after).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
amountCent = after - before
|
||||
return nil
|
||||
})
|
||||
return amountCent, err
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/adminnotification"
|
||||
"hfb_sys/backend/internal/modules/distribution"
|
||||
"hfb_sys/backend/internal/modules/invitation"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
ordermodule "hfb_sys/backend/internal/modules/order"
|
||||
"hfb_sys/backend/internal/modules/rentergrowth"
|
||||
@@ -134,6 +135,24 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.Status == "completed" {
|
||||
if actualSettlement == nil {
|
||||
return ErrInvalidDispute
|
||||
}
|
||||
// 仲裁按实际消耗结算后,订单仍保留原预收金额快照;返利必须使用实际平台毛利。
|
||||
grossProfitCent := actualSettlement.PlatformFeeCent
|
||||
if err := invitation.ProcessOrderInvitationRebateWithOptionsTx(tx, order.ID, invitation.OrderRebateOptions{
|
||||
GrossProfitCent: &grossProfitCent,
|
||||
RateReferenceAt: now,
|
||||
}); err != nil {
|
||||
logging.FromContext(ctx).Error("仲裁结算邀请返利失败",
|
||||
zap.String("module", "dispute"),
|
||||
zap.Uint64("order_id", order.ID),
|
||||
zap.Int64("actual_platform_fee_cent", grossProfitCent),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
if order.DistributionSiteID != nil && *order.DistributionSiteID > 0 {
|
||||
if err := distribution.ApplyRefundTx(tx, order.ID, refundAmountCent); err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,12 +3,14 @@ package dispute
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/database"
|
||||
"hfb_sys/backend/internal/model"
|
||||
ordermodule "hfb_sys/backend/internal/modules/order"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
@@ -536,6 +538,61 @@ func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArbitrationActualSettlementAwardsSellerRebateOnActualProfit(t *testing.T) {
|
||||
db := setupDisputeTestDB(t)
|
||||
repo := NewRepository(db, Dependencies{RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) {
|
||||
return "refunded", nil
|
||||
})})
|
||||
owner, renter, order := createDisputeOrderFixture(t, db, disputePureCoinOrder())
|
||||
|
||||
inviter := model.User{Phone: "13800000003", RealnameStatus: "verified", Status: "active"}
|
||||
if err := db.Create(&inviter).Error; err != nil {
|
||||
t.Fatalf("创建邀请人失败: %v", err)
|
||||
}
|
||||
verifiedAt := time.Now().AddDate(0, 0, -1).Truncate(time.Second)
|
||||
if err := db.Create(&model.UserInvitation{
|
||||
InviterID: inviter.ID, InviteeID: owner.ID, Status: "active", VerifiedAt: &verifiedAt,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("创建邀请关系失败: %v", err)
|
||||
}
|
||||
|
||||
created, err := repo.Create(t.Context(), renter.ID, order.ID, CreateRequest{
|
||||
Type: "haf_coin_dispute",
|
||||
Description: "按实际消耗仲裁",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("创建申诉失败: %v", err)
|
||||
}
|
||||
actualCoinConsumedM := 50.0
|
||||
actualConsumableAmountCent := int64(12000)
|
||||
if _, err := repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{
|
||||
Result: "actual_settlement",
|
||||
Remark: "按实际消耗结算",
|
||||
ActualCoinConsumedM: &actualCoinConsumedM,
|
||||
ActualConsumableAmountCent: &actualConsumableAmountCent,
|
||||
}, AuditMeta{}); err != nil {
|
||||
t.Fatalf("仲裁失败: %v", err)
|
||||
}
|
||||
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("读取订单失败: %v", err)
|
||||
}
|
||||
actual := ordermodule.CalculateActualCheckoutSettlement(saved, actualConsumableAmountCent, actualCoinConsumedM, 0)
|
||||
expectedBasis := actual.PlatformFeeCent / 2
|
||||
expectedRebate := int64(math.Round(float64(expectedBasis) * 0.6))
|
||||
|
||||
var ledger model.InvitationRebateLedger
|
||||
if err := db.Where("order_id = ? AND trade_role = 'seller'", order.ID).First(&ledger).Error; err != nil {
|
||||
t.Fatalf("查询仲裁返利流水失败: %v", err)
|
||||
}
|
||||
if ledger.OrderProfitBasisCent != actual.PlatformFeeCent || ledger.SingleSideBasisCent != expectedBasis || ledger.RebateAmountCent != expectedRebate {
|
||||
t.Fatalf("仲裁返利基数/单边/金额 = %d/%d/%d, want %d/%d/%d",
|
||||
ledger.OrderProfitBasisCent, ledger.SingleSideBasisCent, ledger.RebateAmountCent,
|
||||
actual.PlatformFeeCent, expectedBasis, expectedRebate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildArbitrationSettlementSkipsFrozenReleaseWhenNoFrozenBalance(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
ID: 11,
|
||||
|
||||
@@ -344,8 +344,20 @@ func CalculateRebateRate(verifiedAt time.Time, now time.Time, cfg *model.Invitat
|
||||
return subsequentRate, CycleSubsequent, days
|
||||
}
|
||||
|
||||
// OrderRebateOptions 允许调用方覆盖订单毛利基数与返利计算时刻。
|
||||
// 仲裁按实际消耗结算后,订单快照金额仍是原预收金额,必须传入实际平台毛利。
|
||||
type OrderRebateOptions struct {
|
||||
GrossProfitCent *int64
|
||||
RateReferenceAt time.Time
|
||||
SettledAt time.Time
|
||||
}
|
||||
|
||||
// ProcessOrderInvitationRebate 在订单终态结算时计算并分发买卖双边邀请返利。
|
||||
func (r *Repository) ProcessOrderInvitationRebate(ctx context.Context, tx *gorm.DB, orderID uint64) error {
|
||||
return r.processOrderInvitationRebate(ctx, tx, orderID, OrderRebateOptions{})
|
||||
}
|
||||
|
||||
func (r *Repository) processOrderInvitationRebate(ctx context.Context, tx *gorm.DB, orderID uint64, opts OrderRebateOptions) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
@@ -355,6 +367,9 @@ func (r *Repository) ProcessOrderInvitationRebate(ctx context.Context, tx *gorm.
|
||||
}
|
||||
|
||||
orderGrossProfitCent := order.RentAmountCent - order.OwnerRentAmountCent
|
||||
if opts.GrossProfitCent != nil {
|
||||
orderGrossProfitCent = *opts.GrossProfitCent
|
||||
}
|
||||
if orderGrossProfitCent <= 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -385,6 +400,14 @@ func (r *Repository) ProcessOrderInvitationRebate(ctx context.Context, tx *gorm.
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
rateReferenceAt := now
|
||||
if !opts.RateReferenceAt.IsZero() {
|
||||
rateReferenceAt = opts.RateReferenceAt
|
||||
}
|
||||
settledAt := now
|
||||
if !opts.SettledAt.IsZero() {
|
||||
settledAt = opts.SettledAt
|
||||
}
|
||||
|
||||
// 返利归因站点(统一规则):成本一律由「订单所属站点」承担,并使用该站点自己的返利配置。
|
||||
// 订单未记录站点但存在分销拆分时,回退到拆分方站点(钱实际归它),
|
||||
@@ -411,7 +434,7 @@ func (r *Repository) ProcessOrderInvitationRebate(ctx context.Context, tx *gorm.
|
||||
if buyerBasisCent > 0 {
|
||||
cfg, _ := r.GetRebateConfig(ctx, orderSiteID)
|
||||
if cfg == nil || cfg.Enabled {
|
||||
rate, cycle, days := CalculateRebateRate(*buyerInvitation.VerifiedAt, now, cfg)
|
||||
rate, cycle, days := CalculateRebateRate(*buyerInvitation.VerifiedAt, rateReferenceAt, cfg)
|
||||
rebateCent := int64(math.Round(float64(buyerBasisCent) * rate))
|
||||
if rebateCent > 0 {
|
||||
ledgerNo := fmt.Sprintf("REB-B-%s-%d", order.OrderNo, buyerInvitation.InviterID)
|
||||
@@ -430,7 +453,7 @@ func (r *Repository) ProcessOrderInvitationRebate(ctx context.Context, tx *gorm.
|
||||
RebateAmountCent: rebateCent,
|
||||
WalletLedgerNo: ledgerNo,
|
||||
Status: "settled",
|
||||
SettledAt: now,
|
||||
SettledAt: settledAt,
|
||||
}
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&ledger).Error; err == nil && ledger.ID > 0 {
|
||||
_ = wallet.AppendEntries(tx, wallet.Entry{
|
||||
@@ -468,7 +491,7 @@ func (r *Repository) ProcessOrderInvitationRebate(ctx context.Context, tx *gorm.
|
||||
if sellerBasisCent > 0 {
|
||||
cfg, _ := r.GetRebateConfig(ctx, orderSiteID)
|
||||
if cfg == nil || cfg.Enabled {
|
||||
rate, cycle, days := CalculateRebateRate(*sellerInvitation.VerifiedAt, now, cfg)
|
||||
rate, cycle, days := CalculateRebateRate(*sellerInvitation.VerifiedAt, rateReferenceAt, cfg)
|
||||
rebateCent := int64(math.Round(float64(sellerBasisCent) * rate))
|
||||
if rebateCent > 0 {
|
||||
ledgerNo := fmt.Sprintf("REB-S-%s-%d", order.OrderNo, sellerInvitation.InviterID)
|
||||
@@ -487,7 +510,7 @@ func (r *Repository) ProcessOrderInvitationRebate(ctx context.Context, tx *gorm.
|
||||
RebateAmountCent: rebateCent,
|
||||
WalletLedgerNo: ledgerNo,
|
||||
Status: "settled",
|
||||
SettledAt: now,
|
||||
SettledAt: settledAt,
|
||||
}
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&ledger).Error; err == nil && ledger.ID > 0 {
|
||||
_ = wallet.AppendEntries(tx, wallet.Entry{
|
||||
@@ -1018,11 +1041,16 @@ func (r *Repository) ListAdminRebateLedgers(ctx context.Context, query AdminReba
|
||||
|
||||
// ProcessOrderInvitationRebateTx 便捷事务入口,供订单结账完成终态时调用
|
||||
func ProcessOrderInvitationRebateTx(tx *gorm.DB, orderID uint64) error {
|
||||
return ProcessOrderInvitationRebateWithOptionsTx(tx, orderID, OrderRebateOptions{})
|
||||
}
|
||||
|
||||
// ProcessOrderInvitationRebateWithOptionsTx 支持覆盖实际结算基数与计算时刻。
|
||||
func ProcessOrderInvitationRebateWithOptionsTx(tx *gorm.DB, orderID uint64, opts OrderRebateOptions) error {
|
||||
if tx == nil || orderID == 0 {
|
||||
return nil
|
||||
}
|
||||
repo := NewRepository(tx)
|
||||
return repo.ProcessOrderInvitationRebate(context.Background(), tx, orderID)
|
||||
return repo.processOrderInvitationRebate(context.Background(), tx, orderID, opts)
|
||||
}
|
||||
|
||||
// OnUserRealnameVerifiedTx 便捷事务入口,供实名认证通过时调用
|
||||
|
||||
@@ -192,6 +192,76 @@ func TestInvitationFlowAndOrderRebate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessOrderInvitationRebateUsesActualProfitBasis(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
inviter := model.User{Phone: "13800000011", RealnameStatus: "verified", Status: "active"}
|
||||
if err := db.Create(&inviter).Error; err != nil {
|
||||
t.Fatalf("创建邀请人失败: %v", err)
|
||||
}
|
||||
seller := model.User{Phone: "13800000012", RealnameStatus: "verified", Status: "active"}
|
||||
if err := db.Create(&seller).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: seller.ID, Status: StatusActive, VerifiedAt: &verifiedAt,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("创建邀请关系失败: %v", err)
|
||||
}
|
||||
|
||||
order := model.RentalOrder{
|
||||
OrderNo: fmt.Sprintf("ORD-ACTUAL-%d", time.Now().UnixNano()),
|
||||
OwnerID: seller.ID,
|
||||
RenterID: 99999,
|
||||
Status: "completed",
|
||||
SettlementStatus: "arbitrated",
|
||||
RentAmountCent: 10670,
|
||||
OwnerRentAmountCent: 7270,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatalf("创建订单失败: %v", err)
|
||||
}
|
||||
|
||||
actualPlatformFeeCent := int64(933)
|
||||
if err := ProcessOrderInvitationRebateWithOptionsTx(db, order.ID, OrderRebateOptions{
|
||||
GrossProfitCent: &actualPlatformFeeCent,
|
||||
RateReferenceAt: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatalf("按实际毛利处理返利失败: %v", err)
|
||||
}
|
||||
|
||||
var ledger model.InvitationRebateLedger
|
||||
if err := db.Where("order_id = ? AND trade_role = 'seller'", order.ID).First(&ledger).Error; err != nil {
|
||||
t.Fatalf("查询返利流水失败: %v", err)
|
||||
}
|
||||
if ledger.OrderProfitBasisCent != 933 || ledger.SingleSideBasisCent != 466 || ledger.RebateAmountCent != 280 {
|
||||
t.Fatalf("返利基数/单边/金额 = %d/%d/%d, want 933/466/280",
|
||||
ledger.OrderProfitBasisCent, ledger.SingleSideBasisCent, ledger.RebateAmountCent)
|
||||
}
|
||||
|
||||
var account model.WalletAccount
|
||||
if err := db.Where("user_id = ? AND distribution_site_id = 0", inviter.ID).First(&account).Error; err != nil {
|
||||
t.Fatalf("查询邀请人钱包失败: %v", err)
|
||||
}
|
||||
if account.AvailableBalanceCent != 280 {
|
||||
t.Fatalf("邀请人余额 = %d, want 280", account.AvailableBalanceCent)
|
||||
}
|
||||
|
||||
if err := ProcessOrderInvitationRebateWithOptionsTx(db, order.ID, OrderRebateOptions{
|
||||
GrossProfitCent: &actualPlatformFeeCent,
|
||||
RateReferenceAt: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatalf("重复调用返利失败: %v", err)
|
||||
}
|
||||
var ledgerCount, walletCount int64
|
||||
db.Model(&model.InvitationRebateLedger{}).Where("order_id = ?", order.ID).Count(&ledgerCount)
|
||||
db.Model(&model.WalletLedger{}).Where("biz_no = ? AND biz_type = 'invite_rebate'", order.OrderNo).Count(&walletCount)
|
||||
if ledgerCount != 1 || walletCount != 1 {
|
||||
t.Fatalf("重复调用产生重复流水: rebate=%d wallet=%d", ledgerCount, walletCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossSiteDistributionRebate(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
|
||||
Reference in New Issue
Block a user