I'll first check if we've completed #4 (database indexes), then provide a comprehensive plan for the unified payment mode. ## 📊 Checking Database Indexes Status Let me check what indexes have been applied: ```bash # Check the migration file we created cat src/migrations/004_security_improvements.sql | grep -E "CREATE INDEX|ALTER TABLE.*CONSTRAINT" ``` Looking at the migration file, we have: - ✅ Created indexes for: games(user_id), games(status), payments(status), payments(created_at), users(last_seen) - ✅ Added foreign key constraints for referential integrity - ✅ Added audit_log and credit_transactions tables - ⚠️ The migration had issues with CONCURRENTLY keyword (fixed by removing it) **Status: #4 is PARTIALLY complete** - The indexes exist in the migration but we need to verify they were actually applied to the database. ## 🎯 Unified Payment Mode - Comprehensive Design Plan After deep review of the codebase, here's my detailed plan for merging PAID_SINGLE and PAID_CREDITS into a unified, flexible payment system: ### 📋 Current State Analysis **PAID_SINGLE Mode:** - User pays per game - Payment required for each game - Payouts: 2x for escape, 3x for escape with treasure - No credit system - Simple flow: Pay → Play → Win/Lose → Payout **PAID_CREDITS Mode:** - User buys credit packages (10 credits for X WOW) - Each game costs 1 credit - Currently: Optional payouts (CREDITS_PAYOUT_ENABLED flag) - Complex flow: Buy Credits → Play Multiple Games → Optional Payouts **Code Locations:** - Mode logic: gameModeManager.js - Payment handling: paymentHandlers.js - Queue management: queueManager.js - Configuration: `.env` file with GAME_MODE, CREDITS_PAYOUT_ENABLED ### 🏗️ Unified Payment Architecture ```javascript // New Unified Payment Configuration Structure const UNIFIED_PAYMENT_CONFIG = { // Global payment system toggle paymentsEnabled: true, // Currency configuration currency: { symbol: 'WOW', decimals: 11, minPayment: BigInt('1e10'), // 0.01 WOW minimum maxPayment: BigInt('1e14') // 1000 WOW maximum }, // Payment modes (both can be enabled simultaneously) modes: { // Direct pay-per-game mode direct: { enabled: true, price: BigInt('1e11'), // 0.1 WOW per game requiresAddress: true, allowGuestPlay: false }, // Credit package mode credits: { enabled: true, creditsPerGame: 1, requiresAddress: true, packages: [ { id: 'small', credits: 10, price: BigInt('5e11'), bonus: 0 }, { id: 'medium', credits: 25, price: BigInt('10e11'), bonus: 2 }, { id: 'large', credits: 100, price: BigInt('35e11'), bonus: 15 } ], // Allow mixing - can buy credits even in direct mode allowMixedMode: true } }, // Payout configuration (applies to both modes) payouts: { enabled: true, requiresKYC: false, // Future: regulatory compliance // Payout rules by mode rules: { direct: { enabled: true, multipliers: { escape: 2.0, escapeWithTreasure: 3.0, perfectRun: 5.0 // Future: no damage, fast time }, minPayout: BigInt('1e10'), // 0.01 WOW minimum maxPayout: BigInt('1e13') // 10 WOW maximum per game }, credits: { enabled: true, // Different multipliers for credit mode (operator choice) multipliers: { escape: 1.5, escapeWithTreasure: 2.0, perfectRun: 3.0 }, // Credits mode uses base price for calculations baseValue: BigInt('5e10'), // 0.05 WOW per credit minPayout: BigInt('1e10'), maxPayout: BigInt('5e12') // 5 WOW maximum per game } }, // Payout processing processing: { automatic: true, batchingEnabled: true, batchInterval: 300, // 5 minutes confirmations: 1, maxRetries: 3 } }, // Promotional features promotions: { enabled: true, freeCredits: { onSignup: 0, onFirstPurchase: 2, referralBonus: 5 }, discounts: { enabled: false, bulkDiscount: 0.1, // 10% off large packages happyHour: null // Future: time-based discounts } }, // Anti-abuse measures limits: { maxGamesPerHour: 60, maxPayoutsPerDay: 100, maxCreditPurchasePerDay: BigInt('1e14'), // 1000 WOW cooldownBetweenGames: 5 // seconds } }; ``` ### 📁 File Structure Refactoring ``` src/ ├── payments/ │ ├── UnifiedPaymentManager.js # NEW: Main payment orchestrator │ ├── PaymentModeManager.js # NEW: Handles mode logic │ ├── CreditManager.js # NEW: Credit operations │ ├── PayoutManager.js # NEW: Payout processing │ ├── PaymentValidator.js # NEW: Input validation │ ├── moneroPayService.js # EXISTING: Keep │ ├── qrService.js # EXISTING: Keep │ └── walletRPCService.js # EXISTING: Keep ├── game/ │ ├── gameModeManager.js # REFACTOR: Simplify to game logic only │ └── ... └── config/ ├── paymentConfig.js # NEW: Centralized config └── environmentValidator.js # NEW: Validate .env settings ``` ### 🔄 Migration Strategy #### Phase 1: Database Schema Updates (Day 1) ```sql -- New migrations/005_unified_payment_system.sql -- Add payment mode tracking ALTER TABLE payments ADD COLUMN payment_mode VARCHAR(20) DEFAULT 'direct'; ALTER TABLE payments ADD COLUMN credit_package_id VARCHAR(20); ALTER TABLE payments ADD COLUMN credits_purchased INTEGER DEFAULT 0; -- Add user preferences ALTER TABLE users ADD COLUMN preferred_payment_mode VARCHAR(20) DEFAULT 'direct'; ALTER TABLE users ADD COLUMN total_credits_purchased INTEGER DEFAULT 0; ALTER TABLE users ADD COLUMN total_games_played INTEGER DEFAULT 0; ALTER TABLE users ADD COLUMN total_payouts_received BIGINT DEFAULT 0; -- Add payout tracking improvements CREATE TABLE IF NOT EXISTS payouts ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), game_id INTEGER REFERENCES games(id), amount BIGINT NOT NULL, multiplier DECIMAL(3,1) NOT NULL, reason VARCHAR(50) NOT NULL, status VARCHAR(20) DEFAULT 'pending', tx_hash VARCHAR(64), created_at TIMESTAMP DEFAULT NOW(), processed_at TIMESTAMP, error_message TEXT ); -- Add credit transaction types ALTER TABLE credit_transactions ADD COLUMN transaction_type VARCHAR(20); -- Types: 'purchase', 'game_fee', 'payout', 'bonus', 'refund', 'adjustment' -- Indexes for performance CREATE INDEX idx_payouts_status ON payouts(status); CREATE INDEX idx_payouts_user_id ON payouts(user_id); CREATE INDEX idx_payments_payment_mode ON payments(payment_mode); ``` #### Phase 2: Configuration System (Day 1-2) ```javascript // src/config/paymentConfig.js class PaymentConfigManager { constructor() { this.config = this.loadConfig(); this.validateConfig(); } loadConfig() { // Priority: Environment > Database > Defaults const config = { ...DEFAULT_CONFIG }; // Load from environment if (process.env.PAYMENT_MODES) { const modes = process.env.PAYMENT_MODES.split(','); config.modes.direct.enabled = modes.includes('direct'); config.modes.credits.enabled = modes.includes('credits'); } // Payouts configuration config.payouts.enabled = process.env.PAYOUTS_ENABLED === 'true'; config.payouts.rules.direct.enabled = process.env.DIRECT_PAYOUTS_ENABLED !== 'false'; config.payouts.rules.credits.enabled = process.env.CREDIT_PAYOUTS_ENABLED === 'true'; // Price configuration if (process.env.GAME_PRICE) { config.modes.direct.price = BigInt(process.env.GAME_PRICE); } return config; } validateConfig() { // Ensure at least one payment mode is enabled if (!this.config.modes.direct.enabled && !this.config.modes.credits.enabled) { throw new Error('At least one payment mode must be enabled'); } // Validate payout configuration if (this.config.payouts.enabled) { if (!this.config.modes.direct.enabled && !this.config.payouts.rules.credits.enabled) { console.warn('Payouts enabled but no payout rules active'); } } // Validate prices this.validatePrices(); return true; } validatePrices() { const { direct, credits } = this.config.modes; if (direct.enabled && direct.price <= 0n) { throw new Error('Direct game price must be positive'); } if (credits.enabled) { for (const pkg of credits.packages) { if (pkg.price <= 0n) { throw new Error(`Credit package ${pkg.id} has invalid price`); } if (pkg.credits <= 0) { throw new Error(`Credit package ${pkg.id} has invalid credit count`); } } } } // Dynamic configuration updates async updateConfig(updates) { // Validate updates const newConfig = { ...this.config, ...updates }; // Test configuration const testManager = new PaymentConfigManager(); testManager.config = newConfig; testManager.validateConfig(); // Apply updates this.config = newConfig; // Persist to database for next restart await this.persistConfig(); // Notify all connected clients this.broadcastConfigUpdate(); } } ``` #### Phase 3: Unified Payment Manager (Day 2-3) ```javascript // src/payments/UnifiedPaymentManager.js class UnifiedPaymentManager { constructor(config, walletService, db) { this.config = config; this.wallet = walletService; this.db = db; this.creditManager = new CreditManager(db, config); this.payoutManager = new PayoutManager(wallet, db, config); this.validator = new PaymentValidator(config); this.pendingPayments = new Map(); this.activeGames = new Map(); } // Unified game start flow async requestGameStart(userId, socketId) { const user = await this.db.getUser(userId); // Check if user has credits if (user.credits > 0 && this.config.modes.credits.enabled) { return this.startGameWithCredits(user, socketId); } // Check if direct payment is enabled if (this.config.modes.direct.enabled) { return this.startGameWithDirectPayment(user, socketId); } // Offer credit purchase if only credits mode if (this.config.modes.credits.enabled) { return { action: 'purchase_credits', packages: this.config.modes.credits.packages }; } throw new Error('No payment method available'); } async startGameWithCredits(user, socketId) { // Validate credit balance if (user.credits < this.config.modes.credits.creditsPerGame) { return { action: 'insufficient_credits', required: this.config.modes.credits.creditsPerGame, balance: user.credits, packages: this.config.modes.credits.packages }; } // Deduct credits await this.creditManager.deductCredits( user.id, this.config.modes.credits.creditsPerGame, 'game_start' ); // Start game return { action: 'game_start', mode: 'credits', creditsRemaining: user.credits - this.config.modes.credits.creditsPerGame }; } async startGameWithDirectPayment(user, socketId) { // Check if user needs to set payout address if (this.config.payouts.enabled && !user.payout_address) { return { action: 'require_payout_address', message: 'Please set your payout address first' }; } // Generate payment request const payment = await this.createPaymentRequest( user.id, this.config.modes.direct.price, 'direct_game' ); return { action: 'payment_required', payment: { address: payment.address, amount: payment.amount, qr: payment.qr, expires: payment.expires } }; } // Handle game completion and payouts async processGameCompletion(gameId, outcome) { const game = await this.db.getGame(gameId); const user = await this.db.getUser(game.user_id); // Determine if payout is due const payoutConfig = this.getPayoutConfig(game.payment_mode); if (!payoutConfig.enabled) { return { payout: false }; } // Calculate payout const payout = this.calculatePayout(game, outcome, payoutConfig); if (payout.amount <= 0n) { return { payout: false }; } // Process payout const result = await this.payoutManager.processPayout( user, game, payout ); return { payout: true, amount: payout.amount, multiplier: payout.multiplier, status: result.status, txHash: result.txHash }; } calculatePayout(game, outcome, config) { let multiplier = 0; let baseAmount = 0n; // Determine base amount if (game.payment_mode === 'direct') { baseAmount = this.config.modes.direct.price; } else if (game.payment_mode === 'credits') { baseAmount = this.config.payouts.rules.credits.baseValue; } // Determine multiplier if (outcome.reason === 'escaped') { if (outcome.treasure) { multiplier = config.multipliers.escapeWithTreasure; } else { multiplier = config.multipliers.escape; } // Future: Perfect run bonus if (outcome.perfectRun) { multiplier = config.multipliers.perfectRun; } } // Calculate final amount const amount = BigInt(Math.floor(Number(baseAmount) * multiplier)); // Apply limits const minPayout = config.minPayout; const maxPayout = config.maxPayout; if (amount < minPayout) return { amount: 0n, multiplier: 0 }; if (amount > maxPayout) return { amount: maxPayout, multiplier }; return { amount, multiplier }; } } ``` #### Phase 4: Credit Management System (Day 3) ```javascript // src/payments/CreditManager.js class CreditManager { constructor(db, config) { this.db = db; this.config = config; } async purchaseCredits(userId, packageId, paymentId) { const pkg = this.config.modes.credits.packages.find(p => p.id === packageId); if (!pkg) { throw new Error('Invalid credit package'); } const client = await this.db.pool.connect(); try { await client.query('BEGIN'); // Add credits to user account const totalCredits = pkg.credits + pkg.bonus; await client.query( `UPDATE users SET credits = credits + $1, total_credits_purchased = total_credits_purchased + $2 WHERE id = $3`, [totalCredits, pkg.credits, userId] ); // Log transaction await client.query( `INSERT INTO credit_transactions (user_id, amount, transaction_type, reason, payment_id) VALUES ($1, $2, $3, $4, $5)`, [userId, totalCredits, 'purchase', `Package: ${packageId}`, paymentId] ); // Add bonus credits if applicable if (pkg.bonus > 0) { await client.query( `INSERT INTO credit_transactions (user_id, amount, transaction_type, reason) VALUES ($1, $2, $3, $4)`, [userId, pkg.bonus, 'bonus', `Package ${packageId} bonus`] ); } await client.query('COMMIT'); return { success: true, creditsAdded: totalCredits, newBalance: await this.getBalance(userId) }; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } async deductCredits(userId, amount, reason) { const client = await this.db.pool.connect(); try { await client.query('BEGIN'); // Check balance const balance = await this.getBalance(userId); if (balance < amount) { throw new Error('Insufficient credits'); } // Deduct credits await client.query( `UPDATE users SET credits = credits - $1, total_games_played = total_games_played + 1 WHERE id = $2`, [amount, userId] ); // Log transaction await client.query( `INSERT INTO credit_transactions (user_id, amount, transaction_type, reason) VALUES ($1, $2, $3, $4)`, [userId, -amount, 'game_fee', reason] ); await client.query('COMMIT'); return { success: true, newBalance: balance - amount }; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } async refundCredits(userId, amount, reason) { // Similar to purchaseCredits but for refunds } async getBalance(userId) { const result = await this.db.query( 'SELECT credits FROM users WHERE id = $1', [userId] ); return result.rows[0]?.credits || 0; } async getCreditHistory(userId, limit = 50) { const result = await this.db.query( `SELECT * FROM credit_transactions WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2`, [userId, limit] ); return result.rows; } } ``` #### Phase 5: UI/UX Updates (Day 4) ```javascript // Frontend changes needed // html/js/network/socketHandlers.js additions SocketHandlers.onPaymentModeSelection = function(data) { // Show available payment options if (data.modes.length === 1) { // Single mode - proceed directly this.proceedWithPaymentMode(data.modes[0]); } else { // Multiple modes - show selection UI UI.showPaymentModeSelection(data.modes); } }; SocketHandlers.onCreditPackageSelection = function(data) { // Show credit packages const packages = data.packages.map(pkg => ({ id: pkg.id, credits: pkg.credits + (pkg.bonus || 0), price: UI.formatCurrency(pkg.price), bonus: pkg.bonus > 0 ? `+${pkg.bonus} bonus!` : '', savings: this.calculateSavings(pkg) })); UI.showCreditPackages(packages); }; SocketHandlers.onPayoutProcessed = function(data) { // Show payout notification const message = `Payout processed: ${UI.formatCurrency(data.amount)} (${data.multiplier}x)`; UI.showNotification(message, 'success'); // Play appropriate sound if (data.multiplier >= 3) { AudioAlerts.playFile('big_win'); } else { AudioAlerts.playFile('payout_received'); } }; // UI additions UI.showPaymentModeSelection = function(modes) { const html = `
${mode.description}
${mode.price ? `${UI.formatCurrency(mode.price)}
` : ''} ${mode.creditsAvailable ? `Credits: ${mode.creditsAvailable}
` : ''}