chore(init): 初始化解忧商行项目并完成 Phase 0 基座
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,477 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/database"
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/adminfinance"
|
||||
"hfb_sys/backend/internal/modules/dispute"
|
||||
"hfb_sys/backend/internal/modules/listing"
|
||||
"hfb_sys/backend/internal/modules/order"
|
||||
"hfb_sys/backend/internal/modules/payment"
|
||||
"hfb_sys/backend/internal/modules/paymentaccount"
|
||||
"hfb_sys/backend/internal/modules/paymentconfig"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
"hfb_sys/backend/internal/modules/withdrawal"
|
||||
"hfb_sys/backend/pkg/crypto"
|
||||
)
|
||||
|
||||
func TestRentalFullFlowWithMockPayment(t *testing.T) {
|
||||
db := openE2EDB(t)
|
||||
|
||||
owner, renter, adminID := seedUsers(t, db)
|
||||
services := newFlowServices(db)
|
||||
seedMockPaymentConfig(t, services.paymentConfig, adminID)
|
||||
|
||||
account := createOwnerPaymentAccount(t, services.paymentAccount, owner.ID)
|
||||
listingDTO := createListingUnderReview(t, services.listing, owner.ID)
|
||||
adjusted := adjustAndApproveListing(t, services.listing, adminID, listingDTO.ID)
|
||||
if adjusted.PriceCent != 38300 {
|
||||
t.Fatalf("调价后商品价格 = %d, want 38300", adjusted.PriceCent)
|
||||
}
|
||||
|
||||
orderDTO := mustCreateOrder(t, services.order, renter.ID, adjusted.ID)
|
||||
paymentDTO, err := services.payment.Start(t.Context(), renter.ID, orderDTO.ID, payment.StartPaymentRequest{}, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("启动 mock 支付失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "支付状态", paymentDTO.Status, "paid")
|
||||
assertEqual(t, "支付金额", paymentDTO.AmountCent, int64(53300))
|
||||
|
||||
paidOrder, err := services.order.FindAdmin(t.Context(), orderDTO.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("读取已支付订单失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "支付后订单状态", paidOrder.Status, "pending_handoff")
|
||||
assertEqual(t, "支付后交接状态", paidOrder.HandoffStatus, "pending_owner")
|
||||
|
||||
if _, err := services.order.SubmitHandoff(t.Context(), owner.ID, orderDTO.ID, order.SubmitHandoffRequest{Content: "账号:demo,密码:demo-pass"}); err != nil {
|
||||
t.Fatalf("号主交接失败: %v", err)
|
||||
}
|
||||
if err := services.order.ConfirmReceive(t.Context(), renter.ID, orderDTO.ID); err != nil {
|
||||
t.Fatalf("租客确认收号失败: %v", err)
|
||||
}
|
||||
|
||||
if _, err := services.order.SubmitCheckout(t.Context(), renter.ID, orderDTO.ID, order.SubmitCheckoutRequest{
|
||||
Content: "租客发起结账",
|
||||
ConsumableAmountCent: 700,
|
||||
CoinConsumedM: 90,
|
||||
}); err != nil {
|
||||
t.Fatalf("租客发起结账失败: %v", err)
|
||||
}
|
||||
countered, err := services.order.CounterCheckout(t.Context(), owner.ID, orderDTO.ID, order.CounterCheckoutRequest{
|
||||
ConsumableAmountCent: 1200,
|
||||
CoinConsumedM: 100,
|
||||
DepositDeductAmountCent: 3000,
|
||||
Reason: "补扣消耗和押金赔付",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("号主反价失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "反价状态", countered.Status, "countered")
|
||||
if err := services.order.AcceptCheckout(t.Context(), renter.ID, orderDTO.ID); err != nil {
|
||||
t.Fatalf("租客接受反价失败: %v", err)
|
||||
}
|
||||
|
||||
completed, err := services.order.FindAdmin(t.Context(), orderDTO.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("读取完成订单失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "完成后订单状态", completed.Status, "completed")
|
||||
assertEqual(t, "完成后结算状态", completed.SettlementStatus, "settled")
|
||||
if completed.Checkout == nil {
|
||||
t.Fatal("完成订单缺少结账记录")
|
||||
}
|
||||
assertEqual(t, "结账状态", completed.Checkout.Status, "accepted")
|
||||
assertEqual(t, "实际结账租金", *completed.Checkout.RentAmountCent, int64(27500))
|
||||
assertEqual(t, "号主结算收入", *completed.Checkout.OwnerIncomeAmountCent, int64(27500))
|
||||
assertEqual(t, "平台收入", *completed.Checkout.PlatformFeeCent, int64(3000))
|
||||
|
||||
assertFinanceDashboard(t, services.finance, orderDTO.OrderNo)
|
||||
assertWalletAndWithdrawal(t, services.wallet, services.withdrawal, owner.ID, adminID, account.ID)
|
||||
}
|
||||
|
||||
func TestArbitrationReleaseDepositDoesNotRepublishListing(t *testing.T) {
|
||||
db := openE2EDB(t)
|
||||
|
||||
owner, renter, adminID := seedUsers(t, db)
|
||||
services := newFlowServices(db)
|
||||
seedMockPaymentConfig(t, services.paymentConfig, adminID)
|
||||
|
||||
listingDTO := createListingUnderReview(t, services.listing, owner.ID)
|
||||
approved := adjustAndApproveListing(t, services.listing, adminID, listingDTO.ID)
|
||||
orderDTO := mustCreateOrder(t, services.order, renter.ID, approved.ID)
|
||||
if _, err := services.payment.Start(t.Context(), renter.ID, orderDTO.ID, payment.StartPaymentRequest{}, "127.0.0.1"); err != nil {
|
||||
t.Fatalf("启动 mock 支付失败: %v", err)
|
||||
}
|
||||
if _, err := services.order.SubmitHandoff(t.Context(), owner.ID, orderDTO.ID, order.SubmitHandoffRequest{Content: "账号:demo,密码:demo-pass"}); err != nil {
|
||||
t.Fatalf("号主交接失败: %v", err)
|
||||
}
|
||||
if err := services.order.ConfirmReceive(t.Context(), renter.ID, orderDTO.ID); err != nil {
|
||||
t.Fatalf("租客确认收号失败: %v", err)
|
||||
}
|
||||
if _, err := services.order.SubmitCheckout(t.Context(), renter.ID, orderDTO.ID, order.SubmitCheckoutRequest{
|
||||
Content: "租客发起结账",
|
||||
ConsumableAmountCent: 700,
|
||||
CoinConsumedM: 90,
|
||||
}); err != nil {
|
||||
t.Fatalf("租客发起结账失败: %v", err)
|
||||
}
|
||||
if _, err := services.order.CounterCheckout(t.Context(), owner.ID, orderDTO.ID, order.CounterCheckoutRequest{
|
||||
ConsumableAmountCent: 1200,
|
||||
CoinConsumedM: 100,
|
||||
DepositDeductAmountCent: 3000,
|
||||
Reason: "补扣消耗和押金赔付",
|
||||
}); err != nil {
|
||||
t.Fatalf("号主反价失败: %v", err)
|
||||
}
|
||||
|
||||
disputeDTO, err := services.dispute.Create(t.Context(), renter.ID, orderDTO.ID, dispute.CreateRequest{
|
||||
Type: "checkout_amount",
|
||||
Description: "不同意扣押金,申请客服仲裁",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("创建结账争议失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "争议类型", disputeDTO.Type, "checkout_dispute")
|
||||
resolved, err := services.dispute.Arbitrate(t.Context(), adminID, disputeDTO.ID, dispute.ArbitrateRequest{
|
||||
Result: "release_deposit",
|
||||
Remark: "确认无押金扣除,退回押金",
|
||||
}, auditlog.Meta{IP: "127.0.0.1", RequestID: "e2e-arbitrate"})
|
||||
if err != nil {
|
||||
t.Fatalf("仲裁失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "争议状态", resolved.Status, "resolved")
|
||||
|
||||
completed, err := services.order.FindAdmin(t.Context(), orderDTO.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("读取仲裁后订单失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "仲裁后订单状态", completed.Status, "completed")
|
||||
assertEqual(t, "仲裁后结算状态", completed.SettlementStatus, "arbitrated")
|
||||
|
||||
var orderRow model.RentalOrder
|
||||
if err := db.First(&orderRow, orderDTO.ID).Error; err != nil {
|
||||
t.Fatalf("读取仲裁后订单模型失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "仲裁退款状态", orderRow.RefundStatus, "refunded")
|
||||
assertEqual(t, "仲裁退款金额", orderRow.RefundAmountCent, int64(25800))
|
||||
|
||||
walletAccount, err := services.wallet.Account(t.Context(), owner.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("读取号主钱包失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "仲裁后号主可用余额", walletAccount.AvailableBalanceCent, int64(24500))
|
||||
assertEqual(
|
||||
t,
|
||||
"仲裁资金守恒",
|
||||
orderRow.RefundAmountCent+walletAccount.AvailableBalanceCent+3000,
|
||||
orderRow.RentAmountCent+orderRow.DepositAmountCent,
|
||||
)
|
||||
|
||||
var listingRow model.RentalListing
|
||||
if err := db.First(&listingRow, approved.ID).Error; err != nil {
|
||||
t.Fatalf("读取仲裁后商品失败: %v", err)
|
||||
}
|
||||
var accountRow model.GameAccount
|
||||
if err := db.First(&accountRow, approved.AccountID).Error; err != nil {
|
||||
t.Fatalf("读取仲裁后账号失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "仲裁后商品状态", listingRow.Status, "offline")
|
||||
assertEqual(t, "仲裁后账号状态", accountRow.Status, "offline")
|
||||
}
|
||||
|
||||
type flowServices struct {
|
||||
listing *listing.Service
|
||||
order *order.Service
|
||||
payment *payment.Service
|
||||
paymentConfig *paymentconfig.Service
|
||||
paymentAccount *paymentaccount.Service
|
||||
dispute *dispute.Service
|
||||
wallet *wallet.Service
|
||||
withdrawal *withdrawal.Service
|
||||
finance *adminfinance.Service
|
||||
}
|
||||
|
||||
func newFlowServices(db *gorm.DB) flowServices {
|
||||
listingRepo := listing.NewRepository(db, nil)
|
||||
walletRepo := wallet.NewRepository(db)
|
||||
configRepo := paymentconfig.NewRepository(db, &paymentconfig.MockEncryptor{})
|
||||
var paymentRepo *payment.Repository
|
||||
orderRepo := order.NewRepository(db, order.Dependencies{
|
||||
RefundStarter: order.RefundStarterFunc(func(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (string, error) {
|
||||
refund, err := paymentRepo.StartRefund(ctx, orderID, refundAmountCent, bizType, remark)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return refund.Status, nil
|
||||
}),
|
||||
})
|
||||
paymentRepo = payment.NewRepository(db, configRepo, orderRepo)
|
||||
disputeRepo := dispute.NewRepository(db, dispute.Dependencies{
|
||||
RefundStarter: dispute.RefundStarterFunc(func(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (string, error) {
|
||||
refund, err := paymentRepo.StartRefund(ctx, orderID, refundAmountCent, bizType, remark)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return refund.Status, nil
|
||||
}),
|
||||
})
|
||||
|
||||
return flowServices{
|
||||
listing: listing.NewService(listingRepo, fixedConfig{"listing.review_required": "true"}),
|
||||
order: order.NewService(orderRepo),
|
||||
payment: payment.NewService(paymentRepo),
|
||||
paymentConfig: paymentconfig.NewService(configRepo),
|
||||
paymentAccount: paymentaccount.NewService(paymentaccount.NewRepository(db, &crypto.MockEncryptor{})),
|
||||
dispute: dispute.NewService(disputeRepo),
|
||||
wallet: wallet.NewService(walletRepo),
|
||||
withdrawal: withdrawal.NewService(withdrawal.NewRepository(db, walletRepo, &crypto.MockEncryptor{})),
|
||||
finance: adminfinance.NewService(adminfinance.NewRepository(db)),
|
||||
}
|
||||
}
|
||||
|
||||
type fixedConfig map[string]string
|
||||
|
||||
func (c fixedConfig) FindValue(ctx context.Context, key string) (string, error) {
|
||||
return c[key], nil
|
||||
}
|
||||
|
||||
func openE2EDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
return database.NewTestDB()
|
||||
}
|
||||
|
||||
func seedUsers(t *testing.T, db *gorm.DB) (model.User, model.User, uint64) {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
ownerName, renterName := "张三", "李四"
|
||||
owner := model.User{Phone: "13900000001", Nickname: "测试号主", RealnameStatus: "verified", RiskStatus: "normal", CreditScore: 100, Status: "active"}
|
||||
renter := model.User{Phone: "13900000002", Nickname: "测试租客", RealnameStatus: "verified", RiskStatus: "normal", CreditScore: 100, Status: "active"}
|
||||
if err := db.Create(&owner).Error; err != nil {
|
||||
t.Fatalf("创建号主失败: %v", err)
|
||||
}
|
||||
if err := db.Create(&renter).Error; err != nil {
|
||||
t.Fatalf("创建租客失败: %v", err)
|
||||
}
|
||||
seedRealname(t, db, owner.ID, ownerName, now)
|
||||
seedRealname(t, db, renter.ID, renterName, now)
|
||||
|
||||
admin := model.AdminUser{Username: "e2e_admin", PasswordHash: "noop", Nickname: "测试管理员", Status: "active"}
|
||||
if err := db.Create(&admin).Error; err != nil {
|
||||
t.Fatalf("创建管理员失败: %v", err)
|
||||
}
|
||||
return owner, renter, admin.ID
|
||||
}
|
||||
|
||||
func seedRealname(t *testing.T, db *gorm.DB, userID uint64, name string, verifiedAt time.Time) {
|
||||
t.Helper()
|
||||
encryptedName, err := (&crypto.MockEncryptor{}).Encrypt(name)
|
||||
if err != nil {
|
||||
t.Fatalf("加密实名姓名失败: %v", err)
|
||||
}
|
||||
row := model.UserRealname{
|
||||
UserID: userID,
|
||||
Provider: "e2e",
|
||||
ProviderOrderNo: fmt.Sprintf("RN%d", userID),
|
||||
Status: "verified",
|
||||
MaskedName: string([]rune(name)[0]) + "*",
|
||||
EncryptedName: encryptedName,
|
||||
MaskedIDNo: "110***********001",
|
||||
VerifiedAt: &verifiedAt,
|
||||
}
|
||||
if err := db.Create(&row).Error; err != nil {
|
||||
t.Fatalf("创建实名记录失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func seedMockPaymentConfig(t *testing.T, service *paymentconfig.Service, adminID uint64) {
|
||||
t.Helper()
|
||||
_, err := service.Create(t.Context(), paymentconfig.CreateRequest{
|
||||
Name: "E2E Mock 支付",
|
||||
Provider: "mock",
|
||||
MerchantID: "mock-e2e",
|
||||
IsDefault: true,
|
||||
Status: "active",
|
||||
Environment: "sandbox",
|
||||
BusinessTags: []string{"order_pay", "wallet_recharge"},
|
||||
}, adminID, auditlog.Meta{IP: "127.0.0.1", RequestID: "e2e-payment-config"})
|
||||
if err != nil {
|
||||
t.Fatalf("创建 mock 支付配置失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func createOwnerPaymentAccount(t *testing.T, service *paymentaccount.Service, ownerID uint64) *paymentaccount.PaymentAccountDTO {
|
||||
t.Helper()
|
||||
account, err := service.Create(t.Context(), ownerID, paymentaccount.CreatePaymentAccountRequest{
|
||||
AccountType: "alipay",
|
||||
AccountName: "张三",
|
||||
AccountNo: "owner@example.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("创建号主收款账号失败: %v", err)
|
||||
}
|
||||
return account
|
||||
}
|
||||
|
||||
func createListingUnderReview(t *testing.T, service *listing.Service, ownerID uint64) *listing.ListingDTO {
|
||||
t.Helper()
|
||||
dto, err := service.Create(t.Context(), ownerID, listing.CreateRequest{
|
||||
Title: "E2E 烽火地带账号",
|
||||
Description: "用于完整链路测试",
|
||||
ServerRegion: "烽火地带",
|
||||
LoginPlatform: "QQ",
|
||||
RankLevel: "钻石",
|
||||
HafCoinAmount: 100000000,
|
||||
PriceCent: 35300,
|
||||
DepositAmountCent: 15000,
|
||||
ScreenshotURLS: []string{"https://example.com/e2e-account.png"},
|
||||
AssetSummary: map[string]any{
|
||||
"fire_level": 50,
|
||||
"online_time": map[string]any{
|
||||
"start": "09:00",
|
||||
"end": "23:00",
|
||||
},
|
||||
"price_breakdown": map[string]any{
|
||||
"seller_coin_base_price": 233,
|
||||
"seller_total_price": 353,
|
||||
"seller_ratio": 42.9,
|
||||
"consumable_price": 120,
|
||||
},
|
||||
},
|
||||
AgreedVirtualAssetSale: true,
|
||||
AgreedSellerAgreement: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("创建待审核商品失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "商品审核状态", dto.ReviewStatus, "pending")
|
||||
return dto
|
||||
}
|
||||
|
||||
func adjustAndApproveListing(t *testing.T, service *listing.Service, adminID uint64, listingID uint64) *listing.ListingDTO {
|
||||
t.Helper()
|
||||
adjusted, err := service.AdjustReviewPrice(t.Context(), adminID, listingID, listing.AdminPriceAdjustRequest{
|
||||
BuyerTotalPriceCent: 38300,
|
||||
Reason: "E2E 审核调价",
|
||||
}, auditlog.Meta{IP: "127.0.0.1", RequestID: "e2e-adjust-price"})
|
||||
if err != nil {
|
||||
t.Fatalf("审核调价失败: %v", err)
|
||||
}
|
||||
approved, err := service.Approve(t.Context(), adjusted.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("审核通过失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "审核通过商品状态", approved.Status, "published")
|
||||
return approved
|
||||
}
|
||||
|
||||
func mustCreateOrder(t *testing.T, service *order.Service, renterID uint64, listingID uint64) *order.OrderDTO {
|
||||
t.Helper()
|
||||
dto, err := service.Create(t.Context(), renterID, order.CreateRequest{ListingID: listingID})
|
||||
if err != nil {
|
||||
t.Fatalf("创建订单失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "新订单状态", dto.Status, "pending_payment")
|
||||
assertEqual(t, "订单租金", *dto.RentAmountCent, int64(38300))
|
||||
assertEqual(t, "订单押金", dto.DepositAmountCent, int64(15000))
|
||||
return dto
|
||||
}
|
||||
|
||||
func assertFinanceDashboard(t *testing.T, service *adminfinance.Service, orderNo string) {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
query := adminfinance.DashboardQuery{
|
||||
StartDate: now.Add(-time.Hour),
|
||||
EndDate: now.Add(time.Hour),
|
||||
}
|
||||
dashboard, err := service.Dashboard(t.Context(), query)
|
||||
if err != nil {
|
||||
t.Fatalf("读取财务看板失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "财务总流水", dashboard.Summary.TotalFlowAmountCent, int64(53300))
|
||||
assertEqual(t, "财务已退款", dashboard.Summary.TotalRefundAmountCent, int64(22800))
|
||||
assertEqual(t, "财务平台收入", dashboard.Summary.PlatformIncomeAmountCent, int64(3000))
|
||||
assertEqual(t, "财务号主应收", dashboard.Summary.OwnerShouldIncomeAmountCent, int64(27500))
|
||||
assertEqual(t, "财务号主钱包入账", dashboard.Summary.OwnerWalletIncomeAmountCent, int64(27500))
|
||||
assertEqual(t, "财务结算订单数", dashboard.Summary.SettledOrderCount, int64(1))
|
||||
|
||||
details, err := service.Details(t.Context(), adminfinance.DetailQuery{
|
||||
OrderNo: orderNo,
|
||||
DateType: "settled",
|
||||
StartDate: query.StartDate,
|
||||
EndDate: query.EndDate,
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("读取财务明细失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "财务明细数量", details.Total, int64(1))
|
||||
items, ok := details.Items.([]adminfinance.FinanceDetailDTO)
|
||||
if !ok || len(items) != 1 {
|
||||
t.Fatalf("财务明细类型异常: %#v", details.Items)
|
||||
}
|
||||
assertEqual(t, "明细订单号", items[0].OrderNo, orderNo)
|
||||
assertEqual(t, "明细平台净收入", items[0].PlatformNetAmountCent, int64(3000))
|
||||
}
|
||||
|
||||
func assertWalletAndWithdrawal(t *testing.T, walletService *wallet.Service, withdrawalService *withdrawal.Service, ownerID uint64, adminID uint64, paymentAccountID uint64) {
|
||||
t.Helper()
|
||||
accountBefore, err := walletService.Account(t.Context(), ownerID)
|
||||
if err != nil {
|
||||
t.Fatalf("读取号主钱包失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "提现前可用余额", accountBefore.AvailableBalanceCent, int64(27500))
|
||||
|
||||
req, err := withdrawalService.Create(t.Context(), ownerID, withdrawal.CreateWithdrawalRequest{
|
||||
PaymentAccountID: paymentAccountID,
|
||||
AmountCent: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("创建提现失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "提现申请金额", req.AmountCent, int64(10000))
|
||||
assertEqual(t, "提现申请状态", req.Status, "pending")
|
||||
|
||||
approved := true
|
||||
reviewed, err := withdrawalService.Review(t.Context(), adminID, req.ID, withdrawal.ReviewWithdrawalRequest{
|
||||
Approved: &approved,
|
||||
Remark: "E2E 审核通过",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("审核提现失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "提现审核后状态", reviewed.Status, "processing")
|
||||
|
||||
paid, err := withdrawalService.ConfirmPayment(t.Context(), adminID, req.ID, withdrawal.ConfirmPaymentRequest{
|
||||
PaymentProofURL: "https://example.com/proof.png",
|
||||
Remark: "E2E 已打款",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("确认提现打款失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "提现完成状态", paid.Status, "completed")
|
||||
assertEqual(t, "提现到账金额", paid.ActualAmountCent, int64(10000))
|
||||
|
||||
accountAfter, err := walletService.Account(t.Context(), ownerID)
|
||||
if err != nil {
|
||||
t.Fatalf("读取提现后钱包失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "提现后可用余额", accountAfter.AvailableBalanceCent, int64(17500))
|
||||
assertEqual(t, "提现后冻结余额", accountAfter.FrozenBalanceCent, int64(0))
|
||||
}
|
||||
|
||||
func assertEqual[T comparable](t *testing.T, name string, got T, want T) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Fatalf("%s = %v, want %v", name, got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user