import { Telegraf, Markup } from 'telegraf'; import Database from 'better-sqlite3'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; class BotFramework { constructor(token, dbPath, providerToken) { this.bot = new Telegraf(token, { telegram: { apiRoot: process.env.TELEGRAM_API_URL } }); this.db = new Database(dbPath); this.providerToken = providerToken; this.setupDatabase(); this.keyValueStore = new Map(); this.setupHandlers(); } setupDatabase() { // Users table this.db.exec(` CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY, username TEXT, first_name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, dynamic_fields TEXT, soft_balance INTEGER DEFAULT 0, hard_balance INTEGER DEFAULT 0, subscription_type TEXT DEFAULT 'free', subscription_expiry DATETIME, referred_by INTEGER, total_spent_soft INTEGER DEFAULT 0, total_spent_hard INTEGER DEFAULT 0, total_earned_soft INTEGER DEFAULT 0, total_earned_hard INTEGER DEFAULT 0, is_blocked BOOLEAN DEFAULT FALSE, last_activity DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Products table this.db.exec(` CREATE TABLE IF NOT EXISTS products ( product_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, price_soft INTEGER DEFAULT 0, price_hard INTEGER DEFAULT 0, price_irr INTEGER DEFAULT 0, product_type TEXT DEFAULT 'consumable', stock INTEGER DEFAULT -1, max_per_user INTEGER DEFAULT -1, available BOOLEAN DEFAULT TRUE, metadata TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Subscriptions table this.db.exec(` CREATE TABLE IF NOT EXISTS subscriptions ( subscription_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price_soft INTEGER DEFAULT 0, price_hard INTEGER DEFAULT 0, price_irr INTEGER DEFAULT 0, duration_days INTEGER NOT NULL, benefits TEXT, features TEXT, max_active INTEGER DEFAULT -1, available BOOLEAN DEFAULT TRUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // User subscriptions (active subscriptions) this.db.exec(` CREATE TABLE IF NOT EXISTS user_subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, started_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, is_active BOOLEAN DEFAULT TRUE, auto_renew BOOLEAN DEFAULT FALSE, cancelled_at DATETIME, FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (subscription_id) REFERENCES subscriptions(subscription_id) ) `); // Transactions table this.db.exec(` CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, transaction_type TEXT NOT NULL, amount_soft INTEGER DEFAULT 0, amount_hard INTEGER DEFAULT 0, amount_irr INTEGER DEFAULT 0, product_id INTEGER, subscription_id INTEGER, description TEXT, status TEXT DEFAULT 'completed', metadata TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(user_id) ) `); // Payments table (for real money transactions) this.db.exec(` CREATE TABLE IF NOT EXISTS payments ( payment_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, transaction_id INTEGER, amount_irr INTEGER NOT NULL, payment_method TEXT, provider_payment_id TEXT, status TEXT DEFAULT 'pending', invoice_payload TEXT, telegram_payment_charge_id TEXT, provider_payment_charge_id TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, completed_at DATETIME, failed_at DATETIME, refunded_at DATETIME, FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (transaction_id) REFERENCES transactions(id) ) `); // Product usage/consumption tracking this.db.exec(` CREATE TABLE IF NOT EXISTS product_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, product_id INTEGER NOT NULL, transaction_id INTEGER, quantity INTEGER DEFAULT 1, used_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME, metadata TEXT, FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (product_id) REFERENCES products(product_id), FOREIGN KEY (transaction_id) REFERENCES transactions(id) ) `); // Referrals table this.db.exec(` CREATE TABLE IF NOT EXISTS referrals ( id INTEGER PRIMARY KEY AUTOINCREMENT, referrer_id INTEGER NOT NULL, referred_id INTEGER NOT NULL, reward_soft INTEGER DEFAULT 0, reward_hard INTEGER DEFAULT 0, status TEXT DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, rewarded_at DATETIME, FOREIGN KEY (referrer_id) REFERENCES users(user_id), FOREIGN KEY (referrer_id) REFERENCES users(user_id) ) `); // Promo codes table this.db.exec(` CREATE TABLE IF NOT EXISTS promo_codes ( code TEXT PRIMARY KEY, discount_percent INTEGER DEFAULT 0, discount_amount_soft INTEGER DEFAULT 0, discount_amount_hard INTEGER DEFAULT 0, discount_amount_irr INTEGER DEFAULT 0, max_uses INTEGER DEFAULT -1, current_uses INTEGER DEFAULT 0, valid_from DATETIME DEFAULT CURRENT_TIMESTAMP, valid_until DATETIME, applicable_to TEXT, is_active BOOLEAN DEFAULT TRUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Promo code usage this.db.exec(` CREATE TABLE IF NOT EXISTS promo_code_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL, user_id INTEGER NOT NULL, transaction_id INTEGER, used_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (code) REFERENCES promo_codes(code), FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (transaction_id) REFERENCES transactions(id) ) `); // Create indexes for better performance this.db.exec(` CREATE INDEX IF NOT EXISTS idx_user_subscriptions_user ON user_subscriptions(user_id); CREATE INDEX IF NOT EXISTS idx_user_subscriptions_active ON user_subscriptions(is_active, expires_at); CREATE INDEX IF NOT EXISTS idx_transactions_user ON transactions(user_id); CREATE INDEX IF NOT EXISTS idx_payments_user ON payments(user_id); CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status); CREATE INDEX IF NOT EXISTS idx_product_usage_user ON product_usage(user_id); `); } setupHandlers() { this.bot.on('pre_checkout_query', (ctx) => this.preCheckoutQueryHandler(ctx)); this.bot.on('successful_payment', (ctx) => this.successfulPaymentHandler(ctx)); } // ==================== USER MANAGEMENT ==================== getOrCreateUser(userId, username = null, firstName = null) { let user = this.getUser(userId); if (!user) { const stmt = this.db.prepare(` INSERT INTO users (user_id, username, first_name) VALUES (?, ?, ?) `); stmt.run(userId, username, firstName); user = this.getUser(userId); } return user; } getUser(userId) { const stmt = this.db.prepare('SELECT * FROM users WHERE user_id = ?'); return stmt.get(userId); } updateUser(userId, updates) { const { dynamic_fields, ...rest } = updates; let query = 'UPDATE users SET last_activity = CURRENT_TIMESTAMP'; let params = []; if (dynamic_fields) { query += ', dynamic_fields = ?'; params.push(JSON.stringify(dynamic_fields)); } Object.keys(rest).forEach(key => { query += `, ${key} = ?`; params.push(rest[key]); }); query += ' WHERE user_id = ?'; params.push(userId); const stmt = this.db.prepare(query); return stmt.run(...params); } parseDynamicFields(userId) { const user = this.getUser(userId); if (user && user.dynamic_fields) { return JSON.parse(user.dynamic_fields); } return {}; } blockUser(userId) { return this.updateUser(userId, { is_blocked: true }); } unblockUser(userId) { return this.updateUser(userId, { is_blocked: false }); } // ==================== BALANCE MANAGEMENT ==================== addBalance(userId, amountSoft = 0, amountHard = 0, description = 'افزایش موجودی') { const user = this.getUser(userId); if (!user) return null; const newSoftBalance = user.soft_balance + amountSoft; const newHardBalance = user.hard_balance + amountHard; const newEarnedSoft = user.total_earned_soft + (amountSoft > 0 ? amountSoft : 0); const newEarnedHard = user.total_earned_hard + (amountHard > 0 ? amountHard : 0); this.updateUser(userId, { soft_balance: newSoftBalance, hard_balance: newHardBalance, total_earned_soft: newEarnedSoft, total_earned_hard: newEarnedHard }); this.addTransaction(userId, 'earn', amountSoft, amountHard, 0, null, null, description); return { soft_balance: newSoftBalance, hard_balance: newHardBalance }; } deductBalance(userId, amountSoft = 0, amountHard = 0, description = 'کسر موجودی') { const user = this.getUser(userId); if (!user) return null; if (user.soft_balance < amountSoft || user.hard_balance < amountHard) { return { error: 'موجودی کافی نیست' }; } const newSoftBalance = user.soft_balance - amountSoft; const newHardBalance = user.hard_balance - amountHard; this.updateUser(userId, { soft_balance: newSoftBalance, hard_balance: newHardBalance }); this.addTransaction(userId, 'deduct', -amountSoft, -amountHard, 0, null, null, description); return { soft_balance: newSoftBalance, hard_balance: newHardBalance }; } // ==================== PRODUCT MANAGEMENT ==================== addProduct(name, description, priceSoft = 0, priceHard = 0, priceIRR = 0, options = {}) { const { productType = 'consumable', stock = -1, maxPerUser = -1, metadata = null } = options; const stmt = this.db.prepare(` INSERT INTO products (name, description, price_soft, price_hard, price_irr, product_type, stock, max_per_user, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); return stmt.run(name, description, priceSoft, priceHard, priceIRR, productType, stock, maxPerUser, metadata ? JSON.stringify(metadata) : null); } getProduct(productId) { const stmt = this.db.prepare('SELECT * FROM products WHERE product_id = ?'); return stmt.get(productId); } getAllProducts(availableOnly = true) { const query = availableOnly ? 'SELECT * FROM products WHERE available = TRUE' : 'SELECT * FROM products'; const stmt = this.db.prepare(query); return stmt.all(); } updateProduct(productId, updates) { const { metadata, ...rest } = updates; let query = 'UPDATE products SET updated_at = CURRENT_TIMESTAMP'; let params = []; if (metadata) { query += ', metadata = ?'; params.push(JSON.stringify(metadata)); } Object.keys(rest).forEach(key => { query += `, ${key} = ?`; params.push(rest[key]); }); query += ' WHERE product_id = ?'; params.push(productId); const stmt = this.db.prepare(query); return stmt.run(...params); } canUserPurchaseProduct(userId, productId) { const product = this.getProduct(productId); if (!product || !product.available) { return { canPurchase: false, reason: 'محصول در دسترس نیست' }; } if (product.stock !== -1 && product.stock <= 0) { return { canPurchase: false, reason: 'موجودی محصول تمام شده' }; } if (product.max_per_user !== -1) { const stmt = this.db.prepare(` SELECT COUNT(*) as count FROM product_usage WHERE user_id = ? AND product_id = ? `); const { count } = stmt.get(userId, productId); if (count >= product.max_per_user) { return { canPurchase: false, reason: 'به حداکثر تعداد خرید رسیده‌اید' }; } } return { canPurchase: true }; } purchaseProduct(userId, productId, paymentMethod = 'balance') { const user = this.getUser(userId); const product = this.getProduct(productId); if (!user || !product) { return { success: false, error: 'کاربر یا محصول یافت نشد' }; } const canPurchase = this.canUserPurchaseProduct(userId, productId); if (!canPurchase.canPurchase) { return { success: false, error: canPurchase.reason }; } // Check balance if (user.soft_balance < product.price_soft || user.hard_balance < product.price_hard) { return { success: false, error: 'موجودی کافی نیست' }; } // Deduct balance const newSoftBalance = user.soft_balance - product.price_soft; const newHardBalance = user.hard_balance - product.price_hard; const newSpentSoft = user.total_spent_soft + product.price_soft; const newSpentHard = user.total_spent_hard + product.price_hard; this.updateUser(userId, { soft_balance: newSoftBalance, hard_balance: newHardBalance, total_spent_soft: newSpentSoft, total_spent_hard: newSpentHard }); // Create transaction const transactionId = this.addTransaction( userId, 'purchase', product.price_soft, product.price_hard, 0, productId, null, `خرید ${product.name}` ); // Record product usage this.recordProductUsage(userId, productId, transactionId); // Update stock if (product.stock !== -1) { this.updateProduct(productId, { stock: product.stock - 1 }); } return { success: true, transactionId, newBalance: { soft: newSoftBalance, hard: newHardBalance } }; } recordProductUsage(userId, productId, transactionId, quantity = 1, expiresAt = null, metadata = null) { const stmt = this.db.prepare(` INSERT INTO product_usage (user_id, product_id, transaction_id, quantity, expires_at, metadata) VALUES (?, ?, ?, ?, ?, ?) `); return stmt.run(userId, productId, transactionId, quantity, expiresAt, metadata ? JSON.stringify(metadata) : null); } getUserProductUsage(userId, productId = null) { const query = productId ? 'SELECT * FROM product_usage WHERE user_id = ? AND product_id = ?' : 'SELECT * FROM product_usage WHERE user_id = ?'; const stmt = this.db.prepare(query); return productId ? stmt.all(userId, productId) : stmt.all(userId); } // ==================== SUBSCRIPTION MANAGEMENT ==================== addSubscription(name, durationDays, priceSoft = 0, priceHard = 0, priceIRR = 0, options = {}) { const { benefits = null, features = null, maxActive = -1 } = options; const stmt = this.db.prepare(` INSERT INTO subscriptions (name, price_soft, price_hard, price_irr, duration_days, benefits, features, max_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); return stmt.run(name, priceSoft, priceHard, priceIRR, durationDays, benefits, features ? JSON.stringify(features) : null, maxActive); } getSubscription(subscriptionId) { const stmt = this.db.prepare('SELECT * FROM subscriptions WHERE subscription_id = ?'); return stmt.get(subscriptionId); } getAllSubscriptions(availableOnly = true) { const query = availableOnly ? 'SELECT * FROM subscriptions WHERE available = TRUE' : 'SELECT * FROM subscriptions'; const stmt = this.db.prepare(query); return stmt.all(); } purchaseSubscription(userId, subscriptionId, paymentMethod = 'balance') { const user = this.getUser(userId); const subscription = this.getSubscription(subscriptionId); if (!user || !subscription || !subscription.available) { return { success: false, error: 'کاربر یا اشتراک یافت نشد' }; } // Check if user already has active subscription of this type const activeSubscription = this.getUserActiveSubscription(userId, subscriptionId); if (activeSubscription) { return { success: false, error: 'شما قبلاً این اشتراک را فعال کرده‌اید' }; } // Check balance if (user.soft_balance < subscription.price_soft || user.hard_balance < subscription.price_hard) { return { success: false, error: 'موجودی کافی نیست' }; } // Deduct balance const newSoftBalance = user.soft_balance - subscription.price_soft; const newHardBalance = user.hard_balance - subscription.price_hard; const newSpentSoft = user.total_spent_soft + subscription.price_soft; const newSpentHard = user.total_spent_hard + subscription.price_hard; this.updateUser(userId, { soft_balance: newSoftBalance, hard_balance: newHardBalance, total_spent_soft: newSpentSoft, total_spent_hard: newSpentHard, subscription_type: subscription.name }); // Create transaction const transactionId = this.addTransaction( userId, 'subscription', subscription.price_soft, subscription.price_hard, 0, null, subscriptionId, `خرید اشتراک ${subscription.name}` ); // Activate subscription const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + subscription.duration_days); const stmt = this.db.prepare(` INSERT INTO user_subscriptions (user_id, subscription_id, expires_at) VALUES (?, ?, ?) `); const result = stmt.run(userId, subscriptionId, expiresAt.toISOString()); return { success: true, transactionId, subscriptionId: result.lastInsertRowid, expiresAt, newBalance: { soft: newSoftBalance, hard: newHardBalance } }; } getUserActiveSubscription(userId, subscriptionId = null) { const query = subscriptionId ? `SELECT * FROM user_subscriptions WHERE user_id = ? AND subscription_id = ? AND is_active = TRUE AND expires_at > datetime('now')` : `SELECT * FROM user_subscriptions WHERE user_id = ? AND is_active = TRUE AND expires_at > datetime('now')`; const stmt = this.db.prepare(query); return subscriptionId ? stmt.get(userId, subscriptionId) : stmt.all(userId); } checkAndExpireSubscriptions() { const stmt = this.db.prepare(` UPDATE user_subscriptions SET is_active = FALSE WHERE is_active = TRUE AND expires_at <= datetime('now') `); return stmt.run(); } cancelSubscription(userId, userSubscriptionId) { const stmt = this.db.prepare(` UPDATE user_subscriptions SET is_active = FALSE, cancelled_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ? `); return stmt.run(userSubscriptionId, userId); } renewSubscription(userId, userSubscriptionId) { const userSub = this.db.prepare('SELECT * FROM user_subscriptions WHERE id = ?').get(userSubscriptionId); if (!userSub) return { success: false, error: 'اشتراک یافت نشد' }; const subscription = this.getSubscription(userSub.subscription_id); const user = this.getUser(userId); if (user.soft_balance < subscription.price_soft || user.hard_balance < subscription.price_hard) { return { success: false, error: 'موجودی کافی نیست' }; } // Deduct balance this.deductBalance(userId, subscription.price_soft, subscription.price_hard, `تمدید اشتراک ${subscription.name}`); // Extend expiry const currentExpiry = new Date(userSub.expires_at); const newExpiry = new Date(currentExpiry.getTime() > Date.now() ? currentExpiry : new Date()); newExpiry.setDate(newExpiry.getDate() + subscription.duration_days); const stmt = this.db.prepare(` UPDATE user_subscriptions SET expires_at = ?, is_active = TRUE WHERE id = ? `); stmt.run(newExpiry.toISOString(), userSubscriptionId); return { success: true, newExpiresAt: newExpiry }; } // ==================== TRANSACTION MANAGEMENT ==================== addTransaction(userId, type, amountSoft = 0, amountHard = 0, amountIRR = 0, productId = null, subscriptionId = null, description = '', metadata = null) { const stmt = this.db.prepare(` INSERT INTO transactions (user_id, transaction_type, amount_soft, amount_hard, amount_irr, product_id, subscription_id, description, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run(userId, type, amountSoft, amountHard, amountIRR, productId, subscriptionId, description, metadata ? JSON.stringify(metadata) : null); return result.lastInsertRowid; } getUserTransactions(userId, limit = 50) { const stmt = this.db.prepare(` SELECT * FROM transactions WHERE user_id = ? ORDER BY created_at DESC LIMIT ? `); return stmt.all(userId, limit); } getTransaction(transactionId) { const stmt = this.db.prepare('SELECT * FROM transactions WHERE id = ?'); return stmt.get(transactionId); } // ==================== PAYMENT MANAGEMENT ==================== createPayment(userId, amountIRR, invoicePayload, transactionId = null) { const stmt = this.db.prepare(` INSERT INTO payments (user_id, transaction_id, amount_irr, invoice_payload, status) VALUES (?, ?, ?, ?, 'pending') `); const result = stmt.run(userId, transactionId, amountIRR, invoicePayload); return result.lastInsertRowid; } updatePaymentStatus(paymentId, status, chargeIds = {}) { const { telegramChargeId = null, providerChargeId = null } = chargeIds; const completedAt = status === 'completed' ? new Date().toISOString() : null; const failedAt = status === 'failed' ? new Date().toISOString() : null; const stmt = this.db.prepare(` UPDATE payments SET status = ?, telegram_payment_charge_id = COALESCE(?, telegram_payment_charge_id), provider_payment_charge_id = COALESCE(?, provider_payment_charge_id), completed_at = COALESCE(?, completed_at), failed_at = COALESCE(?, failed_at) WHERE payment_id = ? `); return stmt.run(status, telegramChargeId, providerChargeId, completedAt, failedAt, paymentId); } getPayment(paymentId) { const stmt = this.db.prepare('SELECT * FROM payments WHERE payment_id = ?'); return stmt.get(paymentId); } getUserPayments(userId, limit = 50) { const stmt = this.db.prepare(` SELECT * FROM payments WHERE user_id = ? ORDER BY created_at DESC LIMIT ? `); return stmt.all(userId, limit); } // ==================== INVOICE & PAYMENT HANDLERS ==================== sendInvoice(ctx, productId, options = {}) { const product = this.getProduct(productId); if (!product || !product.available) { return ctx.reply('❌ محصول مورد نظر یافت نشد.'); } const userId = ctx.from.id; const canPurchase = this.canUserPurchaseProduct(userId, productId); if (!canPurchase.canPurchase) { return ctx.reply(`❌ ${canPurchase.reason}`); } const priceInRial = product.price_irr || (product.price_soft * 10); const payload = `product_${productId}_${Date.now()}`; // Create payment record this.createPayment(userId, priceInRial, payload); return ctx.replyWithInvoice({ title: product.name, description: product.description || 'خرید محصول', payload, provider_token: this.providerToken, currency: 'IRR', prices: [{ label: product.name, amount: priceInRial }], ...options }); } sendSubscriptionInvoice(ctx, subscriptionId, options = {}) { const subscription = this.getSubscription(subscriptionId); if (!subscription || !subscription.available) { return ctx.reply('❌ اشتراک مورد نظر یافت نشد.'); } const userId = ctx.from.id; const activeSubscription = this.getUserActiveSubscription(userId, subscriptionId); if (activeSubscription) { return ctx.reply('❌ شما قبلاً این اشتراک را فعال کرده‌اید.'); } const priceInRial = subscription.price_irr || (subscription.price_soft * 10); const payload = `subscription_${subscriptionId}_${Date.now()}`; // Create payment record this.createPayment(userId, priceInRial, payload); return ctx.replyWithInvoice({ title: subscription.name, description: subscription.benefits || `اشتراک ${subscription.duration_days} روزه`, payload, provider_token: this.providerToken, currency: 'IRR', prices: [{ label: subscription.name, amount: priceInRial }], ...options }); } preCheckoutQueryHandler(ctx) { // You can add validation logic here const payload = ctx.preCheckoutQuery.invoice_payload; const userId = ctx.from.id; // Check if user is blocked const user = this.getUser(userId); if (user && user.is_blocked) { return ctx.answerPreCheckoutQuery(false, 'حساب کاربری شما مسدود شده است.'); } // Validate product/subscription availability if (payload.startsWith('product_')) { const productId = parseInt(payload.split('_')[1]); const product = this.getProduct(productId); if (!product || !product.available) { return ctx.answerPreCheckoutQuery(false, 'محصول در دسترس نیست.'); } const canPurchase = this.canUserPurchaseProduct(userId, productId); if (!canPurchase.canPurchase) { return ctx.answerPreCheckoutQuery(false, canPurchase.reason); } } else if (payload.startsWith('subscription_')) { const subscriptionId = parseInt(payload.split('_')[1]); const subscription = this.getSubscription(subscriptionId); if (!subscription || !subscription.available) { return ctx.answerPreCheckoutQuery(false, 'اشتراک در دسترس نیست.'); } } return ctx.answerPreCheckoutQuery(true); } successfulPaymentHandler(ctx) { const payment = ctx.message.successful_payment; const payload = payment.invoice_payload; const userId = ctx.from.id; const user = this.getOrCreateUser(userId, ctx.from.username, ctx.from.first_name); // Find payment record const paymentRecord = this.db.prepare( 'SELECT * FROM payments WHERE invoice_payload = ? AND user_id = ? ORDER BY created_at DESC LIMIT 1' ).get(payload, userId); if (!paymentRecord) { return ctx.reply('❌ خطا در پردازش پرداخت.'); } // Update payment status this.updatePaymentStatus(paymentRecord.payment_id, 'completed', { telegramChargeId: payment.telegram_payment_charge_id, providerChargeId: payment.provider_payment_charge_id }); // Process based on payload type if (payload.startsWith('product_')) { const productId = parseInt(payload.split('_')[1]); const product = this.getProduct(productId); if (!product) { return ctx.reply('❌ خطا: محصول یافت نشد.'); } // Create transaction const transactionId = this.addTransaction( userId, 'purchase_irr', 0, 0, payment.total_amount, productId, null, `خرید ${product.name} با پرداخت ریالی` ); // Record product usage this.recordProductUsage(userId, productId, transactionId); // Update stock if (product.stock !== -1) { this.updateProduct(productId, { stock: product.stock - 1 }); } // Update payment with transaction this.db.prepare('UPDATE payments SET transaction_id = ? WHERE payment_id = ?') .run(transactionId, paymentRecord.payment_id); return ctx.reply( `✅ پرداخت موفق!\n\n` + `محصول: ${product.name}\n` + `مبلغ: ${this.formatCurrency(payment.total_amount / 100)} تومان\n\n` + `از خرید شما متشکریم! 🎉` ); } else if (payload.startsWith('subscription_')) { const subscriptionId = parseInt(payload.split('_')[1]); const subscription = this.getSubscription(subscriptionId); if (!subscription) { return ctx.reply('❌ خطا: اشتراک یافت نشد.'); } // Create transaction const transactionId = this.addTransaction( userId, 'subscription_irr', 0, 0, payment.total_amount, null, subscriptionId, `خرید اشتراک ${subscription.name} با پرداخت ریالی` ); // Activate subscription const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + subscription.duration_days); const stmt = this.db.prepare(` INSERT INTO user_subscriptions (user_id, subscription_id, expires_at) VALUES (?, ?, ?) `); stmt.run(userId, subscriptionId, expiresAt.toISOString()); // Update user subscription type this.updateUser(userId, { subscription_type: subscription.name, subscription_expiry: expiresAt.toISOString() }); // Update payment with transaction this.db.prepare('UPDATE payments SET transaction_id = ? WHERE payment_id = ?') .run(transactionId, paymentRecord.payment_id); return ctx.reply( `✅ پرداخت موفق!\n\n` + `اشتراک: ${subscription.name}\n` + `مدت: ${subscription.duration_days} روز\n` + `تاریخ انقضا: ${expiresAt.toLocaleDateString('fa-IR')}\n` + `مبلغ: ${this.formatCurrency(payment.total_amount / 100)} تومان\n\n` + `اشتراک شما فعال شد! 🎉` ); } else if (payload.startsWith('balance_')) { // Charge balance const amount = parseInt(payload.split('_')[1]); const softAmount = Math.floor(amount / 10); // Example conversion rate this.addBalance(userId, softAmount, 0, `شارژ موجودی با پرداخت ${this.formatCurrency(payment.total_amount / 100)} تومان`); const transactionId = this.addTransaction( userId, 'balance_charge', softAmount, 0, payment.total_amount, null, null, 'شارژ موجودی' ); this.db.prepare('UPDATE payments SET transaction_id = ? WHERE payment_id = ?') .run(transactionId, paymentRecord.payment_id); return ctx.reply( `✅ شارژ موجودی موفق!\n\n` + `مبلغ پرداختی: ${this.formatCurrency(payment.total_amount / 100)} تومان\n` + `سکه دریافتی: ${this.formatCurrency(softAmount)} 🪙\n\n` + `موجودی جدید: ${this.formatCurrency(this.getUser(userId).soft_balance)} سکه` ); } return ctx.reply('✅ پرداخت با موفقیت انجام شد!'); } // ==================== PROMO CODE MANAGEMENT ==================== createPromoCode(code, options = {}) { const { discountPercent = 0, discountAmountSoft = 0, discountAmountHard = 0, discountAmountIRR = 0, maxUses = -1, validFrom = null, validUntil = null, applicableTo = null } = options; const stmt = this.db.prepare(` INSERT INTO promo_codes (code, discount_percent, discount_amount_soft, discount_amount_hard, discount_amount_irr, max_uses, valid_from, valid_until, applicable_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); return stmt.run(code, discountPercent, discountAmountSoft, discountAmountHard, discountAmountIRR, maxUses, validFrom, validUntil, applicableTo); } getPromoCode(code) { const stmt = this.db.prepare('SELECT * FROM promo_codes WHERE code = ?'); return stmt.get(code); } validatePromoCode(code, userId) { const promo = this.getPromoCode(code); if (!promo) { return { valid: false, reason: 'کد تخفیف نامعتبر است' }; } if (!promo.is_active) { return { valid: false, reason: 'کد تخفیف غیرفعال است' }; } if (promo.max_uses !== -1 && promo.current_uses >= promo.max_uses) { return { valid: false, reason: 'ظرفیت استفاده از این کد تمام شده' }; } const now = new Date(); if (promo.valid_from && new Date(promo.valid_from) > now) { return { valid: false, reason: 'کد تخفیف هنوز فعال نشده' }; } if (promo.valid_until && new Date(promo.valid_until) < now) { return { valid: false, reason: 'کد تخفیف منقضی شده' }; } // Check if user already used this code const usageStmt = this.db.prepare( 'SELECT COUNT(*) as count FROM promo_code_usage WHERE code = ? AND user_id = ?' ); const { count } = usageStmt.get(code, userId); if (count > 0) { return { valid: false, reason: 'شما قبلاً از این کد استفاده کرده‌اید' }; } return { valid: true, promo }; } applyPromoCode(code, userId, transactionId = null) { const validation = this.validatePromoCode(code, userId); if (!validation.valid) { return { success: false, error: validation.reason }; } const promo = validation.promo; // Record usage const stmt = this.db.prepare(` INSERT INTO promo_code_usage (code, user_id, transaction_id) VALUES (?, ?, ?) `); stmt.run(code, userId, transactionId); // Update usage count this.db.prepare('UPDATE promo_codes SET current_uses = current_uses + 1 WHERE code = ?') .run(code); return { success: true, discount: { percent: promo.discount_percent, soft: promo.discount_amount_soft, hard: promo.discount_amount_hard, irr: promo.discount_amount_irr } }; } calculateDiscountedPrice(originalPrice, promoCode) { if (!promoCode) return originalPrice; let discountedPrice = originalPrice; if (promoCode.discount_percent > 0) { discountedPrice = originalPrice * (1 - promoCode.discount_percent / 100); } if (promoCode.discount_amount_soft > 0 || promoCode.discount_amount_hard > 0 || promoCode.discount_amount_irr > 0) { discountedPrice = Math.max(0, originalPrice - (promoCode.discount_amount_soft || promoCode.discount_amount_hard || promoCode.discount_amount_irr)); } return Math.floor(discountedPrice); } // ==================== REFERRAL SYSTEM ==================== createReferral(referrerId, referredId, rewardSoft = 0, rewardHard = 0) { const stmt = this.db.prepare(` INSERT INTO referrals (referrer_id, referred_id, reward_soft, reward_hard) VALUES (?, ?, ?, ?) `); return stmt.run(referrerId, referredId, rewardSoft, rewardHard); } processReferralReward(referralId) { const referral = this.db.prepare('SELECT * FROM referrals WHERE id = ?').get(referralId); if (!referral || referral.status === 'completed') { return { success: false, error: 'رفرال نامعتبر یا قبلاً پردازش شده' }; } // Add reward to referrer this.addBalance(referral.referrer_id, referral.reward_soft, referral.reward_hard, 'پاداش معرفی کاربر جدید'); // Update referral status this.db.prepare(` UPDATE referrals SET status = 'completed', rewarded_at = CURRENT_TIMESTAMP WHERE id = ? `).run(referralId); return { success: true }; } getUserReferrals(userId) { const stmt = this.db.prepare('SELECT * FROM referrals WHERE referrer_id = ?'); return stmt.all(userId); } // ==================== STATISTICS & REPORTS ==================== getUserStats(userId) { const user = this.getUser(userId); if (!user) return null; const transactions = this.getUserTransactions(userId); const activeSubscriptions = this.getUserActiveSubscription(userId); const productUsage = this.getUserProductUsage(userId); const referrals = this.getUserReferrals(userId); return { user: { id: user.user_id, username: user.username, firstName: user.first_name, createdAt: user.created_at, lastActivity: user.last_activity }, balance: { soft: user.soft_balance, hard: user.hard_balance }, spending: { totalSoft: user.total_spent_soft, totalHard: user.total_spent_hard }, earning: { totalSoft: user.total_earned_soft, totalHard: user.total_earned_hard }, subscription: { type: user.subscription_type, expiry: user.subscription_expiry, active: activeSubscriptions }, stats: { totalTransactions: transactions.length, totalProducts: productUsage.length, totalReferrals: referrals.length } }; } getSystemStats() { const totalUsers = this.db.prepare('SELECT COUNT(*) as count FROM users').get().count; const activeUsers = this.db.prepare( "SELECT COUNT(*) as count FROM users WHERE last_activity > datetime('now', '-7 days')" ).get().count; const totalTransactions = this.db.prepare('SELECT COUNT(*) as count FROM transactions').get().count; const totalRevenueSoft = this.db.prepare( "SELECT SUM(amount_soft) as total FROM transactions WHERE transaction_type IN ('purchase', 'subscription')" ).get().total || 0; const totalRevenueHard = this.db.prepare( "SELECT SUM(amount_hard) as total FROM transactions WHERE transaction_type IN ('purchase', 'subscription')" ).get().total || 0; const totalRevenueIRR = this.db.prepare( "SELECT SUM(amount_irr) as total FROM transactions WHERE transaction_type IN ('purchase_irr', 'subscription_irr', 'balance_charge')" ).get().total || 0; const totalProducts = this.db.prepare('SELECT COUNT(*) as count FROM products').get().count; const totalSubscriptions = this.db.prepare('SELECT COUNT(*) as count FROM subscriptions').get().count; const activeSubscriptions = this.db.prepare( "SELECT COUNT(*) as count FROM user_subscriptions WHERE is_active = TRUE AND expires_at > datetime('now')" ).get().count; return { users: { total: totalUsers, active: activeUsers }, transactions: { total: totalTransactions }, revenue: { soft: totalRevenueSoft, hard: totalRevenueHard, irr: totalRevenueIRR }, products: { total: totalProducts }, subscriptions: { total: totalSubscriptions, active: activeSubscriptions } }; } // ==================== UTILITY FUNCTIONS ==================== formatCurrency(amount) { return amount.toLocaleString('fa-IR'); } formatDate(date) { return new Date(date).toLocaleDateString('fa-IR', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } // ==================== SCHEDULED TASKS ==================== runScheduledTasks() { // Check and expire subscriptions every hour setInterval(() => { this.checkAndExpireSubscriptions(); console.log('✓ Subscription expiry check completed'); }, 60 * 60 * 1000); // Every hour // Clean up old pending payments setInterval(() => { this.db.prepare(` UPDATE payments SET status = 'expired' WHERE status = 'pending' AND created_at < datetime('now', '-24 hours') `).run(); console.log('✓ Payment cleanup completed'); }, 6 * 60 * 60 * 1000); // Every 6 hours } // ==================== BOT LIFECYCLE ==================== launchBot() { this.runScheduledTasks(); this.bot.launch(); console.log('🤖 Bot started successfully'); } stopBot() { process.once('SIGINT', () => this.bot.stop('SIGINT')); process.once('SIGTERM', () => this.bot.stop('SIGTERM')); } // ==================== ADMIN FUNCTIONS ==================== getAllUsers(limit = 100, offset = 0) { const stmt = this.db.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?'); return stmt.all(limit, offset); } searchUsers(query) { const stmt = this.db.prepare(` SELECT * FROM users WHERE username LIKE ? OR first_name LIKE ? OR CAST(user_id AS TEXT) LIKE ? LIMIT 50 `); const searchTerm = `%${query}%`; return stmt.all(searchTerm, searchTerm, searchTerm); } getAllTransactions(limit = 100, offset = 0) { const stmt = this.db.prepare('SELECT * FROM transactions ORDER BY created_at DESC LIMIT ? OFFSET ?'); return stmt.all(limit, offset); } getRevenueReport(startDate = null, endDate = null) { let query = ` SELECT transaction_type, SUM(amount_soft) as total_soft, SUM(amount_hard) as total_hard, SUM(amount_irr) as total_irr, COUNT(*) as count FROM transactions `; const params = []; if (startDate && endDate) { query += ' WHERE created_at BETWEEN ? AND ?'; params.push(startDate, endDate); } query += ' GROUP BY transaction_type'; const stmt = this.db.prepare(query); return params.length > 0 ? stmt.all(...params) : stmt.all(); } // Backup database backupDatabase(backupPath) { const backup = this.db.backup(backupPath); return new Promise((resolve, reject) => { backup.step(-1); backup.finish(); resolve(backupPath); }); } } export default BotFramework;