From 6fa273f0b698218514a1915b6bb54fbd305849c0 Mon Sep 17 00:00:00 2001 From: Artyom Ashirov <1323ED5@gmail.com> Date: Tue, 19 Nov 2024 05:01:13 +0300 Subject: [PATCH 1/2] Item purchase --- src/config/database.js | 2 + src/handlers/adminProductHandler.js | 4 + src/handlers/userProductHandler.js | 1215 +++++++++++++++------------ src/index.js | 11 +- src/models/User.js | 4 + 5 files changed, 674 insertions(+), 562 deletions(-) diff --git a/src/config/database.js b/src/config/database.js index 33c0ff6..2917fc0 100644 --- a/src/config/database.js +++ b/src/config/database.js @@ -95,6 +95,8 @@ const initDb = async () => { city TEXT, district TEXT, status INTEGER DEFAULT 0, + total_balance REAL DEFAULT 0, + bonus_balance REAL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); diff --git a/src/handlers/adminProductHandler.js b/src/handlers/adminProductHandler.js index 4e0d8d7..4851b63 100644 --- a/src/handlers/adminProductHandler.js +++ b/src/handlers/adminProductHandler.js @@ -370,6 +370,10 @@ export default class AdminProductHandler { text: 'No products for this location', markup: { inline_keyboard: [ + [{ + text: '📥 Import Products', + callback_data: `add_product_${locationId}_${categoryId}_${subcategoryId}` + }], [{text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}`}] ] } diff --git a/src/handlers/userProductHandler.js b/src/handlers/userProductHandler.js index 379e42c..e52272f 100644 --- a/src/handlers/userProductHandler.js +++ b/src/handlers/userProductHandler.js @@ -1,337 +1,348 @@ 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.'); + constructor(bot) { + this.bot = bot; + this.userStates = new Map(); } - } - async handleCountrySelection(callbackQuery) { - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const country = callbackQuery.data.replace('shop_country_', ''); + async showProducts(msg) { + const chatId = msg.chat.id; + const messageId = msg?.message_id; - 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 countries = await db.allAsync( + 'SELECT DISTINCT country FROM locations ORDER BY country' + ); - const products = await db.allAsync( - `SELECT id, name, price, description, quantity_in_stock, photo_url + 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] - ); + [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]); + 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}` } - ]] + 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; } - } - ); - 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}` }] - ] - }; + 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 + 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.'); } - ); - } 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_', ''); + 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 + 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] - ); + [productId] + ); - if (!product) { - throw new Error('Product not found'); - } + if (!product) { + throw new Error('Product not found'); + } - // Delete the previous message - await this.bot.deleteMessage(chatId, messageId); + // Delete the previous message + await this.bot.deleteMessage(chatId, messageId); - const message = ` + const message = ` 📦 ${product.name} 💰 Price: $${product.price} @@ -342,266 +353,354 @@ Category: ${product.category_name} Subcategory: ${product.subcategory_name} `; - let photoMessageId = null; + let photoMessageId = null; - // First send the photo if it exists - if (product.photo_url) { - const photoMessage = await this.bot.sendPhoto(chatId, product.photo_url); - photoMessageId = photoMessage.message_id; - } - - 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 + // 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'}) + } } - ], - [{ 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' - }); + 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}` + }] + ] + }; - // 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.'); + // 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); + 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] - ); + try { + const product = await db.getAsync( + 'SELECT quantity_in_stock FROM products WHERE id = ?', + [productId] + ); - if (!product) { - throw new Error('Product not found'); - } + 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 currentQuantity = state?.quantity || 1; - const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock); + // If already at max stock, silently ignore + if (currentQuantity >= product.quantity_in_stock) { + await this.bot.answerCallbackQuery(callbackQuery.id); + return; + } - // Update state - this.userStates.set(chatId, { - ...state, - quantity: newQuantity - }); + const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock); - // 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 + // 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); } - ]; - - 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); + 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] - ); + try { + const product = await db.getAsync( + 'SELECT quantity_in_stock FROM products WHERE id = ?', + [productId] + ); - if (!product) { - throw new Error('Product not found'); - } + 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 currentQuantity = state?.quantity || 1; - const newQuantity = Math.max(currentQuantity - 1, 1); + // If already at minimum, silently ignore + if (currentQuantity <= 1) { + await this.bot.answerCallbackQuery(callbackQuery.id); + return; + } - // Update state - this.userStates.set(chatId, { - ...state, - quantity: newQuantity - }); + const newQuantity = Math.max(currentQuantity - 1, 1); - // 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 + // 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); } - ]; - - 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); + 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'); - } + 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] - ); + const product = await db.getAsync( + 'SELECT * FROM products WHERE id = ?', + [productId] + ); - if (!product) { - throw new Error('Product not found'); - } + if (!product) { + throw new Error('Product not found'); + } - const quantity = state?.quantity || 1; - const totalPrice = product.price * quantity; + const quantity = state?.quantity || 1; + const totalPrice = product.price * quantity; - // Get user's crypto wallets with balances - const cryptoWallets = await db.allAsync(` + // 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' } - ]] + 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; } - } - ); - 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}` }] - ] - }; + 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 + 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.'); } - ); - } 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; + 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 { - const user = await User.getById(userId); - if (!user) { - await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.'); - return; - } + try { + await User.recalculateBalance(userId); + const user = await User.getById(userId); - const purchases = await db.allAsync(` + 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 @@ -612,45 +711,45 @@ Subcategory: ${product.subcategory_name} 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' } - ]] + 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; } - } - ); - 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`; - } + let message = '🛍 *Your Recent Purchases:*\n\n'; - await this.bot.sendMessage(chatId, message, { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: [[ - { text: '🛍 Browse Products', callback_data: 'shop_start' } - ]] + 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.'); } - }); - } catch (error) { - console.error('Error in showPurchases:', error); - await this.bot.sendMessage(chatId, 'Error loading purchase history. Please try again.'); } - } } \ No newline at end of file diff --git a/src/index.js b/src/index.js index e542d40..d6414dc 100644 --- a/src/index.js +++ b/src/index.js @@ -239,6 +239,9 @@ bot.on('callback_query', async (callbackQuery) => { } else if (action.startsWith('buy_product_')) { logDebug(action, 'handleBuyProduct'); await userProductHandler.handleBuyProduct(callbackQuery); + } else if (action.startsWith('pay_with_')) { + logDebug(action, 'handlePay'); + await userProductHandler.handlePay(callbackQuery); } // Admin location management else if (action === 'add_location') { @@ -287,7 +290,7 @@ bot.on('callback_query', async (callbackQuery) => { logDebug(action, 'handleSubcategorySelection'); await adminProductHandler.handleSubcategorySelection(callbackQuery); } else if (action.startsWith('list_products_')) { - logDebug(action, 'handleSubcategorySelection'); + logDebug(action, 'handleProductListPage'); await adminProductHandler.handleProductListPage(callbackQuery); } else if (action.startsWith('add_product_')) { logDebug(action, 'handleAddProduct'); @@ -299,7 +302,7 @@ bot.on('callback_query', async (callbackQuery) => { logDebug(action, 'handleProductEdit'); await adminProductHandler.handleProductEdit(callbackQuery) } else if (action.startsWith('delete_product_')) { - logDebug(action, 'handleViewProduct'); + logDebug(action, 'handleProductDelete'); await adminProductHandler.handleProductDelete(callbackQuery); } else if (action.startsWith('confirm_delete_product_')) { logDebug(action, 'handleConfirmDelete'); @@ -310,13 +313,13 @@ bot.on('callback_query', async (callbackQuery) => { logDebug(action, 'handleViewUser'); await adminUserHandler.handleViewUser(callbackQuery); } else if (action.startsWith('list_users_')) { - logDebug(action, 'handleViewUser'); + logDebug(action, 'handleUserListPage'); await adminUserHandler.handleUserListPage(callbackQuery); } else if (action.startsWith('delete_user_')) { logDebug(action, 'handleDeleteUser'); await adminUserHandler.handleDeleteUser(callbackQuery); } else if (action.startsWith('block_user_')) { - logDebug(action, 'handleDeleteUser'); + logDebug(action, 'handleBlockUser'); await adminUserHandler.handleBlockUser(callbackQuery); } else if (action.startsWith('confirm_delete_user_')) { logDebug(action, 'handleConfirmDelete'); diff --git a/src/models/User.js b/src/models/User.js index 75e3bf7..405b778 100644 --- a/src/models/User.js +++ b/src/models/User.js @@ -103,4 +103,8 @@ export default class User { throw error; } } + + static async recalculateBalance(telegramId) { + + } } \ No newline at end of file From a35bbbf3d92fd9271bb7685420eb55c2c7fecf1f Mon Sep 17 00:00:00 2001 From: Artyom Ashirov <1323ED5@gmail.com> Date: Tue, 19 Nov 2024 19:19:05 +0300 Subject: [PATCH 2/2] Recalculate balance --- src/models/User.js | 17 ++++++ src/models/Wallet.js | 121 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 src/models/Wallet.js diff --git a/src/models/User.js b/src/models/User.js index 405b778..80a66db 100644 --- a/src/models/User.js +++ b/src/models/User.js @@ -1,4 +1,5 @@ import db from '../config/database.js'; +import Wallet from "./Wallet.js"; export default class User { static async create(telegramId, username) { @@ -105,6 +106,22 @@ export default class User { } static async recalculateBalance(telegramId) { + const user = await User.getById(telegramId); + if (!user) { + return; + } + + const archivedBalance = await Wallet.getArchivedWalletsBalance(user.id); + const activeBalance = await Wallet.getActiveWalletsBalance(user.id); + + const purchases = await db.getAsync( + `SELECT SUM(total_price) as total_sum FROM purchases WHERE user_id = ?`, + [user.id] + ); + + const userTotalBalance = (activeBalance + archivedBalance) - (purchases?.total_sum || 0); + + await db.runAsync(`UPDATE users SET total_balance = ? WHERE id = ?`, [userTotalBalance, user.id]); } } \ No newline at end of file diff --git a/src/models/Wallet.js b/src/models/Wallet.js new file mode 100644 index 0000000..caa606b --- /dev/null +++ b/src/models/Wallet.js @@ -0,0 +1,121 @@ +import db from "../config/database.js"; +import WalletService from "../utils/walletService.js"; + +export default class Wallet { + static getBaseWalletType(walletType) { + if (walletType.includes('TRC-20')) return 'TRON'; + if (walletType.includes('ERC-20')) return 'ETH'; + return walletType; + } + + static async getArchivedWallets(userId) { + const archivedWallets = await db.allAsync(` + SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type LIKE '%_%' + `, [userId]); + + const btcAddress = archivedWallets.find(w => w.wallet_type.startsWith('BTC'))?.address; + const ltcAddress = archivedWallets.find(w => w.wallet_type.startsWith('LTC'))?.address; + const tronAddress = archivedWallets.find(w => w.wallet_type.startsWith('TRON'))?.address; + const ethAddress = archivedWallets.find(w => w.wallet_type.startsWith('ETH'))?.address; + + return { + btc: btcAddress, + ltc: ltcAddress, + tron: tronAddress, + eth: ethAddress, + wallets: archivedWallets + } + } + + static async getActiveWallets(userId) { + const activeWallets = await db.allAsync( + `SELECT wallet_type, address FROM crypto_wallets WHERE user_id = ? ORDER BY wallet_type`, + [userId] + ) + + const btcAddress = activeWallets.find(w => w.wallet_type === 'BTC')?.address; + const ltcAddress = activeWallets.find(w => w.wallet_type === 'LTC')?.address; + const tronAddress = activeWallets.find(w => w.wallet_type === 'TRON')?.address; + const ethAddress = activeWallets.find(w => w.wallet_type === 'ETH')?.address; + + return { + btc: btcAddress, + ltc: ltcAddress, + tron: tronAddress, + eth: ethAddress, + wallets: activeWallets + } + } + + static async getActiveWalletsBalance(userId) { + const activeWallets = await this.getActiveWallets(userId); + + const walletService = new WalletService( + activeWallets.btc, + activeWallets.ltc, + activeWallets.tron, + activeWallets.eth, + userId, + Date.now() - 30 * 24 * 60 * 60 * 1000 + ); + + const balances = await walletService.getAllBalances(); + + let totalUsdBalance = 0; + + for (const [type, balance] of Object.entries(balances)) { + const baseType = this.getBaseWalletType(type); + const wallet = activeWallets.wallets.find(w => + w.wallet_type === baseType || + (type.includes('TRC-20') && w.wallet_type === 'TRON') || + (type.includes('ERC-20') && w.wallet_type === 'ETH') + ); + + if (!wallet) { + continue; + } + + if (wallet) { + totalUsdBalance += balance.usdValue; + } + } + + return totalUsdBalance; + } + + static async getArchivedWalletsBalance(userId) { + const archiveWallets = await this.getArchivedWallets(userId); + + const walletService = new WalletService( + archiveWallets.btc, + archiveWallets.ltc, + archiveWallets.tron, + archiveWallets.eth, + userId, + Date.now() - 30 * 24 * 60 * 60 * 1000 + ); + + const balances = await walletService.getAllBalances(); + + let totalUsdBalance = 0; + + for (const [type, balance] of Object.entries(balances)) { + const baseType = this.getBaseWalletType(type); + const wallet = archiveWallets.wallets.find(w => + w.wallet_type === baseType || + (type.includes('TRC-20') && w.wallet_type.startsWith('TRON')) || + (type.includes('ERC-20') && w.wallet_type.startsWith('ETH')) + ); + + if (!wallet) { + continue; + } + + if (wallet) { + totalUsdBalance += balance.usdValue; + } + } + + return totalUsdBalance; + } +} \ No newline at end of file