fix(distribution): 人工批次窗口期防退款穿透与退款中途金额单调性加固
This commit is contained in:
@@ -415,6 +415,15 @@ func (r *Repository) markRefundFailedOrderStatus(ctx context.Context, orderID ui
|
||||
return r.markOrderRefundFailed(ctx, orderID, refundAmountCent)
|
||||
}
|
||||
|
||||
func refundedTotalTx(tx *gorm.DB, orderID uint64) (int64, error) {
|
||||
var refundedTotal int64
|
||||
err := tx.Model(&model.PaymentOrder{}).
|
||||
Where("order_id = ? AND status = ?", orderID, "refunded").
|
||||
Where("biz_type IN ?", refundBizTypes).
|
||||
Select("COALESCE(SUM(amount_cent), 0)").Scan(&refundedTotal).Error
|
||||
return refundedTotal, err
|
||||
}
|
||||
|
||||
func (r *Repository) updateOrderRefundStatus(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
@@ -424,11 +433,8 @@ func (r *Repository) updateOrderRefundStatus(ctx context.Context, orderID uint64
|
||||
// 同一订单可能多笔退款,且退款结果查询会重复回调:
|
||||
// 以已退款成功流水合计为口径,并取本次金额与累计额的较大值,
|
||||
// 保证 order.RefundAmountCent 单调不回退、重复回调幂等。
|
||||
var refundedTotal int64
|
||||
if err := tx.Model(&model.PaymentOrder{}).
|
||||
Where("order_id = ? AND status = ?", orderID, "refunded").
|
||||
Where("biz_type IN ?", refundBizTypes).
|
||||
Select("COALESCE(SUM(amount_cent), 0)").Scan(&refundedTotal).Error; err != nil {
|
||||
refundedTotal, err := refundedTotalTx(tx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cumulative := max(refundAmountCent, refundedTotal)
|
||||
@@ -448,18 +454,31 @@ func (r *Repository) updateOrderRefundStatus(ctx context.Context, orderID uint64
|
||||
})
|
||||
}
|
||||
func (r *Repository) markOrderRefunding(ctx context.Context, orderID uint64, refundAmountCent int64) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
|
||||
"refund_status": "refunding",
|
||||
"refund_amount_cent": refundAmountCent,
|
||||
"refunded_at": nil,
|
||||
}).Error
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
refundedTotal, err := refundedTotalTx(tx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cumulative := max(refundAmountCent, refundedTotal)
|
||||
return tx.Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
|
||||
"refund_status": "refunding",
|
||||
"refund_amount_cent": cumulative,
|
||||
"refunded_at": nil,
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
func (r *Repository) markOrderRefundFailed(ctx context.Context, orderID uint64, refundAmountCent int64) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
|
||||
"refund_status": "failed",
|
||||
"refund_amount_cent": refundAmountCent,
|
||||
"refunded_at": nil,
|
||||
}).Error
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
refundedTotal, err := refundedTotalTx(tx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
|
||||
"refund_status": "failed",
|
||||
"refund_amount_cent": refundedTotal,
|
||||
"refunded_at": nil,
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
func (r *Repository) updateRefundRawRequest(ctx context.Context, paymentID uint64, rawRequest map[string]string) error {
|
||||
if rawRequest == nil {
|
||||
|
||||
@@ -94,3 +94,50 @@ func TestUpdateOrderRefundStatusUsesCumulativeRefundedAmount(t *testing.T) {
|
||||
t.Fatalf("重复回调后 order.RefundAmountCent = %d, want 1000(单调不回退)", saved.RefundAmountCent)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMarkOrderRefundingAndFailedPreserveCumulativeRefund 断言已成功退款的订单
|
||||
// 发生后续退款中或退款失败时,累计退款额不会被单笔处理金额冲减缩小。
|
||||
func TestMarkOrderRefundingAndFailedPreserveCumulativeRefund(t *testing.T) {
|
||||
db := setupPaymentTestDB(t)
|
||||
repo := NewRepository(db, nil, nil)
|
||||
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-REF-PRESERVE", ListingID: 1, AccountID: 1, OwnerID: 1, RenterID: 2,
|
||||
Status: "completed", RentAmountCent: 10000, OwnerRentAmountCent: 6000,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 先前已成功退款 5000。
|
||||
prevRefund := model.PaymentOrder{
|
||||
PaymentNo: "REF-PREV-1", OrderID: order.ID, OrderNo: order.OrderNo, UserID: order.RenterID,
|
||||
Provider: "mock", ThirdOrderID: "REF-PREV-1", AmountCent: 5000, BizType: "admin_refund", Status: "refunded",
|
||||
}
|
||||
if err := db.Create(&prevRefund).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 此时发起了另一笔 2000 的退款中:累计额应为 max(2000, 5000) = 5000。
|
||||
if err := repo.markOrderRefunding(t.Context(), order.ID, 2000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.RefundStatus != "refunding" || saved.RefundAmountCent != 5000 {
|
||||
t.Fatalf("markOrderRefunding 后状态 = %s/%d, want refunding/5000", saved.RefundStatus, saved.RefundAmountCent)
|
||||
}
|
||||
|
||||
// 该笔 2000 退款最终失败:累计退款额还原为已成功流水合计 5000,而不是单笔 2000。
|
||||
if err := repo.markOrderRefundFailed(t.Context(), order.ID, 2000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.RefundStatus != "failed" || saved.RefundAmountCent != 5000 {
|
||||
t.Fatalf("markOrderRefundFailed 后状态 = %s/%d, want failed/5000", saved.RefundStatus, saved.RefundAmountCent)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user