Files
sample-idea/micro-tranactions-telegram-bale.js

1245 lines
45 KiB
JavaScript

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;