package payment import ( "errors" "testing" "time" "hfb_sys/backend/internal/database" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/paymentconfig" "hfb_sys/backend/internal/modules/qualityowner" ) func TestConfirmQualityOwnerPaymentFreezesDepositAndIsIdempotent(t *testing.T) { db := database.NewTestDB() user := model.User{ Phone: "13900009401", RealnameStatus: "verified", Status: "active", } if err := db.Create(&user).Error; err != nil { t.Fatalf("创建用户失败: %v", err) } if err := db.Create(&model.WalletAccount{ UserID: user.ID, AvailableBalanceCent: 0, Status: "active", }).Error; err != nil { t.Fatalf("创建钱包失败: %v", err) } payment := model.PaymentOrder{ PaymentNo: "PAYQUALITYOWNER01", OrderID: 0, OrderNo: "QOQUALITY", BizID: user.ID, UserID: user.ID, Provider: "mock", ThirdOrderID: "PAYQUALITYOWNER01", AmountCent: 20_000, BizType: qualityOwnerPayBizType, Status: "paying", } if err := db.Create(&payment).Error; err != nil { t.Fatalf("创建支付单失败: %v", err) } repo := NewRepository(db, nil, nil, WithQualityOwnerRepo(qualityowner.NewRepository(db))) raw := map[string]string{"status": "2", "third_order_id": payment.ThirdOrderID} if err := repo.confirmPaid(t.Context(), &payment, "2", time.Now(), raw, channelSourceMock); err != nil { t.Fatalf("确认保证金支付失败: %v", err) } if err := repo.confirmPaid(t.Context(), &payment, "2", time.Now(), raw, channelSourceMock); err != nil { t.Fatalf("重复确认保证金支付失败: %v", err) } var gotUser model.User if err := db.First(&gotUser, user.ID).Error; err != nil { t.Fatalf("读取用户失败: %v", err) } if gotUser.QualityOwnerStatus != qualityowner.StatusActive || gotUser.QualityOwnerDepositCent != 20_000 { t.Fatalf("用户未开通优质号主: %+v", gotUser) } var account model.WalletAccount if err := db.Where("user_id = ? AND distribution_site_id = 0", user.ID).First(&account).Error; err != nil { t.Fatalf("读取钱包失败: %v", err) } if account.AvailableBalanceCent != 0 || account.FrozenBalanceCent != 20_000 { t.Fatalf("保证金钱包余额不正确: %+v", account) } var ledgerCount int64 if err := db.Model(&model.WalletLedger{}). Where("user_id = ? AND biz_type = ?", user.ID, "quality_owner_deposit"). Count(&ledgerCount).Error; err != nil { t.Fatalf("统计保证金流水失败: %v", err) } if ledgerCount != 1 { t.Fatalf("保证金流水条数 = %d, want 1", ledgerCount) } } func TestNewQualityOwnerPaymentUsesBizIDAndMockCashier(t *testing.T) { payment, err := newQualityOwnerPayment(88, 20_000, StartPaymentRequest{PayWay: "WXZF"}, runtimePaymentConfig{ Provider: "mock", MerchantID: "mock-merchant", PayWay: "WXZF", JSPayFlag: "2", }) if err != nil { t.Fatalf("newQualityOwnerPayment() error = %v", err) } if payment.BizType != qualityOwnerPayBizType || payment.BizID != 88 || payment.UserID != 88 { t.Fatalf("保证金支付单业务字段不正确: %+v", payment) } if payment.OrderID != 0 || payment.AmountCent != 20_000 { t.Fatalf("保证金支付单订单/金额不正确: %+v", payment) } if payment.TDCode == "" || payment.ProviderOrderID == "" { t.Fatalf("mock 支付单未生成收银台: %+v", payment) } } func TestCanReuseQualityOwnerPaymentRejectsPaidActivation(t *testing.T) { config := runtimePaymentConfig{ Provider: "leshua", MerchantID: "merchant-1", PayWay: "ZFBZF", } paid := model.PaymentOrder{ Status: "paid", Provider: "leshua", MerchantID: "merchant-1", PayWay: "ZFBZF", } if canReuseQualityOwnerPayment(paid, config) { t.Fatal("已完成的上轮保证金支付单不应被复用") } paying := paid paying.Status = "paying" if !canReuseQualityOwnerPayment(paying, config) { t.Fatal("同一配置下尚未完成的支付单应继续复用") } } func TestQualityOwnerCanReactivateAfterCancelAndOldPaymentCannotReactivate(t *testing.T) { db := setupPaymentTestDB(t) user := model.User{ Phone: "13900009402", RealnameStatus: "verified", Status: "active", } if err := db.Create(&user).Error; err != nil { t.Fatalf("创建用户失败: %v", err) } if err := db.Create(&model.WalletAccount{ UserID: user.ID, Status: "active", }).Error; err != nil { t.Fatalf("创建钱包失败: %v", err) } config := model.PaymentMerchantConfig{ Name: "优质号主模拟支付", Provider: "mock", MerchantID: "QUALITY_OWNER_MOCK", PayWay: "ZFBZF", IsDefault: true, Status: "active", Environment: "sandbox", } if err := db.Create(&config).Error; err != nil { t.Fatalf("创建支付配置失败: %v", err) } qualityOwnerRepo := qualityowner.NewRepository(db) paymentRepo := NewRepository( db, paymentconfig.NewRepository(db, &paymentconfig.MockEncryptor{}), nil, WithQualityOwnerRepo(qualityOwnerRepo), ) first, err := paymentRepo.StartQualityOwner(t.Context(), user.ID, StartPaymentRequest{PayWay: "ZFBZF"}, "") if err != nil { t.Fatalf("首次在线开通失败: %v", err) } if !first.Paid { t.Fatalf("首次支付状态 = %q, want paid", first.Status) } status, err := qualityOwnerRepo.Status(t.Context(), user.ID) if err != nil { t.Fatalf("读取首次开通状态失败: %v", err) } if !status.Active { t.Fatal("首次支付后应开通优质号主") } status, err = qualityOwnerRepo.Cancel(t.Context(), user.ID, qualityowner.AuditMeta{}) if err != nil { t.Fatalf("取消优质号主失败: %v", err) } if status.Active { t.Fatal("取消后不应保持优质号主身份") } var firstPayment model.PaymentOrder if err := db.First(&firstPayment, first.ID).Error; err != nil { t.Fatalf("读取首次支付单失败: %v", err) } if err := paymentRepo.confirmPaid( t.Context(), &firstPayment, "2", time.Now(), map[string]string{"status": "2", "third_order_id": firstPayment.ThirdOrderID}, channelSourceMock, ); err != nil { t.Fatalf("重放旧支付回调失败: %v", err) } status, err = qualityOwnerRepo.Status(t.Context(), user.ID) if err != nil { t.Fatalf("读取旧回调后状态失败: %v", err) } if status.Active { t.Fatal("取消后重放旧支付回调不应重新开通优质号主") } second, err := paymentRepo.StartQualityOwner(t.Context(), user.ID, StartPaymentRequest{PayWay: "ZFBZF"}, "") if err != nil { t.Fatalf("取消后再次在线开通失败: %v", err) } if !second.Paid { t.Fatalf("第二次支付状态 = %q, want paid", second.Status) } if second.ID == first.ID || second.PaymentNo == first.PaymentNo { t.Fatalf("第二次开通复用了旧支付单: first=%+v second=%+v", first, second) } status, err = qualityOwnerRepo.Status(t.Context(), user.ID) if err != nil { t.Fatalf("读取第二次开通状态失败: %v", err) } if !status.Active || status.DepositSource != qualityowner.DepositSourcePaid { t.Fatalf("第二次在线支付后状态不正确: %+v", status) } var paymentCount int64 if err := db.Model(&model.PaymentOrder{}). Where("user_id = ? AND biz_type = ?", user.ID, qualityOwnerPayBizType). Count(&paymentCount).Error; err != nil { t.Fatalf("统计保证金支付单失败: %v", err) } if paymentCount != 2 { t.Fatalf("保证金支付单数量 = %d, want 2", paymentCount) } var duplicateFrozenCount int64 if err := db.Model(&model.WalletLedger{}). Where( "user_id = ? AND biz_type = ? AND biz_no = ? AND direction = ? AND balance_type = ?", user.ID, "quality_owner_deposit", first.PaymentNo, "in", "frozen", ). Count(&duplicateFrozenCount).Error; err != nil { t.Fatalf("统计首次保证金冻结流水失败: %v", err) } if duplicateFrozenCount != 1 { t.Fatalf("首次支付冻结流水数量 = %d, want 1", duplicateFrozenCount) } } func TestConfirmQualityOwnerPaymentRejectsEmptyPaymentNo(t *testing.T) { db := setupPaymentTestDB(t) repo := NewRepository(db, nil, nil, WithQualityOwnerRepo(qualityowner.NewRepository(db))) payment := model.PaymentOrder{ BizType: qualityOwnerPayBizType, UserID: 1, AmountCent: 20_000, } err := repo.confirmPaid(t.Context(), &payment, "2", time.Now(), nil, channelSourceMock) if !errors.Is(err, qualityowner.ErrInvalidUser) { t.Fatalf("空支付单号 error = %v, want quality owner invalid user", err) } }