修复代管订单押金赔付待打款金额

This commit is contained in:
yml2213
2026-08-30 20:37:41 +08:00
parent 98f6ee9451
commit 2337395d8f
11 changed files with 238 additions and 111 deletions
+35 -15
View File
@@ -153,7 +153,7 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
"order_no": order.OrderNo, "order_no": order.OrderNo,
"result": req.Result, "result": req.Result,
"remark": req.Remark, "remark": req.Remark,
"input_amount_cent": req.AmountCent, "input_amount_cent": arbitrationInputAmountCent(req),
"renter_refund_amount_cent": settlement.RenterRefundAmountCent, "renter_refund_amount_cent": settlement.RenterRefundAmountCent,
"owner_income_amount_cent": settlement.OwnerIncomeAmountCent, "owner_income_amount_cent": settlement.OwnerIncomeAmountCent,
"deposit_deduct_amount_cent": settlement.DepositDeductAmountCent, "deposit_deduct_amount_cent": settlement.DepositDeductAmountCent,
@@ -245,7 +245,7 @@ func buildActualCheckoutArbitrationSettlement(tx *gorm.DB, row model.Dispute, or
if err != nil { if err != nil {
return settlement, nil, linkedCheckout, 0, err return settlement, nil, linkedCheckout, 0, err
} }
depositDeductAmountCent, err := arbitrationDepositDeductAmountCent(order, req) depositDeductAmountCent, err := arbitrationDepositDeductAmountCent(order, linkedCheckout, req)
if err != nil { if err != nil {
return settlement, nil, linkedCheckout, 0, err return settlement, nil, linkedCheckout, 0, err
} }
@@ -299,21 +299,24 @@ func arbitrationConsumableAmountCent(order model.RentalOrder, checkout *model.Or
return 0, ErrInvalidDispute return 0, ErrInvalidDispute
} }
func arbitrationDepositDeductAmountCent(order model.RentalOrder, req ArbitrateRequest) (int64, error) { func arbitrationDepositDeductAmountCent(order model.RentalOrder, checkout *model.OrderCheckout, req ArbitrateRequest) (int64, error) {
switch req.Result { switch req.Result {
case "actual_settlement": case "actual_settlement":
if req.AmountCent <= 0 { if req.AmountCent == nil {
if checkout != nil {
return arbitrationCheckoutDepositDeductCent(*checkout), nil
}
return 0, nil return 0, nil
} }
if req.AmountCent > order.DepositAmountCent { if *req.AmountCent < 0 || *req.AmountCent > order.DepositAmountCent {
return 0, ErrInvalidDispute return 0, ErrInvalidDispute
} }
return req.AmountCent, nil return *req.AmountCent, nil
case "release_deposit": case "release_deposit":
return 0, nil return 0, nil
case "deduct_deposit", "compensate_owner": case "deduct_deposit", "compensate_owner":
deductAmountCent := req.AmountCent deductAmountCent := arbitrationInputAmountCent(req)
if deductAmountCent <= 0 { if req.AmountCent == nil || deductAmountCent <= 0 {
deductAmountCent = order.DepositAmountCent deductAmountCent = order.DepositAmountCent
} }
if deductAmountCent > order.DepositAmountCent { if deductAmountCent > order.DepositAmountCent {
@@ -325,6 +328,23 @@ func arbitrationDepositDeductAmountCent(order model.RentalOrder, req ArbitrateRe
} }
} }
func arbitrationInputAmountCent(req ArbitrateRequest) int64 {
if req.AmountCent == nil {
return 0
}
return *req.AmountCent
}
func arbitrationCheckoutDepositDeductCent(checkout model.OrderCheckout) int64 {
if checkout.DepositDeductAmountCent > 0 {
return checkout.DepositDeductAmountCent
}
if checkout.OtherAmountCent > 0 {
return checkout.OtherAmountCent
}
return 0
}
func applyArbitrationActualSettlement(order *model.RentalOrder, checkout *model.OrderCheckout, settlement ordermodule.ActualCheckoutSettlement, depositDeductAmountCent int64) { func applyArbitrationActualSettlement(order *model.RentalOrder, checkout *model.OrderCheckout, settlement ordermodule.ActualCheckoutSettlement, depositDeductAmountCent int64) {
order.ActualCoinConsumedM = settlement.CoinConsumedM order.ActualCoinConsumedM = settlement.CoinConsumedM
order.ActualPureCoinAmountCent = settlement.PureCoinAmountCent order.ActualPureCoinAmountCent = settlement.PureCoinAmountCent
@@ -350,7 +370,7 @@ func applyArbitrationActualSettlement(order *model.RentalOrder, checkout *model.
} }
func isPlatformManagedOrder(order model.RentalOrder) bool { func isPlatformManagedOrder(order model.RentalOrder) bool {
return order.SettlementMode == "platform_managed" || order.HandoffMode == "platform" return order.SettlementMode == "platform_managed"
} }
func appendPlatformManagedAdminNotification(tx *gorm.DB, order model.RentalOrder, typ string, title string, content string) error { func appendPlatformManagedAdminNotification(tx *gorm.DB, order model.RentalOrder, typ string, title string, content string) error {
@@ -459,15 +479,15 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
addRenterRefund(totalCent, "仲裁全额退款") addRenterRefund(totalCent, "仲裁全额退款")
settlement.RenterDepositRefundCent = depositAmountCent settlement.RenterDepositRefundCent = depositAmountCent
case "partial_refund": case "partial_refund":
if req.AmountCent <= 0 || req.AmountCent > totalCent { if req.AmountCent == nil || *req.AmountCent <= 0 || *req.AmountCent > totalCent {
return settlement, ErrInvalidDispute return settlement, ErrInvalidDispute
} }
addRenterRefund(req.AmountCent, "仲裁部分退款") addRenterRefund(*req.AmountCent, "仲裁部分退款")
// 部分退款为合并金额,无法精确拆分租金/押金,按押金优先归类以便暂扣。 // 部分退款为合并金额,无法精确拆分租金/押金,按押金优先归类以便暂扣。
settlement.RenterDepositRefundCent = money.MinCent(req.AmountCent, depositAmountCent) settlement.RenterDepositRefundCent = money.MinCent(*req.AmountCent, depositAmountCent)
// 号主仅拿「未退租金中的号主份额 + 未退押金」,平台加价按未退租金比例预留,避免整笔剩余进号主。 // 号主仅拿「未退租金中的号主份额 + 未退押金」,平台加价按未退租金比例预留,避免整笔剩余进号主。
addOwnerIncome( addOwnerIncome(
partialRefundOwnerIncomeCent(rentAmountCent, ownerRentAmountCent, depositAmountCent, req.AmountCent), partialRefundOwnerIncomeCent(rentAmountCent, ownerRentAmountCent, depositAmountCent, *req.AmountCent),
"仲裁剩余金额结算给号主", "仲裁剩余金额结算给号主",
) )
case "release_deposit": case "release_deposit":
@@ -475,8 +495,8 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
addRenterRefund(depositAmountCent, "仲裁释放押金给租客") addRenterRefund(depositAmountCent, "仲裁释放押金给租客")
settlement.RenterDepositRefundCent = depositAmountCent settlement.RenterDepositRefundCent = depositAmountCent
case "deduct_deposit", "compensate_owner": case "deduct_deposit", "compensate_owner":
deductAmountCent := req.AmountCent deductAmountCent := arbitrationInputAmountCent(req)
if deductAmountCent <= 0 { if req.AmountCent == nil || deductAmountCent <= 0 {
deductAmountCent = depositAmountCent deductAmountCent = depositAmountCent
} }
if deductAmountCent > depositAmountCent { if deductAmountCent > depositAmountCent {
+36 -35
View File
@@ -9,40 +9,41 @@ import (
) )
type DisputeDTO struct { type DisputeDTO struct {
ID uint64 `json:"id"` ID uint64 `json:"id"`
OrderID uint64 `json:"order_id"` OrderID uint64 `json:"order_id"`
OrderNo string `json:"order_no"` OrderNo string `json:"order_no"`
OrderStatus string `json:"order_status"` OrderStatus string `json:"order_status"`
HandoffStatus string `json:"handoff_status"` HandoffStatus string `json:"handoff_status"`
SettlementStatus string `json:"settlement_status"` SettlementStatus string `json:"settlement_status"`
ExtraItemOriginalAmountCent int64 `json:"extra_item_original_amount_cent,omitempty"` ExtraItemOriginalAmountCent int64 `json:"extra_item_original_amount_cent,omitempty"`
ListingNo string `json:"listing_no"` ListingNo string `json:"listing_no"`
Title string `json:"title"` Title string `json:"title"`
OwnerID uint64 `json:"owner_id"` OwnerID uint64 `json:"owner_id"`
RenterID uint64 `json:"renter_id"` RenterID uint64 `json:"renter_id"`
OwnerPhone string `json:"owner_phone"` OwnerPhone string `json:"owner_phone"`
RenterPhone string `json:"renter_phone"` RenterPhone string `json:"renter_phone"`
InitiatorID uint64 `json:"initiator_id"` InitiatorID uint64 `json:"initiator_id"`
InitiatorType string `json:"initiator_type"` InitiatorType string `json:"initiator_type"`
InitiatorAdminID *uint64 `json:"initiator_admin_id,omitempty"` InitiatorAdminID *uint64 `json:"initiator_admin_id,omitempty"`
TargetUserID uint64 `json:"target_user_id"` TargetUserID uint64 `json:"target_user_id"`
Type string `json:"type"` Type string `json:"type"`
Status string `json:"status"` Status string `json:"status"`
Description string `json:"description"` Description string `json:"description"`
EvidenceURLS datatypes.JSON `json:"evidence_urls"` EvidenceURLS datatypes.JSON `json:"evidence_urls"`
PreviousOrderStatus string `json:"previous_order_status"` PreviousOrderStatus string `json:"previous_order_status"`
PreviousHandoffStatus string `json:"previous_handoff_status"` PreviousHandoffStatus string `json:"previous_handoff_status"`
PreviousSettlementStatus string `json:"previous_settlement_status"` PreviousSettlementStatus string `json:"previous_settlement_status"`
CheckoutID *uint64 `json:"checkout_id"` CheckoutID *uint64 `json:"checkout_id"`
CheckoutCoinConsumedM *float64 `json:"checkout_coin_consumed_m,omitempty"` CheckoutCoinConsumedM *float64 `json:"checkout_coin_consumed_m,omitempty"`
CheckoutConsumableAmountCent *int64 `json:"checkout_consumable_amount_cent,omitempty"` CheckoutConsumableAmountCent *int64 `json:"checkout_consumable_amount_cent,omitempty"`
PreviousCheckoutStatus string `json:"previous_checkout_status"` CheckoutDepositDeductAmountCent *int64 `json:"checkout_deposit_deduct_amount_cent,omitempty"`
ArbitrationResult string `json:"arbitration_result"` PreviousCheckoutStatus string `json:"previous_checkout_status"`
ArbitrationRemark string `json:"arbitration_remark"` ArbitrationResult string `json:"arbitration_result"`
HandledBy *uint64 `json:"handled_by"` ArbitrationRemark string `json:"arbitration_remark"`
HandledAt *time.Time `json:"handled_at"` HandledBy *uint64 `json:"handled_by"`
CreatedAt time.Time `json:"created_at"` HandledAt *time.Time `json:"handled_at"`
UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
type CreateRequest struct { type CreateRequest struct {
@@ -61,7 +62,7 @@ type AdminCreateRequest struct {
type ArbitrateRequest struct { type ArbitrateRequest struct {
Result string `json:"result" binding:"required"` Result string `json:"result" binding:"required"`
Remark string `json:"remark" binding:"required"` Remark string `json:"remark" binding:"required"`
AmountCent int64 `json:"amount_cent"` AmountCent *int64 `json:"amount_cent"`
ActualCoinConsumedM *float64 `json:"actual_coin_consumed_m"` ActualCoinConsumedM *float64 `json:"actual_coin_consumed_m"`
ActualConsumableAmountCent *int64 `json:"actual_consumable_amount_cent"` ActualConsumableAmountCent *int64 `json:"actual_consumable_amount_cent"`
} }
+49 -47
View File
@@ -10,57 +10,59 @@ import (
type disputeRow struct { type disputeRow struct {
model.Dispute model.Dispute
OrderNo string OrderNo string
OrderStatus string OrderStatus string
HandoffStatus string HandoffStatus string
SettlementStatus string SettlementStatus string
ExtraItemOriginalAmountCent int64 ExtraItemOriginalAmountCent int64
ListingNo string ListingNo string
Title string Title string
OwnerID uint64 OwnerID uint64
RenterID uint64 RenterID uint64
OwnerPhone string OwnerPhone string
RenterPhone string RenterPhone string
CheckoutCoinConsumedM *float64 CheckoutCoinConsumedM *float64
CheckoutConsumableAmountCent *int64 CheckoutConsumableAmountCent *int64
CheckoutDepositDeductAmountCent *int64
} }
func (row disputeRow) toDTO() DisputeDTO { func (row disputeRow) toDTO() DisputeDTO {
return DisputeDTO{ return DisputeDTO{
ID: row.ID, ID: row.ID,
OrderID: row.OrderID, OrderID: row.OrderID,
OrderNo: row.OrderNo, OrderNo: row.OrderNo,
OrderStatus: row.OrderStatus, OrderStatus: row.OrderStatus,
HandoffStatus: row.HandoffStatus, HandoffStatus: row.HandoffStatus,
SettlementStatus: row.SettlementStatus, SettlementStatus: row.SettlementStatus,
ExtraItemOriginalAmountCent: row.ExtraItemOriginalAmountCent, ExtraItemOriginalAmountCent: row.ExtraItemOriginalAmountCent,
ListingNo: row.ListingNo, ListingNo: row.ListingNo,
Title: row.Title, Title: row.Title,
OwnerID: row.OwnerID, OwnerID: row.OwnerID,
RenterID: row.RenterID, RenterID: row.RenterID,
OwnerPhone: row.OwnerPhone, OwnerPhone: row.OwnerPhone,
RenterPhone: row.RenterPhone, RenterPhone: row.RenterPhone,
InitiatorID: row.InitiatorID, InitiatorID: row.InitiatorID,
InitiatorType: effectiveInitiatorType(row.Dispute), InitiatorType: effectiveInitiatorType(row.Dispute),
InitiatorAdminID: row.InitiatorAdminID, InitiatorAdminID: row.InitiatorAdminID,
TargetUserID: row.TargetUserID, TargetUserID: row.TargetUserID,
Type: row.Type, Type: row.Type,
Status: row.Status, Status: row.Status,
Description: row.Description, Description: row.Description,
EvidenceURLS: row.EvidenceURLS, EvidenceURLS: row.EvidenceURLS,
PreviousOrderStatus: row.PreviousOrderStatus, PreviousOrderStatus: row.PreviousOrderStatus,
PreviousHandoffStatus: row.PreviousHandoffStatus, PreviousHandoffStatus: row.PreviousHandoffStatus,
PreviousSettlementStatus: row.PreviousSettlementStatus, PreviousSettlementStatus: row.PreviousSettlementStatus,
CheckoutID: row.CheckoutID, CheckoutID: row.CheckoutID,
CheckoutCoinConsumedM: row.CheckoutCoinConsumedM, CheckoutCoinConsumedM: row.CheckoutCoinConsumedM,
CheckoutConsumableAmountCent: row.CheckoutConsumableAmountCent, CheckoutConsumableAmountCent: row.CheckoutConsumableAmountCent,
PreviousCheckoutStatus: row.PreviousCheckoutStatus, CheckoutDepositDeductAmountCent: row.CheckoutDepositDeductAmountCent,
ArbitrationResult: row.ArbitrationResult, PreviousCheckoutStatus: row.PreviousCheckoutStatus,
ArbitrationRemark: row.ArbitrationRemark, ArbitrationResult: row.ArbitrationResult,
HandledBy: row.HandledBy, ArbitrationRemark: row.ArbitrationRemark,
HandledAt: row.HandledAt, HandledBy: row.HandledBy,
CreatedAt: row.CreatedAt, HandledAt: row.HandledAt,
UpdatedAt: row.UpdatedAt, CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
} }
} }
+2 -1
View File
@@ -94,7 +94,8 @@ func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
o.owner_id, o.renter_id, o.extra_item_original_amount_cent, o.owner_id, o.renter_id, o.extra_item_original_amount_cent,
owner.phone AS owner_phone, renter.phone AS renter_phone, owner.phone AS owner_phone, renter.phone AS renter_phone,
l.listing_no, a.title, c.coin_consumed_m AS checkout_coin_consumed_m, l.listing_no, a.title, c.coin_consumed_m AS checkout_coin_consumed_m,
c.consumable_amount_cent AS checkout_consumable_amount_cent`) c.consumable_amount_cent AS checkout_consumable_amount_cent,
CASE WHEN c.deposit_deduct_amount_cent > 0 THEN c.deposit_deduct_amount_cent ELSE c.other_amount_cent END AS checkout_deposit_deduct_amount_cent`)
} }
func (r *Repository) adminFilterQuery(ctx context.Context) *gorm.DB { func (r *Repository) adminFilterQuery(ctx context.Context) *gorm.DB {
@@ -14,6 +14,8 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
func int64Ptr(value int64) *int64 { return &value }
func disputePureCoinOrder() model.RentalOrder { func disputePureCoinOrder() model.RentalOrder {
return model.RentalOrder{ return model.RentalOrder{
Status: "renting", Status: "renting",
@@ -322,6 +324,64 @@ func TestPlatformManagedArbitrationUsesOfflineSettlement(t *testing.T) {
} }
} }
func TestPlatformManagedCheckoutArbitrationKeepsExistingDepositCompensation(t *testing.T) {
db := setupDisputeTestDB(t)
repo := NewRepository(db, Dependencies{RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) {
return "refunded", nil
})})
adminID := uint64(78)
_, renter, order := createDisputeOrderFixture(t, db, model.RentalOrder{
Status: "pending_checkout_confirm",
HandoffStatus: "pending_owner_checkout",
SettlementStatus: "pending",
HandoffMode: "platform",
SettlementMode: "platform_managed",
ManagedAdminID: &adminID,
RentAmountCent: 10000,
OwnerRentAmountCent: 8000,
PlatformFeeCent: 2000,
DepositAmountCent: 5000,
})
checkout := model.OrderCheckout{
OrderID: order.ID,
InitiatedBy: renter.ID,
Status: "submitted",
CoinConsumedM: 0,
DepositDeductAmountCent: 3000,
OtherAmountCent: 3000,
}
if err := db.Create(&checkout).Error; err != nil {
t.Fatalf("创建结账记录失败: %v", err)
}
created, err := repo.Create(t.Context(), renter.ID, order.ID, CreateRequest{
Type: "checkout_amount",
Description: "结账押金损耗争议",
})
if err != nil {
t.Fatalf("创建结账争议失败: %v", err)
}
if created.CheckoutDepositDeductAmountCent == nil || *created.CheckoutDepositDeductAmountCent != 3000 {
t.Fatalf("争议未带出押金赔付金额: %v", created.CheckoutDepositDeductAmountCent)
}
actualCoinConsumedM := 0.0
actualConsumableAmountCent := int64(0)
if _, err := repo.Arbitrate(t.Context(), adminID, 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)
}
if saved.OfflineSettlementAmountCent != 11000 {
t.Fatalf("仲裁后待打款 = %d, want 11000", saved.OfflineSettlementAmountCent)
}
}
func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) { func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) {
db := setupDisputeTestDB(t) db := setupDisputeTestDB(t)
repo := NewRepository(db, Dependencies{RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) { repo := NewRepository(db, Dependencies{RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) {
@@ -394,7 +454,7 @@ func TestActualSettlementArbitrationUsesOwnerCompensationAmount(t *testing.T) {
settlement, actualSettlement, _, depositDeductAmountCent, err = buildActualCheckoutArbitrationSettlement(nil, model.Dispute{}, order, ArbitrateRequest{ settlement, actualSettlement, _, depositDeductAmountCent, err = buildActualCheckoutArbitrationSettlement(nil, model.Dispute{}, order, ArbitrateRequest{
Result: "actual_settlement", Result: "actual_settlement",
Remark: "赔付号主", Remark: "赔付号主",
AmountCent: compensationCent, AmountCent: &compensationCent,
ActualCoinConsumedM: &actualCoinConsumedM, ActualCoinConsumedM: &actualCoinConsumedM,
ActualConsumableAmountCent: &actualConsumableAmountCent, ActualConsumableAmountCent: &actualConsumableAmountCent,
}, 0) }, 0)
@@ -406,6 +466,21 @@ func TestActualSettlementArbitrationUsesOwnerCompensationAmount(t *testing.T) {
} }
} }
func TestActualSettlementArbitrationInheritsCheckoutDepositCompensation(t *testing.T) {
order := disputePureCoinOrder()
checkout := &model.OrderCheckout{DepositDeductAmountCent: 3000}
amount, err := arbitrationDepositDeductAmountCent(order, checkout, ArbitrateRequest{Result: "actual_settlement"})
if err != nil || amount != 3000 {
t.Fatalf("omitted arbitration compensation = %d/%v, want 3000/nil", amount, err)
}
zero := int64(0)
amount, err = arbitrationDepositDeductAmountCent(order, checkout, ArbitrateRequest{Result: "actual_settlement", AmountCent: &zero})
if err != nil || amount != 0 {
t.Fatalf("explicit zero arbitration compensation = %d/%v, want 0/nil", amount, err)
}
}
func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) { func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) {
db := setupDisputeTestDB(t) db := setupDisputeTestDB(t)
repo := NewRepository(db, Dependencies{RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) { repo := NewRepository(db, Dependencies{RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) {
@@ -417,11 +492,12 @@ func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) {
inputOrder.SettlementStatus = "pending" inputOrder.SettlementStatus = "pending"
_, renter, order := createDisputeOrderFixture(t, db, inputOrder) _, renter, order := createDisputeOrderFixture(t, db, inputOrder)
checkout := model.OrderCheckout{ checkout := model.OrderCheckout{
OrderID: order.ID, OrderID: order.ID,
InitiatedBy: renter.ID, InitiatedBy: renter.ID,
Status: "submitted", Status: "submitted",
ConsumableAmountCent: 7000, ConsumableAmountCent: 7000,
CoinConsumedM: 25, CoinConsumedM: 25,
DepositDeductAmountCent: 3000,
} }
if err := db.Create(&checkout).Error; err != nil { if err := db.Create(&checkout).Error; err != nil {
t.Fatalf("创建结账记录失败: %v", err) t.Fatalf("创建结账记录失败: %v", err)
@@ -433,8 +509,8 @@ func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("创建结账争议失败: %v", err) t.Fatalf("创建结账争议失败: %v", err)
} }
if created.CheckoutID == nil || *created.CheckoutID != checkout.ID || created.CheckoutCoinConsumedM == nil || *created.CheckoutCoinConsumedM != 25 || created.CheckoutConsumableAmountCent == nil || *created.CheckoutConsumableAmountCent != 7000 { if created.CheckoutID == nil || *created.CheckoutID != checkout.ID || created.CheckoutCoinConsumedM == nil || *created.CheckoutCoinConsumedM != 25 || created.CheckoutConsumableAmountCent == nil || *created.CheckoutConsumableAmountCent != 7000 || created.CheckoutDepositDeductAmountCent == nil || *created.CheckoutDepositDeductAmountCent != 3000 {
t.Fatalf("关联结账/M/额外物品 = %v/%v/%v, want %d/25/7000", created.CheckoutID, created.CheckoutCoinConsumedM, created.CheckoutConsumableAmountCent, checkout.ID) t.Fatalf("关联结账/M/额外物品/押金赔付 = %v/%v/%v/%v, want %d/25/7000/3000", created.CheckoutID, created.CheckoutCoinConsumedM, created.CheckoutConsumableAmountCent, created.CheckoutDepositDeductAmountCent, checkout.ID)
} }
if _, err := repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{ if _, err := repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{
@@ -566,7 +642,7 @@ func TestPartialRefundReservesPlatformFee(t *testing.T) {
settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{ settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{
Result: "partial_refund", Result: "partial_refund",
AmountCent: 5000, AmountCent: int64Ptr(5000),
Remark: "没打完 号主登录不上", Remark: "没打完 号主登录不上",
}, 0) }, 0)
if err != nil { if err != nil {
@@ -599,7 +675,7 @@ func TestPartialRefundFullRentRetainedGivesOwnerFullOwnerRent(t *testing.T) {
} }
settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{ settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{
Result: "partial_refund", Result: "partial_refund",
AmountCent: 20000, AmountCent: int64Ptr(20000),
}, 0) }, 0)
if err != nil { if err != nil {
t.Fatalf("buildArbitrationSettlement() error = %v", err) t.Fatalf("buildArbitrationSettlement() error = %v", err)
@@ -93,6 +93,9 @@ func (s *Service) Arbitrate(ctx context.Context, adminID uint64, id uint64, req
if req.ActualCoinConsumedM != nil && (*req.ActualCoinConsumedM < 0 || *req.ActualCoinConsumedM > 1_000_000_000) { if req.ActualCoinConsumedM != nil && (*req.ActualCoinConsumedM < 0 || *req.ActualCoinConsumedM > 1_000_000_000) {
return nil, ErrInvalidDispute return nil, ErrInvalidDispute
} }
if req.AmountCent != nil && (*req.AmountCent < 0 || *req.AmountCent > 100_000_000_000) {
return nil, ErrInvalidDispute
}
if req.ActualConsumableAmountCent != nil && (*req.ActualConsumableAmountCent < 0 || *req.ActualConsumableAmountCent > 100_000_000_000) { if req.ActualConsumableAmountCent != nil && (*req.ActualConsumableAmountCent < 0 || *req.ActualConsumableAmountCent > 100_000_000_000) {
return nil, ErrInvalidDispute return nil, ErrInvalidDispute
} }
+1 -1
View File
@@ -289,7 +289,7 @@ func refreshCheckoutSettlementDTO(dto *CheckoutDTO, order model.RentalOrder, che
if checkout.Status != checkoutStatusSubmitted && checkout.Status != checkoutStatusCountered { if checkout.Status != checkoutStatusSubmitted && checkout.Status != checkoutStatusCountered {
return return
} }
settlement := calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, checkout.DepositDeductAmountCent) settlement := calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, effectiveCheckoutDepositDeductCent(checkout))
rent := settlement.ActualRentAmountCent rent := settlement.ActualRentAmountCent
ownerRent := settlement.OwnerRentIncomeCent ownerRent := settlement.OwnerRentIncomeCent
platform := settlement.PlatformFeeCent platform := settlement.PlatformFeeCent
+10 -1
View File
@@ -278,7 +278,16 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
} }
func buildCheckoutSettlement(order model.RentalOrder, checkout *model.OrderCheckout) checkoutSettlement { func buildCheckoutSettlement(order model.RentalOrder, checkout *model.OrderCheckout) checkoutSettlement {
return calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, checkout.DepositDeductAmountCent) return calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, effectiveCheckoutDepositDeductCent(*checkout))
}
// effectiveCheckoutDepositDeductCent keeps legacy checkouts that only populated
// other_amount_cent from losing their deposit compensation at finalization.
func effectiveCheckoutDepositDeductCent(checkout model.OrderCheckout) int64 {
if checkout.DepositDeductAmountCent > 0 {
return checkout.DepositDeductAmountCent
}
return maxCent(checkout.OtherAmountCent, 0)
} }
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement { func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement {
@@ -381,6 +381,20 @@ func TestBuildCheckoutSettlementWithDepositDeduct(t *testing.T) {
} }
} }
func TestBuildCheckoutSettlementFallsBackToLegacyOtherAmount(t *testing.T) {
order := model.RentalOrder{
RentAmountCent: 24000,
OwnerRentAmountCent: 21600,
DepositAmountCent: 5000,
}
checkout := &model.OrderCheckout{OtherAmountCent: 2000}
settlement := buildCheckoutSettlement(order, checkout)
if settlement.DepositCompensationCent != 2000 || settlement.OwnerIncomeCent != 23600 {
t.Fatalf("legacy other amount settlement = compensation %d, owner income %d, want 2000/23600", settlement.DepositCompensationCent, settlement.OwnerIncomeCent)
}
}
// TestBuildCheckoutSettlementWithConsumable 测试额外消耗 // TestBuildCheckoutSettlementWithConsumable 测试额外消耗
func TestBuildCheckoutSettlementWithConsumable(t *testing.T) { func TestBuildCheckoutSettlementWithConsumable(t *testing.T) {
order := model.RentalOrder{ order := model.RentalOrder{
@@ -184,7 +184,7 @@ function openArbitration(row: Dispute) {
activeDispute.value = row activeDispute.value = row
result.value = editableArbitrationResult(row.arbitration_result) result.value = editableArbitrationResult(row.arbitration_result)
remark.value = row.arbitration_remark || '' remark.value = row.arbitration_remark || ''
amount.value = undefined amount.value = centToYuan(row.checkout_deposit_deduct_amount_cent ?? 0)
actualCoinConsumedM.value = row.checkout_coin_consumed_m ?? undefined actualCoinConsumedM.value = row.checkout_coin_consumed_m ?? undefined
actualConsumableAmountYuan.value = centToYuan( actualConsumableAmountYuan.value = centToYuan(
row.checkout_consumable_amount_cent ?? row.extra_item_original_amount_cent ?? 0 row.checkout_consumable_amount_cent ?? row.extra_item_original_amount_cent ?? 0
@@ -31,6 +31,7 @@ export interface Dispute {
checkout_id?: number checkout_id?: number
checkout_coin_consumed_m?: number checkout_coin_consumed_m?: number
checkout_consumable_amount_cent?: number checkout_consumable_amount_cent?: number
checkout_deposit_deduct_amount_cent?: number
previous_checkout_status?: string previous_checkout_status?: string
arbitration_result: string arbitration_result: string
arbitration_remark: string arbitration_remark: string