Files

164 lines
6.2 KiB
Go

package payment
import (
"errors"
"testing"
"time"
"hfb_jysh/backend/internal/model"
ordermodule "hfb_jysh/backend/internal/modules/order"
)
// TestConfirmPaidSettlesPaymentWhenOrderAlreadyCancelled 钉住本次事故的根因:
// 渠道回调带着「已支付」到达,而订单已经被取消。原先订单侧报 ErrOrderCannotPay 会把
// 整笔事务回滚,支付单停在 paying 且 paid_at 为空,于是钱收了却既补不回入账
// (订单不再是待支付),也退不出去——后台人工退款要求原支付单是已支付。
// 现在必须如实把支付单记成已支付,人工退款才有的可退。
func TestConfirmPaidSettlesPaymentWhenOrderAlreadyCancelled(t *testing.T) {
db := setupPaymentTestDB(t)
repo := NewRepository(db, nil, ordermodule.NewRepository(db))
fixture := createPayableOrderFixture(t, db, "LP01", true)
if err := db.Model(&model.RentalOrder{}).Where("id = ?", fixture.Order.ID).
Updates(map[string]any{"status": "cancelled", "handoff_status": "cancelled"}).Error; err != nil {
t.Fatalf("cancel order failed: %v", err)
}
paidAt := time.Date(2026, 9, 15, 23, 22, 47, 0, time.UTC)
if err := repo.confirmPaid(t.Context(), &fixture.Payment, "paid", paidAt, map[string]string{
"provider_order_id": "PROVIDERLP01",
}, channelSourceNotify); err != nil {
t.Fatalf("confirmPaid() error = %v, want nil so the channel stops retrying", err)
}
var payment model.PaymentOrder
if err := db.First(&payment, fixture.Payment.ID).Error; err != nil {
t.Fatalf("find payment failed: %v", err)
}
if payment.Status != "paid" {
t.Fatalf("payment status = %q, want paid", payment.Status)
}
if payment.PaidAt == nil {
t.Fatal("payment paid_at = nil, want the channel pay time")
}
if payment.ProviderOrderID != "PROVIDERLP01" {
t.Fatalf("provider order id = %q, want PROVIDERLP01", payment.ProviderOrderID)
}
var order model.RentalOrder
if err := db.First(&order, fixture.Order.ID).Error; err != nil {
t.Fatalf("find order failed: %v", err)
}
if order.Status != "cancelled" {
t.Fatalf("order status = %q, want cancelled (payment must not revive it)", order.Status)
}
}
// TestConfirmPaidDoesNotAutoRefundUnpayableOrder 退款一律由客服人工发起,
// 入账路径不能自己动钱。
func TestConfirmPaidDoesNotAutoRefundUnpayableOrder(t *testing.T) {
db := setupPaymentTestDB(t)
repo := NewRepository(db, nil, ordermodule.NewRepository(db))
fixture := createPayableOrderFixture(t, db, "LP02", true)
if err := db.Model(&model.RentalOrder{}).Where("id = ?", fixture.Order.ID).
Update("status", "closed").Error; err != nil {
t.Fatalf("close order failed: %v", err)
}
if err := repo.confirmPaid(t.Context(), &fixture.Payment, "paid", time.Now(), map[string]string{
"provider_order_id": "PROVIDERLP02",
}, channelSourceNotify); err != nil {
t.Fatalf("confirmPaid() error = %v", err)
}
var refundCount int64
if err := db.Model(&model.PaymentOrder{}).
Where("order_id = ? AND biz_type <> ?", fixture.Order.ID, "order_pay").
Count(&refundCount).Error; err != nil {
t.Fatalf("count refunds failed: %v", err)
}
if refundCount != 0 {
t.Fatalf("refund payment count = %d, want 0 (refund must stay manual)", refundCount)
}
var order model.RentalOrder
if err := db.First(&order, fixture.Order.ID).Error; err != nil {
t.Fatalf("find order failed: %v", err)
}
if order.RefundStatus == "refunded" || order.RefundStatus == "refunding" {
t.Fatalf("order refund status = %q, want untouched", order.RefundStatus)
}
}
// TestConfirmPaidSettlementIsIdempotent 乐刷会重试十次同一个回调,重复落账不能出错。
func TestConfirmPaidSettlementIsIdempotent(t *testing.T) {
db := setupPaymentTestDB(t)
repo := NewRepository(db, nil, ordermodule.NewRepository(db))
fixture := createPayableOrderFixture(t, db, "LP03", true)
if err := db.Model(&model.RentalOrder{}).Where("id = ?", fixture.Order.ID).
Update("status", "cancelled").Error; err != nil {
t.Fatalf("cancel order failed: %v", err)
}
paidAt := time.Date(2026, 9, 15, 23, 22, 47, 0, time.UTC)
for i := 0; i < 3; i++ {
if err := repo.confirmPaid(t.Context(), &fixture.Payment, "paid", paidAt.Add(time.Duration(i)*time.Minute), map[string]string{
"provider_order_id": "PROVIDERLP03",
}, channelSourceNotify); err != nil {
t.Fatalf("confirmPaid() attempt %d error = %v", i+1, err)
}
}
var payment model.PaymentOrder
if err := db.First(&payment, fixture.Payment.ID).Error; err != nil {
t.Fatalf("find payment failed: %v", err)
}
if payment.Status != "paid" {
t.Fatalf("payment status = %q, want paid", payment.Status)
}
// 首次落账的时间必须保留,后续重试不能把 paid_at 往后推。
if payment.PaidAt == nil || !payment.PaidAt.UTC().Equal(paidAt) {
t.Fatalf("payment paid_at = %v, want %v", payment.PaidAt, paidAt)
}
}
// TestConfirmPaidKeepsErrorForUnrelatedFailures 只有「订单再也无法入账」才走落账。
// 其他错误仍要原样抛出,否则会把真正的故障当成已处理而静默掉。
func TestConfirmPaidKeepsErrorForUnrelatedFailures(t *testing.T) {
db := setupPaymentTestDB(t)
repo := NewRepository(db, nil, nil)
fixture := createPayableOrderFixture(t, db, "LP04", true)
err := repo.confirmPaid(t.Context(), &fixture.Payment, "paid", time.Now(), map[string]string{}, channelSourceNotify)
if !errors.Is(err, ErrDependencyUnavailable) {
t.Fatalf("confirmPaid() error = %v, want ErrDependencyUnavailable", err)
}
var payment model.PaymentOrder
if findErr := db.First(&payment, fixture.Payment.ID).Error; findErr != nil {
t.Fatalf("find payment failed: %v", findErr)
}
if payment.Status != "paying" {
t.Fatalf("payment status = %q, want paying", payment.Status)
}
}
func TestIsOrderUnpayable(t *testing.T) {
cases := []struct {
name string
err error
want bool
}{
{"订单已取消或已关单", ordermodule.ErrOrderCannotPay, true},
{"商品已被他人租走", ordermodule.ErrListingUnavailable, true},
{"依赖缺失", ErrDependencyUnavailable, false},
{"无错误", nil, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := isOrderUnpayable(tc.err); got != tc.want {
t.Fatalf("isOrderUnpayable(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}