优化仲裁
This commit is contained in:
@@ -53,7 +53,7 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settlement, err := buildArbitrationSettlement(order, req, frozenBalance)
|
||||
settlement, actualSettlement, linkedCheckout, depositDeductAmountCent, err := buildArbitrationDecision(tx, row, order, req, frozenBalance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -114,23 +114,16 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
||||
return err
|
||||
}
|
||||
if order.Status == "completed" {
|
||||
coinConsumedM, linkedCheckout, err := arbitrationCoinConsumedM(tx, row, req)
|
||||
if err != nil {
|
||||
return err
|
||||
if actualSettlement == nil {
|
||||
return ErrInvalidDispute
|
||||
}
|
||||
pureCoin := ordermodule.CalculateActualPureCoinAmounts(order, coinConsumedM)
|
||||
order.ActualCoinConsumedM = pureCoin.ConsumedM
|
||||
order.ActualPureCoinAmountCent = pureCoin.AmountCent
|
||||
order.ActualPureCoinDiscountCent = pureCoin.DiscountCent
|
||||
applyArbitrationActualSettlement(&order, linkedCheckout, *actualSettlement, depositDeductAmountCent)
|
||||
if linkedCheckout != nil {
|
||||
linkedCheckout.PureCoinAmountCent = pureCoin.AmountCent
|
||||
linkedCheckout.PureCoinDiscountCent = pureCoin.DiscountCent
|
||||
linkedCheckout.PureCoinPayableCent = pureCoin.PayableCent
|
||||
if err := tx.Save(linkedCheckout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := rentergrowth.AwardOrderCompleted(tx, &order, pureCoin.AmountCent, rentergrowth.SourceOrderCompleted); err != nil {
|
||||
if err := rentergrowth.AwardOrderCompleted(tx, &order, actualSettlement.PureCoinAmountCent, rentergrowth.SourceOrderCompleted); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -166,6 +159,7 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
||||
"deposit_deduct_amount_cent": settlement.DepositDeductAmountCent,
|
||||
"actual_coin_consumed_m": order.ActualCoinConsumedM,
|
||||
"actual_pure_coin_cent": order.ActualPureCoinAmountCent,
|
||||
"actual_extra_item_cent": order.ActualExtraItemAmountCent,
|
||||
"before_order_status": beforeOrderStatus,
|
||||
"after_order_status": order.Status,
|
||||
"before_handoff_status": beforeHandoffStatus,
|
||||
@@ -220,22 +214,131 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func arbitrationCoinConsumedM(tx *gorm.DB, row model.Dispute, req ArbitrateRequest) (float64, *model.OrderCheckout, error) {
|
||||
var checkout *model.OrderCheckout
|
||||
if row.CheckoutID != nil && *row.CheckoutID > 0 {
|
||||
var saved model.OrderCheckout
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&saved, *row.CheckoutID).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
checkout = &saved
|
||||
func buildArbitrationDecision(tx *gorm.DB, row model.Dispute, order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, *ordermodule.ActualCheckoutSettlement, *model.OrderCheckout, int64, error) {
|
||||
if arbitrationUsesActualCheckout(req.Result) {
|
||||
return buildActualCheckoutArbitrationSettlement(tx, row, order, req, renterFrozenBalanceCent)
|
||||
}
|
||||
settlement, err := buildArbitrationSettlement(order, req, renterFrozenBalanceCent)
|
||||
return settlement, nil, nil, 0, err
|
||||
}
|
||||
|
||||
func arbitrationUsesActualCheckout(result string) bool {
|
||||
switch result {
|
||||
case "release_deposit", "deduct_deposit", "compensate_owner":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func buildActualCheckoutArbitrationSettlement(tx *gorm.DB, row model.Dispute, order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, *ordermodule.ActualCheckoutSettlement, *model.OrderCheckout, int64, error) {
|
||||
settlement := newArbitrationSettlement(order, renterFrozenBalanceCent)
|
||||
linkedCheckout, err := lockArbitrationCheckout(tx, row)
|
||||
if err != nil {
|
||||
return settlement, nil, nil, 0, err
|
||||
}
|
||||
coinConsumedM, err := arbitrationCoinConsumedM(linkedCheckout, req)
|
||||
if err != nil {
|
||||
return settlement, nil, linkedCheckout, 0, err
|
||||
}
|
||||
consumableAmountCent, err := arbitrationConsumableAmountCent(order, linkedCheckout, req)
|
||||
if err != nil {
|
||||
return settlement, nil, linkedCheckout, 0, err
|
||||
}
|
||||
depositDeductAmountCent, err := arbitrationDepositDeductAmountCent(order, req)
|
||||
if err != nil {
|
||||
return settlement, nil, linkedCheckout, 0, err
|
||||
}
|
||||
actualSettlement := ordermodule.CalculateActualCheckoutSettlement(order, consumableAmountCent, coinConsumedM, depositDeductAmountCent)
|
||||
if actualSettlement.ShortfallCent > 0 {
|
||||
return settlement, nil, linkedCheckout, 0, ErrInvalidDispute
|
||||
}
|
||||
if actualSettlement.OwnerIncomeCent > 0 {
|
||||
settlement.addOwnerIncome(order, actualSettlement.OwnerIncomeCent, "仲裁按实际消耗结算给号主")
|
||||
}
|
||||
if actualSettlement.RenterRefundCent > 0 {
|
||||
settlement.RenterRefundAmountCent = actualSettlement.RenterRefundCent
|
||||
settlement.RenterDepositRefundCent = actualSettlement.DepositRefundCent
|
||||
}
|
||||
settlement.DepositDeductAmountCent = depositDeductAmountCent + actualSettlement.OvershootAmountCent
|
||||
settlement.ActualExtraItemAmountCent = actualSettlement.ExtraItemAmountCent
|
||||
return settlement, &actualSettlement, linkedCheckout, depositDeductAmountCent, nil
|
||||
}
|
||||
|
||||
func lockArbitrationCheckout(tx *gorm.DB, row model.Dispute) (*model.OrderCheckout, error) {
|
||||
if row.CheckoutID == nil || *row.CheckoutID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var checkout model.OrderCheckout
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&checkout, *row.CheckoutID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &checkout, nil
|
||||
}
|
||||
|
||||
func arbitrationCoinConsumedM(checkout *model.OrderCheckout, req ArbitrateRequest) (float64, error) {
|
||||
if req.ActualCoinConsumedM != nil {
|
||||
return *req.ActualCoinConsumedM, checkout, nil
|
||||
return *req.ActualCoinConsumedM, nil
|
||||
}
|
||||
if checkout != nil {
|
||||
return checkout.CoinConsumedM, checkout, nil
|
||||
return checkout.CoinConsumedM, nil
|
||||
}
|
||||
return 0, nil, ErrInvalidDispute
|
||||
return 0, ErrInvalidDispute
|
||||
}
|
||||
|
||||
func arbitrationConsumableAmountCent(order model.RentalOrder, checkout *model.OrderCheckout, req ArbitrateRequest) (int64, error) {
|
||||
if req.ActualConsumableAmountCent != nil {
|
||||
return *req.ActualConsumableAmountCent, nil
|
||||
}
|
||||
if checkout != nil {
|
||||
return checkout.ConsumableAmountCent, nil
|
||||
}
|
||||
if order.ExtraItemOriginalAmountCent <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, ErrInvalidDispute
|
||||
}
|
||||
|
||||
func arbitrationDepositDeductAmountCent(order model.RentalOrder, req ArbitrateRequest) (int64, error) {
|
||||
switch req.Result {
|
||||
case "release_deposit":
|
||||
return 0, nil
|
||||
case "deduct_deposit", "compensate_owner":
|
||||
deductAmountCent := req.AmountCent
|
||||
if deductAmountCent <= 0 {
|
||||
deductAmountCent = order.DepositAmountCent
|
||||
}
|
||||
if deductAmountCent > order.DepositAmountCent {
|
||||
return 0, ErrInvalidDispute
|
||||
}
|
||||
return deductAmountCent, nil
|
||||
default:
|
||||
return 0, ErrInvalidDispute
|
||||
}
|
||||
}
|
||||
|
||||
func applyArbitrationActualSettlement(order *model.RentalOrder, checkout *model.OrderCheckout, settlement ordermodule.ActualCheckoutSettlement, depositDeductAmountCent int64) {
|
||||
order.ActualCoinConsumedM = settlement.CoinConsumedM
|
||||
order.ActualPureCoinAmountCent = settlement.PureCoinAmountCent
|
||||
order.ActualPureCoinDiscountCent = settlement.PureCoinDiscountCent
|
||||
order.ActualExtraItemAmountCent = settlement.ExtraItemAmountCent
|
||||
if checkout == nil {
|
||||
return
|
||||
}
|
||||
checkout.RentAmountCent = settlement.ActualRentAmountCent
|
||||
checkout.OwnerRentAmountCent = settlement.OwnerRentIncomeCent
|
||||
checkout.PlatformFeeCent = settlement.PlatformFeeCent
|
||||
checkout.PureCoinAmountCent = settlement.PureCoinAmountCent
|
||||
checkout.PureCoinDiscountCent = settlement.PureCoinDiscountCent
|
||||
checkout.PureCoinPayableCent = settlement.PureCoinPayableCent
|
||||
checkout.ConsumableAmountCent = settlement.ExtraItemAmountCent
|
||||
checkout.CoinConsumedM = settlement.CoinConsumedM
|
||||
checkout.DepositDeductAmountCent = depositDeductAmountCent
|
||||
checkout.OtherAmountCent = depositDeductAmountCent
|
||||
checkout.RenterRefundAmountCent = settlement.RenterRefundCent
|
||||
checkout.OwnerIncomeAmountCent = settlement.OwnerIncomeCent
|
||||
checkout.ShortfallCent = settlement.ShortfallCent
|
||||
checkout.OvershootAmountCent = settlement.OvershootAmountCent
|
||||
}
|
||||
|
||||
func isPlatformManagedOrder(order model.RentalOrder) bool {
|
||||
@@ -275,24 +378,18 @@ func applyPlatformManagedSettlement(order *model.RentalOrder, settlement *arbitr
|
||||
}
|
||||
|
||||
type arbitrationSettlement struct {
|
||||
Entries []wallet.Entry
|
||||
RenterRefundAmountCent int64
|
||||
RenterDepositRefundCent int64
|
||||
OwnerIncomeAmountCent int64
|
||||
DepositDeductAmountCent int64
|
||||
Entries []wallet.Entry
|
||||
RenterRefundAmountCent int64
|
||||
RenterDepositRefundCent int64
|
||||
OwnerIncomeAmountCent int64
|
||||
DepositDeductAmountCent int64
|
||||
ActualExtraItemAmountCent int64
|
||||
}
|
||||
|
||||
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, error) {
|
||||
rentAmountCent := order.RentAmountCent
|
||||
depositAmountCent := order.DepositAmountCent
|
||||
ownerRentAmountCent := order.OwnerRentAmountCent
|
||||
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
|
||||
ownerRentAmountCent = rentAmountCent
|
||||
}
|
||||
totalCent := rentAmountCent + depositAmountCent
|
||||
func newArbitrationSettlement(order model.RentalOrder, renterFrozenBalanceCent int64) arbitrationSettlement {
|
||||
settlement := arbitrationSettlement{}
|
||||
orderID := order.ID
|
||||
releaseFrozenAmountCent := money.MinCent(totalCent, renterFrozenBalanceCent)
|
||||
releaseFrozenAmountCent := money.MinCent(order.RentAmountCent+order.DepositAmountCent, renterFrozenBalanceCent)
|
||||
if releaseFrozenAmountCent > 0 {
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
@@ -305,6 +402,36 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
|
||||
Remark: "仲裁释放冻结金额",
|
||||
})
|
||||
}
|
||||
return settlement
|
||||
}
|
||||
|
||||
func (settlement *arbitrationSettlement) addOwnerIncome(order model.RentalOrder, amountCent int64, remark string) {
|
||||
if settlement == nil || amountCent <= 0 {
|
||||
return
|
||||
}
|
||||
orderID := order.ID
|
||||
settlement.OwnerIncomeAmountCent += amountCent
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
AmountCent: amountCent,
|
||||
BalanceType: "available",
|
||||
BizType: "arbitration_owner_income",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: remark,
|
||||
})
|
||||
}
|
||||
|
||||
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, error) {
|
||||
rentAmountCent := order.RentAmountCent
|
||||
depositAmountCent := order.DepositAmountCent
|
||||
ownerRentAmountCent := order.OwnerRentAmountCent
|
||||
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
|
||||
ownerRentAmountCent = rentAmountCent
|
||||
}
|
||||
totalCent := rentAmountCent + depositAmountCent
|
||||
settlement := newArbitrationSettlement(order, renterFrozenBalanceCent)
|
||||
|
||||
addRenterRefund := func(amountCent int64, remark string) {
|
||||
if amountCent <= 0 {
|
||||
@@ -316,17 +443,7 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
|
||||
if amountCent <= 0 {
|
||||
return
|
||||
}
|
||||
settlement.OwnerIncomeAmountCent += amountCent
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
AmountCent: amountCent,
|
||||
BalanceType: "available",
|
||||
BizType: "arbitration_owner_income",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: remark,
|
||||
})
|
||||
settlement.addOwnerIncome(order, amountCent, remark)
|
||||
}
|
||||
|
||||
switch req.Result {
|
||||
@@ -503,6 +620,9 @@ func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitration
|
||||
if settlement.DepositDeductAmountCent > 0 {
|
||||
content += "\n押金赔付扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent)
|
||||
}
|
||||
if settlement.ActualExtraItemAmountCent > 0 {
|
||||
content += "\n额外物品消耗:" + money.FormatWithSymbol(settlement.ActualExtraItemAmountCent)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
|
||||
@@ -9,38 +9,40 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
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 {
|
||||
@@ -57,10 +59,11 @@ type AdminCreateRequest struct {
|
||||
}
|
||||
|
||||
type ArbitrateRequest struct {
|
||||
Result string `json:"result" binding:"required"`
|
||||
Remark string `json:"remark" binding:"required"`
|
||||
AmountCent int64 `json:"amount_cent"`
|
||||
ActualCoinConsumedM *float64 `json:"actual_coin_consumed_m"`
|
||||
Result string `json:"result" binding:"required"`
|
||||
Remark string `json:"remark" binding:"required"`
|
||||
AmountCent int64 `json:"amount_cent"`
|
||||
ActualCoinConsumedM *float64 `json:"actual_coin_consumed_m"`
|
||||
ActualConsumableAmountCent *int64 `json:"actual_consumable_amount_cent"`
|
||||
}
|
||||
|
||||
type AdminListQuery struct {
|
||||
|
||||
@@ -10,53 +10,57 @@ import (
|
||||
|
||||
type disputeRow struct {
|
||||
model.Dispute
|
||||
OrderNo string
|
||||
OrderStatus string
|
||||
HandoffStatus string
|
||||
SettlementStatus string
|
||||
ListingNo string
|
||||
Title string
|
||||
OwnerID uint64
|
||||
RenterID uint64
|
||||
OwnerPhone string
|
||||
RenterPhone string
|
||||
CheckoutCoinConsumedM *float64
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
PreviousCheckoutStatus: row.PreviousCheckoutStatus,
|
||||
ArbitrationResult: row.ArbitrationResult,
|
||||
ArbitrationRemark: row.ArbitrationRemark,
|
||||
HandledBy: row.HandledBy,
|
||||
HandledAt: row.HandledAt,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,8 +91,10 @@ func applyUserVisibleFilter(db *gorm.DB, userID uint64) *gorm.DB {
|
||||
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
||||
return r.adminFilterQuery(ctx).
|
||||
Select(`d.*, o.order_no, o.status AS order_status, o.handoff_status, o.settlement_status,
|
||||
o.owner_id, o.renter_id, owner.phone AS owner_phone, renter.phone AS renter_phone,
|
||||
l.listing_no, a.title, c.coin_consumed_m AS checkout_coin_consumed_m`)
|
||||
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`)
|
||||
}
|
||||
|
||||
func (r *Repository) adminFilterQuery(ctx context.Context) *gorm.DB {
|
||||
|
||||
@@ -345,10 +345,21 @@ func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) {
|
||||
}
|
||||
|
||||
actualCoinConsumedM := 50.0
|
||||
if _, err := repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{
|
||||
actualConsumableAmountCent := int64(12000)
|
||||
_, err = repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{
|
||||
Result: "release_deposit",
|
||||
Remark: "确认实际消耗 50M",
|
||||
ActualCoinConsumedM: &actualCoinConsumedM,
|
||||
}, AuditMeta{})
|
||||
if !errors.Is(err, ErrInvalidDispute) {
|
||||
t.Fatalf("缺少实际额外物品金额的仲裁错误 = %v, want ErrInvalidDispute", err)
|
||||
}
|
||||
|
||||
if _, err := repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{
|
||||
Result: "release_deposit",
|
||||
Remark: "确认实际消耗 50M 和额外物品",
|
||||
ActualCoinConsumedM: &actualCoinConsumedM,
|
||||
ActualConsumableAmountCent: &actualConsumableAmountCent,
|
||||
}, AuditMeta{}); err != nil {
|
||||
t.Fatalf("带实际消耗 M 仲裁失败: %v", err)
|
||||
}
|
||||
@@ -356,8 +367,8 @@ func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) {
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("读取订单失败: %v", err)
|
||||
}
|
||||
if saved.ActualCoinConsumedM != 50 || saved.ActualPureCoinAmountCent != 13150 || saved.GrowthPointsAwarded != 131 {
|
||||
t.Fatalf("最终 M/纯币/积分 = %.2f/%d/%d, want 50/13150/131", saved.ActualCoinConsumedM, saved.ActualPureCoinAmountCent, saved.GrowthPointsAwarded)
|
||||
if saved.ActualCoinConsumedM != 50 || saved.ActualPureCoinAmountCent != 13150 || saved.ActualExtraItemAmountCent != 12000 || saved.GrowthPointsAwarded != 131 {
|
||||
t.Fatalf("最终 M/纯币/额外物品/积分 = %.2f/%d/%d/%d, want 50/13150/12000/131", saved.ActualCoinConsumedM, saved.ActualPureCoinAmountCent, saved.ActualExtraItemAmountCent, saved.GrowthPointsAwarded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,10 +383,11 @@ 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",
|
||||
CoinConsumedM: 25,
|
||||
OrderID: order.ID,
|
||||
InitiatedBy: renter.ID,
|
||||
Status: "submitted",
|
||||
ConsumableAmountCent: 7000,
|
||||
CoinConsumedM: 25,
|
||||
}
|
||||
if err := db.Create(&checkout).Error; err != nil {
|
||||
t.Fatalf("创建结账记录失败: %v", err)
|
||||
@@ -387,8 +399,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 {
|
||||
t.Fatalf("关联结账/M = %v/%v, want %d/25", created.CheckoutID, created.CheckoutCoinConsumedM, checkout.ID)
|
||||
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 _, err := repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{
|
||||
@@ -401,8 +413,15 @@ func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) {
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("读取订单失败: %v", err)
|
||||
}
|
||||
if saved.ActualCoinConsumedM != 25 || saved.ActualPureCoinAmountCent != 6575 || saved.GrowthPointsAwarded != 65 {
|
||||
t.Fatalf("最终 M/纯币/积分 = %.2f/%d/%d, want 25/6575/65", saved.ActualCoinConsumedM, saved.ActualPureCoinAmountCent, saved.GrowthPointsAwarded)
|
||||
if saved.ActualCoinConsumedM != 25 || saved.ActualPureCoinAmountCent != 6575 || saved.ActualExtraItemAmountCent != 7000 || saved.GrowthPointsAwarded != 65 {
|
||||
t.Fatalf("最终 M/纯币/额外物品/积分 = %.2f/%d/%d/%d, want 25/6575/7000/65", saved.ActualCoinConsumedM, saved.ActualPureCoinAmountCent, saved.ActualExtraItemAmountCent, saved.GrowthPointsAwarded)
|
||||
}
|
||||
var savedCheckout model.OrderCheckout
|
||||
if err := db.First(&savedCheckout, checkout.ID).Error; err != nil {
|
||||
t.Fatalf("读取结账记录失败: %v", err)
|
||||
}
|
||||
if savedCheckout.ConsumableAmountCent != 7000 || savedCheckout.RentAmountCent != 13509 || savedCheckout.RenterRefundAmountCent != 39528 {
|
||||
t.Fatalf("仲裁后结账消耗品/租金/退款 = %d/%d/%d, want 7000/13509/39528", savedCheckout.ConsumableAmountCent, savedCheckout.RentAmountCent, savedCheckout.RenterRefundAmountCent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,5 +93,8 @@ 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.ActualConsumableAmountCent != nil && (*req.ActualConsumableAmountCent < 0 || *req.ActualConsumableAmountCent > 100_000_000_000) {
|
||||
return nil, ErrInvalidDispute
|
||||
}
|
||||
return s.repo.Arbitrate(ctx, adminID, id, req, meta)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
||||
order.ActualCoinConsumedM = settlement.CoinConsumedM
|
||||
order.ActualPureCoinAmountCent = settlement.PureCoinAmountCent
|
||||
order.ActualPureCoinDiscountCent = settlement.PureCoinDiscountCent
|
||||
order.ActualExtraItemAmountCent = settlement.ExtraItemAmountCent
|
||||
if err := appendCheckoutCompletedNotifications(tx, order, renterContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ type OrderDTO struct {
|
||||
ActualCoinConsumedM float64 `json:"actual_coin_consumed_m,omitempty"`
|
||||
ActualPureCoinAmountCent int64 `json:"actual_pure_coin_amount_cent,omitempty"`
|
||||
ActualPureCoinDiscountCent int64 `json:"actual_pure_coin_discount_cent,omitempty"`
|
||||
ActualExtraItemAmountCent int64 `json:"actual_extra_item_amount_cent,omitempty"`
|
||||
RenterGrowthLevel string `json:"renter_growth_level,omitempty"`
|
||||
RenterGrowthLevelName string `json:"renter_growth_level_name,omitempty"`
|
||||
RenterDiscountBps int `json:"renter_discount_bps,omitempty"`
|
||||
|
||||
@@ -92,6 +92,7 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
||||
ActualCoinConsumedM: row.ActualCoinConsumedM,
|
||||
ActualPureCoinAmountCent: row.ActualPureCoinAmountCent,
|
||||
ActualPureCoinDiscountCent: row.ActualPureCoinDiscountCent,
|
||||
ActualExtraItemAmountCent: row.ActualExtraItemAmountCent,
|
||||
RenterGrowthLevel: effectiveRenterGrowthLevel(row.RentalOrder),
|
||||
RenterGrowthLevelName: effectiveRenterGrowthLevelName(row.RentalOrder),
|
||||
RenterDiscountBps: effectiveRenterDiscountBps(row.RentalOrder),
|
||||
@@ -330,6 +331,7 @@ func applyOrderPriceView(dto *OrderDTO, order model.RentalOrder, userID uint64)
|
||||
dto.ActualCoinConsumedM = 0
|
||||
dto.ActualPureCoinAmountCent = 0
|
||||
dto.ActualPureCoinDiscountCent = 0
|
||||
dto.ActualExtraItemAmountCent = 0
|
||||
dto.RenterGrowthLevel = ""
|
||||
dto.RenterGrowthLevelName = ""
|
||||
dto.RenterDiscountBps = 0
|
||||
@@ -362,6 +364,7 @@ func applyOrderPriceView(dto *OrderDTO, order model.RentalOrder, userID uint64)
|
||||
dto.ActualCoinConsumedM = 0
|
||||
dto.ActualPureCoinAmountCent = 0
|
||||
dto.ActualPureCoinDiscountCent = 0
|
||||
dto.ActualExtraItemAmountCent = 0
|
||||
dto.RenterGrowthLevel = ""
|
||||
dto.RenterGrowthLevelName = ""
|
||||
dto.RenterDiscountBps = 0
|
||||
|
||||
@@ -50,9 +50,28 @@ type ActualPureCoinAmounts struct {
|
||||
ConsumedM float64
|
||||
}
|
||||
|
||||
// ActualCheckoutSettlement 是正常结账与仲裁共用的实际消耗结算结果。
|
||||
type ActualCheckoutSettlement struct {
|
||||
OwnerRentIncomeCent int64
|
||||
DepositCompensationCent int64
|
||||
OwnerIncomeCent int64
|
||||
RentRefundCent int64
|
||||
DepositRefundCent int64
|
||||
RenterRefundCent int64
|
||||
PlatformFeeCent int64
|
||||
ActualRentAmountCent int64
|
||||
PureCoinAmountCent int64
|
||||
PureCoinDiscountCent int64
|
||||
PureCoinPayableCent int64
|
||||
ExtraItemAmountCent int64
|
||||
CoinConsumedM float64
|
||||
OvershootAmountCent int64
|
||||
ShortfallCent int64
|
||||
}
|
||||
|
||||
// CalculateActualPureCoinAmounts 按订单价格快照和实际消耗 M 计算纯币原价及等级优惠。
|
||||
func CalculateActualPureCoinAmounts(order model.RentalOrder, coinConsumedM float64) ActualPureCoinAmounts {
|
||||
settlement := calculateCheckoutSettlement(order, 0, coinConsumedM, 0)
|
||||
settlement := CalculateActualCheckoutSettlement(order, 0, coinConsumedM, 0)
|
||||
return ActualPureCoinAmounts{
|
||||
AmountCent: settlement.PureCoinAmountCent,
|
||||
DiscountCent: settlement.PureCoinDiscountCent,
|
||||
@@ -61,6 +80,31 @@ func CalculateActualPureCoinAmounts(order model.RentalOrder, coinConsumedM float
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateActualCheckoutSettlement 按实际纯币和额外物品消耗计算完整结算。
|
||||
func CalculateActualCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) ActualCheckoutSettlement {
|
||||
return toActualCheckoutSettlement(calculateCheckoutSettlement(order, consumableAmountCent, coinConsumedM, depositDeductAmountCent))
|
||||
}
|
||||
|
||||
func toActualCheckoutSettlement(settlement checkoutSettlement) ActualCheckoutSettlement {
|
||||
return ActualCheckoutSettlement{
|
||||
OwnerRentIncomeCent: settlement.OwnerRentIncomeCent,
|
||||
DepositCompensationCent: settlement.DepositCompensationCent,
|
||||
OwnerIncomeCent: settlement.OwnerIncomeCent,
|
||||
RentRefundCent: settlement.RentRefundCent,
|
||||
DepositRefundCent: settlement.DepositRefundCent,
|
||||
RenterRefundCent: settlement.RenterRefundCent,
|
||||
PlatformFeeCent: settlement.PlatformFeeCent,
|
||||
ActualRentAmountCent: settlement.ActualRentAmountCent,
|
||||
PureCoinAmountCent: settlement.PureCoinAmountCent,
|
||||
PureCoinDiscountCent: settlement.PureCoinDiscountCent,
|
||||
PureCoinPayableCent: settlement.PureCoinPayableCent,
|
||||
ExtraItemAmountCent: settlement.ExtraItemAmountCent,
|
||||
CoinConsumedM: settlement.CoinConsumedM,
|
||||
OvershootAmountCent: settlement.OvershootAmountCent,
|
||||
ShortfallCent: settlement.ShortfallCent,
|
||||
}
|
||||
}
|
||||
|
||||
func orderDurationHours(order model.RentalOrder) int {
|
||||
if order.EstimatedDurationHours > 0 {
|
||||
return order.EstimatedDurationHours
|
||||
|
||||
Reference in New Issue
Block a user