fix(distribution): 结算幂等与退款口径加固,补关单释放与回归测试
- 结算明细加唯一键 (order_id, party_id):自动释放与手工批次并发时以唯一键冲突为准跳过该参与方,不再重复入账 - 自动释放先无锁筛出本站未入账参与方,收敛加锁范围;手工批次建批次后先锁周期内财务快照,与自动释放串行化 - 累计退款额改为「本次回调金额与已退款成功流水合计取较大值」,多笔退款与退款结果重复回调不再把口径写小 - 关单三路径(AdminClose / AdminRejectRefund 的 close 分支 / AdminSeal)补写 release_at,接线原死代码 EnsureCompletedReleaseAtTx - 仲裁分销重算改用实际退款额(扣除押金暂扣),避免站点被多扣 - 订单列表与详情的提价分成取 min(收入, 货架加价),退款压缩加价后不再显示负数 - 新增迁移 000070(结算明细唯一键)、000071(补回填 closed 订单释放时间) - 新增 5 个测试文件:结算幂等与展示钳制、结算 job 幂等、累计退款、关单补写释放时间、仲裁押金暂扣口径
This commit is contained in:
@@ -132,7 +132,7 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
||||
return err
|
||||
}
|
||||
if order.DistributionSiteID != nil && *order.DistributionSiteID > 0 {
|
||||
if err := distribution.ApplyRefundTx(tx, order.ID, settlement.RenterRefundAmountCent); err != nil {
|
||||
if err := distribution.ApplyRefundTx(tx, order.ID, refundAmountCent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package dispute
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/database"
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// createHeldDepositDistributionFixture 构造「押金暂扣 + 分销站点」的仲裁前置数据:
|
||||
// 站点 + 生效规则版本 + 进行中订单(押金 60 元全额暂扣)+ 待结算分销财务快照(毛收入 = 订单租金)。
|
||||
func createHeldDepositDistributionFixture(t *testing.T, db *gorm.DB) (model.DistributionSite, model.DistributionRuleVersion, model.RentalOrder, model.DistributionOrderFinancial) {
|
||||
t.Helper()
|
||||
if err := database.AutoMigrateIfMissing(db,
|
||||
&model.DistributionSite{},
|
||||
&model.DistributionRuleVersion{},
|
||||
&model.DistributionOrderFinancial{},
|
||||
); err != nil {
|
||||
t.Fatalf("补齐分销表失败: %v", err)
|
||||
}
|
||||
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "arb_hold_site",
|
||||
SiteName: "仲裁暂扣站",
|
||||
Domain: "arb-hold.dist.com",
|
||||
Status: "active",
|
||||
DisplayAccountEnabled: true,
|
||||
SettlementHoldDays: 3,
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatalf("创建分销站点失败: %v", err)
|
||||
}
|
||||
rule := model.DistributionRuleVersion{
|
||||
SiteID: &site.ID,
|
||||
Mode: "two_party_distributor_receive",
|
||||
Version: 1,
|
||||
Status: "active",
|
||||
DistributorReceiveRatio: 35,
|
||||
PlatformSellRatio: 40,
|
||||
ServiceFeeValue: 25,
|
||||
ServiceFeeUnit: "percent",
|
||||
PaymentFeeBPS: 50,
|
||||
EffectiveFrom: time.Now().Add(-time.Hour),
|
||||
}
|
||||
if err := db.Create(&rule).Error; err != nil {
|
||||
t.Fatalf("创建规则版本失败: %v", err)
|
||||
}
|
||||
|
||||
_, _, order := createDisputeOrderFixture(t, db, model.RentalOrder{
|
||||
Status: "renting",
|
||||
HandoffStatus: "received",
|
||||
SettlementStatus: "unsettled",
|
||||
RentAmountCent: 10000,
|
||||
OwnerRentAmountCent: 8000,
|
||||
DepositAmountCent: 6000,
|
||||
DepositHoldStatus: model.DepositHoldStatusHeld,
|
||||
DepositHoldAmountCent: 6000,
|
||||
DistributionSiteID: &site.ID,
|
||||
DistributionRuleVersionID: &rule.ID,
|
||||
})
|
||||
|
||||
financial := model.DistributionOrderFinancial{
|
||||
OrderID: order.ID,
|
||||
SiteID: site.ID,
|
||||
RuleVersionID: rule.ID,
|
||||
GrossPaidAmountCent: order.RentAmountCent,
|
||||
NetPaidAmountCent: order.RentAmountCent,
|
||||
SplitBaseAmountCent: 2000,
|
||||
SettlementStatus: "pending",
|
||||
}
|
||||
if err := db.Create(&financial).Error; err != nil {
|
||||
t.Fatalf("创建分销财务快照失败: %v", err)
|
||||
}
|
||||
return site, rule, order, financial
|
||||
}
|
||||
|
||||
// TestArbitrationDistributionRefundUsesActualRefundWithDepositHold 断言押金暂扣订单仲裁时,
|
||||
// 分销重算使用「实际退款额」(裁决退款扣除被暂扣押金部分),而不是裁决书上的退款额。
|
||||
// 用未扣除的裁决额重算会让分销快照多记退款、net_paid 少记(站点被多扣)。
|
||||
func TestArbitrationDistributionRefundUsesActualRefundWithDepositHold(t *testing.T) {
|
||||
db := setupDisputeTestDB(t)
|
||||
startedRefundCent := int64(-1)
|
||||
repo := NewRepository(db, Dependencies{
|
||||
RefundStarter: RefundStarterFunc(func(_ context.Context, _ uint64, refundAmountCent int64, _ string, _ string) (string, error) {
|
||||
startedRefundCent = refundAmountCent
|
||||
return "refunded", nil
|
||||
}),
|
||||
})
|
||||
_, _, order, financial := createHeldDepositDistributionFixture(t, db)
|
||||
|
||||
created, err := repo.Create(t.Context(), order.RenterID, order.ID, CreateRequest{
|
||||
Type: "haf_coin_dispute",
|
||||
Description: "押金暂扣期间申请仲裁全额退款",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("创建申诉失败: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.Arbitrate(t.Context(), 9001, created.ID, ArbitrateRequest{
|
||||
Result: "full_refund",
|
||||
Remark: "仲裁全额退款,押金部分暂扣中",
|
||||
}, AuditMeta{}); err != nil {
|
||||
t.Fatalf("仲裁失败: %v", err)
|
||||
}
|
||||
|
||||
// 裁决额 = 租金 + 押金;押金全额暂扣 → 实际退款 = 裁决额 − 押金。
|
||||
arbitrationRefundCent := order.RentAmountCent + order.DepositAmountCent
|
||||
actualRefundCent := arbitrationRefundCent - order.DepositHoldAmountCent
|
||||
if actualRefundCent >= arbitrationRefundCent {
|
||||
t.Fatalf("fixture 未构造出「实际退款 < 裁决额」:actual=%d arbitration=%d", actualRefundCent, arbitrationRefundCent)
|
||||
}
|
||||
if startedRefundCent != actualRefundCent {
|
||||
t.Fatalf("实际发起退款 = %d, want %d(裁决额 %d 扣除被暂扣押金 %d)",
|
||||
startedRefundCent, actualRefundCent, arbitrationRefundCent, order.DepositHoldAmountCent)
|
||||
}
|
||||
|
||||
var savedOrder model.RentalOrder
|
||||
if err := db.First(&savedOrder, order.ID).Error; err != nil {
|
||||
t.Fatalf("读取订单失败: %v", err)
|
||||
}
|
||||
if savedOrder.RefundStatus != "pending" || savedOrder.RefundAmountCent != actualRefundCent {
|
||||
t.Fatalf("订单退款 = %s/%d, want pending/%d", savedOrder.RefundStatus, savedOrder.RefundAmountCent, actualRefundCent)
|
||||
}
|
||||
|
||||
// 押金退款不计入分销毛收入:实际退款先抵扣押金,剩余才是租金退款。
|
||||
wantRentRefundCent := actualRefundCent - order.DepositAmountCent
|
||||
unheldRentRefundCent := arbitrationRefundCent - order.DepositAmountCent
|
||||
var snapshot model.DistributionOrderFinancial
|
||||
if err := db.First(&snapshot, financial.ID).Error; err != nil {
|
||||
t.Fatalf("读取分销快照失败: %v", err)
|
||||
}
|
||||
if snapshot.RefundAmountCent != wantRentRefundCent {
|
||||
t.Fatalf("分销快照 refund_amount_cent = %d, want %d(按未扣除的裁决额 %d 重算会得到 %d)",
|
||||
snapshot.RefundAmountCent, wantRentRefundCent, arbitrationRefundCent, unheldRentRefundCent)
|
||||
}
|
||||
if wantNetPaid := order.RentAmountCent - wantRentRefundCent; snapshot.NetPaidAmountCent != wantNetPaid {
|
||||
t.Fatalf("分销快照 net_paid_amount_cent = %d, want %d(毛收入 %d − 实际租金退款 %d)",
|
||||
snapshot.NetPaidAmountCent, wantNetPaid, order.RentAmountCent, wantRentRefundCent)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user