package mysql import ( "context" "database/sql" "encoding/json" "strings" "time" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" ) // WalletOutboxRecord is a committed wallet fact ready for external fanout. type WalletOutboxRecord struct { AppCode string EventID string EventType string TransactionID string CommandID string UserID int64 AssetType string AvailableDelta int64 FrozenDelta int64 PayloadJSON string CreatedAtMS int64 RetryCount int LastError string WorkerID string LockUntilMS int64 } // WalletOutboxClaimFilter narrows one MQ fanout lane by event type. type WalletOutboxClaimFilter struct { // IncludeEventTypes lets a realtime worker own only the configured UI event facts. IncludeEventTypes []string // ExcludeEventTypes lets the generic wallet worker skip facts owned by the realtime lane. ExcludeEventTypes []string } type walletOutboxEvent struct { AppCode string EventID string EventType string TransactionID string CommandID string UserID int64 AssetType string AvailableDelta int64 FrozenDelta int64 Payload any CreatedAtMS int64 } func (r *Repository) insertWalletOutbox(ctx context.Context, tx *sql.Tx, events []walletOutboxEvent) error { for _, event := range events { payloadJSON, err := json.Marshal(event.Payload) if err != nil { return err } if _, err := tx.ExecContext(ctx, `INSERT INTO wallet_outbox ( app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type, available_delta, frozen_delta, payload, status, retry_count, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`, normalizedEntryAppCode(ctx, event.AppCode), event.EventID, event.EventType, event.TransactionID, event.CommandID, event.UserID, event.AssetType, event.AvailableDelta, event.FrozenDelta, string(payloadJSON), outboxStatusPending, event.CreatedAtMS, event.CreatedAtMS, ); err != nil { return err } } return nil } // ClaimPendingWalletOutbox atomically claims committed wallet facts for MQ fanout. func (r *Repository) ClaimPendingWalletOutbox(ctx context.Context, workerID string, limit int, lockUntilMS int64) ([]WalletOutboxRecord, error) { return r.ClaimPendingWalletOutboxFiltered(ctx, workerID, limit, lockUntilMS, WalletOutboxClaimFilter{}) } // ClaimPendingWalletOutboxFiltered atomically claims committed wallet facts for one MQ fanout lane. func (r *Repository) ClaimPendingWalletOutboxFiltered(ctx context.Context, workerID string, limit int, lockUntilMS int64, filter WalletOutboxClaimFilter) ([]WalletOutboxRecord, error) { if r == nil || r.db == nil { return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured") } workerID = strings.TrimSpace(workerID) if workerID == "" { return nil, xerr.New(xerr.InvalidArgument, "worker_id is required") } if limit <= 0 { limit = 100 } if limit > 500 { limit = 500 } nowMS := time.Now().UTC().UnixMilli() if lockUntilMS <= nowMS { lockUntilMS = time.Now().UTC().Add(30 * time.Second).UnixMilli() } appCode := appcode.FromContext(ctx) eventFilterSQL, eventFilterArgs := walletOutboxEventFilterSQL(filter) pendingIndex, claimIndex := walletOutboxClaimIndexes(filter) tx, err := r.db.BeginTx(ctx, nil) if err != nil { return nil, err } defer func() { _ = tx.Rollback() }() records := make([]WalletOutboxRecord, 0, limit) claimBranches := []struct { query string args []any }{ { query: walletOutboxClaimQuery(pendingIndex, ` AND next_retry_at_ms IS NULL AND lock_until_ms IS NULL`, eventFilterSQL), args: append([]any{appCode, outboxStatusPending}, eventFilterArgs...), }, { query: walletOutboxClaimQuery(pendingIndex, ` AND next_retry_at_ms <= ? AND lock_until_ms IS NULL`, eventFilterSQL), args: append([]any{appCode, outboxStatusRetryable, nowMS}, eventFilterArgs...), }, { query: walletOutboxClaimQuery(claimIndex, ` AND lock_until_ms <= ?`, eventFilterSQL), args: append([]any{appCode, outboxStatusDelivering, nowMS}, eventFilterArgs...), }, } for _, branch := range claimBranches { remaining := limit - len(records) if remaining <= 0 { break } args := append(append([]any{}, branch.args...), remaining) branchRecords, err := queryWalletOutboxRecords(ctx, tx, branch.query, args...) if err != nil { return nil, err } records = append(records, branchRecords...) } for _, record := range records { if _, err := tx.ExecContext(ctx, ` UPDATE wallet_outbox SET status = ?, worker_id = ?, lock_until_ms = ?, updated_at_ms = ? WHERE app_code = ? AND event_id = ?`, outboxStatusDelivering, workerID, lockUntilMS, nowMS, record.AppCode, record.EventID, ); err != nil { return nil, err } } if err := tx.Commit(); err != nil { return nil, err } for index := range records { records[index].WorkerID = workerID records[index].LockUntilMS = lockUntilMS } return records, nil } func walletOutboxClaimQuery(indexName string, statusCondition string, eventFilterSQL string) string { return ` SELECT app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type, available_delta, frozen_delta, COALESCE(CAST(payload AS CHAR), '{}'), created_at_ms, retry_count, COALESCE(last_error, ''), COALESCE(worker_id, ''), COALESCE(lock_until_ms, 0) FROM wallet_outbox FORCE INDEX (` + indexName + `) WHERE app_code = ? AND status = ?` + statusCondition + eventFilterSQL + ` ORDER BY created_at_ms ASC, event_id ASC LIMIT ? FOR UPDATE SKIP LOCKED` } func walletOutboxEventFilterSQL(filter WalletOutboxClaimFilter) (string, []any) { include := normalizeWalletOutboxEventTypes(filter.IncludeEventTypes) if len(include) > 0 { return " AND event_type IN (" + walletOutboxPlaceholders(len(include)) + ")", walletOutboxEventArgs(include) } exclude := normalizeWalletOutboxEventTypes(filter.ExcludeEventTypes) if len(exclude) > 0 { return " AND event_type NOT IN (" + walletOutboxPlaceholders(len(exclude)) + ")", walletOutboxEventArgs(exclude) } return "", nil } func walletOutboxClaimIndexes(filter WalletOutboxClaimFilter) (string, string) { if len(normalizeWalletOutboxEventTypes(filter.IncludeEventTypes)) > 0 { return "idx_wallet_outbox_event_pending", "idx_wallet_outbox_event_claim" } return "idx_wallet_outbox_pending", "idx_wallet_outbox_claim" } func normalizeWalletOutboxEventTypes(values []string) []string { normalized := make([]string, 0, len(values)) seen := make(map[string]struct{}, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" { continue } if _, exists := seen[value]; exists { continue } seen[value] = struct{}{} normalized = append(normalized, value) } return normalized } func walletOutboxPlaceholders(count int) string { if count <= 0 { return "" } placeholders := make([]string, count) for index := range placeholders { placeholders[index] = "?" } return strings.Join(placeholders, ",") } func walletOutboxEventArgs(values []string) []any { args := make([]any, 0, len(values)) for _, value := range values { args = append(args, value) } return args } func queryWalletOutboxRecords(ctx context.Context, tx *sql.Tx, query string, args ...any) ([]WalletOutboxRecord, error) { rows, err := tx.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() records := make([]WalletOutboxRecord, 0) for rows.Next() { var record WalletOutboxRecord if err := rows.Scan( &record.AppCode, &record.EventID, &record.EventType, &record.TransactionID, &record.CommandID, &record.UserID, &record.AssetType, &record.AvailableDelta, &record.FrozenDelta, &record.PayloadJSON, &record.CreatedAtMS, &record.RetryCount, &record.LastError, &record.WorkerID, &record.LockUntilMS, ); err != nil { return nil, err } records = append(records, record) } if err := rows.Err(); err != nil { return nil, err } return records, nil } // MarkWalletOutboxDelivered marks one wallet fact as accepted by the MQ broker. func (r *Repository) MarkWalletOutboxDelivered(ctx context.Context, eventID string) error { if r == nil || r.db == nil { return xerr.New(xerr.Unavailable, "mysql repository is not configured") } _, err := r.db.ExecContext(ctx, ` UPDATE wallet_outbox SET status = ?, worker_id = '', lock_until_ms = NULL, next_retry_at_ms = NULL, last_error = NULL, updated_at_ms = ? WHERE app_code = ? AND event_id = ?`, outboxStatusDelivered, time.Now().UTC().UnixMilli(), appcode.FromContext(ctx), eventID, ) return err } // MarkWalletOutboxRetryable releases one failed MQ publish with a bounded retry schedule. func (r *Repository) MarkWalletOutboxRetryable(ctx context.Context, eventID string, lastErr string, nextRetryAtMS int64) error { if r == nil || r.db == nil { return xerr.New(xerr.Unavailable, "mysql repository is not configured") } _, err := r.db.ExecContext(ctx, ` UPDATE wallet_outbox SET status = ?, worker_id = '', lock_until_ms = NULL, retry_count = retry_count + 1, next_retry_at_ms = ?, last_error = ?, updated_at_ms = ? WHERE app_code = ? AND event_id = ?`, outboxStatusRetryable, nextRetryAtMS, trimWalletOutboxError(lastErr), time.Now().UTC().UnixMilli(), appcode.FromContext(ctx), eventID, ) return err } // MarkWalletOutboxDead stops retrying a poison wallet fact after the configured retry ceiling. func (r *Repository) MarkWalletOutboxDead(ctx context.Context, eventID string, lastErr string) error { if r == nil || r.db == nil { return xerr.New(xerr.Unavailable, "mysql repository is not configured") } _, err := r.db.ExecContext(ctx, ` UPDATE wallet_outbox SET status = ?, worker_id = '', lock_until_ms = NULL, last_error = ?, updated_at_ms = ? WHERE app_code = ? AND event_id = ?`, outboxStatusFailed, trimWalletOutboxError(lastErr), time.Now().UTC().UnixMilli(), appcode.FromContext(ctx), eventID, ) return err } func trimWalletOutboxError(value string) string { value = strings.TrimSpace(value) if len(value) > 1024 { return value[:1024] } return value } func balanceChangedEvent(transactionID string, commandID string, userID int64, assetType string, availableDelta int64, frozenDelta int64, availableAfter int64, frozenAfter int64, balanceVersion int64, payload any, nowMs int64, bizTypes ...string) walletOutboxEvent { bizType := "" if len(bizTypes) > 0 { bizType = strings.TrimSpace(bizTypes[0]) } return walletOutboxEvent{ EventID: eventID(transactionID, "WalletBalanceChanged", userID, assetType), EventType: "WalletBalanceChanged", TransactionID: transactionID, CommandID: commandID, UserID: userID, AssetType: assetType, AvailableDelta: availableDelta, FrozenDelta: frozenDelta, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "user_id": userID, "asset_type": assetType, "biz_type": bizType, "available_delta": availableDelta, "frozen_delta": frozenDelta, "available_after": availableAfter, "frozen_after": frozenAfter, "balance_version": balanceVersion, "metadata": payload, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func coinSellerTransferredEvent(transactionID string, commandID string, sellerUserID int64, availableDelta int64, payload any, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletCoinSellerTransferred", sellerUserID, ledger.AssetCoinSellerCoin), EventType: "WalletCoinSellerTransferred", TransactionID: transactionID, CommandID: commandID, UserID: sellerUserID, AssetType: ledger.AssetCoinSellerCoin, AvailableDelta: availableDelta, FrozenDelta: 0, Payload: payload, CreatedAtMS: nowMs, } } func coinSellerSubTransferredEvent(transactionID string, commandID string, metadata coinSellerSubTransferMetadata, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletCoinSellerSubTransferred", metadata.ParentUserID, ledger.AssetCoinSellerCoin), EventType: "WalletCoinSellerSubTransferred", TransactionID: transactionID, CommandID: commandID, UserID: metadata.ParentUserID, AssetType: ledger.AssetCoinSellerCoin, AvailableDelta: -metadata.Amount, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "parent_user_id": metadata.ParentUserID, "child_user_id": metadata.ChildUserID, "region_id": metadata.RegionID, "amount": metadata.Amount, "asset_type": metadata.AssetType, "parent_balance_after": metadata.ParentBalanceAfter, "child_balance_after": metadata.ChildBalanceAfter, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func rechargeRecordedEvent(transactionID string, commandID string, targetUserID int64, availableDelta int64, payload any, nowMs int64) walletOutboxEvent { assetType := ledger.AssetCoin if metadata, ok := payload.(coinSellerTransferMetadata); ok { if strings.TrimSpace(metadata.TargetAssetType) != "" { assetType = metadata.TargetAssetType } rechargeType := strings.TrimSpace(metadata.RechargeType) if rechargeType == "" { rechargeType = "coin_seller_transfer" } payload = map[string]any{ "app_code": metadata.AppCode, "payment_order_id": metadata.PaymentOrderID, "provider": metadata.ProviderCode, "payment_method_id": metadata.PaymentMethodID, "seller_user_id": metadata.SellerUserID, "target_user_id": metadata.TargetUserID, "target_country_id": metadata.TargetCountryID, "country_id": metadata.TargetCountryID, "google_product_id": metadata.GoogleProductID, "product_code": metadata.ProductCode, "product_name": metadata.ProductName, "seller_region_id": metadata.SellerRegionID, "target_region_id": metadata.TargetRegionID, "region_id": metadata.TargetRegionID, "amount": metadata.Amount, "reason": metadata.Reason, "seller_asset_type": metadata.SellerAssetType, "target_asset_type": metadata.TargetAssetType, "seller_balance_after": metadata.SellerBalanceAfter, "target_balance_after": metadata.TargetBalanceAfter, "recharge_sequence": metadata.RechargeSequence, "recharge_usd_minor": metadata.RechargeUSDMinor, "recharge_currency_code": metadata.RechargeCurrencyCode, "recharge_policy_id": metadata.RechargePolicyID, "recharge_policy_version": metadata.RechargePolicyVersion, "recharge_policy_coin_amount": metadata.RechargePolicyCoinAmount, "recharge_policy_usd_minor_amount": metadata.RechargePolicyUSDMinorAmount, "recharge_type": rechargeType, "created_at_ms": nowMs, } } return walletOutboxEvent{ EventID: eventID(transactionID, "WalletRechargeRecorded", targetUserID, assetType), EventType: "WalletRechargeRecorded", TransactionID: transactionID, CommandID: commandID, UserID: targetUserID, AssetType: assetType, AvailableDelta: availableDelta, FrozenDelta: 0, Payload: payload, CreatedAtMS: nowMs, } } func taskRewardCreditedEvent(transactionID string, commandID string, metadata taskRewardMetadata, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletTaskRewardCredited", metadata.TargetUserID, metadata.AssetType), EventType: "WalletTaskRewardCredited", TransactionID: transactionID, CommandID: commandID, UserID: metadata.TargetUserID, AssetType: metadata.AssetType, AvailableDelta: metadata.Amount, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "user_id": metadata.TargetUserID, "task_type": metadata.TaskType, "task_id": metadata.TaskID, "cycle_key": metadata.CycleKey, "asset_type": metadata.AssetType, "amount": metadata.Amount, "reason": metadata.Reason, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func luckyGiftRewardCreditedEvent(transactionID string, commandID string, metadata luckyGiftRewardMetadata, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletLuckyGiftRewardCredited", metadata.TargetUserID, ledger.AssetCoin), EventType: "WalletLuckyGiftRewardCredited", TransactionID: transactionID, CommandID: commandID, UserID: metadata.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: metadata.Amount, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "user_id": metadata.TargetUserID, "draw_id": metadata.DrawID, "room_id": metadata.RoomID, "visible_region_id": metadata.VisibleRegionID, "region_id": metadata.VisibleRegionID, "country_id": metadata.CountryID, "gift_id": metadata.GiftID, "pool_id": metadata.PoolID, "amount": metadata.Amount, "reason": metadata.Reason, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func wheelRewardCreditedEvent(transactionID string, commandID string, metadata wheelRewardMetadata, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletWheelRewardCredited", metadata.TargetUserID, ledger.AssetCoin), EventType: "WalletWheelRewardCredited", TransactionID: transactionID, CommandID: commandID, UserID: metadata.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: metadata.Amount, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "user_id": metadata.TargetUserID, "draw_id": metadata.DrawID, "wheel_id": metadata.WheelID, "selected_tier_id": metadata.SelectedTierID, "visible_region_id": metadata.VisibleRegionID, "region_id": metadata.VisibleRegionID, "amount": metadata.Amount, "reason": metadata.Reason, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func roomTurnoverRewardCreditedEvent(transactionID string, commandID string, metadata roomTurnoverRewardMetadata, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletRoomTurnoverRewardCredited", metadata.TargetUserID, ledger.AssetCoin), EventType: "WalletRoomTurnoverRewardCredited", TransactionID: transactionID, CommandID: commandID, UserID: metadata.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: metadata.Amount, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "user_id": metadata.TargetUserID, "settlement_id": metadata.SettlementID, "room_id": metadata.RoomID, "period_start_ms": metadata.PeriodStartMS, "period_end_ms": metadata.PeriodEndMS, "coin_spent": metadata.CoinSpent, "tier_id": metadata.TierID, "tier_code": metadata.TierCode, "amount": metadata.Amount, "reason": metadata.Reason, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func inviteActivityRewardCreditedEvent(transactionID string, commandID string, metadata inviteActivityRewardMetadata, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletInviteActivityRewardCredited", metadata.TargetUserID, ledger.AssetCoin), EventType: "WalletInviteActivityRewardCredited", TransactionID: transactionID, CommandID: commandID, UserID: metadata.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: metadata.Amount, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "user_id": metadata.TargetUserID, "claim_id": metadata.ClaimID, "reward_type": metadata.RewardType, "tier_id": metadata.TierID, "tier_code": metadata.TierCode, "cycle_key": metadata.CycleKey, "reached_value": metadata.ReachedValue, "amount": metadata.Amount, "reason": metadata.Reason, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func agencyOpeningRewardCreditedEvent(transactionID string, commandID string, metadata agencyOpeningRewardMetadata, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletAgencyOpeningRewardCredited", metadata.TargetUserID, ledger.AssetCoin), EventType: "WalletAgencyOpeningRewardCredited", TransactionID: transactionID, CommandID: commandID, UserID: metadata.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: metadata.Amount, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "user_id": metadata.TargetUserID, "application_id": metadata.ApplicationID, "cycle_id": metadata.CycleID, "agency_id": metadata.AgencyID, "rank_no": metadata.RankNo, "score_coins": metadata.ScoreCoins, "amount": metadata.Amount, "reason": metadata.Reason, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func coinSellerStockCreditedEvent(transactionID string, commandID string, metadata coinSellerStockMetadata, nowMs int64) walletOutboxEvent { eventType := "WalletCoinSellerCoinCompensated" if metadata.StockType == ledger.StockTypeUSDTPurchase || metadata.StockType == ledger.StockTypeUSDTDeduction { eventType = "WalletCoinSellerStockPurchased" } return walletOutboxEvent{ EventID: eventID(transactionID, eventType, metadata.SellerUserID, ledger.AssetCoinSellerCoin), EventType: eventType, TransactionID: transactionID, CommandID: commandID, UserID: metadata.SellerUserID, AssetType: ledger.AssetCoinSellerCoin, AvailableDelta: metadata.CoinAmount, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "seller_user_id": metadata.SellerUserID, "seller_country_id": metadata.SellerCountryID, "seller_region_id": metadata.SellerRegionID, "country_id": metadata.SellerCountryID, "region_id": metadata.SellerRegionID, "stock_type": metadata.StockType, "coin_amount": metadata.CoinAmount, "paid_currency_code": metadata.PaidCurrencyCode, "paid_amount_micro": metadata.PaidAmountMicro, "counts_as_seller_recharge": metadata.CountsAsSellerRecharge, "operator_user_id": metadata.OperatorUserID, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func giftDebitedEvent(transactionID string, commandID string, userID int64, assetType string, availableDelta int64, frozenDelta int64, payload any, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletGiftDebited", userID, assetType), EventType: "WalletGiftDebited", TransactionID: transactionID, CommandID: commandID, UserID: userID, AssetType: assetType, AvailableDelta: availableDelta, FrozenDelta: frozenDelta, Payload: payload, CreatedAtMS: nowMs, } } func giftIncomeCreditedEvent(transactionID string, commandID string, metadata giftMetadata, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletGiftIncomeCredited", metadata.TargetUserID, ledger.AssetCoin), EventType: "WalletGiftIncomeCredited", TransactionID: transactionID, CommandID: commandID, UserID: metadata.TargetUserID, AssetType: ledger.AssetCoin, AvailableDelta: metadata.GiftIncomeCoinAmount, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "user_id": metadata.TargetUserID, "sender_user_id": metadata.SenderUserID, "room_id": metadata.RoomID, "direct_gift": metadata.DirectGift, "gift_id": metadata.GiftID, "gift_count": metadata.GiftCount, "gift_type_code": metadata.GiftTypeCode, "gift_charge_amount": metadata.ChargeAmount, "charge_source": metadata.ChargeSource, "amount": metadata.GiftIncomeCoinAmount, "ratio_percent": metadata.GiftIncomeRatioPercent, "ratio_region_id": metadata.GiftIncomeRatioRegionID, "gift_income_balance_after": metadata.GiftIncomeBalanceAfter, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func hostPeriodDiamondCreditedEvent(transactionID string, commandID string, metadata giftMetadata, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "HostPeriodDiamondCredited", metadata.TargetUserID, ledger.AssetHostPeriodDiamond), EventType: "HostPeriodDiamondCredited", TransactionID: transactionID, CommandID: commandID, UserID: metadata.TargetUserID, AssetType: ledger.AssetHostPeriodDiamond, AvailableDelta: metadata.HostPeriodDiamondAdded, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "host_user_id": metadata.TargetUserID, "cycle_key": metadata.HostPeriodCycleKey, "region_id": metadata.TargetHostRegionID, "agency_owner_user_id": metadata.TargetAgencyOwnerUserID, "diamond_delta": metadata.HostPeriodDiamondAdded, "diamond_after": metadata.HostPeriodDiamondAfter, "gift_charge_asset_type": metadata.ChargeAssetType, "gift_charge_amount": metadata.ChargeAmount, "gift_id": metadata.GiftID, "gift_count": metadata.GiftCount, "sender_user_id": metadata.SenderUserID, "sender_region_id": metadata.SenderRegionID, "room_id": metadata.RoomID, "gift_diamond_ratio_percent": metadata.GiftDiamondRatioPercent, "gift_diamond_ratio_region_id": metadata.GiftDiamondRatioRegionID, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } } func hostPointCreditedEvent(transactionID string, commandID string, metadata giftMetadata, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "HostPointCredited", metadata.TargetUserID, ledger.AssetPoint), EventType: "HostPointCredited", TransactionID: transactionID, CommandID: commandID, UserID: metadata.TargetUserID, AssetType: ledger.AssetPoint, AvailableDelta: metadata.HostPointAdded, FrozenDelta: 0, Payload: map[string]any{ "transaction_id": transactionID, "command_id": commandID, "host_user_id": metadata.TargetUserID, "sender_user_id": metadata.SenderUserID, "room_id": metadata.RoomID, "region_id": metadata.TargetHostRegionID, "asset_type": ledger.AssetPoint, "point_delta": metadata.HostPointAdded, "point_balance_after": metadata.HostPointBalanceAfter, "gift_charge_asset_type": metadata.ChargeAssetType, "gift_charge_amount": metadata.ChargeAmount, "charge_source": metadata.ChargeSource, "gift_id": metadata.GiftID, "gift_count": metadata.GiftCount, "policy_instance_code": metadata.HostPointPolicyInstanceCode, "policy_template_code": metadata.HostPointTemplateCode, "policy_template_version": metadata.HostPointTemplateVersion, "host_point_ratio_ppm": metadata.HostPointRatioPPM, "points_per_usd": metadata.PointsPerUSD, "allow_self_brushing": true, "created_at_ms": nowMs, }, CreatedAtMS: nowMs, } }