Merge pull request 'main' (#27) from main into feature/admin-section
Reviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
@@ -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
|
||||
)
|
||||
`);
|
||||
|
||||
@@ -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}`}]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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) {
|
||||
@@ -47,7 +49,7 @@ export default class UserProductHandler {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
await this.bot.sendMessage(chatId, message, { reply_markup: keyboard });
|
||||
await this.bot.sendMessage(chatId, message, {reply_markup: keyboard});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in showProducts:', error);
|
||||
@@ -72,7 +74,7 @@ export default class UserProductHandler {
|
||||
text: loc.city,
|
||||
callback_data: `shop_city_${country}_${loc.city}`
|
||||
}]),
|
||||
[{ text: '« Back to Countries', callback_data: 'shop_start' }]
|
||||
[{text: '« Back to Countries', callback_data: 'shop_start'}]
|
||||
]
|
||||
};
|
||||
|
||||
@@ -107,7 +109,7 @@ export default class UserProductHandler {
|
||||
text: loc.district,
|
||||
callback_data: `shop_district_${country}_${city}_${loc.district}`
|
||||
}]),
|
||||
[{ text: '« Back to Cities', callback_data: `shop_country_${country}` }]
|
||||
[{text: '« Back to Cities', callback_data: `shop_country_${country}`}]
|
||||
]
|
||||
};
|
||||
|
||||
@@ -153,7 +155,7 @@ export default class UserProductHandler {
|
||||
message_id: messageId,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '« Back to Districts', callback_data: `shop_city_${country}_${city}` }
|
||||
{text: '« Back to Districts', callback_data: `shop_city_${country}_${city}`}
|
||||
]]
|
||||
}
|
||||
}
|
||||
@@ -167,7 +169,7 @@ export default class UserProductHandler {
|
||||
text: cat.name,
|
||||
callback_data: `shop_category_${location.id}_${cat.id}`
|
||||
}]),
|
||||
[{ text: '« Back to Districts', callback_data: `shop_city_${country}_${city}` }]
|
||||
[{text: '« Back to Districts', callback_data: `shop_city_${country}_${city}`}]
|
||||
]
|
||||
};
|
||||
|
||||
@@ -209,7 +211,10 @@ export default class UserProductHandler {
|
||||
message_id: messageId,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '« Back to Categories', callback_data: `shop_district_${location.country}_${location.city}_${location.district}` }
|
||||
{
|
||||
text: '« Back to Categories',
|
||||
callback_data: `shop_district_${location.country}_${location.city}_${location.district}`
|
||||
}
|
||||
]]
|
||||
}
|
||||
}
|
||||
@@ -223,7 +228,10 @@ export default class UserProductHandler {
|
||||
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}` }]
|
||||
[{
|
||||
text: '« Back to Categories',
|
||||
callback_data: `shop_district_${location.country}_${location.city}_${location.district}`
|
||||
}]
|
||||
]
|
||||
};
|
||||
|
||||
@@ -277,7 +285,10 @@ export default class UserProductHandler {
|
||||
message_id: messageId,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}` }
|
||||
{
|
||||
text: '« Back to Subcategories',
|
||||
callback_data: `shop_category_${locationId}_${categoryId}`
|
||||
}
|
||||
]]
|
||||
}
|
||||
}
|
||||
@@ -291,7 +302,7 @@ export default class UserProductHandler {
|
||||
text: `${prod.name} - $${prod.price}`,
|
||||
callback_data: `shop_product_${prod.id}`
|
||||
}]),
|
||||
[{ text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}` }]
|
||||
[{text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}`}]
|
||||
]
|
||||
};
|
||||
|
||||
@@ -345,28 +356,35 @@ Subcategory: ${product.subcategory_name}
|
||||
let photoMessageId = null;
|
||||
|
||||
// First send the photo if it exists
|
||||
let photoMessage;
|
||||
if (product.photo_url) {
|
||||
const photoMessage = await this.bot.sendPhoto(chatId, product.photo_url);
|
||||
photoMessageId = photoMessage.message_id;
|
||||
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: '🛒 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: '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}` }]
|
||||
[{
|
||||
text: `« Back to ${product.subcategory_name}`,
|
||||
callback_data: `shop_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}_${photoMessageId}`
|
||||
}]
|
||||
]
|
||||
};
|
||||
|
||||
@@ -429,7 +447,7 @@ Subcategory: ${product.subcategory_name}
|
||||
callback_data: `decrease_quantity_${productId}`,
|
||||
callback_game: newQuantity <= 1 ? {} : null
|
||||
},
|
||||
{ text: newQuantity.toString(), callback_data: 'current_quantity' },
|
||||
{text: newQuantity.toString(), callback_data: 'current_quantity'},
|
||||
{
|
||||
text: '➕',
|
||||
callback_data: `increase_quantity_${productId}`,
|
||||
@@ -438,7 +456,7 @@ Subcategory: ${product.subcategory_name}
|
||||
];
|
||||
|
||||
await this.bot.editMessageReplyMarkup(
|
||||
{ inline_keyboard: keyboard },
|
||||
{inline_keyboard: keyboard},
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId
|
||||
@@ -492,7 +510,7 @@ Subcategory: ${product.subcategory_name}
|
||||
callback_data: `decrease_quantity_${productId}`,
|
||||
callback_game: newQuantity <= 1 ? {} : null
|
||||
},
|
||||
{ text: newQuantity.toString(), callback_data: 'current_quantity' },
|
||||
{text: newQuantity.toString(), callback_data: 'current_quantity'},
|
||||
{
|
||||
text: '➕',
|
||||
callback_data: `increase_quantity_${productId}`,
|
||||
@@ -501,7 +519,7 @@ Subcategory: ${product.subcategory_name}
|
||||
];
|
||||
|
||||
await this.bot.editMessageReplyMarkup(
|
||||
{ inline_keyboard: keyboard },
|
||||
{inline_keyboard: keyboard},
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId
|
||||
@@ -554,7 +572,7 @@ Subcategory: ${product.subcategory_name}
|
||||
{
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '➕ Add Wallet', callback_data: 'add_wallet' }
|
||||
{text: '➕ Add Wallet', callback_data: 'add_wallet'}
|
||||
]]
|
||||
}
|
||||
}
|
||||
@@ -568,7 +586,7 @@ Subcategory: ${product.subcategory_name}
|
||||
text: `Pay with ${wallet.wallet_type}`,
|
||||
callback_data: `pay_with_${wallet.wallet_type}_${productId}_${quantity}`
|
||||
}]),
|
||||
[{ text: '« Cancel', callback_data: `shop_product_${productId}` }]
|
||||
[{text: '« Cancel', callback_data: `shop_product_${productId}`}]
|
||||
]
|
||||
};
|
||||
|
||||
@@ -590,6 +608,87 @@ Subcategory: ${product.subcategory_name}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -619,7 +718,7 @@ Subcategory: ${product.subcategory_name}
|
||||
{
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '🛍 Browse Products', callback_data: 'shop_start' }
|
||||
{text: '🛍 Browse Products', callback_data: 'shop_start'}
|
||||
]]
|
||||
}
|
||||
}
|
||||
@@ -644,7 +743,7 @@ Subcategory: ${product.subcategory_name}
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '🛍 Browse Products', callback_data: 'shop_start' }
|
||||
{text: '🛍 Browse Products', callback_data: 'shop_start'}
|
||||
]]
|
||||
}
|
||||
});
|
||||
|
||||
11
src/index.js
11
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');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import db from '../config/database.js';
|
||||
import Wallet from "./Wallet.js";
|
||||
|
||||
export default class User {
|
||||
static async create(telegramId, username) {
|
||||
@@ -103,4 +104,24 @@ export default class User {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
121
src/models/Wallet.js
Normal file
121
src/models/Wallet.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user