package payment import ( "errors" "testing" "time" "hfb_sys/backend/internal/model" ordermodule "hfb_sys/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) } }) } }