// Package mysqltest provides real wallet-service MySQL repositories for tests. package mysqltest import ( "context" "database/sql" "fmt" "strings" "testing" "time" "hyapp/internal/testutil/mysqlschema" "hyapp/services/wallet-service/internal/domain/ledger" mysqlstorage "hyapp/services/wallet-service/internal/storage/mysql" ) // Repository wraps the production MySQL wallet repository with test seed helpers. type Repository struct { *mysqlstorage.Repository t testing.TB schema *mysqlschema.Schema } type VipRewardResourceSeed struct { ResourceID int64 ResourceCode string ResourceType string Name string GrantStrategy string SortOrder int } // CountRows returns a simple count from a known wallet table for persistence assertions. func (r *Repository) CountRows(table string, where string, args ...any) int { r.t.Helper() query := "SELECT COUNT(*) FROM " + validateTableName(table) if where != "" { query += " WHERE " + where } var count int if err := r.schema.DB.QueryRowContext(context.Background(), query, args...).Scan(&count); err != nil { r.t.Fatalf("count %s rows failed: %v\nquery=%s", table, err, query) } return count } // SetRechargeProductCode overwrites product_code to model legacy rows created before code matched product_name. func (r *Repository) SetRechargeProductCode(productID int64, productCode string) { r.t.Helper() if _, err := r.schema.DB.ExecContext(context.Background(), ` UPDATE wallet_recharge_products SET product_code = ? WHERE product_id = ?`, productCode, productID, ); err != nil { r.t.Fatalf("set recharge product code failed: %v", err) } } func (r *Repository) ActiveResourceEntitlement(userID int64, resourceID int64) (expiresAtMS int64, quantity int64, remainingQuantity int64) { r.t.Helper() err := r.schema.DB.QueryRowContext(context.Background(), ` SELECT expires_at_ms, quantity, remaining_quantity FROM user_resource_entitlements WHERE user_id = ? AND resource_id = ? AND status = 'active' ORDER BY expires_at_ms DESC, created_at_ms DESC LIMIT 1 `, userID, resourceID).Scan(&expiresAtMS, &quantity, &remainingQuantity) if err != nil { r.t.Fatalf("query active resource entitlement failed: %v", err) } return expiresAtMS, quantity, remainingQuantity } // ExpireEntitlement moves a granted entitlement into the past for expiry-path assertions. func (r *Repository) ExpireEntitlement(entitlementID string, expiresAtMS int64) { r.t.Helper() if _, err := r.schema.DB.ExecContext(context.Background(), ` UPDATE user_resource_entitlements SET expires_at_ms = ?, updated_at_ms = ? WHERE entitlement_id = ?`, expiresAtMS, time.Now().UnixMilli(), entitlementID, ); err != nil { r.t.Fatalf("expire entitlement failed: %v", err) } } // SetResourceGrantStatus lets service tests model legacy or abnormal grant rows without bypassing production grant creation. func (r *Repository) SetResourceGrantStatus(grantID string, status string) { r.t.Helper() if _, err := r.schema.DB.ExecContext(context.Background(), ` UPDATE resource_grants SET status = ?, updated_at_ms = ? WHERE grant_id = ?`, status, time.Now().UnixMilli(), grantID, ); err != nil { r.t.Fatalf("set resource grant status failed: %v", err) } } // HostSalarySettlementIDs returns the persisted settlement IDs for one host/cycle in creation order. func (r *Repository) HostSalarySettlementIDs(userID int64, cycleKey string) []string { r.t.Helper() rows, err := r.schema.DB.QueryContext(context.Background(), ` SELECT settlement_id FROM host_salary_settlement_records WHERE user_id = ? AND cycle_key = ? ORDER BY created_at_ms ASC, settlement_id ASC `, userID, cycleKey) if err != nil { r.t.Fatalf("query host salary settlement ids failed: %v", err) } defer rows.Close() var ids []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { r.t.Fatalf("scan host salary settlement id failed: %v", err) } ids = append(ids, id) } if err := rows.Err(); err != nil { r.t.Fatalf("iterate host salary settlement ids failed: %v", err) } return ids } // MoveHostPeriodCycle merges a real gift-created current-cycle account into another cycle for month-boundary tests. func (r *Repository) MoveHostPeriodCycle(userID int64, fromCycle string, toCycle string) { r.t.Helper() if fromCycle == "" || toCycle == "" || fromCycle == toCycle { return } tx, err := r.schema.DB.BeginTx(context.Background(), nil) if err != nil { r.t.Fatalf("begin move host period cycle failed: %v", err) } defer func() { _ = tx.Rollback() }() var sourceTotal int64 var sourceGiftTotal int64 var sourceLastGiftAt int64 var sourceUpdatedAt int64 err = tx.QueryRowContext(context.Background(), ` SELECT total_diamonds, gift_diamond_total, last_gift_at_ms, updated_at_ms FROM host_period_diamond_accounts WHERE user_id = ? AND cycle_key = ? FOR UPDATE `, userID, fromCycle).Scan(&sourceTotal, &sourceGiftTotal, &sourceLastGiftAt, &sourceUpdatedAt) if err != nil { r.t.Fatalf("read source host period cycle failed: %v", err) } var targetTotal int64 err = tx.QueryRowContext(context.Background(), ` SELECT total_diamonds FROM host_period_diamond_accounts WHERE user_id = ? AND cycle_key = ? FOR UPDATE `, userID, toCycle).Scan(&targetTotal) if err != nil { if err == sql.ErrNoRows { if _, err := tx.ExecContext(context.Background(), ` UPDATE host_period_diamond_accounts SET cycle_key = ? WHERE user_id = ? AND cycle_key = ? `, toCycle, userID, fromCycle); err != nil { r.t.Fatalf("move first host period account failed: %v", err) } } else { r.t.Fatalf("read target host period cycle failed: %v", err) } } else { if _, err := tx.ExecContext(context.Background(), ` UPDATE host_period_diamond_accounts SET total_diamonds = total_diamonds + ?, gift_diamond_total = gift_diamond_total + ?, version = version + 1, last_gift_at_ms = ?, updated_at_ms = ? WHERE user_id = ? AND cycle_key = ? `, sourceTotal, sourceGiftTotal, sourceLastGiftAt, sourceUpdatedAt, userID, toCycle); err != nil { r.t.Fatalf("merge target host period account failed: %v", err) } if _, err := tx.ExecContext(context.Background(), ` DELETE FROM host_period_diamond_accounts WHERE user_id = ? AND cycle_key = ? `, userID, fromCycle); err != nil { r.t.Fatalf("delete merged source host period account failed: %v", err) } } if _, err := tx.ExecContext(context.Background(), ` UPDATE host_period_diamond_entries SET cycle_key = ? WHERE user_id = ? AND cycle_key = ? `, toCycle, userID, fromCycle); err != nil { r.t.Fatalf("move host period entries failed: %v", err) } if err := tx.Commit(); err != nil { r.t.Fatalf("commit move host period cycle failed: %v", err) } } // HostPeriodGiftDiamondTotal returns the current accumulated gift diamonds for a host. func (r *Repository) HostPeriodGiftDiamondTotal(userID int64) int64 { r.t.Helper() var total int64 err := r.schema.DB.QueryRowContext(context.Background(), ` SELECT COALESCE(SUM(gift_diamond_total), 0) FROM host_period_diamond_accounts WHERE user_id = ?`, userID).Scan(&total) if err != nil { r.t.Fatalf("query host period gift diamond total failed: %v", err) } return total } // InsertRawGiftPrice allows tests to seed inactive or future prices without service helpers. func (r *Repository) InsertRawGiftPrice(giftID string, priceVersion string, status string, effectiveAtMs int64) { r.t.Helper() nowMs := time.Now().UnixMilli() _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO wallet_gift_prices ( gift_id, price_version, status, coin_price, gift_point_amount, heat_value, effective_at_ms, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, 1, 1, 1, ?, ?, ?) `, giftID, priceVersion, status, effectiveAtMs, nowMs, nowMs) if err != nil { r.t.Fatalf("insert raw gift price failed: %v", err) } } // NewRepository creates an isolated wallet schema and opens the production repository. func NewRepository(t testing.TB) *Repository { t.Helper() schema := mysqlschema.New(t, mysqlschema.Config{ EnvVar: "WALLET_SERVICE_MYSQL_TEST_DSN", InitDBPath: initDBPath(t), DatabasePrefix: "hyapp_wallet_test", }) repository, err := mysqlstorage.Open(context.Background(), schema.DSN) if err != nil { t.Fatalf("open wallet mysql repository failed: %v", err) } wrapped := &Repository{Repository: repository, t: t, schema: schema} t.Cleanup(wrapped.Close) return wrapped } // Close shuts down the repository; mysqlschema owns schema cleanup. func (r *Repository) Close() { r.t.Helper() if r.Repository != nil { _ = r.Repository.Close() } } // SetBalance prepares a sender COIN account for debit tests. func (r *Repository) SetBalance(userID int64, balance int64) { r.SetAssetBalance(userID, "COIN", balance) } func (r *Repository) SeedVIPRewardGroup(level int32, groupID int64, resources []VipRewardResourceSeed) { r.t.Helper() nowMs := time.Now().UnixMilli() _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO resource_groups ( group_id, group_code, name, status, description, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, 'active', '', 0, 0, 0, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), updated_at_ms = VALUES(updated_at_ms) `, groupID, fmt.Sprintf("vip_test_group_%d", groupID), fmt.Sprintf("VIP test group %d", groupID), nowMs, nowMs) if err != nil { r.t.Fatalf("seed vip reward group failed: %v", err) } if _, err := r.schema.DB.ExecContext(context.Background(), `DELETE FROM resource_group_items WHERE group_id = ?`, groupID); err != nil { r.t.Fatalf("clear vip reward group items failed: %v", err) } for index, resource := range resources { sortOrder := resource.SortOrder if sortOrder == 0 { sortOrder = (index + 1) * 10 } name := resource.Name if name == "" { name = resource.ResourceCode } _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO resources ( resource_id, resource_code, resource_type, name, status, grantable, manager_grant_enabled, grant_strategy, wallet_asset_type, wallet_asset_amount, price_type, coin_price, gift_point_amount, usage_scope_json, asset_url, preview_url, animation_url, metadata_json, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, 'active', TRUE, TRUE, ?, '', 0, '', 0, 0, JSON_ARRAY(), '', '', '', NULL, ?, 0, 0, ?, ?) ON DUPLICATE KEY UPDATE resource_type = VALUES(resource_type), name = VALUES(name), status = VALUES(status), grant_strategy = VALUES(grant_strategy), sort_order = VALUES(sort_order), updated_at_ms = VALUES(updated_at_ms) `, resource.ResourceID, resource.ResourceCode, resource.ResourceType, name, resource.GrantStrategy, sortOrder, nowMs, nowMs) if err != nil { r.t.Fatalf("seed vip reward resource failed: %v", err) } _, err = r.schema.DB.ExecContext(context.Background(), ` INSERT INTO resource_group_items ( group_id, item_type, resource_id, wallet_asset_type, wallet_asset_amount, quantity, duration_ms, sort_order, created_at_ms, updated_at_ms ) VALUES (?, 'resource', ?, '', 0, 1, 0, ?, ?, ?) `, groupID, resource.ResourceID, sortOrder, nowMs, nowMs) if err != nil { r.t.Fatalf("seed vip reward group item failed: %v", err) } } if _, err := r.schema.DB.ExecContext(context.Background(), ` UPDATE vip_levels SET reward_resource_group_id = ?, updated_at_ms = ? WHERE level = ? `, groupID, nowMs, level); err != nil { r.t.Fatalf("update vip level reward group failed: %v", err) } } // SetAssetBalance prepares a sender account for debit tests. func (r *Repository) SetAssetBalance(userID int64, assetType string, balance int64) { r.t.Helper() nowMs := time.Now().UnixMilli() _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO wallet_accounts (user_id, asset_type, available_amount, frozen_amount, version, created_at_ms, updated_at_ms) VALUES (?, ?, ?, 0, 1, ?, ?) ON DUPLICATE KEY UPDATE available_amount = VALUES(available_amount), frozen_amount = VALUES(frozen_amount), version = version + 1, updated_at_ms = VALUES(updated_at_ms) `, userID, assetType, balance, nowMs, nowMs) if err != nil { r.t.Fatalf("seed wallet balance failed: %v", err) } } // SetCoinSellerSalaryExchangeRateTier seeds an active salary transfer tier for coin seller transfer tests. func (r *Repository) SetCoinSellerSalaryExchangeRateTier(appCode string, regionID int64, minUSDMinor int64, maxUSDMinor int64, coinPerUSD int64) { r.t.Helper() nowMs := time.Now().UnixMilli() _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO coin_seller_salary_exchange_rate_tiers ( app_code, region_id, min_usd_minor, max_usd_minor, coin_per_usd, status, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, 'active', 10, 0, 0, ?, ?) ON DUPLICATE KEY UPDATE coin_per_usd = VALUES(coin_per_usd), status = VALUES(status), updated_at_ms = VALUES(updated_at_ms) `, appCode, regionID, minUSDMinor, maxUSDMinor, coinPerUSD, nowMs, nowMs) if err != nil { r.t.Fatalf("seed coin seller salary exchange rate tier failed: %v", err) } } // CreateH5RechargeProduct 通过生产仓储写入 H5 档位,测试只提供角色、区域、美元微单位和金币数。 func (r *Repository) CreateH5RechargeProduct(audienceType string, regionID int64, amountMicro int64, coinAmount int64) ledger.RechargeProduct { r.t.Helper() product, err := r.CreateRechargeProduct(context.Background(), ledger.RechargeProductCommand{ AppCode: "lalu", AudienceType: audienceType, AmountMicro: amountMicro, CoinAmount: coinAmount, ProductName: fmt.Sprintf("H5 %s %d", audienceType, coinAmount), Platform: ledger.RechargeProductPlatformWeb, RegionIDs: []int64{regionID}, Enabled: true, OperatorUserID: 90001, }) if err != nil { r.t.Fatalf("create h5 recharge product failed: %v", err) } return product } // ThirdPartyPaymentMethodID 返回 initdb/迁移种子里的真实 MiFaPay method_id,用于 H5 下单和跨国家拒绝测试。 func (r *Repository) ThirdPartyPaymentMethodID(countryCode string, payWay string, payType string) int64 { return r.ThirdPartyProviderPaymentMethodID(ledger.PaymentProviderMifaPay, countryCode, payWay, payType) } // ThirdPartyProviderPaymentMethodID 返回指定三方 provider 的真实 method_id,覆盖 V5Pay 和 MiFaPay 共用的支付方式模型。 func (r *Repository) ThirdPartyProviderPaymentMethodID(providerCode string, countryCode string, payWay string, payType string) int64 { r.t.Helper() var methodID int64 err := r.schema.DB.QueryRowContext(context.Background(), ` SELECT method_id FROM third_party_payment_methods WHERE app_code = 'lalu' AND provider_code = ? AND country_code = ? AND pay_way = ? AND pay_type = ? LIMIT 1`, providerCode, countryCode, payWay, payType, ).Scan(&methodID) if err != nil { r.t.Fatalf("query third party payment method %s/%s/%s/%s failed: %v", providerCode, countryCode, payWay, payType, err) } return methodID } // SetThirdPartyPaymentRate updates the production payment method row so service tests can lock provider amount semantics. func (r *Repository) SetThirdPartyPaymentRate(methodID int64, rate string) { r.t.Helper() if _, err := r.schema.DB.ExecContext(context.Background(), ` UPDATE third_party_payment_methods SET usd_to_currency_rate = ?, updated_at_ms = ? WHERE method_id = ?`, strings.TrimSpace(rate), time.Now().UnixMilli(), methodID, ); err != nil { r.t.Fatalf("set third party payment rate failed: %v", err) } } // SetGiftPrice overrides or inserts a gift settlement price for server-side billing tests. func (r *Repository) SetGiftPrice(giftID string, priceVersion string, coinPrice int64, giftPointAmount int64, heatValue int64) { r.t.Helper() nowMs := time.Now().UnixMilli() // DebitGift 真实链路先校验 active gift_config,再读取 wallet_gift_prices;测试 seed 必须同时补齐资源、礼物配置和区域关系。 result, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO resources ( resource_code, resource_type, name, status, grantable, manager_grant_enabled, grant_strategy, price_type, coin_price, gift_point_amount, usage_scope_json, created_at_ms, updated_at_ms ) VALUES (?, 'gift', ?, 'active', TRUE, TRUE, 'new_entitlement', 'coin', ?, ?, '[]', ?, ?) ON DUPLICATE KEY UPDATE resource_id = LAST_INSERT_ID(resource_id), status = VALUES(status), coin_price = VALUES(coin_price), gift_point_amount = VALUES(gift_point_amount), updated_at_ms = VALUES(updated_at_ms) `, "test-gift-"+giftID, giftID, coinPrice, giftPointAmount, nowMs, nowMs) if err != nil { r.t.Fatalf("seed gift resource failed: %v", err) } resourceID, err := result.LastInsertId() if err != nil { r.t.Fatalf("read gift resource id failed: %v", err) } if _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO gift_configs ( gift_id, resource_id, status, name, sort_order, presentation_json, gift_type_code, effective_from_ms, effective_to_ms, effect_types_json, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms ) VALUES (?, ?, 'active', ?, 0, JSON_OBJECT(), 'normal', 0, 0, JSON_ARRAY(), 0, 0, ?, ?) ON DUPLICATE KEY UPDATE resource_id = VALUES(resource_id), status = VALUES(status), name = VALUES(name), gift_type_code = VALUES(gift_type_code), effective_from_ms = VALUES(effective_from_ms), effective_to_ms = VALUES(effective_to_ms), updated_at_ms = VALUES(updated_at_ms) `, giftID, resourceID, giftID, nowMs, nowMs); err != nil { r.t.Fatalf("seed gift config failed: %v", err) } if _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO gift_config_regions (gift_id, region_id, created_at_ms, updated_at_ms) VALUES (?, 0, ?, ?) ON DUPLICATE KEY UPDATE updated_at_ms = VALUES(updated_at_ms) `, giftID, nowMs, nowMs); err != nil { r.t.Fatalf("seed gift config region failed: %v", err) } _, err = r.schema.DB.ExecContext(context.Background(), ` INSERT INTO wallet_gift_prices ( gift_id, price_version, status, coin_price, gift_point_amount, heat_value, effective_at_ms, created_at_ms, updated_at_ms ) VALUES (?, ?, 'active', ?, ?, ?, 0, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), coin_price = VALUES(coin_price), gift_point_amount = VALUES(gift_point_amount), heat_value = VALUES(heat_value), updated_at_ms = VALUES(updated_at_ms) `, giftID, priceVersion, coinPrice, giftPointAmount, heatValue, nowMs, nowMs) if err != nil { r.t.Fatalf("seed wallet gift price failed: %v", err) } } // SetGiftType updates a seeded gift config to the requested gift type. func (r *Repository) SetGiftType(giftID string, giftTypeCode string) { r.t.Helper() _, err := r.schema.DB.ExecContext(context.Background(), ` UPDATE gift_configs SET gift_type_code = ?, updated_at_ms = ? WHERE gift_id = ?`, giftTypeCode, time.Now().UnixMilli(), giftID) if err != nil { r.t.Fatalf("set gift type failed: %v", err) } } // SetGiftDiamondRatio configures the gift diamond ratio for a room region and gift type. func (r *Repository) SetGiftDiamondRatio(regionID int64, giftTypeCode string, ratioPercent string) { r.t.Helper() nowMs := time.Now().UnixMilli() _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO gift_diamond_ratio_configs ( region_id, gift_type_code, status, ratio_percent, coin_return_ratio_percent, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms ) VALUES (?, ?, 'active', ?, ?, 0, 0, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), ratio_percent = VALUES(ratio_percent), updated_at_ms = VALUES(updated_at_ms)`, regionID, giftTypeCode, ratioPercent, defaultGiftReturnCoinRatioPercent(giftTypeCode), nowMs, nowMs) if err != nil { r.t.Fatalf("set gift diamond ratio failed: %v", err) } } // SetGiftReturnCoinRatio configures the receiver COIN return ratio without changing host-period diamond rules. func (r *Repository) SetGiftReturnCoinRatio(regionID int64, giftTypeCode string, ratioPercent string) { r.t.Helper() nowMs := time.Now().UnixMilli() _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO gift_diamond_ratio_configs ( region_id, gift_type_code, status, ratio_percent, coin_return_ratio_percent, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms ) VALUES (?, ?, 'active', ?, ?, 0, 0, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), coin_return_ratio_percent = VALUES(coin_return_ratio_percent), updated_at_ms = VALUES(updated_at_ms)`, regionID, giftTypeCode, defaultGiftDiamondRatioPercent(giftTypeCode), ratioPercent, nowMs, nowMs) if err != nil { r.t.Fatalf("set gift return coin ratio failed: %v", err) } } func defaultGiftDiamondRatioPercent(giftTypeCode string) string { switch strings.TrimSpace(giftTypeCode) { case "lucky": return "10.00" case "super_lucky": return "1.00" default: return "100.00" } } func defaultGiftReturnCoinRatioPercent(giftTypeCode string) string { switch strings.TrimSpace(giftTypeCode) { case "lucky": return "10.00" case "super_lucky": return "1.00" default: return "30.00" } } // SetRechargePolicy 配置区域充值定价,用于外部充值类事实测试。 func (r *Repository) SetRechargePolicy(regionID int64, policyVersion string, coinAmount int64, usdMinorAmount int64) { r.t.Helper() nowMs := time.Now().UnixMilli() _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO wallet_recharge_policies ( region_id, policy_version, status, currency_code, coin_amount, usd_minor_amount, effective_from_ms, created_at_ms, updated_at_ms ) VALUES (?, ?, 'active', 'USD', ?, ?, 0, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), currency_code = VALUES(currency_code), coin_amount = VALUES(coin_amount), usd_minor_amount = VALUES(usd_minor_amount), updated_at_ms = VALUES(updated_at_ms) `, regionID, policyVersion, coinAmount, usdMinorAmount, nowMs, nowMs) if err != nil { r.t.Fatalf("seed wallet recharge policy failed: %v", err) } } // SetRechargeStats prepares cumulative recharge stats for recharge-related wallet tests. func (r *Repository) SetRechargeStats(userID int64, totalCoinAmount int64) { r.t.Helper() nowMs := time.Now().UnixMilli() _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO wallet_user_recharge_stats ( user_id, recharge_count, first_transaction_id, first_recharged_at_ms, total_coin_amount, total_usd_minor_amount, created_at_ms, updated_at_ms ) VALUES (?, 1, 'test_recharge', ?, ?, 0, ?, ?) ON DUPLICATE KEY UPDATE total_coin_amount = VALUES(total_coin_amount), updated_at_ms = VALUES(updated_at_ms) `, userID, nowMs, totalCoinAmount, nowMs, nowMs) if err != nil { r.t.Fatalf("seed wallet recharge stats failed: %v", err) } } // SetVIPLevelRechargeThreshold writes a legacy VIP threshold directly, so tests can verify old rows no longer affect purchase eligibility. func (r *Repository) SetVIPLevelRechargeThreshold(level int32, totalCoinAmount int64) { r.t.Helper() nowMs := time.Now().UnixMilli() if _, err := r.schema.DB.ExecContext(context.Background(), ` UPDATE vip_levels SET required_recharge_coin_amount = ?, updated_at_ms = ? WHERE level = ?`, totalCoinAmount, nowMs, level, ); err != nil { r.t.Fatalf("set vip level recharge threshold failed: %v", err) } } // SetHostSalaryPolicy seeds the wallet runtime policy snapshot used by salary settlement tests. func (r *Repository) SetHostSalaryPolicy(policy ledger.HostSalaryPolicy) { r.t.Helper() nowMs := time.Now().UnixMilli() if policy.PolicyID == 0 { policy.PolicyID = 1 } if policy.Status == "" { policy.Status = "active" } if policy.SettlementMode == "" { policy.SettlementMode = ledger.HostSalarySettlementModeDaily } if policy.SettlementTriggerMode == "" { // 测试默认覆盖生产 cron 路径;需要验证人工政策时再显式传 manual。 policy.SettlementTriggerMode = ledger.HostSalarySettlementTriggerAutomatic } if policy.GiftCoinToDiamondRatio == "" { policy.GiftCoinToDiamondRatio = "1" } if policy.ResidualDiamondToUSDRate == "" { policy.ResidualDiamondToUSDRate = "0" } _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO host_agency_salary_policies ( policy_id, name, region_id, status, settlement_mode, settlement_trigger_mode, gift_coin_to_diamond_ratio, residual_diamond_to_usd_rate, effective_from_ms, effective_to_ms, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), region_id = VALUES(region_id), status = VALUES(status), settlement_mode = VALUES(settlement_mode), settlement_trigger_mode = VALUES(settlement_trigger_mode), gift_coin_to_diamond_ratio = VALUES(gift_coin_to_diamond_ratio), residual_diamond_to_usd_rate = VALUES(residual_diamond_to_usd_rate), effective_from_ms = VALUES(effective_from_ms), effective_to_ms = VALUES(effective_to_ms), updated_at_ms = VALUES(updated_at_ms) `, policy.PolicyID, policy.Name, policy.RegionID, policy.Status, policy.SettlementMode, policy.SettlementTriggerMode, policy.GiftCoinToDiamondRatio, policy.ResidualDiamondToUSDRate, policy.EffectiveFromMs, policy.EffectiveToMs, nowMs, nowMs) if err != nil { r.t.Fatalf("seed host salary policy failed: %v", err) } if _, err := r.schema.DB.ExecContext(context.Background(), `DELETE FROM host_agency_salary_policy_levels WHERE policy_id = ?`, policy.PolicyID); err != nil { r.t.Fatalf("clear host salary policy levels failed: %v", err) } for _, level := range policy.Levels { if level.Status == "" { level.Status = "active" } if _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO host_agency_salary_policy_levels ( policy_id, level_no, required_diamonds, host_salary_usd_minor, host_coin_reward, agency_salary_usd_minor, status, sort_order, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, policy.PolicyID, level.LevelNo, level.RequiredDiamonds, level.HostSalaryUSDMinor, level.HostCoinReward, level.AgencySalaryUSDMinor, level.Status, level.SortOrder, nowMs, nowMs, ); err != nil { r.t.Fatalf("seed host salary policy level %d failed: %v", level.LevelNo, err) } } } func initDBPath(t testing.TB) string { t.Helper() return mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_wallet_service.sql", ) } func validateTableName(table string) string { switch table { case "wallet_transactions", "wallet_entries", "wallet_outbox", "wallet_accounts", "wallet_recharge_records", "payment_orders", "wallet_user_recharge_stats", "external_recharge_orders", "third_party_payment_methods", "host_period_diamond_accounts", "host_period_diamond_entries", "host_agency_salary_policies", "host_agency_salary_policy_levels", "host_salary_settlement_progress", "host_salary_settlement_records", "coin_seller_stock_records", "gift_diamond_ratio_configs", "wallet_diamond_exchange_rules", "vip_levels", "user_vip_memberships", "vip_purchase_orders", "user_vip_history", "resources", "resource_groups", "resource_group_items", "resource_shop_items", "resource_shop_purchase_orders", "gift_type_configs", "gift_configs", "gift_config_regions", "user_gift_wall", "wallet_projection_events", "resource_grants", "resource_grant_items", "user_resource_entitlements", "user_resource_equipment": return table default: panic(fmt.Sprintf("unsupported wallet test table %q", table)) } }