修复仲裁退款并新增免押额度
This commit is contained in:
@@ -3,6 +3,7 @@ package dispute
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
@@ -17,13 +18,28 @@ import (
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
refundFunc RefundFunc
|
||||
}
|
||||
|
||||
// RefundFunc 由 payment 模块注入,避免 dispute 与 payment 形成循环依赖。
|
||||
type RefundFunc func(orderID uint64, refundAmountCent int64, bizType string, remark string) (status string, err error)
|
||||
|
||||
type refundAction struct {
|
||||
OrderID uint64
|
||||
RefundAmountCent int64
|
||||
BizType string
|
||||
Remark string
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
func (r *Repository) SetRefundFunc(fn RefundFunc) {
|
||||
r.refundFunc = fn
|
||||
}
|
||||
|
||||
func (r *Repository) Create(userID uint64, orderID uint64, req CreateRequest) (*DisputeDTO, error) {
|
||||
var createdID uint64
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
@@ -172,6 +188,7 @@ func (r *Repository) ListAdmin(page, pageSize int) (*PaginatedResult, error) {
|
||||
}
|
||||
|
||||
func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest, meta AuditMeta) (*DisputeDTO, error) {
|
||||
var refund *refundAction
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var row model.Dispute
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, id).Error; err != nil {
|
||||
@@ -197,7 +214,11 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
|
||||
beforeSettlementStatus := order.SettlementStatus
|
||||
beforeListingStatus := listing.Status
|
||||
beforeAccountStatus := account.Status
|
||||
settlement, err := buildArbitrationSettlement(order, req)
|
||||
frozenBalance, err := renterFrozenBalance(tx, order.RenterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settlement, err := buildArbitrationSettlement(order, req, frozenBalance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -227,6 +248,14 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
|
||||
if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil {
|
||||
return err
|
||||
}
|
||||
if settlement.RenterRefundAmount > 0 {
|
||||
refundCent := int64(math.Round(settlement.RenterRefundAmount * 100))
|
||||
action, err := r.prepareRefund(&order, refundCent, "arbitration_refund", "仲裁退款原路退还")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
}
|
||||
if err := tx.Save(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -288,6 +317,7 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.startRefundBestEffort(refund)
|
||||
var row disputeRow
|
||||
if err := r.baseQuery().Where("d.id = ?", id).First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -303,7 +333,7 @@ type arbitrationSettlement struct {
|
||||
DepositDeductAmount float64
|
||||
}
|
||||
|
||||
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (arbitrationSettlement, error) {
|
||||
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalance float64) (arbitrationSettlement, error) {
|
||||
total := roundMoney(order.RentAmount + order.DepositAmount)
|
||||
ownerRentAmount := roundMoney(order.OwnerRentAmount)
|
||||
if ownerRentAmount <= 0 || ownerRentAmount > order.RentAmount {
|
||||
@@ -311,16 +341,17 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
|
||||
}
|
||||
settlement := arbitrationSettlement{}
|
||||
orderID := order.ID
|
||||
if total > 0 {
|
||||
releaseFrozenAmount := minMoney(total, roundMoney(renterFrozenBalance))
|
||||
if releaseFrozenAmount > 0 {
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
Direction: "out",
|
||||
Amount: total,
|
||||
Amount: releaseFrozenAmount,
|
||||
BalanceType: "frozen",
|
||||
BizType: "arbitration_release_frozen",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "仲裁释放开发态模拟冻结金额",
|
||||
Remark: "仲裁释放冻结金额",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -329,16 +360,6 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
|
||||
return
|
||||
}
|
||||
settlement.RenterRefundAmount += amount
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
Amount: amount,
|
||||
BalanceType: "available",
|
||||
BizType: "arbitration_renter_refund",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: remark,
|
||||
})
|
||||
}
|
||||
addOwnerIncome := func(amount float64, remark string) {
|
||||
if amount <= 0 {
|
||||
@@ -391,6 +412,45 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
|
||||
return settlement, nil
|
||||
}
|
||||
|
||||
func (r *Repository) prepareRefund(order *model.RentalOrder, amountCent int64, bizType string, remark string) (*refundAction, error) {
|
||||
if amountCent <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if r.refundFunc == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
order.RefundStatus = "pending"
|
||||
order.RefundAmountCent = amountCent
|
||||
order.RefundedAt = nil
|
||||
return &refundAction{
|
||||
OrderID: order.ID,
|
||||
RefundAmountCent: amountCent,
|
||||
BizType: bizType,
|
||||
Remark: remark,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) startRefundBestEffort(action *refundAction) {
|
||||
if action == nil || r.refundFunc == nil {
|
||||
return
|
||||
}
|
||||
_, _ = r.refundFunc(action.OrderID, action.RefundAmountCent, action.BizType, action.Remark)
|
||||
}
|
||||
|
||||
func renterFrozenBalance(tx *gorm.DB, renterID uint64) (float64, error) {
|
||||
var account model.WalletAccount
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("user_id = ?", renterID).
|
||||
First(&account).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return account.FrozenBalance, nil
|
||||
}
|
||||
|
||||
// minMoney 返回较小金额(角精度)
|
||||
func minMoney(a float64, b float64) float64 {
|
||||
return money.Min(a, b)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package dispute
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
)
|
||||
|
||||
func TestBuildArbitrationSettlementSkipsFrozenReleaseWhenNoFrozenBalance(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
ID: 11,
|
||||
OrderNo: "RO202606080001",
|
||||
RenterID: 101,
|
||||
OwnerID: 202,
|
||||
RentAmount: 200,
|
||||
OwnerRentAmount: 180,
|
||||
DepositAmount: 100,
|
||||
}
|
||||
|
||||
settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{
|
||||
Result: "release_deposit",
|
||||
Remark: "测试裁决",
|
||||
}, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("buildArbitrationSettlement() error = %v", err)
|
||||
}
|
||||
|
||||
for _, entry := range settlement.Entries {
|
||||
if entry.BizType == "arbitration_release_frozen" {
|
||||
t.Fatalf("不应在无冻结余额时生成解冻流水: %+v", entry)
|
||||
}
|
||||
}
|
||||
if settlement.OwnerIncomeAmount != 180 {
|
||||
t.Fatalf("OwnerIncomeAmount = %.1f, want 180.0", settlement.OwnerIncomeAmount)
|
||||
}
|
||||
if settlement.RenterRefundAmount != 100 {
|
||||
t.Fatalf("RenterRefundAmount = %.1f, want 100.0", settlement.RenterRefundAmount)
|
||||
}
|
||||
for _, entry := range settlement.Entries {
|
||||
if entry.UserID == order.RenterID && entry.BizType == "arbitration_renter_refund" {
|
||||
t.Fatalf("外部支付订单不应生成租客钱包退款流水: %+v", entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildArbitrationSettlementLimitsFrozenReleaseToExistingBalance(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
ID: 12,
|
||||
OrderNo: "RO202606080002",
|
||||
RenterID: 101,
|
||||
OwnerID: 202,
|
||||
RentAmount: 200,
|
||||
OwnerRentAmount: 180,
|
||||
DepositAmount: 100,
|
||||
}
|
||||
|
||||
settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{
|
||||
Result: "order_close",
|
||||
Remark: "测试裁决",
|
||||
}, 120)
|
||||
if err != nil {
|
||||
t.Fatalf("buildArbitrationSettlement() error = %v", err)
|
||||
}
|
||||
|
||||
releaseEntryCount := 0
|
||||
for _, entry := range settlement.Entries {
|
||||
if entry.BizType != "arbitration_release_frozen" {
|
||||
continue
|
||||
}
|
||||
releaseEntryCount++
|
||||
if entry.Amount != 120 {
|
||||
t.Fatalf("release frozen amount = %.1f, want 120.0", entry.Amount)
|
||||
}
|
||||
}
|
||||
if releaseEntryCount != 1 {
|
||||
t.Fatalf("release frozen entry count = %d, want 1", releaseEntryCount)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user