telegram-shop/src/handlers/userProductHandler.js
2024-11-19 05:01:13 +03:00

755 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import db from '../config/database.js';
import User from '../models/User.js';
import WalletService from "../utils/walletService.js";
import config from "../config/config.js";
export default class UserProductHandler {
constructor(bot) {
this.bot = bot;
this.userStates = new Map();
}
async showProducts(msg) {
const chatId = msg.chat.id;
const messageId = msg?.message_id;
try {
const countries = await db.allAsync(
'SELECT DISTINCT country FROM locations ORDER BY country'
);
if (countries.length === 0) {
const message = 'No products available at the moment.';
if (messageId) {
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId
});
} else {
await this.bot.sendMessage(chatId, message);
}
return;
}
const keyboard = {
inline_keyboard: countries.map(loc => [{
text: loc.country,
callback_data: `shop_country_${loc.country}`
}])
};
const message = '🌍 Select your country:';
try {
if (messageId) {
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
});
}
} catch (error) {
await this.bot.sendMessage(chatId, message, {reply_markup: keyboard});
}
} catch (error) {
console.error('Error in showProducts:', error);
await this.bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
async handleCountrySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('shop_country_', '');
try {
const cities = await db.allAsync(
'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city',
[country]
);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
text: loc.city,
callback_data: `shop_city_${country}_${loc.city}`
}]),
[{text: '« Back to Countries', callback_data: 'shop_start'}]
]
};
await this.bot.editMessageText(
`🏙 Select city in ${country}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCountrySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading cities. Please try again.');
}
}
async handleCitySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('shop_city_', '').split('_');
try {
const districts = await db.allAsync(
'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district',
[country, city]
);
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `shop_district_${country}_${city}_${loc.district}`
}]),
[{text: '« Back to Cities', callback_data: `shop_country_${country}`}]
]
};
await this.bot.editMessageText(
`📍 Select district in ${city}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCitySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading districts. Please try again.');
}
}
async handleDistrictSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city, district] = callbackQuery.data.replace('shop_district_', '').split('_');
try {
const location = await db.getAsync(
'SELECT id FROM locations WHERE country = ? AND city = ? AND district = ?',
[country, city, district]
);
if (!location) {
throw new Error('Location not found');
}
const categories = await db.allAsync(
'SELECT id, name FROM categories WHERE location_id = ? ORDER BY name',
[location.id]
);
if (categories.length === 0) {
await this.bot.editMessageText(
'No products available in this location yet.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{text: '« Back to Districts', callback_data: `shop_city_${country}_${city}`}
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...categories.map(cat => [{
text: cat.name,
callback_data: `shop_category_${location.id}_${cat.id}`
}]),
[{text: '« Back to Districts', callback_data: `shop_city_${country}_${city}`}]
]
};
await this.bot.editMessageText(
'📦 Select category:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleDistrictSelection:', error);
await this.bot.sendMessage(chatId, 'Error loading categories. Please try again.');
}
}
async handleCategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId] = callbackQuery.data.replace('shop_category_', '').split('_');
try {
const subcategories = await db.allAsync(
'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name',
[categoryId]
);
const location = await db.getAsync(
'SELECT country, city, district FROM locations WHERE id = ?',
[locationId]
);
if (subcategories.length === 0) {
await this.bot.editMessageText(
'No products available in this category yet.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{
text: '« Back to Categories',
callback_data: `shop_district_${location.country}_${location.city}_${location.district}`
}
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...subcategories.map(sub => [{
text: sub.name,
callback_data: `shop_subcategory_${locationId}_${categoryId}_${sub.id}`
}]),
[{
text: '« Back to Categories',
callback_data: `shop_district_${location.country}_${location.city}_${location.district}`
}]
]
};
await this.bot.editMessageText(
'📦 Select subcategory:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCategorySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading subcategories. Please try again.');
}
}
async handleSubcategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId, photoMessageId] = callbackQuery.data.replace('shop_subcategory_', '').split('_');
try {
// Delete the photo message if it exists
if (photoMessageId) {
try {
await this.bot.deleteMessage(chatId, photoMessageId);
} catch (error) {
console.error('Error deleting photo message:', error);
}
}
const products = await db.allAsync(
`SELECT id, name, price, description, quantity_in_stock, photo_url
FROM products
WHERE location_id = ? AND category_id = ? AND subcategory_id = ?
AND quantity_in_stock > 0
ORDER BY name`,
[locationId, categoryId, subcategoryId]
);
const location = await db.getAsync('SELECT * FROM locations WHERE id = ?', [locationId]);
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]);
if (products.length === 0) {
await this.bot.editMessageText(
'No products available in this subcategory.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{
text: '« Back to Subcategories',
callback_data: `shop_category_${locationId}_${categoryId}`
}
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...products.map(prod => [{
text: `${prod.name} - $${prod.price}`,
callback_data: `shop_product_${prod.id}`
}]),
[{text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}`}]
]
};
await this.bot.editMessageText(
`📦 Products in ${subcategory.name}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSubcategorySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
async handleProductSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('shop_product_', '');
try {
const product = await db.getAsync(
`SELECT p.*, c.name as category_name, s.name as subcategory_name
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN subcategories s ON p.subcategory_id = s.id
WHERE p.id = ?`,
[productId]
);
if (!product) {
throw new Error('Product not found');
}
// Delete the previous message
await this.bot.deleteMessage(chatId, messageId);
const message = `
📦 ${product.name}
💰 Price: $${product.price}
📝 Description: ${product.description}
📦 Available: ${product.quantity_in_stock} pcs
Category: ${product.category_name}
Subcategory: ${product.subcategory_name}
`;
let photoMessageId = null;
// First send the photo if it exists
let photoMessage;
if (product.photo_url) {
try {
photoMessage = await this.bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'});
} catch (e) {
photoMessage = await this.bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'})
}
}
const keyboard = {
inline_keyboard: [
[{text: '🛒 Buy Now', callback_data: `buy_product_${productId}`}],
[
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: {} // Initially disabled as quantity starts at 1
},
{text: '1', callback_data: 'current_quantity'},
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: product.quantity_in_stock <= 1 ? {} : null // Disabled if stock is 1 or less
}
],
[{
text: `« Back to ${product.subcategory_name}`,
callback_data: `shop_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}_${photoMessageId}`
}]
]
};
// Then send the message with controls
await this.bot.sendMessage(chatId, message, {
reply_markup: keyboard,
parse_mode: 'HTML'
});
// Store the current quantity and photo message ID in user state
this.userStates.set(chatId, {
action: 'buying_product',
productId,
quantity: 1,
photoMessageId
});
} catch (error) {
console.error('Error in handleProductSelection:', error);
await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.');
}
}
async handleIncreaseQuantity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('increase_quantity_', '');
const state = this.userStates.get(chatId);
try {
const product = await db.getAsync(
'SELECT quantity_in_stock FROM products WHERE id = ?',
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const currentQuantity = state?.quantity || 1;
// If already at max stock, silently ignore
if (currentQuantity >= product.quantity_in_stock) {
await this.bot.answerCallbackQuery(callbackQuery.id);
return;
}
const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock);
// Update state
this.userStates.set(chatId, {
...state,
quantity: newQuantity
});
// Update quantity display in keyboard
const keyboard = callbackQuery.message.reply_markup.inline_keyboard;
keyboard[1] = [
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: newQuantity <= 1 ? {} : null
},
{text: newQuantity.toString(), callback_data: 'current_quantity'},
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: newQuantity >= product.quantity_in_stock ? {} : null
}
];
await this.bot.editMessageReplyMarkup(
{inline_keyboard: keyboard},
{
chat_id: chatId,
message_id: messageId
}
);
await this.bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
console.error('Error in handleIncreaseQuantity:', error);
await this.bot.answerCallbackQuery(callbackQuery.id);
}
}
async handleDecreaseQuantity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('decrease_quantity_', '');
const state = this.userStates.get(chatId);
try {
const product = await db.getAsync(
'SELECT quantity_in_stock FROM products WHERE id = ?',
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const currentQuantity = state?.quantity || 1;
// If already at minimum, silently ignore
if (currentQuantity <= 1) {
await this.bot.answerCallbackQuery(callbackQuery.id);
return;
}
const newQuantity = Math.max(currentQuantity - 1, 1);
// Update state
this.userStates.set(chatId, {
...state,
quantity: newQuantity
});
// Update quantity display in keyboard
const keyboard = callbackQuery.message.reply_markup.inline_keyboard;
keyboard[1] = [
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: newQuantity <= 1 ? {} : null
},
{text: newQuantity.toString(), callback_data: 'current_quantity'},
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: newQuantity >= product.quantity_in_stock ? {} : null
}
];
await this.bot.editMessageReplyMarkup(
{inline_keyboard: keyboard},
{
chat_id: chatId,
message_id: messageId
}
);
await this.bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
console.error('Error in handleDecreaseQuantity:', error);
await this.bot.answerCallbackQuery(callbackQuery.id);
}
}
async handleBuyProduct(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
const productId = callbackQuery.data.replace('buy_product_', '');
const state = this.userStates.get(chatId);
try {
const user = await User.getById(userId);
if (!user) {
throw new Error('User not found');
}
const product = await db.getAsync(
'SELECT * FROM products WHERE id = ?',
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const quantity = state?.quantity || 1;
const totalPrice = product.price * quantity;
// Get user's crypto wallets with balances
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [user.id]);
if (cryptoWallets.length === 0) {
await this.bot.sendMessage(
chatId,
'You need to add a crypto wallet first to make purchases.',
{
reply_markup: {
inline_keyboard: [[
{text: ' Add Wallet', callback_data: 'add_wallet'}
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...cryptoWallets.map(wallet => [{
text: `Pay with ${wallet.wallet_type}`,
callback_data: `pay_with_${wallet.wallet_type}_${productId}_${quantity}`
}]),
[{text: '« Cancel', callback_data: `shop_product_${productId}`}]
]
};
await this.bot.editMessageText(
`🛒 Purchase Summary:\n\n` +
`Product: ${product.name}\n` +
`Quantity: ${quantity}\n` +
`Total: $${totalPrice}\n\n` +
`Select payment method:`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleBuyProduct:', error);
await this.bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
}
}
async handlePay(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
const state = this.userStates.get(chatId);
try {
await User.recalculateBalance(userId);
const user = await User.getById(userId);
if (!user) {
throw new Error('User not found');
}
const product = await db.getAsync(
'SELECT * FROM products WHERE id = ?',
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const totalPrice = product.price * quantity;
const balance = user.total_balance + user.bonus_balance;
if (totalPrice > balance) {
this.userStates.delete(chatId);
await this.bot.editMessageText(`Not enough money`, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
});
return;
}
await db.runAsync(
'INSERT INTO purchases (user_id, product_id, wallet_type, tx_hash, quantity, total_price) VALUES (?, ?, ?, ?, ?, ?)',
[user.id, product.id, walletType, "null", quantity, totalPrice]
);
let hiddenPhotoMessage;
if (product.hidden_photo_url) {
try {
hiddenPhotoMessage = await this.bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'});
} catch (e) {
hiddenPhotoMessage = await this.bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'})
}
}
const message = `
📦 Product Details:
Name: ${product.name}
Price: $${product.price}
Description: ${product.description}
Stock: ${product.quantity_in_stock}
Location: ${product.country}, ${product.city}, ${product.district}
Category: ${product.category_name}
Subcategory: ${product.subcategory_name}
🔒 Private Information:
${product.private_data}
Hidden Location: ${product.hidden_description}
Coordinates: ${product.hidden_coordinates}
`;
const keyboard = {
inline_keyboard: [
[{text: "I've got it!", callback_data: "Asdasdasd"}],
[{text: "Contact support", url: config.SUPPORT_LINK}]
]
};
await this.bot.sendMessage(chatId, message, {reply_markup: keyboard});
await this.bot.deleteMessage(chatId, callbackQuery.message.message_id);
} catch (error) {
console.error('Error in handleBuyProduct:', error);
await this.bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
}
}
async showPurchases(msg) {
const chatId = msg.chat.id;
const userId = msg.from.id;
try {
const user = await User.getById(userId);
if (!user) {
await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
const purchases = await db.allAsync(`
SELECT p.*, pr.name as product_name, pr.description,
l.country, l.city, l.district
FROM purchases p
JOIN products pr ON p.product_id = pr.id
JOIN locations l ON pr.location_id = l.id
WHERE p.user_id = ?
ORDER BY p.purchase_date DESC
LIMIT 10
`, [user.id]);
if (purchases.length === 0) {
await this.bot.sendMessage(
chatId,
'You haven\'t made any purchases yet.',
{
reply_markup: {
inline_keyboard: [[
{text: '🛍 Browse Products', callback_data: 'shop_start'}
]]
}
}
);
return;
}
let message = '🛍 *Your Recent Purchases:*\n\n';
for (const purchase of purchases) {
const date = new Date(purchase.purchase_date).toLocaleString();
message += `📦 *${purchase.product_name}*\n`;
message += `├ Quantity: ${purchase.quantity}\n`;
message += `├ Total: $${purchase.total_price}\n`;
message += `├ Location: ${purchase.country}, ${purchase.city}\n`;
message += `├ Payment: ${purchase.wallet_type}\n`;
message += `├ TX: \`${purchase.tx_hash}\`\n`;
message += `└ Date: ${date}\n\n`;
}
await this.bot.sendMessage(chatId, message, {
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{text: '🛍 Browse Products', callback_data: 'shop_start'}
]]
}
});
} catch (error) {
console.error('Error in showPurchases:', error);
await this.bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
}
}
}