From 78423c232096f3f15894068cea6d1f8c1cc4699d Mon Sep 17 00:00:00 2001 From: felfel Date: Fri, 1 May 2026 03:28:55 +0330 Subject: [PATCH] Update micro-tranactions-telegram-bale.js --- micro-tranactions-telegram-bale.js | 1244 ++++++++++++++++++++++++++++ 1 file changed, 1244 insertions(+) diff --git a/micro-tranactions-telegram-bale.js b/micro-tranactions-telegram-bale.js index e69de29..5f8063f 100644 --- a/micro-tranactions-telegram-bale.js +++ b/micro-tranactions-telegram-bale.js @@ -0,0 +1,1244 @@ +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;