diff --git a/backend/internal/modules/dispute/arbitration.go b/backend/internal/modules/dispute/arbitration.go index 158171e..0f5a6ac 100644 --- a/backend/internal/modules/dispute/arbitration.go +++ b/backend/internal/modules/dispute/arbitration.go @@ -153,7 +153,7 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r "order_no": order.OrderNo, "result": req.Result, "remark": req.Remark, - "input_amount_cent": req.AmountCent, + "input_amount_cent": arbitrationInputAmountCent(req), "renter_refund_amount_cent": settlement.RenterRefundAmountCent, "owner_income_amount_cent": settlement.OwnerIncomeAmountCent, "deposit_deduct_amount_cent": settlement.DepositDeductAmountCent, @@ -245,7 +245,7 @@ func buildActualCheckoutArbitrationSettlement(tx *gorm.DB, row model.Dispute, or if err != nil { return settlement, nil, linkedCheckout, 0, err } - depositDeductAmountCent, err := arbitrationDepositDeductAmountCent(order, req) + depositDeductAmountCent, err := arbitrationDepositDeductAmountCent(order, linkedCheckout, req) if err != nil { return settlement, nil, linkedCheckout, 0, err } @@ -299,21 +299,24 @@ func arbitrationConsumableAmountCent(order model.RentalOrder, checkout *model.Or 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 { case "actual_settlement": - if req.AmountCent <= 0 { + if req.AmountCent == nil { + if checkout != nil { + return arbitrationCheckoutDepositDeductCent(*checkout), nil + } return 0, nil } - if req.AmountCent > order.DepositAmountCent { + if *req.AmountCent < 0 || *req.AmountCent > order.DepositAmountCent { return 0, ErrInvalidDispute } - return req.AmountCent, nil + return *req.AmountCent, nil case "release_deposit": return 0, nil case "deduct_deposit", "compensate_owner": - deductAmountCent := req.AmountCent - if deductAmountCent <= 0 { + deductAmountCent := arbitrationInputAmountCent(req) + if req.AmountCent == nil || deductAmountCent <= 0 { 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) { order.ActualCoinConsumedM = settlement.CoinConsumedM order.ActualPureCoinAmountCent = settlement.PureCoinAmountCent @@ -350,7 +370,7 @@ func applyArbitrationActualSettlement(order *model.RentalOrder, checkout *model. } 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 { @@ -459,15 +479,15 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r addRenterRefund(totalCent, "仲裁全额退款") settlement.RenterDepositRefundCent = depositAmountCent case "partial_refund": - if req.AmountCent <= 0 || req.AmountCent > totalCent { + if req.AmountCent == nil || *req.AmountCent <= 0 || *req.AmountCent > totalCent { return settlement, ErrInvalidDispute } - addRenterRefund(req.AmountCent, "仲裁部分退款") + addRenterRefund(*req.AmountCent, "仲裁部分退款") // 部分退款为合并金额,无法精确拆分租金/押金,按押金优先归类以便暂扣。 - settlement.RenterDepositRefundCent = money.MinCent(req.AmountCent, depositAmountCent) + settlement.RenterDepositRefundCent = money.MinCent(*req.AmountCent, depositAmountCent) // 号主仅拿「未退租金中的号主份额 + 未退押金」,平台加价按未退租金比例预留,避免整笔剩余进号主。 addOwnerIncome( - partialRefundOwnerIncomeCent(rentAmountCent, ownerRentAmountCent, depositAmountCent, req.AmountCent), + partialRefundOwnerIncomeCent(rentAmountCent, ownerRentAmountCent, depositAmountCent, *req.AmountCent), "仲裁剩余金额结算给号主", ) case "release_deposit": @@ -475,8 +495,8 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r addRenterRefund(depositAmountCent, "仲裁释放押金给租客") settlement.RenterDepositRefundCent = depositAmountCent case "deduct_deposit", "compensate_owner": - deductAmountCent := req.AmountCent - if deductAmountCent <= 0 { + deductAmountCent := arbitrationInputAmountCent(req) + if req.AmountCent == nil || deductAmountCent <= 0 { deductAmountCent = depositAmountCent } if deductAmountCent > depositAmountCent { diff --git a/backend/internal/modules/dispute/dto.go b/backend/internal/modules/dispute/dto.go index d57364c..272ea54 100644 --- a/backend/internal/modules/dispute/dto.go +++ b/backend/internal/modules/dispute/dto.go @@ -9,40 +9,41 @@ import ( ) type DisputeDTO struct { - ID uint64 `json:"id"` - OrderID uint64 `json:"order_id"` - OrderNo string `json:"order_no"` - OrderStatus string `json:"order_status"` - HandoffStatus string `json:"handoff_status"` - SettlementStatus string `json:"settlement_status"` - ExtraItemOriginalAmountCent int64 `json:"extra_item_original_amount_cent,omitempty"` - ListingNo string `json:"listing_no"` - Title string `json:"title"` - OwnerID uint64 `json:"owner_id"` - RenterID uint64 `json:"renter_id"` - OwnerPhone string `json:"owner_phone"` - RenterPhone string `json:"renter_phone"` - InitiatorID uint64 `json:"initiator_id"` - InitiatorType string `json:"initiator_type"` - InitiatorAdminID *uint64 `json:"initiator_admin_id,omitempty"` - TargetUserID uint64 `json:"target_user_id"` - Type string `json:"type"` - Status string `json:"status"` - Description string `json:"description"` - EvidenceURLS datatypes.JSON `json:"evidence_urls"` - PreviousOrderStatus string `json:"previous_order_status"` - PreviousHandoffStatus string `json:"previous_handoff_status"` - PreviousSettlementStatus string `json:"previous_settlement_status"` - CheckoutID *uint64 `json:"checkout_id"` - CheckoutCoinConsumedM *float64 `json:"checkout_coin_consumed_m,omitempty"` - CheckoutConsumableAmountCent *int64 `json:"checkout_consumable_amount_cent,omitempty"` - PreviousCheckoutStatus string `json:"previous_checkout_status"` - ArbitrationResult string `json:"arbitration_result"` - ArbitrationRemark string `json:"arbitration_remark"` - HandledBy *uint64 `json:"handled_by"` - HandledAt *time.Time `json:"handled_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint64 `json:"id"` + OrderID uint64 `json:"order_id"` + OrderNo string `json:"order_no"` + OrderStatus string `json:"order_status"` + HandoffStatus string `json:"handoff_status"` + SettlementStatus string `json:"settlement_status"` + ExtraItemOriginalAmountCent int64 `json:"extra_item_original_amount_cent,omitempty"` + ListingNo string `json:"listing_no"` + Title string `json:"title"` + OwnerID uint64 `json:"owner_id"` + RenterID uint64 `json:"renter_id"` + OwnerPhone string `json:"owner_phone"` + RenterPhone string `json:"renter_phone"` + InitiatorID uint64 `json:"initiator_id"` + InitiatorType string `json:"initiator_type"` + InitiatorAdminID *uint64 `json:"initiator_admin_id,omitempty"` + TargetUserID uint64 `json:"target_user_id"` + Type string `json:"type"` + Status string `json:"status"` + Description string `json:"description"` + EvidenceURLS datatypes.JSON `json:"evidence_urls"` + PreviousOrderStatus string `json:"previous_order_status"` + PreviousHandoffStatus string `json:"previous_handoff_status"` + PreviousSettlementStatus string `json:"previous_settlement_status"` + CheckoutID *uint64 `json:"checkout_id"` + CheckoutCoinConsumedM *float64 `json:"checkout_coin_consumed_m,omitempty"` + CheckoutConsumableAmountCent *int64 `json:"checkout_consumable_amount_cent,omitempty"` + CheckoutDepositDeductAmountCent *int64 `json:"checkout_deposit_deduct_amount_cent,omitempty"` + PreviousCheckoutStatus string `json:"previous_checkout_status"` + ArbitrationResult string `json:"arbitration_result"` + ArbitrationRemark string `json:"arbitration_remark"` + HandledBy *uint64 `json:"handled_by"` + HandledAt *time.Time `json:"handled_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CreateRequest struct { @@ -61,7 +62,7 @@ type AdminCreateRequest struct { type ArbitrateRequest struct { Result string `json:"result" 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"` ActualConsumableAmountCent *int64 `json:"actual_consumable_amount_cent"` } diff --git a/backend/internal/modules/dispute/presenter.go b/backend/internal/modules/dispute/presenter.go index 1740fc5..18069df 100644 --- a/backend/internal/modules/dispute/presenter.go +++ b/backend/internal/modules/dispute/presenter.go @@ -10,57 +10,59 @@ import ( type disputeRow struct { model.Dispute - OrderNo string - OrderStatus string - HandoffStatus string - SettlementStatus string - ExtraItemOriginalAmountCent int64 - ListingNo string - Title string - OwnerID uint64 - RenterID uint64 - OwnerPhone string - RenterPhone string - CheckoutCoinConsumedM *float64 - CheckoutConsumableAmountCent *int64 + OrderNo string + OrderStatus string + HandoffStatus string + SettlementStatus string + ExtraItemOriginalAmountCent int64 + ListingNo string + Title string + OwnerID uint64 + RenterID uint64 + OwnerPhone string + RenterPhone string + CheckoutCoinConsumedM *float64 + CheckoutConsumableAmountCent *int64 + CheckoutDepositDeductAmountCent *int64 } func (row disputeRow) toDTO() DisputeDTO { return DisputeDTO{ - ID: row.ID, - OrderID: row.OrderID, - OrderNo: row.OrderNo, - OrderStatus: row.OrderStatus, - HandoffStatus: row.HandoffStatus, - SettlementStatus: row.SettlementStatus, - ExtraItemOriginalAmountCent: row.ExtraItemOriginalAmountCent, - ListingNo: row.ListingNo, - Title: row.Title, - OwnerID: row.OwnerID, - RenterID: row.RenterID, - OwnerPhone: row.OwnerPhone, - RenterPhone: row.RenterPhone, - InitiatorID: row.InitiatorID, - InitiatorType: effectiveInitiatorType(row.Dispute), - InitiatorAdminID: row.InitiatorAdminID, - TargetUserID: row.TargetUserID, - Type: row.Type, - Status: row.Status, - Description: row.Description, - EvidenceURLS: row.EvidenceURLS, - PreviousOrderStatus: row.PreviousOrderStatus, - PreviousHandoffStatus: row.PreviousHandoffStatus, - PreviousSettlementStatus: row.PreviousSettlementStatus, - CheckoutID: row.CheckoutID, - CheckoutCoinConsumedM: row.CheckoutCoinConsumedM, - CheckoutConsumableAmountCent: row.CheckoutConsumableAmountCent, - PreviousCheckoutStatus: row.PreviousCheckoutStatus, - ArbitrationResult: row.ArbitrationResult, - ArbitrationRemark: row.ArbitrationRemark, - HandledBy: row.HandledBy, - HandledAt: row.HandledAt, - CreatedAt: row.CreatedAt, - UpdatedAt: row.UpdatedAt, + ID: row.ID, + OrderID: row.OrderID, + OrderNo: row.OrderNo, + OrderStatus: row.OrderStatus, + HandoffStatus: row.HandoffStatus, + SettlementStatus: row.SettlementStatus, + ExtraItemOriginalAmountCent: row.ExtraItemOriginalAmountCent, + ListingNo: row.ListingNo, + Title: row.Title, + OwnerID: row.OwnerID, + RenterID: row.RenterID, + OwnerPhone: row.OwnerPhone, + RenterPhone: row.RenterPhone, + InitiatorID: row.InitiatorID, + InitiatorType: effectiveInitiatorType(row.Dispute), + InitiatorAdminID: row.InitiatorAdminID, + TargetUserID: row.TargetUserID, + Type: row.Type, + Status: row.Status, + Description: row.Description, + EvidenceURLS: row.EvidenceURLS, + PreviousOrderStatus: row.PreviousOrderStatus, + PreviousHandoffStatus: row.PreviousHandoffStatus, + PreviousSettlementStatus: row.PreviousSettlementStatus, + CheckoutID: row.CheckoutID, + CheckoutCoinConsumedM: row.CheckoutCoinConsumedM, + CheckoutConsumableAmountCent: row.CheckoutConsumableAmountCent, + CheckoutDepositDeductAmountCent: row.CheckoutDepositDeductAmountCent, + PreviousCheckoutStatus: row.PreviousCheckoutStatus, + ArbitrationResult: row.ArbitrationResult, + ArbitrationRemark: row.ArbitrationRemark, + HandledBy: row.HandledBy, + HandledAt: row.HandledAt, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, } } diff --git a/backend/internal/modules/dispute/query.go b/backend/internal/modules/dispute/query.go index 6d86f91..f18a2ac 100644 --- a/backend/internal/modules/dispute/query.go +++ b/backend/internal/modules/dispute/query.go @@ -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, owner.phone AS owner_phone, renter.phone AS renter_phone, 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 { diff --git a/backend/internal/modules/dispute/repository_test.go b/backend/internal/modules/dispute/repository_test.go index 68fad02..a6772e2 100644 --- a/backend/internal/modules/dispute/repository_test.go +++ b/backend/internal/modules/dispute/repository_test.go @@ -14,6 +14,8 @@ import ( "gorm.io/gorm" ) +func int64Ptr(value int64) *int64 { return &value } + func disputePureCoinOrder() model.RentalOrder { return model.RentalOrder{ 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) { db := setupDisputeTestDB(t) 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{ Result: "actual_settlement", Remark: "赔付号主", - AmountCent: compensationCent, + AmountCent: &compensationCent, ActualCoinConsumedM: &actualCoinConsumedM, ActualConsumableAmountCent: &actualConsumableAmountCent, }, 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) { db := setupDisputeTestDB(t) 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" _, renter, order := createDisputeOrderFixture(t, db, inputOrder) checkout := model.OrderCheckout{ - OrderID: order.ID, - InitiatedBy: renter.ID, - Status: "submitted", - ConsumableAmountCent: 7000, - CoinConsumedM: 25, + OrderID: order.ID, + InitiatedBy: renter.ID, + Status: "submitted", + ConsumableAmountCent: 7000, + CoinConsumedM: 25, + DepositDeductAmountCent: 3000, } if err := db.Create(&checkout).Error; err != nil { t.Fatalf("创建结账记录失败: %v", err) @@ -433,8 +509,8 @@ func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) { if err != nil { t.Fatalf("创建结账争议失败: %v", err) } - if created.CheckoutID == nil || *created.CheckoutID != checkout.ID || created.CheckoutCoinConsumedM == nil || *created.CheckoutCoinConsumedM != 25 || created.CheckoutConsumableAmountCent == nil || *created.CheckoutConsumableAmountCent != 7000 { - t.Fatalf("关联结账/M/额外物品 = %v/%v/%v, want %d/25/7000", created.CheckoutID, created.CheckoutCoinConsumedM, created.CheckoutConsumableAmountCent, checkout.ID) + 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/%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{ @@ -566,7 +642,7 @@ func TestPartialRefundReservesPlatformFee(t *testing.T) { settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{ Result: "partial_refund", - AmountCent: 5000, + AmountCent: int64Ptr(5000), Remark: "没打完 号主登录不上", }, 0) if err != nil { @@ -599,7 +675,7 @@ func TestPartialRefundFullRentRetainedGivesOwnerFullOwnerRent(t *testing.T) { } settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{ Result: "partial_refund", - AmountCent: 20000, + AmountCent: int64Ptr(20000), }, 0) if err != nil { t.Fatalf("buildArbitrationSettlement() error = %v", err) diff --git a/backend/internal/modules/dispute/service.go b/backend/internal/modules/dispute/service.go index 099c8d5..819e1a8 100644 --- a/backend/internal/modules/dispute/service.go +++ b/backend/internal/modules/dispute/service.go @@ -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) { 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) { return nil, ErrInvalidDispute } diff --git a/backend/internal/modules/order/presenter.go b/backend/internal/modules/order/presenter.go index 0282517..2abe4b7 100644 --- a/backend/internal/modules/order/presenter.go +++ b/backend/internal/modules/order/presenter.go @@ -289,7 +289,7 @@ func refreshCheckoutSettlementDTO(dto *CheckoutDTO, order model.RentalOrder, che if checkout.Status != checkoutStatusSubmitted && checkout.Status != checkoutStatusCountered { return } - settlement := calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, checkout.DepositDeductAmountCent) + settlement := calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, effectiveCheckoutDepositDeductCent(checkout)) rent := settlement.ActualRentAmountCent ownerRent := settlement.OwnerRentIncomeCent platform := settlement.PlatformFeeCent diff --git a/backend/internal/modules/order/pricing.go b/backend/internal/modules/order/pricing.go index 47c6569..e7424c2 100644 --- a/backend/internal/modules/order/pricing.go +++ b/backend/internal/modules/order/pricing.go @@ -278,7 +278,16 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c } 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 { diff --git a/backend/internal/modules/order/repository_integration_test.go b/backend/internal/modules/order/repository_integration_test.go index 0dfdea4..5d2dd05 100644 --- a/backend/internal/modules/order/repository_integration_test.go +++ b/backend/internal/modules/order/repository_integration_test.go @@ -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 测试额外消耗 func TestBuildCheckoutSettlementWithConsumable(t *testing.T) { order := model.RentalOrder{ diff --git a/frontend/src/features/admin/views/AdminDisputesView.vue b/frontend/src/features/admin/views/AdminDisputesView.vue index e279011..11d5d86 100644 --- a/frontend/src/features/admin/views/AdminDisputesView.vue +++ b/frontend/src/features/admin/views/AdminDisputesView.vue @@ -184,7 +184,7 @@ function openArbitration(row: Dispute) { activeDispute.value = row result.value = editableArbitrationResult(row.arbitration_result) 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 actualConsumableAmountYuan.value = centToYuan( row.checkout_consumable_amount_cent ?? row.extra_item_original_amount_cent ?? 0 diff --git a/frontend/src/features/disputes/api/disputes.ts b/frontend/src/features/disputes/api/disputes.ts index 67913fb..018d11a 100644 --- a/frontend/src/features/disputes/api/disputes.ts +++ b/frontend/src/features/disputes/api/disputes.ts @@ -31,6 +31,7 @@ export interface Dispute { checkout_id?: number checkout_coin_consumed_m?: number checkout_consumable_amount_cent?: number + checkout_deposit_deduct_amount_cent?: number previous_checkout_status?: string arbitration_result: string arbitration_remark: string