1245 lines
45 KiB
JavaScript
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;
|