Compare commits
62 Commits
feature/do
...
refactorin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ff01f5f68 | ||
| ea7f5dbd11 | |||
| 0d8230e93c | |||
|
|
7fd4cfa671 | ||
|
|
64e6397570 | ||
|
|
5d4f56e265 | ||
| dcc678a42b | |||
| 68a220de2e | |||
|
|
275a6ab493 | ||
|
|
1f7d1a144c | ||
|
|
0371f70546 | ||
|
|
3c942da274 | ||
|
|
a760cb2d23 | ||
|
|
d4eb5d46c3 | ||
|
|
3153ecf325 | ||
|
|
97073eb939 | ||
|
|
627f9e417e | ||
|
|
c1276e1187 | ||
|
|
1cb0467f6c | ||
|
|
563d9d27c7 | ||
|
|
56698c28c4 | ||
|
|
a35bbbf3d9 | ||
|
|
6fa273f0b6 | ||
|
|
626435a3de | ||
| 1b1a9468a0 | |||
| 072067eb4a | |||
|
|
b45aa35527 | ||
|
|
e3f2e87fcc | ||
|
|
44ae5a6631 | ||
|
|
ec96f67dfe | ||
|
|
aed9a2ba56 | ||
|
|
772cd738ca | ||
|
|
18e09119b3 | ||
|
|
d760c77593 | ||
|
|
89a7a8b9c5 | ||
|
|
e1eda05afe | ||
|
|
df3149e59a | ||
|
|
de5e405093 | ||
|
|
4251f1a0bd | ||
| 7c79e7fd94 | |||
| f7d21d9d0d | |||
|
|
373e8e2567 | ||
|
|
b45f7daa6f | ||
|
|
ebad9da439 | ||
| 82afcf9854 | |||
| 9c4253ba58 | |||
|
|
f504d5fb7b | ||
| 38ba2356bc | |||
| 1cabe48594 | |||
| 5eec9586b7 | |||
| 97079ce1d5 | |||
| f737d46802 | |||
| 82132c7466 | |||
|
|
d506d79367 | ||
|
|
93290dee1c | ||
|
|
2beaa324fa | ||
|
|
52779d20ab | ||
|
|
a3bde60b87 | ||
|
|
7825b801a0 | ||
|
|
6de9ef1aa9 | ||
|
|
893ac470c3 | ||
|
|
40400ee2b0 |
30
.gitea/workflows/demo.yaml
Normal file
30
.gitea/workflows/demo.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
name: "Telegram Shop Bot CI [DEMO]"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["demo"]
|
||||
pull_request:
|
||||
branches: ["demo"]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest # Запускаем на универсальной платформе
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js 22
|
||||
run: |
|
||||
# Устанавливаем nvm (Node Version Manager)
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
nvm install 22
|
||||
nvm use 22
|
||||
node -v # Проверка установленной версии Node.js
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
33
.gitea/workflows/dev.yaml
Normal file
33
.gitea/workflows/dev.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "Telegram Shop Bot CI [DEV]"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest # Запуск на Ubuntu
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js 22
|
||||
run: |
|
||||
echo "Устанавливаем Node.js 22"
|
||||
curl -sL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
node -v # Проверка установленной версии Node.js
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
echo "Устанавливаем зависимости"
|
||||
npm install
|
||||
|
||||
- name: Run lint
|
||||
run: |
|
||||
echo "Запуск линтинга"
|
||||
npm run lint
|
||||
30
.gitea/workflows/prod.yaml
Normal file
30
.gitea/workflows/prod.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
name: "Telegram Shop Bot CI [PROD]"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["prod"]
|
||||
pull_request:
|
||||
branches: ["prod"]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest # Запускаем на универсальной платформе
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js 22
|
||||
run: |
|
||||
# Устанавливаем nvm (Node Version Manager)
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
nvm install 22
|
||||
nvm use 22
|
||||
node -v # Проверка установленной версии Node.js
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1 @@
|
||||
node_modules
|
||||
db
|
||||
@@ -4,8 +4,8 @@ WORKDIR /app
|
||||
|
||||
COPY package*.json /app/
|
||||
COPY src/ /app/src/
|
||||
COPY db/shop.db /app/shop.db
|
||||
#COPY db/shop.db /app/shop.db
|
||||
|
||||
RUN npm install
|
||||
|
||||
CMD ["node", "src/index.js"]
|
||||
CMD ["node", "src/index.js"]
|
||||
|
||||
BIN
corrupt-photo.jpg
Normal file
BIN
corrupt-photo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
db/shop.db
BIN
db/shop.db
Binary file not shown.
@@ -10,7 +10,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
- BOT_TOKEN=7626758249:AAEdcbXJpW1VsnJJtc8kZ5VBsYMFR242wgk
|
||||
- ADMIN_IDS=732563549,390431690
|
||||
- ADMIN_IDS=732563549,390431690,217546867
|
||||
- SUPPORT_LINK=https://t.me/neroworm
|
||||
- CATALOG_PATH=./catalog
|
||||
volumes:
|
||||
|
||||
1175
package-lock.json
generated
1175
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,12 @@
|
||||
"dev": "nodemon src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.7.7",
|
||||
"bip39": "^3.1.0",
|
||||
"bitcoinjs-lib": "^6.1.6",
|
||||
"crypto-js": "^4.2.0",
|
||||
"decompress": "^4.2.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"ecpair": "^2.1.0",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
|
||||
@@ -90,9 +90,13 @@ const initDb = async () => {
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
telegram_id TEXT UNIQUE NOT NULL,
|
||||
username TEXT,
|
||||
country TEXT,
|
||||
city TEXT,
|
||||
district TEXT,
|
||||
status INTEGER DEFAULT 0,
|
||||
total_balance REAL DEFAULT 0,
|
||||
bonus_balance REAL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
17
src/context/bot.js
Normal file
17
src/context/bot.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import TelegramBot from "node-telegram-bot-api";
|
||||
import config from "../config/config.js";
|
||||
|
||||
const initBot = () => {
|
||||
try {
|
||||
const bot = new TelegramBot(config.BOT_TOKEN, {polling: true});
|
||||
console.log('Bot initialized successfully');
|
||||
return bot;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize bot:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const bot = initBot();
|
||||
|
||||
export default bot;
|
||||
2
src/context/userStates.js
Normal file
2
src/context/userStates.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const userStates = new Map();
|
||||
export default userStates;
|
||||
@@ -1,35 +0,0 @@
|
||||
import db from '../config/database.js';
|
||||
import config from '../config/config.js';
|
||||
|
||||
export default class AdminHandler {
|
||||
constructor(bot) {
|
||||
this.bot = bot;
|
||||
}
|
||||
|
||||
isAdmin(userId) {
|
||||
return config.ADMIN_IDS.includes(userId.toString());
|
||||
}
|
||||
|
||||
async handleAdminCommand(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
if (!this.isAdmin(msg.from.id)) {
|
||||
await this.bot.sendMessage(chatId, 'Unauthorized access.');
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
['👥 Manage Users', '📦 Manage Products'],
|
||||
['💰 Manage Wallets', '📍 Manage Locations'],
|
||||
['💾 Database Backup']
|
||||
],
|
||||
resize_keyboard: true
|
||||
}
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, 'Admin Panel:', keyboard);
|
||||
}
|
||||
|
||||
}
|
||||
161
src/handlers/adminHandlers/adminDumpHandler.js
Normal file
161
src/handlers/adminHandlers/adminDumpHandler.js
Normal file
@@ -0,0 +1,161 @@
|
||||
import config from '../../config/config.js';
|
||||
import fs from "fs";
|
||||
import db from "../../config/database.js";
|
||||
import archiver from "archiver";
|
||||
import decompress from "decompress";
|
||||
import bot from "../../context/bot.js";
|
||||
import userStates from "../../context/userStates.js";
|
||||
|
||||
export default class AdminDumpHandler {
|
||||
static isAdmin(userId) {
|
||||
return config.ADMIN_IDS.includes(userId.toString());
|
||||
}
|
||||
|
||||
static async handleDump(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
if (!this.isAdmin(msg.from.id)) {
|
||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{text: '📥 Import dump', callback_data: 'import_database'},
|
||||
{text: '📤 Export dump', callback_data: 'export_database'},
|
||||
],
|
||||
[{text: '« Back', callback_data: 'admin_menu'}]
|
||||
]
|
||||
}
|
||||
|
||||
await bot.sendMessage(chatId, 'Choose an option', {reply_markup: keyboard});
|
||||
}
|
||||
|
||||
static async handleExportDatabase(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
const tables = [
|
||||
"categories",
|
||||
"crypto_wallets",
|
||||
"locations",
|
||||
"products",
|
||||
"purchases",
|
||||
"transactions",
|
||||
"users"
|
||||
]
|
||||
|
||||
const dumpPath = "./dump"
|
||||
|
||||
try {
|
||||
fs.rmdirSync(dumpPath, {recursive: true, force: true});
|
||||
fs.rmSync(`${dumpPath}/dump.zip`);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
fs.mkdirSync(dumpPath);
|
||||
|
||||
for (const table of tables) {
|
||||
const result = await db.allAsync(`SELECT * FROM ${table}`);
|
||||
const tableData = JSON.stringify(result);
|
||||
fs.writeFileSync(`${dumpPath}/${table}.json`, tableData);
|
||||
}
|
||||
|
||||
const archive = archiver('zip', {zlib: { level: 9 } });
|
||||
archive.directory(dumpPath, false);
|
||||
|
||||
const output = fs.createWriteStream('./dump.zip');
|
||||
archive.pipe(output);
|
||||
|
||||
await archive.finalize();
|
||||
|
||||
output.on('close', () => {
|
||||
bot.sendDocument(chatId, './dump.zip', {caption: 'Database dump'});
|
||||
});
|
||||
}
|
||||
|
||||
static async handleImportDatabase(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
userStates.set(chatId, { action: 'upload_database_dump' });
|
||||
|
||||
await bot.editMessageText(
|
||||
'Please upload database dump',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static async getDumpStatistic() {
|
||||
const tables = [
|
||||
"categories",
|
||||
"crypto_wallets",
|
||||
"locations",
|
||||
"products",
|
||||
"purchases",
|
||||
"transactions",
|
||||
"users"
|
||||
]
|
||||
|
||||
const stat = {}
|
||||
|
||||
for (const table of tables) {
|
||||
const jsonContent = await fs.readFileSync(`./dump/${table}.json`, 'utf8');
|
||||
const data = JSON.parse(jsonContent);
|
||||
stat[table] = data.length
|
||||
}
|
||||
|
||||
return stat;
|
||||
}
|
||||
|
||||
static async handleDumpImport(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
const state = userStates.get(chatId);
|
||||
|
||||
if (!state || state.action !== 'upload_database_dump') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.isAdmin(msg.from.id)) {
|
||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.document) {
|
||||
if (!msg.document.file_name.endsWith('.zip')) {
|
||||
await bot.sendMessage(chatId, 'Please upload a .zip file.');
|
||||
return true;
|
||||
}
|
||||
|
||||
const file = await bot.getFile(msg.document.file_id);
|
||||
const fileContent = await bot.downloadFile(file.file_id, '.');
|
||||
|
||||
await decompress(fileContent, './dump');
|
||||
|
||||
const statistics = await this.getDumpStatistic();
|
||||
await bot.sendMessage(chatId, JSON.stringify(statistics, null, 2));
|
||||
userStates.delete(chatId);
|
||||
} else {
|
||||
await bot.sendMessage(chatId, 'Please upload a valid .zip file.');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static async confirmImport(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/handlers/adminHandlers/adminHandler.js
Normal file
30
src/handlers/adminHandlers/adminHandler.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import config from '../../config/config.js';
|
||||
import bot from "../../context/bot.js";
|
||||
|
||||
export default class AdminHandler {
|
||||
static isAdmin(userId) {
|
||||
return config.ADMIN_IDS.includes(userId.toString());
|
||||
}
|
||||
|
||||
static async handleAdminCommand(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
if (!this.isAdmin(msg.from.id)) {
|
||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
['👥 Manage Users', '📦 Manage Products'],
|
||||
['💰 Manage Wallets', '📍 Manage Locations'],
|
||||
['💾 Database Backup']
|
||||
],
|
||||
resize_keyboard: true
|
||||
}
|
||||
};
|
||||
|
||||
await bot.sendMessage(chatId, 'Admin Panel:', keyboard);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
import db from '../config/database.js';
|
||||
import Validators from '../utils/validators.js';
|
||||
import config from '../config/config.js';
|
||||
import db from '../../config/database.js';
|
||||
import Validators from '../../utils/validators.js';
|
||||
import config from '../../config/config.js';
|
||||
import userStates from "../../context/userStates.js";
|
||||
import bot from "../../context/bot.js";
|
||||
|
||||
export default class AdminLocationHandler {
|
||||
constructor(bot) {
|
||||
this.bot = bot;
|
||||
this.userStates = new Map();
|
||||
}
|
||||
|
||||
isAdmin(userId) {
|
||||
static isAdmin(userId) {
|
||||
return config.ADMIN_IDS.includes(userId.toString());
|
||||
}
|
||||
|
||||
async handleAddLocation(callbackQuery) {
|
||||
static async handleAddLocation(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
this.userStates.set(chatId, { action: 'add_location' });
|
||||
userStates.set(chatId, { action: 'add_location' });
|
||||
|
||||
await this.bot.editMessageText(
|
||||
await bot.editMessageText(
|
||||
'Please enter the location in the following format:\nCountry|City|District',
|
||||
{
|
||||
chat_id: chatId,
|
||||
@@ -29,15 +30,22 @@ export default class AdminLocationHandler {
|
||||
);
|
||||
}
|
||||
|
||||
async handleLocationInput(msg) {
|
||||
static async handleLocationInput(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const state = this.userStates.get(chatId);
|
||||
const state = userStates.get(chatId);
|
||||
|
||||
if (!state || state.action !== 'add_location') return false;
|
||||
if (!state || state.action !== 'add_location') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.isAdmin(msg.from.id)) {
|
||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = msg.text.split('|').map(s => s.trim());
|
||||
if (parts.length !== 3) {
|
||||
await this.bot.sendMessage(
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
'Invalid format. Please use: Country|City|District'
|
||||
);
|
||||
@@ -47,7 +55,7 @@ export default class AdminLocationHandler {
|
||||
const [country, city, district] = parts;
|
||||
|
||||
if (!Validators.isValidLocation(country, city, district)) {
|
||||
await this.bot.sendMessage(
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
'Invalid location data. All fields are required.'
|
||||
);
|
||||
@@ -65,7 +73,7 @@ export default class AdminLocationHandler {
|
||||
await db.runAsync('COMMIT');
|
||||
|
||||
if (result.changes > 0) {
|
||||
await this.bot.sendMessage(
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
`✅ Location added successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
|
||||
{
|
||||
@@ -81,12 +89,12 @@ export default class AdminLocationHandler {
|
||||
throw new Error('Failed to insert location');
|
||||
}
|
||||
|
||||
this.userStates.delete(chatId);
|
||||
userStates.delete(chatId);
|
||||
} catch (error) {
|
||||
await db.runAsync('ROLLBACK');
|
||||
|
||||
if (error.code === 'SQLITE_CONSTRAINT') {
|
||||
await this.bot.sendMessage(
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
'❌ This location already exists.',
|
||||
{
|
||||
@@ -99,7 +107,7 @@ export default class AdminLocationHandler {
|
||||
);
|
||||
} else {
|
||||
console.error('Error adding location:', error);
|
||||
await this.bot.sendMessage(
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
'❌ Error adding location. Please try again.',
|
||||
{
|
||||
@@ -116,15 +124,17 @@ export default class AdminLocationHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleViewLocations(msg) {
|
||||
static async handleViewLocations(msg) {
|
||||
const chatId = msg.chat?.id || msg.message?.chat.id;
|
||||
const messageId = msg.message?.message_id;
|
||||
|
||||
if (!this.isAdmin(msg.from?.id || msg.message?.from.id)) {
|
||||
await this.bot.sendMessage(chatId, 'Unauthorized access.');
|
||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||
return;
|
||||
}
|
||||
|
||||
userStates.delete(chatId);
|
||||
|
||||
try {
|
||||
const locations = await db.allAsync(`
|
||||
SELECT l.*,
|
||||
@@ -144,13 +154,13 @@ export default class AdminLocationHandler {
|
||||
};
|
||||
|
||||
if (messageId) {
|
||||
await this.bot.editMessageText(message, {
|
||||
await bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} else {
|
||||
await this.bot.sendMessage(chatId, message, { reply_markup: keyboard });
|
||||
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -177,25 +187,29 @@ export default class AdminLocationHandler {
|
||||
};
|
||||
|
||||
if (messageId) {
|
||||
await this.bot.editMessageText(message, {
|
||||
await bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
} else {
|
||||
await this.bot.sendMessage(chatId, message, {
|
||||
await bot.sendMessage(chatId, message, {
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error viewing locations:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.');
|
||||
await bot.sendMessage(chatId, 'Error loading locations. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleDeleteLocation(callbackQuery) {
|
||||
static async handleDeleteLocation(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
@@ -213,13 +227,13 @@ export default class AdminLocationHandler {
|
||||
const keyboard = {
|
||||
inline_keyboard: locations.map(loc => [{
|
||||
text: `${loc.country} > ${loc.city} > ${loc.district} (P:${loc.product_count} C:${loc.category_count})`,
|
||||
callback_data: `confirm_delete_${loc.country}_${loc.city}_${loc.district}`
|
||||
callback_data: `confirm_delete_location_${loc.country}_${loc.city}_${loc.district}`
|
||||
}])
|
||||
};
|
||||
|
||||
keyboard.inline_keyboard.push([{ text: '« Back', callback_data: 'view_locations' }]);
|
||||
|
||||
await this.bot.editMessageText(
|
||||
await bot.editMessageText(
|
||||
'❌ Select location to delete:\n\n*Note:* Deleting a location will also remove all associated products and categories!',
|
||||
{
|
||||
chat_id: chatId,
|
||||
@@ -230,14 +244,18 @@ export default class AdminLocationHandler {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleDeleteLocation:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.');
|
||||
await bot.sendMessage(chatId, 'Error loading locations. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleConfirmDelete(callbackQuery) {
|
||||
static async handleConfirmDelete(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const [country, city, district] = callbackQuery.data
|
||||
.replace('confirm_delete_', '')
|
||||
.replace('confirm_delete_location_', '')
|
||||
.split('_');
|
||||
|
||||
try {
|
||||
@@ -251,7 +269,7 @@ export default class AdminLocationHandler {
|
||||
await db.runAsync('COMMIT');
|
||||
|
||||
if (result.changes > 0) {
|
||||
await this.bot.editMessageText(
|
||||
await bot.editMessageText(
|
||||
`✅ Location deleted successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
@@ -267,7 +285,7 @@ export default class AdminLocationHandler {
|
||||
} catch (error) {
|
||||
await db.runAsync('ROLLBACK');
|
||||
console.error('Error deleting location:', error);
|
||||
await this.bot.sendMessage(
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
'❌ Error deleting location. Please try again.',
|
||||
{
|
||||
@@ -278,4 +296,35 @@ export default class AdminLocationHandler {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static async backToMenu(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
|
||||
const keyboard = {
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
['👥 Manage Users', '📦 Manage Products'],
|
||||
['💰 Manage Wallets', '📍 Manage Locations'],
|
||||
['💾 Database Backup']
|
||||
],
|
||||
resize_keyboard: true
|
||||
}
|
||||
};
|
||||
|
||||
await bot.editMessageText(
|
||||
`You we're returned to the admin menu`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
|
||||
userStates.delete(chatId);
|
||||
}
|
||||
}
|
||||
1058
src/handlers/adminHandlers/adminProductHandler.js
Normal file
1058
src/handlers/adminHandlers/adminProductHandler.js
Normal file
File diff suppressed because it is too large
Load Diff
416
src/handlers/adminHandlers/adminUserHandler.js
Normal file
416
src/handlers/adminHandlers/adminUserHandler.js
Normal file
@@ -0,0 +1,416 @@
|
||||
import config from '../../config/config.js';
|
||||
import db from '../../config/database.js';
|
||||
import bot from "../../context/bot.js";
|
||||
import UserService from "../../services/userService.js";
|
||||
import userStates from "../../context/userStates.js";
|
||||
|
||||
export default class AdminUserHandler {
|
||||
static isAdmin(userId) {
|
||||
return config.ADMIN_IDS.includes(userId.toString());
|
||||
}
|
||||
|
||||
static async calculateStatistics() {
|
||||
try {
|
||||
const users = await db.allAsync(`
|
||||
SELECT
|
||||
u.*,
|
||||
COUNT(DISTINCT p.id) as total_purchases,
|
||||
COUNT(DISTINCT cw.id) as total_wallets
|
||||
FROM users u
|
||||
LEFT JOIN purchases p ON u.id = p.user_id
|
||||
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
|
||||
LEFT JOIN transactions t ON u.id = t.user_id
|
||||
GROUP BY u.id
|
||||
ORDER BY u.created_at DESC
|
||||
` );
|
||||
|
||||
// Calculate general statistics
|
||||
const totalUsers = users.length;
|
||||
const activeUsers = users.filter(u => u.total_purchases > 0).length;
|
||||
const totalBalance = users.reduce((sum, u) => sum + (u.total_balance || 0), 0);
|
||||
const bonusBalance = users.reduce((sum, u) => sum + (u.bonus_balance || 0), 0);
|
||||
const totalPurchases = users.reduce((sum, u) => sum + (u.total_purchases || 0), 0);
|
||||
|
||||
// Create statistics message
|
||||
let message = `📊 System Statistics\n\n`;
|
||||
message += `👥 Total Users: ${totalUsers}\n`;
|
||||
message += `✅ Active Users: ${activeUsers}\n`;
|
||||
message += `💰 Bonus Balance: $${bonusBalance.toFixed(2)}\n`;
|
||||
message += `💰 Total Balance: $${(totalBalance + bonusBalance).toFixed(2)}\n`;
|
||||
message += `🛍 Total Purchases: ${totalPurchases}`;
|
||||
|
||||
return message;
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
static async viewUserPage(page) {
|
||||
const limit = 10;
|
||||
const offset = (page || 0) * limit;
|
||||
|
||||
const previousPage = page > 0 ? page - 1 : 0;
|
||||
const nextPage = page + 1;
|
||||
|
||||
try {
|
||||
const users = await db.allAsync(`
|
||||
SELECT
|
||||
u.*,
|
||||
COUNT(DISTINCT p.id) as total_purchases,
|
||||
COUNT(DISTINCT cw.id) as total_wallets
|
||||
FROM users u
|
||||
LEFT JOIN purchases p ON u.id = p.user_id
|
||||
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
|
||||
LEFT JOIN transactions t ON u.id = t.user_id
|
||||
GROUP BY u.id
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT ?
|
||||
OFFSET ?
|
||||
`, [limit, offset]);
|
||||
if ((users.length === 0) && (page == 0)) {
|
||||
return {text: 'No users registered yet.'};
|
||||
}
|
||||
|
||||
if ((users.length === 0) && (page > 0)) {
|
||||
return await this.viewUserPage(page - 1);
|
||||
}
|
||||
|
||||
const statistics = await this.calculateStatistics()
|
||||
const message = `${statistics}\n\nSelect a user from the list below:`;
|
||||
|
||||
// Create inline keyboard with user list
|
||||
const keyboard = {
|
||||
inline_keyboard: users.map(user => [{
|
||||
text: `ID: ${user.telegram_id} | Nickname: ${user.username ? "@" + user.username : "None"} | Balance: $${(user.total_balance || 0) + (user.bonus_balance || 0)}`,
|
||||
callback_data: `view_user_${user.telegram_id}`
|
||||
}])
|
||||
};
|
||||
|
||||
keyboard.inline_keyboard.push([
|
||||
{text: `«`, callback_data: `list_users_${previousPage}`},
|
||||
{text: `»`, callback_data: `list_users_${nextPage}`},
|
||||
])
|
||||
|
||||
return {text: message, markup: keyboard}
|
||||
} catch (error) {
|
||||
console.error('Error in handleUserList:', error);
|
||||
return {text: 'Error loading user list. Please try again.'}
|
||||
}
|
||||
}
|
||||
|
||||
static async handleUserList(msg) {
|
||||
if (!this.isAdmin(msg.from.id)) {
|
||||
await bot.sendMessage(msg.chat.id, 'Unauthorized access.');
|
||||
return;
|
||||
}
|
||||
|
||||
const {text, markup} = await this.viewUserPage(0);
|
||||
await bot.sendMessage(msg.chat.id, text, {reply_markup: markup})
|
||||
}
|
||||
|
||||
static async handleUserListPage(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const page = parseInt(callbackQuery.data.replace('list_users_', ''));
|
||||
|
||||
try {
|
||||
const {text, markup} = await this.viewUserPage(page);
|
||||
await bot.editMessageText(text, {
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: markup,
|
||||
parse_mode: 'HTML'
|
||||
});
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static async handleViewUser(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) return;
|
||||
|
||||
const telegramId = callbackQuery.data.replace('view_user_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
const detailedUser = await UserService.getDetailedUserByTelegramId(telegramId);
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
|
||||
if (!detailedUser) {
|
||||
await bot.sendMessage(chatId, 'User not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get recent transactions
|
||||
const transactions = await db.allAsync(`
|
||||
SELECT t.amount, t.created_at, t.wallet_type, t.tx_hash
|
||||
FROM transactions t
|
||||
JOIN users u ON t.user_id = u.id
|
||||
WHERE u.telegram_id = ?
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT 5
|
||||
`, [telegramId]);
|
||||
|
||||
// Get recent purchases
|
||||
const purchases = await db.allAsync(`
|
||||
SELECT p.quantity, p.total_price, p.purchase_date,
|
||||
pr.name as product_name
|
||||
FROM purchases p
|
||||
JOIN products pr ON p.product_id = pr.id
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE u.telegram_id = ?
|
||||
ORDER BY p.purchase_date DESC
|
||||
LIMIT 5
|
||||
`, [telegramId]);
|
||||
|
||||
const message = `
|
||||
👤 User Profile:
|
||||
|
||||
ID: ${telegramId}
|
||||
📍 Location: ${detailedUser.country || 'Not set'}, ${detailedUser.city || 'Not set'}, ${detailedUser.district || 'Not set'}
|
||||
|
||||
📊 Activity:
|
||||
- Total Purchases: ${detailedUser.purchase_count}
|
||||
- Total Spent: $${detailedUser.total_spent || 0}
|
||||
- Active Wallets: ${detailedUser.crypto_wallet_count}
|
||||
- Bonus Balance: $${user.bonus_balance || 0}
|
||||
- Total Balance: $${(user.total_balance || 0) + (user.bonus_balance || 0)}
|
||||
|
||||
💰 Recent Transactions:
|
||||
${transactions.map(t => ` • ${t.amount} ${t.wallet_type} (${t.tx_hash})`).join('\n')}
|
||||
|
||||
🛍 Recent Purchases:
|
||||
${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price}`).join('\n')}
|
||||
|
||||
📅 Registered: ${new Date(detailedUser.created_at).toLocaleString()}
|
||||
`;
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{text: '💰 Edit Balance', callback_data: `edit_user_balance_${telegramId}`},
|
||||
{text: '📍 Edit Location', callback_data: `edit_user_location_${telegramId}`}
|
||||
],
|
||||
[
|
||||
{text: '🚫 Block User', callback_data: `block_user_${telegramId}`},
|
||||
{text: '❌ Delete User', callback_data: `delete_user_${telegramId}`}
|
||||
],
|
||||
[{text: '« Back to User List', callback_data: `list_users_0`}]
|
||||
]
|
||||
};
|
||||
|
||||
await bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'HTML'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleViewUser:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading user details. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleDeleteUser(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const telegramId = callbackQuery.data.replace('delete_user_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{text: '✅ Confirm Delete', callback_data: `confirm_delete_user_${telegramId}`},
|
||||
{text: '❌ Cancel', callback_data: `view_user_${telegramId}`}
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await bot.editMessageText(
|
||||
`⚠️ Are you sure you want to delete user ${telegramId}?\n\nThis action will:\n- Delete all user data\n- Remove all wallets\n- Erase purchase history\n\nThis action cannot be undone!`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'HTML'
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleDeleteUser:', error);
|
||||
await bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleConfirmDelete(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const telegramId = callbackQuery.data.replace('confirm_delete_user_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
await UserService.updateUserStatus(telegramId, 1);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{text: '« Back to User List', callback_data: 'admin_users'}]
|
||||
]
|
||||
};
|
||||
|
||||
try {
|
||||
await bot.sendMessage(telegramId, '⚠️Your account has been deleted by administrator');
|
||||
} catch (e) {
|
||||
// ignore if we can't notify user
|
||||
}
|
||||
|
||||
await bot.editMessageText(
|
||||
`✅ User ${telegramId} has been successfully deleted.`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleConfirmDelete:', error);
|
||||
await bot.sendMessage(chatId, 'Error deleting user. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleBlockUser(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const telegramId = callbackQuery.data.replace('block_user_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{text: '✅ Confirm Block', callback_data: `confirm_block_user_${telegramId}`},
|
||||
{text: '❌ Cancel', callback_data: `view_user_${telegramId}`}
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await bot.editMessageText(
|
||||
`⚠️ Are you sure you want to block user ${telegramId}?`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'HTML'
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleBlockUser:', error);
|
||||
await bot.sendMessage(chatId, 'Error processing block request. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleConfirmBlock(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const telegramId = callbackQuery.data.replace('confirm_block_user_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
await UserService.updateUserStatus(telegramId, 2);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{text: '« Back to User List', callback_data: 'admin_users'}]
|
||||
]
|
||||
};
|
||||
|
||||
try {
|
||||
await bot.sendMessage(telegramId, '⚠️Your account has been blocked by administrator');
|
||||
} catch (e) {
|
||||
// ignore if we can't notify user
|
||||
}
|
||||
|
||||
await bot.editMessageText(
|
||||
`✅ User ${telegramId} has been successfully blocked.`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleConfirmBlock:', error);
|
||||
await bot.sendMessage(chatId, 'Error blocking user. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleEditUserBalance(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const telegramId = callbackQuery.data.replace('edit_user_balance_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
|
||||
if (!user) {
|
||||
await bot.sendMessage(chatId, 'User not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
await bot.editMessageText(
|
||||
`Enter new value for bonus balance. \n\n👥 User: ${telegramId}\n💰 Bonus Balance Now: $${user.bonus_balance.toFixed(2)}`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
}
|
||||
);
|
||||
|
||||
userStates.set(chatId, { action: "edit_bonus_balance", telegram_id: telegramId });
|
||||
} catch (error) {
|
||||
console.error('Error in handleEditUserBalance:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading user wallets. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleBonusBalanceInput(msg) {
|
||||
if (!this.isAdmin(msg.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = msg.chat.id;
|
||||
const state = userStates.get(chatId);
|
||||
|
||||
if (!state || state.action !== 'edit_bonus_balance') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newValue = parseFloat(msg.text);
|
||||
|
||||
if (isNaN(newValue)) {
|
||||
await bot.sendMessage(chatId, 'Invalid value. Try again');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await db.runAsync(`UPDATE users SET bonus_balance = ? WHERE telegram_id = ?`, [newValue, state.telegram_id])
|
||||
await bot.sendMessage(chatId, '✅ Done')
|
||||
} catch (e) {
|
||||
await bot.sendMessage(chatId, 'Something went wrong');
|
||||
}
|
||||
|
||||
userStates.delete(chatId);
|
||||
}
|
||||
}
|
||||
168
src/handlers/adminHandlers/adminUserLocationHandler.js
Normal file
168
src/handlers/adminHandlers/adminUserLocationHandler.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import db from '../../config/database.js';
|
||||
import config from "../../config/config.js";
|
||||
import LocationService from "../../services/locationService.js";
|
||||
import bot from "../../context/bot.js";
|
||||
import UserService from "../../services/userService.js";
|
||||
|
||||
export default class AdminUserLocationHandler {
|
||||
static isAdmin(userId) {
|
||||
return config.ADMIN_IDS.includes(userId.toString());
|
||||
}
|
||||
|
||||
static async handleEditUserLocation(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = callbackQuery.data.replace('edit_user_location_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
|
||||
try {
|
||||
const countries = await LocationService.getCountries();
|
||||
|
||||
if (countries.length === 0) {
|
||||
await bot.editMessageText(
|
||||
'No locations available yet.',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{text: '« Back to User', callback_data: `view_user_${userId}`}
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...countries.map(loc => [{
|
||||
text: loc.country,
|
||||
callback_data: `edit_user_country_${loc.country}_${userId}`
|
||||
}]),
|
||||
[{text: '« Back to User', callback_data: `view_user_${userId}`}]
|
||||
]
|
||||
};
|
||||
|
||||
await bot.editMessageText(
|
||||
'🌍 Select user country:',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleSetLocation:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading countries. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleEditUserCountry(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const [country, userId] = callbackQuery.data.replace('edit_user_country_', '').split("_");
|
||||
|
||||
try {
|
||||
const cities = await LocationService.getCitiesByCountry(country);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...cities.map(loc => [{
|
||||
text: loc.city,
|
||||
callback_data: `edit_user_city_${country}_${loc.city}_${userId}`
|
||||
}]),
|
||||
[{text: '« Back to Countries', callback_data: `edit_user_location_${userId}`}]
|
||||
]
|
||||
};
|
||||
|
||||
await bot.editMessageText(
|
||||
`🏙 Select city in ${country}:`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleSetCountry:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleEditUserCity(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const [country, city, userId] = callbackQuery.data.replace('edit_user_city_', '').split('_');
|
||||
|
||||
try {
|
||||
const districts = LocationService.getDistrictsByCountryAndCity(country, city)
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...districts.map(loc => [{
|
||||
text: loc.district,
|
||||
callback_data: `edit_user_district_${country}_${city}_${loc.district}_${userId}`
|
||||
}]),
|
||||
[{text: '« Back to Cities', callback_data: `edit_user_country_${country}_${userId}`}]
|
||||
]
|
||||
};
|
||||
|
||||
await bot.editMessageText(
|
||||
`📍 Select district in ${city}:`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleSetCity:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleEditUserDistrict(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const [country, city, district, telegramId] = callbackQuery.data.replace('edit_user_district_', '').split('_');
|
||||
|
||||
try {
|
||||
await db.runAsync('BEGIN TRANSACTION');
|
||||
await UserService.updateUserLocation(telegramId.toString(), country, city, district)
|
||||
await db.runAsync('COMMIT');
|
||||
|
||||
await bot.editMessageText(
|
||||
`✅ Location updated successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{text: '« Back to User', callback_data: `view_user_${userId}`}
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
await db.runAsync('ROLLBACK');
|
||||
console.error('Error in handleSetDistrict:', error);
|
||||
await bot.sendMessage(chatId, 'Error updating location. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,580 +0,0 @@
|
||||
import db from '../config/database.js';
|
||||
import config from '../config/config.js';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
export default class AdminProductHandler {
|
||||
constructor(bot) {
|
||||
this.bot = bot;
|
||||
this.userStates = new Map();
|
||||
}
|
||||
|
||||
isAdmin(userId) {
|
||||
return config.ADMIN_IDS.includes(userId.toString());
|
||||
}
|
||||
|
||||
async handleProductManagement(msg) {
|
||||
const chatId = msg.chat?.id || msg.message?.chat.id;
|
||||
|
||||
if (!this.isAdmin(msg.from?.id || msg.message?.from.id)) {
|
||||
await this.bot.sendMessage(chatId, 'Unauthorized access.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const countries = await db.allAsync(
|
||||
'SELECT DISTINCT country FROM locations ORDER BY country'
|
||||
);
|
||||
|
||||
if (countries.length === 0) {
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'No locations available. Please add locations first.',
|
||||
{
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '📍 Manage Locations', callback_data: 'view_locations' }
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: countries.map(loc => [{
|
||||
text: loc.country,
|
||||
callback_data: `prod_country_${loc.country}`
|
||||
}])
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'🌍 Select country to manage products:',
|
||||
{ reply_markup: keyboard }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleProductManagement:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleCountrySelection(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const country = callbackQuery.data.replace('prod_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: `prod_city_${country}_${loc.city}`
|
||||
}]),
|
||||
[{ text: '« Back', callback_data: 'manage_products' }]
|
||||
]
|
||||
};
|
||||
|
||||
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('prod_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: `prod_district_${country}_${city}_${loc.district}`
|
||||
}]),
|
||||
[{ text: '« Back', callback_data: `prod_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('prod_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]
|
||||
);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...categories.map(cat => [{
|
||||
text: cat.name,
|
||||
callback_data: `prod_category_${location.id}_${cat.id}`
|
||||
}]),
|
||||
[{ text: '➕ Add Category', callback_data: `add_category_${location.id}` }],
|
||||
[{ text: '« Back', callback_data: `prod_city_${country}_${city}` }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(
|
||||
'📦 Select or add 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 handleCategoryInput(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const state = this.userStates.get(chatId);
|
||||
|
||||
if (!state || !state.action?.startsWith('add_category_')) return false;
|
||||
|
||||
try {
|
||||
const locationId = state.action.replace('add_category_', '');
|
||||
|
||||
await db.runAsync(
|
||||
'INSERT INTO categories (location_id, name) VALUES (?, ?)',
|
||||
[locationId, msg.text]
|
||||
);
|
||||
|
||||
const location = await db.getAsync(
|
||||
'SELECT country, city, district FROM locations WHERE id = ?',
|
||||
[locationId]
|
||||
);
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
`✅ Category "${msg.text}" added successfully!`,
|
||||
{
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '« Back to Categories', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.userStates.delete(chatId);
|
||||
} catch (error) {
|
||||
if (error.code === 'SQLITE_CONSTRAINT') {
|
||||
await this.bot.sendMessage(chatId, 'This category already exists in this location.');
|
||||
} else {
|
||||
console.error('Error adding category:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error adding category. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleAddCategory(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const locationId = callbackQuery.data.replace('add_category_', '');
|
||||
|
||||
this.userStates.set(chatId, { action: `add_category_${locationId}` });
|
||||
|
||||
await this.bot.editMessageText(
|
||||
'Please enter the name for the new category:',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '❌ Cancel', callback_data: `prod_district_${locationId}` }
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async handleCategorySelection(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_');
|
||||
|
||||
try {
|
||||
const subcategories = await db.allAsync(
|
||||
'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name',
|
||||
[categoryId]
|
||||
);
|
||||
|
||||
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
|
||||
const location = await db.getAsync('SELECT country, city, district FROM locations WHERE id = ?', [locationId]);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...subcategories.map(sub => [{
|
||||
text: sub.name,
|
||||
callback_data: `prod_subcategory_${locationId}_${categoryId}_${sub.id}`
|
||||
}]),
|
||||
[{ text: '➕ Add Subcategory', callback_data: `add_subcategory_${locationId}_${categoryId}` }],
|
||||
[{ text: '✏️ Edit Category', callback_data: `edit_category_${locationId}_${categoryId}` }],
|
||||
[{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(
|
||||
`📦 Category: ${category.name}\nSelect or add 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 handleSubcategoryInput(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const state = this.userStates.get(chatId);
|
||||
|
||||
if (!state || !state.action?.startsWith('add_subcategory_')) return false;
|
||||
|
||||
try {
|
||||
const [locationId, categoryId] = state.action.replace('add_subcategory_', '').split('_');
|
||||
|
||||
await db.runAsync(
|
||||
'INSERT INTO subcategories (category_id, name) VALUES (?, ?)',
|
||||
[categoryId, msg.text]
|
||||
);
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
`✅ Subcategory "${msg.text}" added successfully!`,
|
||||
{
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '« Back to Subcategories', callback_data: `prod_category_${locationId}_${categoryId}` }
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.userStates.delete(chatId);
|
||||
} catch (error) {
|
||||
if (error.code === 'SQLITE_CONSTRAINT') {
|
||||
await this.bot.sendMessage(chatId, 'This subcategory already exists in this category.');
|
||||
} else {
|
||||
console.error('Error adding subcategory:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error adding subcategory. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleAddSubcategory(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const [locationId, categoryId] = callbackQuery.data.replace('add_subcategory_', '').split('_');
|
||||
|
||||
this.userStates.set(chatId, { action: `add_subcategory_${locationId}_${categoryId}` });
|
||||
|
||||
await this.bot.editMessageText(
|
||||
'Please enter the name for the new subcategory:',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '❌ Cancel', callback_data: `prod_category_${locationId}_${categoryId}` }
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async handleSubcategorySelection(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('prod_subcategory_', '').split('_');
|
||||
|
||||
try {
|
||||
const products = await db.allAsync(
|
||||
`SELECT id, name, price, quantity_in_stock
|
||||
FROM products
|
||||
WHERE location_id = ? AND category_id = ? AND subcategory_id = ?
|
||||
ORDER BY name`,
|
||||
[locationId, categoryId, subcategoryId]
|
||||
);
|
||||
|
||||
const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]);
|
||||
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...products.map(prod => [{
|
||||
text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`,
|
||||
callback_data: `view_product_${prod.id}`
|
||||
}]),
|
||||
[{ text: '📥 Import Products', callback_data: `add_product_${locationId}_${categoryId}_${subcategoryId}` }],
|
||||
[{ text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}` }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(
|
||||
`📦 ${category.name} > ${subcategory.name}\nSelect product or import new ones:`,
|
||||
{
|
||||
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 handleAddProduct(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('add_product_', '').split('_');
|
||||
|
||||
try {
|
||||
const location = await db.getAsync(
|
||||
'SELECT country, city, district 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 sampleProducts = [
|
||||
{
|
||||
name: "Sample Product 1",
|
||||
price: 100,
|
||||
description: "Product description",
|
||||
private_data: "Hidden details about the product",
|
||||
quantity_in_stock: 10,
|
||||
photo_url: "https://example.com/photo.jpg",
|
||||
hidden_photo_url: "https://example.com/hidden.jpg",
|
||||
hidden_coordinates: "40.7128,-74.0060",
|
||||
hidden_description: "Secret location details"
|
||||
}
|
||||
];
|
||||
|
||||
const jsonExample = JSON.stringify(sampleProducts, null, 2);
|
||||
const message = `To import products, send a JSON file with an array of products in the following format:\n\n<pre>${jsonExample}</pre>\n\nEach product must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
|
||||
|
||||
this.userStates.set(chatId, {
|
||||
action: 'import_products',
|
||||
locationId,
|
||||
categoryId,
|
||||
subcategoryId
|
||||
});
|
||||
|
||||
await this.bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
parse_mode: 'HTML',
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '❌ Cancel', callback_data: `prod_subcategory_${locationId}_${categoryId}_${subcategoryId}` }
|
||||
]]
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleAddProduct:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error preparing product import. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleProductImport(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const state = this.userStates.get(chatId);
|
||||
|
||||
if (!state || state.action !== 'import_products') return false;
|
||||
|
||||
try {
|
||||
let products;
|
||||
let jsonContent;
|
||||
|
||||
// Handle file upload
|
||||
if (msg.document) {
|
||||
if (!msg.document.file_name.endsWith('.json')) {
|
||||
await this.bot.sendMessage(chatId, 'Please upload a .json file.');
|
||||
return true;
|
||||
}
|
||||
|
||||
const file = await this.bot.getFile(msg.document.file_id);
|
||||
const fileContent = await this.bot.downloadFile(file.file_id, '/tmp');
|
||||
jsonContent = await fs.readFile(fileContent, 'utf8');
|
||||
} else if (msg.text) {
|
||||
jsonContent = msg.text;
|
||||
} else {
|
||||
await this.bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.');
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
products = JSON.parse(jsonContent);
|
||||
if (!Array.isArray(products)) {
|
||||
throw new Error('Input must be an array of products');
|
||||
}
|
||||
} catch (e) {
|
||||
await this.bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.');
|
||||
return true;
|
||||
}
|
||||
|
||||
await db.runAsync('BEGIN TRANSACTION');
|
||||
|
||||
for (const product of products) {
|
||||
await db.runAsync(
|
||||
`INSERT INTO products (
|
||||
location_id, category_id, subcategory_id,
|
||||
name, price, description, private_data,
|
||||
quantity_in_stock, photo_url, hidden_photo_url,
|
||||
hidden_coordinates, hidden_description
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
state.locationId, state.categoryId, state.subcategoryId,
|
||||
product.name, product.price, product.description, product.private_data,
|
||||
product.quantity_in_stock, product.photo_url, product.hidden_photo_url,
|
||||
product.hidden_coordinates, product.hidden_description
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
await db.runAsync('COMMIT');
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
`✅ Successfully imported ${products.length} products!`,
|
||||
{
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '« Back to Products', callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}` }
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.userStates.delete(chatId);
|
||||
} catch (error) {
|
||||
await db.runAsync('ROLLBACK');
|
||||
console.error('Error importing products:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleViewProduct(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const productId = callbackQuery.data.replace('view_product_', '');
|
||||
|
||||
try {
|
||||
const product = await db.getAsync(
|
||||
`SELECT p.*, c.name as category_name, s.name as subcategory_name,
|
||||
l.country, l.city, l.district
|
||||
FROM products p
|
||||
JOIN categories c ON p.category_id = c.id
|
||||
JOIN subcategories s ON p.subcategory_id = s.id
|
||||
JOIN locations l ON p.location_id = l.id
|
||||
WHERE p.id = ?`,
|
||||
[productId]
|
||||
);
|
||||
|
||||
if (!product) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
|
||||
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: '✏️ Edit', callback_data: `edit_product_${productId}` },
|
||||
{ text: '❌ Delete', callback_data: `delete_product_${productId}` }
|
||||
],
|
||||
[{ text: '« Back', callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}` }]
|
||||
]
|
||||
};
|
||||
|
||||
// Send product photos
|
||||
if (product.photo_url) {
|
||||
await this.bot.sendPhoto(chatId, product.photo_url, { caption: 'Public photo' });
|
||||
}
|
||||
if (product.hidden_photo_url) {
|
||||
await this.bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' });
|
||||
}
|
||||
|
||||
await this.bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleViewProduct:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
import User from '../models/User.js';
|
||||
import config from '../config/config.js';
|
||||
import db from '../config/database.js';
|
||||
|
||||
export default class AdminUserHandler {
|
||||
constructor(bot) {
|
||||
this.bot = bot;
|
||||
}
|
||||
|
||||
isAdmin(userId) {
|
||||
return config.ADMIN_IDS.includes(userId.toString());
|
||||
}
|
||||
|
||||
async handleUserList(msg) {
|
||||
if (!this.isAdmin(msg.from.id)) {
|
||||
await this.bot.sendMessage(msg.chat.id, 'Unauthorized access.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const users = await db.allAsync(`
|
||||
SELECT
|
||||
u.*,
|
||||
COUNT(DISTINCT p.id) as total_purchases,
|
||||
COUNT(DISTINCT cw.id) as total_wallets,
|
||||
COALESCE(SUM(t.amount), 0) as total_balance
|
||||
FROM users u
|
||||
LEFT JOIN purchases p ON u.id = p.user_id
|
||||
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
|
||||
LEFT JOIN transactions t ON u.id = t.user_id
|
||||
GROUP BY u.id
|
||||
ORDER BY u.created_at DESC
|
||||
`);
|
||||
|
||||
if (users.length === 0) {
|
||||
await this.bot.sendMessage(msg.chat.id, 'No users registered yet.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate general statistics
|
||||
const totalUsers = users.length;
|
||||
const activeUsers = users.filter(u => u.total_purchases > 0).length;
|
||||
const totalBalance = users.reduce((sum, u) => sum + (u.total_balance || 0), 0);
|
||||
const totalPurchases = users.reduce((sum, u) => sum + (u.total_purchases || 0), 0);
|
||||
|
||||
// Create statistics message
|
||||
let message = `📊 System Statistics\n\n`;
|
||||
message += `👥 Total Users: ${totalUsers}\n`;
|
||||
message += `✅ Active Users: ${activeUsers}\n`;
|
||||
message += `💰 Total Balance: $${totalBalance.toFixed(2)}\n`;
|
||||
message += `🛍 Total Purchases: ${totalPurchases}\n\n`;
|
||||
message += `Select a user from the list below:`;
|
||||
|
||||
// Create inline keyboard with user list
|
||||
const keyboard = {
|
||||
inline_keyboard: users.map(user => [{
|
||||
text: `ID: ${user.telegram_id} | Balance: $${user.total_balance || 0}`,
|
||||
callback_data: `view_user_${user.telegram_id}`
|
||||
}])
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(msg.chat.id, message, {
|
||||
parse_mode: 'HTML',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleUserList:', error);
|
||||
await this.bot.sendMessage(msg.chat.id, 'Error loading user list. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleViewUser(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) return;
|
||||
|
||||
const userId = callbackQuery.data.replace('view_user_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
const userStats = await db.getAsync(`
|
||||
SELECT
|
||||
u.*,
|
||||
COUNT(DISTINCT p.id) as purchase_count,
|
||||
COALESCE(SUM(p.total_price), 0) as total_spent,
|
||||
COUNT(DISTINCT cw.id) as wallet_count,
|
||||
COALESCE(SUM(t.amount), 0) as total_balance
|
||||
FROM users u
|
||||
LEFT JOIN purchases p ON u.id = p.user_id
|
||||
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
|
||||
LEFT JOIN transactions t ON u.id = t.user_id
|
||||
WHERE u.telegram_id = ?
|
||||
GROUP BY u.id
|
||||
`, [userId]);
|
||||
|
||||
if (!userStats) {
|
||||
await this.bot.sendMessage(chatId, 'User not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get recent transactions
|
||||
const transactions = await db.allAsync(`
|
||||
SELECT t.amount, t.created_at, t.wallet_type, t.tx_hash
|
||||
FROM transactions t
|
||||
JOIN users u ON t.user_id = u.id
|
||||
WHERE u.telegram_id = ?
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT 5
|
||||
`, [userId]);
|
||||
|
||||
// Get recent purchases
|
||||
const purchases = await db.allAsync(`
|
||||
SELECT p.quantity, p.total_price, p.purchase_date,
|
||||
pr.name as product_name
|
||||
FROM purchases p
|
||||
JOIN products pr ON p.product_id = pr.id
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE u.telegram_id = ?
|
||||
ORDER BY p.purchase_date DESC
|
||||
LIMIT 5
|
||||
`, [userId]);
|
||||
|
||||
const message = `
|
||||
👤 User Profile:
|
||||
|
||||
ID: ${userId}
|
||||
📍 Location: ${userStats.country || 'Not set'}, ${userStats.city || 'Not set'}, ${userStats.district || 'Not set'}
|
||||
|
||||
📊 Activity:
|
||||
- Total Purchases: ${userStats.purchase_count}
|
||||
- Total Spent: $${userStats.total_spent || 0}
|
||||
- Active Wallets: ${userStats.wallet_count}
|
||||
- Total Balance: $${userStats.total_balance || 0}
|
||||
|
||||
💰 Recent Transactions:
|
||||
${transactions.map(t => ` • ${t.amount} ${t.wallet_type} (${t.tx_hash})`).join('\n')}
|
||||
|
||||
🛍 Recent Purchases:
|
||||
${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price}`).join('\n')}
|
||||
|
||||
📅 Registered: ${new Date(userStats.created_at).toLocaleString()}
|
||||
`;
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '💰 Edit Balance', callback_data: `edit_user_balance_${userId}` },
|
||||
{ text: '📍 Edit Location', callback_data: `edit_user_location_${userId}` }
|
||||
],
|
||||
[
|
||||
{ text: '🚫 Block User', callback_data: `block_user_${userId}` },
|
||||
{ text: '❌ Delete User', callback_data: `delete_user_${userId}` }
|
||||
],
|
||||
[{ text: '« Back to User List', callback_data: 'admin_users' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'HTML'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleViewUser:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading user details. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleDeleteUser(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) return;
|
||||
|
||||
const userId = callbackQuery.data.replace('delete_user_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Confirm Delete', callback_data: `confirm_delete_user_${userId}` },
|
||||
{ text: '❌ Cancel', callback_data: `view_user_${userId}` }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(
|
||||
`⚠️ Are you sure you want to delete user ${userId}?\n\nThis action will:\n- Delete all user data\n- Remove all wallets\n- Erase purchase history\n\nThis action cannot be undone!`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'HTML'
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleDeleteUser:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleConfirmDelete(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) return;
|
||||
|
||||
const userId = callbackQuery.data.replace('confirm_delete_user_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
await User.delete(userId);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{ text: '« Back to User List', callback_data: 'admin_users' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(
|
||||
`✅ User ${userId} has been successfully deleted.`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleConfirmDelete:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error deleting user. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleEditUserBalance(callbackQuery) {
|
||||
if (!this.isAdmin(callbackQuery.from.id)) return;
|
||||
|
||||
const userId = callbackQuery.data.replace('edit_user_balance_', '');
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
try {
|
||||
const user = await User.getById(userId);
|
||||
|
||||
if (!user) {
|
||||
await this.bot.sendMessage(chatId, 'User not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const wallets = await db.allAsync(`
|
||||
SELECT wallet_type, address
|
||||
FROM crypto_wallets
|
||||
WHERE user_id = (SELECT id FROM users WHERE telegram_id = ?)
|
||||
ORDER BY wallet_type
|
||||
`, [userId]);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...wallets.map(wallet => [
|
||||
{ text: `${wallet.wallet_type}: ${wallet.address}`, callback_data: `edit_wallet_${wallet.wallet_type}` }
|
||||
]),
|
||||
[{ text: '« Back', callback_data: `view_user_${userId}` }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(
|
||||
`Select wallet to edit for user ${userId}:`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleEditUserBalance:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading user wallets. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import db from '../config/database.js';
|
||||
import User from '../models/User.js';
|
||||
|
||||
export default class UserHandler {
|
||||
constructor(bot) {
|
||||
this.bot = bot;
|
||||
}
|
||||
|
||||
async showProfile(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const userId = msg.from.id;
|
||||
|
||||
try {
|
||||
const userStats = await User.getUserStats(userId);
|
||||
|
||||
if (!userStats) {
|
||||
await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
|
||||
return;
|
||||
}
|
||||
|
||||
const locationText = userStats.country && userStats.city && userStats.district
|
||||
? `${userStats.country}, ${userStats.city}, ${userStats.district}`
|
||||
: 'Not set';
|
||||
|
||||
const text = `
|
||||
👤 *Your Profile*
|
||||
|
||||
📱 Telegram ID: \`${userId}\`
|
||||
📍 Location: ${locationText}
|
||||
|
||||
📊 Statistics:
|
||||
├ Total Purchases: ${userStats.purchase_count || 0}
|
||||
├ Total Spent: $${userStats.total_spent || 0}
|
||||
├ Active Wallets: ${userStats.crypto_wallet_count || 0}
|
||||
└ Total Balance: $${userStats.total_balance || 0}
|
||||
|
||||
📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()}
|
||||
`;
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{ text: '📍 Set Location', callback_data: 'set_location' }],
|
||||
[{ text: '❌ Delete Account', callback_data: 'delete_account' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, text, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in showProfile:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading profile. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleStart(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const userId = msg.from.id;
|
||||
|
||||
try {
|
||||
// Create user profile
|
||||
await User.create(userId);
|
||||
|
||||
const keyboard = {
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
['📦 Products', '👤 Profile'],
|
||||
['🛍 Purchases', '💰 Wallets']
|
||||
],
|
||||
resize_keyboard: true
|
||||
}
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'Welcome to the shop! Choose an option:',
|
||||
keyboard
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleStart:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error creating user profile. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleBackToProfile(callbackQuery) {
|
||||
await this.showProfile({
|
||||
chat: { id: callbackQuery.message.chat.id },
|
||||
from: { id: callbackQuery.from.id }
|
||||
});
|
||||
await this.bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
|
||||
}
|
||||
}
|
||||
120
src/handlers/userHandlers/userHandler.js
Normal file
120
src/handlers/userHandlers/userHandler.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import config from "../../config/config.js";
|
||||
import bot from "../../context/bot.js";
|
||||
import UserService from "../../services/userService.js";
|
||||
|
||||
export default class UserHandler {
|
||||
static async canUseBot(msg) {
|
||||
const telegramId = msg.from.id;
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{text: "Contact support", url: config.SUPPORT_LINK}]
|
||||
]
|
||||
};
|
||||
|
||||
switch (user?.status) {
|
||||
case 0:
|
||||
return true;
|
||||
case 1:
|
||||
await bot.sendMessage(telegramId, '⚠️Your account has been deleted by administrator', {reply_markup: keyboard});
|
||||
return false;
|
||||
case 2:
|
||||
await bot.sendMessage(telegramId, '⚠️Your account has been blocked by administrator', {reply_markup: keyboard});
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static async showProfile(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const telegramId = msg.from.id;
|
||||
|
||||
try {
|
||||
await UserService.recalculateUserBalanceByTelegramId(telegramId);
|
||||
const userStats = await UserService.getDetailedUserByTelegramId(telegramId);
|
||||
|
||||
if (!userStats) {
|
||||
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
|
||||
return;
|
||||
}
|
||||
|
||||
const locationText = userStats.country && userStats.city && userStats.district
|
||||
? `${userStats.country}, ${userStats.city}, ${userStats.district}`
|
||||
: 'Not set';
|
||||
|
||||
const text = `
|
||||
👤 *Your Profile*
|
||||
|
||||
📱 Telegram ID: \`${telegramId}\`
|
||||
📍 Location: ${locationText}
|
||||
|
||||
📊 Statistics:
|
||||
├ Total Purchases: ${userStats.purchase_count || 0}
|
||||
├ Total Spent: $${userStats.total_spent || 0}
|
||||
├ Active Wallets: ${userStats.crypto_wallet_count || 0}
|
||||
├ Bonus Balance: $${userStats.bonus_balance || 0}
|
||||
└ Total Balance: $${(userStats.total_balance || 0) + (userStats.bonus_balance || 0)}
|
||||
|
||||
📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()}
|
||||
`;
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{text: '📍 Set Location', callback_data: 'set_location'}],
|
||||
[{text: '❌ Delete Account', callback_data: 'delete_account'}]
|
||||
]
|
||||
};
|
||||
|
||||
await bot.sendMessage(chatId, text, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in showProfile:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading profile. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleStart(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const telegramId = msg.from.id;
|
||||
const username = msg.chat.username;
|
||||
|
||||
try {
|
||||
// Create user profile
|
||||
await UserService.createUser({
|
||||
telegram_id: telegramId,
|
||||
username: username
|
||||
});
|
||||
|
||||
const keyboard = {
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
['📦 Products', '👤 Profile'],
|
||||
['🛍 Purchases', '💰 Wallets']
|
||||
],
|
||||
resize_keyboard: true
|
||||
}
|
||||
};
|
||||
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
'Welcome to the shop! Choose an option:',
|
||||
keyboard
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleStart:', error);
|
||||
await bot.sendMessage(chatId, 'Error creating user profile. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleBackToProfile(callbackQuery) {
|
||||
await this.showProfile({
|
||||
chat: {id: callbackQuery.message.chat.id},
|
||||
from: {id: callbackQuery.from.id}
|
||||
});
|
||||
await bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
|
||||
}
|
||||
}
|
||||
147
src/handlers/userHandlers/userLocationHandler.js
Normal file
147
src/handlers/userHandlers/userLocationHandler.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import db from '../../config/database.js';
|
||||
import LocationService from "../../services/locationService.js";
|
||||
import bot from "../../context/bot.js";
|
||||
import UserService from "../../services/userService.js";
|
||||
|
||||
export default class UserLocationHandler {
|
||||
static async handleSetLocation(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
|
||||
try {
|
||||
const countries = await LocationService.getCountries();
|
||||
|
||||
if (countries.length === 0) {
|
||||
await bot.editMessageText(
|
||||
'No locations available yet.',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{text: '« Back to Profile', callback_data: 'back_to_profile'}
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...countries.map(loc => [{
|
||||
text: loc.country,
|
||||
callback_data: `set_country_${loc.country}`
|
||||
}]),
|
||||
[{text: '« Back to Profile', callback_data: 'back_to_profile'}]
|
||||
]
|
||||
};
|
||||
|
||||
await bot.editMessageText(
|
||||
'🌍 Select your country:',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleSetLocation:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading countries. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleSetCountry(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const country = callbackQuery.data.replace('set_country_', '');
|
||||
|
||||
try {
|
||||
const cities = await LocationService.getCitiesByCountry(country);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...cities.map(loc => [{
|
||||
text: loc.city,
|
||||
callback_data: `set_city_${country}_${loc.city}`
|
||||
}]),
|
||||
[{text: '« Back to Countries', callback_data: 'set_location'}]
|
||||
]
|
||||
};
|
||||
|
||||
await bot.editMessageText(
|
||||
`🏙 Select city in ${country}:`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleSetCountry:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleSetCity(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const [country, city] = callbackQuery.data.replace('set_city_', '').split('_');
|
||||
|
||||
try {
|
||||
const districts = await LocationService.getDistrictsByCountryAndCity(country, city);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...districts.map(loc => [{
|
||||
text: loc.district,
|
||||
callback_data: `set_district_${country}_${city}_${loc.district}`
|
||||
}]),
|
||||
[{text: '« Back to Cities', callback_data: `set_country_${country}`}]
|
||||
]
|
||||
};
|
||||
|
||||
await bot.editMessageText(
|
||||
`📍 Select district in ${city}:`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleSetCity:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleSetDistrict(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const [country, city, district] = callbackQuery.data.replace('set_district_', '').split('_');
|
||||
|
||||
try {
|
||||
await db.runAsync('BEGIN TRANSACTION');
|
||||
await UserService.updateUserLocation(telegramId, country, city, district);
|
||||
await db.runAsync('COMMIT');
|
||||
|
||||
await bot.editMessageText(
|
||||
`✅ Location updated successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{text: '« Back to Profile', callback_data: 'back_to_profile'}
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
await db.runAsync('ROLLBACK');
|
||||
console.error('Error in handleSetDistrict:', error);
|
||||
await bot.sendMessage(chatId, 'Error updating location. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
637
src/handlers/userHandlers/userProductHandler.js
Normal file
637
src/handlers/userHandlers/userProductHandler.js
Normal file
@@ -0,0 +1,637 @@
|
||||
import db from '../../config/database.js';
|
||||
import config from "../../config/config.js";
|
||||
import LocationService from "../../services/locationService.js";
|
||||
import bot from "../../context/bot.js";
|
||||
import userStates from "../../context/userStates.js";
|
||||
import ProductService from "../../services/productService.js";
|
||||
import CategoryService from "../../services/categoryService.js";
|
||||
import UserService from "../../services/userService.js";
|
||||
import PurchaseService from "../../services/purchaseService.js";
|
||||
|
||||
export default class UserProductHandler {
|
||||
static async showProducts(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const messageId = msg?.message_id;
|
||||
|
||||
try {
|
||||
const countries = await LocationService.getCountries()
|
||||
|
||||
if (countries.length === 0) {
|
||||
const message = 'No products available at the moment.';
|
||||
if (messageId) {
|
||||
await bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: messageId
|
||||
});
|
||||
} else {
|
||||
await 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 bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
await bot.sendMessage(chatId, message, {reply_markup: keyboard});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in showProducts:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static 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 LocationService.getCitiesByCountry(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 bot.editMessageText(
|
||||
`🏙 Select city in ${country}:`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleCountrySelection:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static 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 LocationService.getDistrictsByCountryAndCity(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 bot.editMessageText(
|
||||
`📍 Select district in ${city}:`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleCitySelection:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static 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 LocationService.getLocation(country, city, district);
|
||||
|
||||
if (!location) {
|
||||
throw new Error('Location not found');
|
||||
}
|
||||
|
||||
const categories = await CategoryService.getCategoriesByLocationId(location.id);
|
||||
|
||||
if (categories.length === 0) {
|
||||
await 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 bot.editMessageText(
|
||||
'📦 Select category:',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleDistrictSelection:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading categories. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static 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 CategoryService.getSubcategoriesByCategoryId(categoryId);
|
||||
const location = await LocationService.getLocationById(locationId);
|
||||
|
||||
if (subcategories.length === 0) {
|
||||
await 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 bot.editMessageText(
|
||||
'📦 Select subcategory:',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleCategorySelection:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading subcategories. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static 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 bot.deleteMessage(chatId, photoMessageId);
|
||||
} catch (error) {
|
||||
console.error('Error deleting photo message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const products = await ProductService.getProductsByLocationAndCategory(locationId, categoryId, subcategoryId);
|
||||
const subcategory = await CategoryService.getSubcategoryById(subcategoryId);
|
||||
|
||||
if (products.length === 0) {
|
||||
await 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 bot.editMessageText(
|
||||
`📦 Products in ${subcategory.name}:`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleSubcategorySelection:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static 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 ProductService.getDetailedProductById(productId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
|
||||
// Delete the previous message
|
||||
await 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 bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'});
|
||||
} catch (e) {
|
||||
photoMessage = await 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 bot.sendMessage(chatId, message, {
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'HTML'
|
||||
});
|
||||
|
||||
// Store the current quantity and photo message ID in user state
|
||||
userStates.set(chatId, {
|
||||
action: 'buying_product',
|
||||
productId,
|
||||
quantity: 1,
|
||||
photoMessageId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleProductSelection:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading product details. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handleIncreaseQuantity(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const productId = callbackQuery.data.replace('increase_quantity_', '');
|
||||
const state = userStates.get(chatId);
|
||||
|
||||
try {
|
||||
const product = await ProductService.getProductById(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 bot.answerCallbackQuery(callbackQuery.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock);
|
||||
|
||||
// Update state
|
||||
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 bot.editMessageReplyMarkup(
|
||||
{inline_keyboard: keyboard},
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId
|
||||
}
|
||||
);
|
||||
|
||||
await bot.answerCallbackQuery(callbackQuery.id);
|
||||
} catch (error) {
|
||||
console.error('Error in handleIncreaseQuantity:', error);
|
||||
await bot.answerCallbackQuery(callbackQuery.id);
|
||||
}
|
||||
}
|
||||
|
||||
static async handleDecreaseQuantity(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const productId = callbackQuery.data.replace('decrease_quantity_', '');
|
||||
const state = userStates.get(chatId);
|
||||
|
||||
try {
|
||||
const product = await ProductService.getProductById(productId)
|
||||
|
||||
if (!product) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
|
||||
const currentQuantity = state?.quantity || 1;
|
||||
|
||||
// If already at minimum, silently ignore
|
||||
if (currentQuantity <= 1) {
|
||||
await bot.answerCallbackQuery(callbackQuery.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const newQuantity = Math.max(currentQuantity - 1, 1);
|
||||
|
||||
// Update state
|
||||
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 bot.editMessageReplyMarkup(
|
||||
{inline_keyboard: keyboard},
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId
|
||||
}
|
||||
);
|
||||
|
||||
await bot.answerCallbackQuery(callbackQuery.id);
|
||||
} catch (error) {
|
||||
console.error('Error in handleDecreaseQuantity:', error);
|
||||
await bot.answerCallbackQuery(callbackQuery.id);
|
||||
}
|
||||
}
|
||||
|
||||
static async handleBuyProduct(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const productId = callbackQuery.data.replace('buy_product_', '');
|
||||
const state = userStates.get(chatId);
|
||||
|
||||
try {
|
||||
const user = await UserService.getUserByTelegramId(telegramId)
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const product = await ProductService.getProductById(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 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 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 bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async handlePay(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
|
||||
const state = userStates.get(chatId);
|
||||
|
||||
try {
|
||||
await UserService.recalculateUserBalanceByTelegramId(telegramId);
|
||||
const user = await UserService.getUserByTelegramId(telegramId)
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const product = await ProductService.getProductById(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) {
|
||||
userStates.delete(chatId);
|
||||
await bot.editMessageText(`Not enough money`, {
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await PurchaseService.createPurchase(user.id, product.id, walletType, quantity, totalPrice)
|
||||
|
||||
let hiddenPhotoMessage;
|
||||
if (product.hidden_photo_url) {
|
||||
try {
|
||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'});
|
||||
} catch (e) {
|
||||
hiddenPhotoMessage = await 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 bot.sendMessage(chatId, message, {reply_markup: keyboard});
|
||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||
} catch (error) {
|
||||
console.error('Error in handleBuyProduct:', error);
|
||||
await bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
160
src/handlers/userHandlers/userPurchaseHandler.js
Normal file
160
src/handlers/userHandlers/userPurchaseHandler.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import config from "../../config/config.js";
|
||||
import PurchaseService from "../../services/purchaseService.js";
|
||||
import UserService from "../../services/userService.js";
|
||||
import bot from "../../context/bot.js";
|
||||
import ProductService from "../../services/productService.js";
|
||||
|
||||
export default class UserPurchaseHandler {
|
||||
static async viewPurchasePage(userId, page) {
|
||||
try {
|
||||
const limit = 10;
|
||||
const offset = (page || 0) * limit;
|
||||
|
||||
const previousPage = page > 0 ? page - 1 : 0;
|
||||
const nextPage = page + 1;
|
||||
|
||||
const purchases = await PurchaseService.getPurchasesByUserId(userId, limit, offset);
|
||||
|
||||
if ((purchases.length === 0) && (page == 0)) {
|
||||
return {
|
||||
text: 'You haven\'t made any purchases yet.',
|
||||
markup: [[
|
||||
{text: '🛍 Browse Products', callback_data: 'shop_start'}
|
||||
]]
|
||||
}
|
||||
}
|
||||
|
||||
if ((purchases.length === 0) && (page > 0)) {
|
||||
return await this.viewPurchasePage(userId, previousPage);
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...purchases.map(item => [{
|
||||
text: `${item.product_name} [${new Date(item.purchase_date).toLocaleString()}]`,
|
||||
callback_data: `view_purchase_${item.id}`
|
||||
}]),
|
||||
]
|
||||
};
|
||||
|
||||
keyboard.inline_keyboard.push([
|
||||
{text: `«`, callback_data: `list_purchases_${previousPage}`},
|
||||
{text: `»`, callback_data: `list_purchases_${nextPage}`},
|
||||
]);
|
||||
|
||||
keyboard.inline_keyboard.push([
|
||||
{text: '🛍 Browse Products', callback_data: 'shop_start'}
|
||||
]);
|
||||
|
||||
return {
|
||||
text: `📦 Select purchase to view detailed information:`,
|
||||
markup: keyboard
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in showPurchases:', error);
|
||||
return {text: 'Error loading purchase history. Please try again.'};
|
||||
}
|
||||
}
|
||||
|
||||
static async handlePurchaseListPage(callbackQuery) {
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
const page = callbackQuery.data.replace('list_purchases_', '');
|
||||
|
||||
try {
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
|
||||
if (!user) {
|
||||
await bot.sendMessage(chatId, 'User not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const {text, markup} = await this.viewPurchasePage(user.id, parseInt(page));
|
||||
await bot.editMessageText(text, {
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
reply_markup: markup,
|
||||
parse_mode: 'Markdown',
|
||||
});
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static async showPurchases(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const telegramId = msg.from.id;
|
||||
|
||||
try {
|
||||
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
|
||||
if (!user) {
|
||||
await bot.sendMessage(chatId, 'User not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const {text, markup} = await this.viewPurchasePage(user.id, 0);
|
||||
|
||||
await bot.sendMessage(chatId, text, {reply_markup: markup, parse_mode: 'Markdown'});
|
||||
} catch (error) {
|
||||
console.error('Error in handleSubcategorySelection:', error);
|
||||
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
static async viewPurchase(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const purchaseId = callbackQuery.data.replace('view_purchase_', '');
|
||||
|
||||
const purchase = await PurchaseService.getPurchaseById(purchaseId);
|
||||
|
||||
if (!purchase) {
|
||||
await bot.sendMessage(chatId, "No such purchase");
|
||||
return;
|
||||
}
|
||||
|
||||
const product = await ProductService.getProductById(purchase.product_id)
|
||||
|
||||
if (!product) {
|
||||
await bot.sendMessage(chatId, "No such product");
|
||||
return;
|
||||
}
|
||||
|
||||
let hiddenPhotoMessage;
|
||||
if (product.hidden_photo_url) {
|
||||
try {
|
||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'});
|
||||
} catch (e) {
|
||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'})
|
||||
}
|
||||
}
|
||||
|
||||
const message = `
|
||||
📦 Purchase Details:
|
||||
Name: ${purchase.product_name}
|
||||
Quantity: ${purchase.quantity}
|
||||
Total: $${purchase.total_price}
|
||||
Location: ${purchase.country}, ${purchase.city}
|
||||
Payment: ${purchase.wallet_type}
|
||||
Date: ${new Date(purchase.purchase_date).toLocaleString()}
|
||||
|
||||
🔒 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 bot.sendMessage(chatId, message, {reply_markup: keyboard});
|
||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,19 @@
|
||||
import db from '../config/database.js';
|
||||
import User from '../models/User.js';
|
||||
import WalletGenerator from '../utils/walletGenerator.js';
|
||||
import WalletService from '../utils/walletService.js';
|
||||
import db from '../../config/database.js';
|
||||
import WalletGenerator from '../../utils/walletGenerator.js';
|
||||
import WalletService from '../../utils/walletService.js';
|
||||
import UserService from "../../services/userService.js";
|
||||
import bot from "../../context/bot.js";
|
||||
|
||||
export default class UserWalletsHandler {
|
||||
constructor(bot) {
|
||||
this.bot = bot;
|
||||
this.userStates = new Map();
|
||||
}
|
||||
|
||||
async showBalance(msg) {
|
||||
static async showBalance(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const userId = msg.from.id;
|
||||
const telegramId = msg.from.id;
|
||||
|
||||
try {
|
||||
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
|
||||
const user = await UserService.getUserByTelegramId(telegramId.toString());
|
||||
|
||||
if (!user) {
|
||||
await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
|
||||
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,16 +88,17 @@ export default class UserWalletsHandler {
|
||||
]);
|
||||
}
|
||||
|
||||
await this.bot.sendMessage(chatId, message, {
|
||||
await bot.sendMessage(chatId, message, {
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in showBalance:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading balance. Please try again.');
|
||||
await bot.sendMessage(chatId, 'Error loading balance. Please try again.');
|
||||
}
|
||||
}
|
||||
async handleAddWallet(callbackQuery) {
|
||||
|
||||
static async handleAddWallet(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
const cryptoOptions = [
|
||||
@@ -122,7 +119,7 @@ export default class UserWalletsHandler {
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(
|
||||
await bot.editMessageText(
|
||||
'🔐 Select cryptocurrency to generate wallet:',
|
||||
{
|
||||
chat_id: chatId,
|
||||
@@ -131,13 +128,15 @@ export default class UserWalletsHandler {
|
||||
}
|
||||
);
|
||||
}
|
||||
async handleGenerateWallet(callbackQuery) {
|
||||
|
||||
static async handleGenerateWallet(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const userId = callbackQuery.from.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
|
||||
|
||||
try {
|
||||
const user = await User.getById(userId);
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
@@ -148,7 +147,7 @@ export default class UserWalletsHandler {
|
||||
// Generate new wallets
|
||||
const mnemonic = await WalletGenerator.generateMnemonic();
|
||||
const wallets = await WalletGenerator.generateWallets(mnemonic);
|
||||
const encryptedMnemonic = await WalletGenerator.encryptMnemonic(mnemonic, userId);
|
||||
const encryptedMnemonic = await WalletGenerator.encryptMnemonic(mnemonic, telegramId);
|
||||
|
||||
// Get the base wallet type (ETH for ERC-20, TRON for TRC-20)
|
||||
const baseType = this.getBaseWalletType(walletType);
|
||||
@@ -197,7 +196,7 @@ export default class UserWalletsHandler {
|
||||
|
||||
message += `\n⚠️ Important: Your recovery phrase has been securely stored. Keep your wallet address safe!`;
|
||||
|
||||
await this.bot.editMessageText(message, {
|
||||
await bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
parse_mode: 'Markdown',
|
||||
@@ -215,7 +214,7 @@ export default class UserWalletsHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating wallet:', error);
|
||||
await this.bot.editMessageText(
|
||||
await bot.editMessageText(
|
||||
'❌ Error generating wallet. Please try again.',
|
||||
{
|
||||
chat_id: chatId,
|
||||
@@ -229,12 +228,13 @@ export default class UserWalletsHandler {
|
||||
);
|
||||
}
|
||||
}
|
||||
async handleTopUpWallet(callbackQuery) {
|
||||
|
||||
static async handleTopUpWallet(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const userId = callbackQuery.from.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
|
||||
try {
|
||||
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
|
||||
// Get crypto wallets
|
||||
const cryptoWallets = await db.allAsync(`
|
||||
@@ -245,7 +245,7 @@ export default class UserWalletsHandler {
|
||||
`, [user.id]);
|
||||
|
||||
if (cryptoWallets.length === 0) {
|
||||
await this.bot.editMessageText(
|
||||
await bot.editMessageText(
|
||||
'You don\'t have any wallets yet.',
|
||||
{
|
||||
chat_id: chatId,
|
||||
@@ -295,7 +295,7 @@ export default class UserWalletsHandler {
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(message, {
|
||||
await bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
parse_mode: 'Markdown',
|
||||
@@ -303,15 +303,16 @@ export default class UserWalletsHandler {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleTopUpWallet:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading wallets. Please try again.');
|
||||
await bot.sendMessage(chatId, 'Error loading wallets. Please try again.');
|
||||
}
|
||||
}
|
||||
async handleWalletHistory(callbackQuery) {
|
||||
|
||||
static async handleWalletHistory(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const userId = callbackQuery.from.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
|
||||
try {
|
||||
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
|
||||
const user = UserService.getUserByTelegramId(telegramId);
|
||||
|
||||
const transactions = await db.allAsync(`
|
||||
SELECT type, amount, tx_hash, created_at, wallet_type
|
||||
@@ -322,7 +323,7 @@ export default class UserWalletsHandler {
|
||||
`, [user.id]);
|
||||
|
||||
if (transactions.length === 0) {
|
||||
await this.bot.editMessageText(
|
||||
await bot.editMessageText(
|
||||
'No transactions found.',
|
||||
{
|
||||
chat_id: chatId,
|
||||
@@ -346,7 +347,7 @@ export default class UserWalletsHandler {
|
||||
message += `🕒 ${date}\n\n`;
|
||||
});
|
||||
|
||||
await this.bot.editMessageText(message, {
|
||||
await bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
parse_mode: 'Markdown',
|
||||
@@ -358,15 +359,16 @@ export default class UserWalletsHandler {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleWalletHistory:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
|
||||
await bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
|
||||
}
|
||||
}
|
||||
async handleViewArchivedWallets(callbackQuery) {
|
||||
|
||||
static async handleViewArchivedWallets(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const userId = callbackQuery.from.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
|
||||
try {
|
||||
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
|
||||
const user = await UserService.getUserByTelegramId(telegramId.toString());
|
||||
|
||||
// Get archived wallets and validate timestamps
|
||||
const archivedWallets = await db.allAsync(`
|
||||
@@ -384,7 +386,7 @@ export default class UserWalletsHandler {
|
||||
});
|
||||
|
||||
if (validArchivedWallets.length === 0) {
|
||||
await this.bot.editMessageText(
|
||||
await bot.editMessageText(
|
||||
'No archived wallets found.',
|
||||
{
|
||||
chat_id: chatId,
|
||||
@@ -482,7 +484,7 @@ export default class UserWalletsHandler {
|
||||
|
||||
message += `💰 *Total Value of Archived Wallets:* $${totalUsdValue.toFixed(2)}`;
|
||||
|
||||
await this.bot.editMessageText(message, {
|
||||
await bot.editMessageText(message, {
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
parse_mode: 'Markdown',
|
||||
@@ -494,15 +496,16 @@ export default class UserWalletsHandler {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleViewArchivedWallets:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading archived wallets. Please try again.');
|
||||
await bot.sendMessage(chatId, 'Error loading archived wallets. Please try again.');
|
||||
}
|
||||
}
|
||||
async handleRefreshBalance(callbackQuery) {
|
||||
|
||||
static async handleRefreshBalance(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
|
||||
try {
|
||||
await this.bot.editMessageText(
|
||||
await bot.editMessageText(
|
||||
'🔄 Refreshing balances...',
|
||||
{
|
||||
chat_id: chatId,
|
||||
@@ -517,10 +520,10 @@ export default class UserWalletsHandler {
|
||||
});
|
||||
|
||||
// Delete the "refreshing" message
|
||||
await this.bot.deleteMessage(chatId, messageId);
|
||||
await bot.deleteMessage(chatId, messageId);
|
||||
} catch (error) {
|
||||
console.error('Error in handleRefreshBalance:', error);
|
||||
await this.bot.editMessageText(
|
||||
await bot.editMessageText(
|
||||
'❌ Error refreshing balances. Please try again.',
|
||||
{
|
||||
chat_id: chatId,
|
||||
@@ -534,21 +537,23 @@ export default class UserWalletsHandler {
|
||||
);
|
||||
}
|
||||
}
|
||||
async handleBackToBalance(callbackQuery) {
|
||||
|
||||
static async handleBackToBalance(callbackQuery) {
|
||||
await this.showBalance({
|
||||
chat: { id: callbackQuery.message.chat.id },
|
||||
from: { id: callbackQuery.from.id }
|
||||
});
|
||||
await this.bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
|
||||
await bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
getBaseWalletType(walletType) {
|
||||
static getBaseWalletType(walletType) {
|
||||
if (walletType.includes('TRC-20')) return 'TRON';
|
||||
if (walletType.includes('ERC-20')) return 'ETH';
|
||||
return walletType;
|
||||
}
|
||||
getWalletAddress(wallets, walletType) {
|
||||
|
||||
static getWalletAddress(wallets, walletType) {
|
||||
if (walletType.includes('TRC-20')) return wallets.TRON.address;
|
||||
if (walletType.includes('ERC-20')) return wallets.ETH.address;
|
||||
if (walletType === 'BTC') return wallets.BTC.address;
|
||||
@@ -556,7 +561,8 @@ export default class UserWalletsHandler {
|
||||
if (walletType === 'ETH') return wallets.ETH.address;
|
||||
throw new Error('Invalid wallet type');
|
||||
}
|
||||
getNetworkName(walletType) {
|
||||
|
||||
static getNetworkName(walletType) {
|
||||
if (walletType.includes('TRC-20')) return 'Tron Network (TRC-20)';
|
||||
if (walletType.includes('ERC-20')) return 'Ethereum Network (ERC-20)';
|
||||
if (walletType === 'BTC') return 'Bitcoin Network';
|
||||
@@ -1,160 +0,0 @@
|
||||
import db from '../config/database.js';
|
||||
import Validators from '../utils/validators.js';
|
||||
|
||||
export default class UserLocationHandler {
|
||||
constructor(bot) {
|
||||
this.bot = bot;
|
||||
}
|
||||
|
||||
async handleSetLocation(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
|
||||
try {
|
||||
const countries = await db.allAsync('SELECT DISTINCT country FROM locations ORDER BY country');
|
||||
|
||||
if (countries.length === 0) {
|
||||
await this.bot.editMessageText(
|
||||
'No locations available yet.',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '« Back to Profile', callback_data: 'back_to_profile' }
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...countries.map(loc => [{
|
||||
text: loc.country,
|
||||
callback_data: `set_country_${loc.country}`
|
||||
}]),
|
||||
[{ text: '« Back to Profile', callback_data: 'back_to_profile' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(
|
||||
'🌍 Select your country:',
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleSetLocation:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading countries. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleSetCountry(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const country = callbackQuery.data.replace('set_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: `set_city_${country}_${loc.city}`
|
||||
}]),
|
||||
[{ text: '« Back to Countries', callback_data: 'set_location' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.editMessageText(
|
||||
`🏙 Select city in ${country}:`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in handleSetCountry:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleSetCity(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const [country, city] = callbackQuery.data.replace('set_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: `set_district_${country}_${city}_${loc.district}`
|
||||
}]),
|
||||
[{ text: '« Back to Cities', callback_data: `set_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 handleSetCity:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleSetDistrict(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const userId = callbackQuery.from.id;
|
||||
const [country, city, district] = callbackQuery.data.replace('set_district_', '').split('_');
|
||||
|
||||
try {
|
||||
await db.runAsync('BEGIN TRANSACTION');
|
||||
|
||||
await db.runAsync(
|
||||
'UPDATE users SET country = ?, city = ?, district = ? WHERE telegram_id = ?',
|
||||
[country, city, district, userId.toString()]
|
||||
);
|
||||
|
||||
await db.runAsync('COMMIT');
|
||||
|
||||
await this.bot.editMessageText(
|
||||
`✅ Location updated successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '« Back to Profile', callback_data: 'back_to_profile' }
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
await db.runAsync('ROLLBACK');
|
||||
console.error('Error in handleSetDistrict:', error);
|
||||
await this.bot.sendMessage(chatId, 'Error updating location. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,656 +0,0 @@
|
||||
import db from '../config/database.js';
|
||||
import User from '../models/User.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
|
||||
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
|
||||
}
|
||||
],
|
||||
[{ 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 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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
559
src/index.js
559
src/index.js
@@ -1,278 +1,361 @@
|
||||
import TelegramBot from 'node-telegram-bot-api';
|
||||
import config from './config/config.js';
|
||||
import UserHandler from './handlers/userHandler.js';
|
||||
import UserProductHandler from './handlers/userProductHandler.js';
|
||||
import UserWalletsHandler from './handlers/userWalletsHandler.js';
|
||||
import UserLocationHandler from './handlers/userLocationHandler.js';
|
||||
import AdminHandler from './handlers/adminHandler.js';
|
||||
import AdminUserHandler from './handlers/adminUserHandler.js';
|
||||
import AdminLocationHandler from './handlers/adminLocationHandler.js';
|
||||
import AdminProductHandler from './handlers/adminProductHandler.js';
|
||||
import adminUserHandler from './handlers/adminHandlers/adminUserHandler.js';
|
||||
import ErrorHandler from './utils/errorHandler.js';
|
||||
import User from './models/User.js';
|
||||
import bot from "./context/bot.js";
|
||||
import userHandler from "./handlers/userHandlers/userHandler.js";
|
||||
import userPurchaseHandler from "./handlers/userHandlers/userPurchaseHandler.js";
|
||||
import userLocationHandler from "./handlers/userHandlers/userLocationHandler.js";
|
||||
import userProductHandler from "./handlers/userHandlers/userProductHandler.js";
|
||||
import userWalletsHandler from "./handlers/userHandlers/userWalletsHandler.js";
|
||||
import adminHandler from "./handlers/adminHandlers/adminHandler.js";
|
||||
import adminUserLocationHandler from "./handlers/adminHandlers/adminUserLocationHandler.js";
|
||||
import adminDumpHandler from "./handlers/adminHandlers/adminDumpHandler.js";
|
||||
import adminLocationHandler from "./handlers/adminHandlers/adminLocationHandler.js";
|
||||
import adminProductHandler from "./handlers/adminHandlers/adminProductHandler.js";
|
||||
|
||||
// Debug logging function
|
||||
const logDebug = (action, functionName) => {
|
||||
console.log(`[DEBUG] Button Press: ${action}`);
|
||||
console.log(`[DEBUG] Calling Function: ${functionName}`);
|
||||
console.log(`[DEBUG] Button Press: ${action}`);
|
||||
console.log(`[DEBUG] Calling Function: ${functionName}`);
|
||||
};
|
||||
|
||||
const initBot = () => {
|
||||
try {
|
||||
const bot = new TelegramBot(config.BOT_TOKEN, { polling: true });
|
||||
console.log('Bot initialized successfully');
|
||||
return bot;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize bot:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const bot = initBot();
|
||||
const userHandler = new UserHandler(bot);
|
||||
const userProductHandler = new UserProductHandler(bot);
|
||||
const userWalletsHandler = new UserWalletsHandler(bot);
|
||||
const userLocationHandler = new UserLocationHandler(bot);
|
||||
const adminHandler = new AdminHandler(bot);
|
||||
const adminUserHandler = new AdminUserHandler(bot);
|
||||
const adminLocationHandler = new AdminLocationHandler(bot);
|
||||
const adminProductHandler = new AdminProductHandler(bot);
|
||||
|
||||
// Start command - Create user profile
|
||||
bot.onText(/\/start/, async (msg) => {
|
||||
logDebug('/start', 'handleStart');
|
||||
try {
|
||||
await userHandler.handleStart(msg);
|
||||
} catch (error) {
|
||||
await ErrorHandler.handleError(bot, msg.chat.id, error, 'start command');
|
||||
}
|
||||
logDebug('/start', 'handleStart');
|
||||
|
||||
const canUse = await userHandler.canUseBot(msg);
|
||||
|
||||
if (!canUse) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await userHandler.handleStart(msg);
|
||||
} catch (error) {
|
||||
await ErrorHandler.handleError(bot, msg.chat.id, error, 'start command');
|
||||
}
|
||||
});
|
||||
|
||||
// Admin command
|
||||
bot.onText(/\/admin/, async (msg) => {
|
||||
logDebug('/admin', 'handleAdminCommand');
|
||||
try {
|
||||
await adminHandler.handleAdminCommand(msg);
|
||||
} catch (error) {
|
||||
await ErrorHandler.handleError(bot, msg.chat.id, error, 'admin command');
|
||||
}
|
||||
logDebug('/admin', 'handleAdminCommand');
|
||||
try {
|
||||
await adminHandler.handleAdminCommand(msg);
|
||||
} catch (error) {
|
||||
await ErrorHandler.handleError(bot, msg.chat.id, error, 'admin command');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle user menu buttons
|
||||
bot.on('message', async (msg) => {
|
||||
if (!msg.text) return;
|
||||
|
||||
try {
|
||||
// Check for admin location input
|
||||
if (await adminLocationHandler.handleLocationInput(msg)) {
|
||||
return;
|
||||
if (msg.text && msg.text.toLowerCase() === '/start') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for admin category input
|
||||
if (await adminProductHandler.handleCategoryInput(msg)) {
|
||||
return;
|
||||
const canUse = await userHandler.canUseBot(msg);
|
||||
|
||||
if (!canUse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for admin subcategory input
|
||||
if (await adminProductHandler.handleSubcategoryInput(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for product import
|
||||
if (await adminProductHandler.handleProductImport(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
logDebug(msg.text, 'handleMessage');
|
||||
|
||||
switch (msg.text) {
|
||||
case '📦 Products':
|
||||
await userProductHandler.showProducts(msg);
|
||||
break;
|
||||
case '👤 Profile':
|
||||
await userHandler.showProfile(msg);
|
||||
break;
|
||||
case '💰 Wallets':
|
||||
await userWalletsHandler.showBalance(msg);
|
||||
break;
|
||||
case '🛍 Purchases':
|
||||
await userProductHandler.showPurchases(msg);
|
||||
break;
|
||||
case '📦 Manage Products':
|
||||
if (adminHandler.isAdmin(msg.from.id)) {
|
||||
await adminProductHandler.handleProductManagement(msg);
|
||||
try {
|
||||
// Check for admin location input
|
||||
if (await adminLocationHandler.handleLocationInput(msg)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case '👥 Manage Users':
|
||||
if (adminHandler.isAdmin(msg.from.id)) {
|
||||
await adminUserHandler.handleUserList(msg);
|
||||
|
||||
// Check for admin category input
|
||||
if (await adminProductHandler.handleCategoryInput(msg)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case '📍 Manage Locations':
|
||||
if (adminHandler.isAdmin(msg.from.id)) {
|
||||
await adminLocationHandler.handleViewLocations(msg);
|
||||
|
||||
// Check for admin subcategory input
|
||||
if (await adminProductHandler.handleSubcategoryInput(msg)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
// Check for product import
|
||||
if (await adminProductHandler.handleProductImport(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for product edition
|
||||
if (await adminProductHandler.handleProductEditImport(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for database dump import
|
||||
if (await adminDumpHandler.handleDumpImport(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for bonus balance input
|
||||
if (await adminUserHandler.handleBonusBalanceInput(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for category update input
|
||||
if (await adminProductHandler.handleCategoryUpdate(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
logDebug(msg.text, 'handleMessage');
|
||||
|
||||
switch (msg.text) {
|
||||
case '📦 Products':
|
||||
await userProductHandler.showProducts(msg);
|
||||
break;
|
||||
case '👤 Profile':
|
||||
await userHandler.showProfile(msg);
|
||||
break;
|
||||
case '💰 Wallets':
|
||||
await userWalletsHandler.showBalance(msg);
|
||||
break;
|
||||
case '🛍 Purchases':
|
||||
await userPurchaseHandler.showPurchases(msg);
|
||||
break;
|
||||
case '📦 Manage Products':
|
||||
if (adminHandler.isAdmin(msg.from.id)) {
|
||||
await adminProductHandler.handleProductManagement(msg);
|
||||
}
|
||||
break;
|
||||
case '👥 Manage Users':
|
||||
if (adminHandler.isAdmin(msg.from.id)) {
|
||||
await adminUserHandler.handleUserList(msg);
|
||||
}
|
||||
break;
|
||||
case '📍 Manage Locations':
|
||||
if (adminHandler.isAdmin(msg.from.id)) {
|
||||
await adminLocationHandler.handleViewLocations(msg);
|
||||
}
|
||||
break;
|
||||
case '💾 Database Backup':
|
||||
if (adminHandler.isAdmin(msg.from.id)) {
|
||||
await adminDumpHandler.handleDump(msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
await ErrorHandler.handleError(bot, msg.chat.id, error, 'message handler');
|
||||
}
|
||||
} catch (error) {
|
||||
await ErrorHandler.handleError(bot, msg.chat.id, error, 'message handler');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle callback queries
|
||||
bot.on('callback_query', async (callbackQuery) => {
|
||||
const action = callbackQuery.data;
|
||||
const msg = callbackQuery.message;
|
||||
const action = callbackQuery.data;
|
||||
const msg = callbackQuery.message;
|
||||
|
||||
try {
|
||||
// Profile and location management
|
||||
if (action === 'set_location') {
|
||||
logDebug(action, 'handleSetLocation');
|
||||
await userLocationHandler.handleSetLocation(callbackQuery);
|
||||
} else if (action.startsWith('set_country_')) {
|
||||
logDebug(action, 'handleSetCountry');
|
||||
await userLocationHandler.handleSetCountry(callbackQuery);
|
||||
} else if (action.startsWith('set_city_')) {
|
||||
logDebug(action, 'handleSetCity');
|
||||
await userLocationHandler.handleSetCity(callbackQuery);
|
||||
} else if (action.startsWith('set_district_')) {
|
||||
logDebug(action, 'handleSetDistrict');
|
||||
await userLocationHandler.handleSetDistrict(callbackQuery);
|
||||
} else if (action === 'back_to_profile') {
|
||||
logDebug(action, 'handleBackToProfile');
|
||||
await userHandler.handleBackToProfile(callbackQuery);
|
||||
} else if (action === 'back_to_balance') {
|
||||
logDebug(action, 'handleBackToBalance');
|
||||
await userWalletsHandler.handleBackToBalance(callbackQuery);
|
||||
const canUse = await userHandler.canUseBot(callbackQuery);
|
||||
|
||||
if (!canUse) {
|
||||
await bot.answerCallbackQuery(callbackQuery.id);
|
||||
return;
|
||||
}
|
||||
// Wallet management
|
||||
else if (action === 'add_wallet') {
|
||||
logDebug(action, 'handleAddWallet');
|
||||
await userWalletsHandler.handleAddWallet(callbackQuery);
|
||||
} else if (action === 'top_up_wallet') {
|
||||
logDebug(action, 'handleTopUpWallet');
|
||||
await userWalletsHandler.handleTopUpWallet(callbackQuery);
|
||||
} else if (action === 'wallet_history') {
|
||||
logDebug(action, 'handleWalletHistory');
|
||||
await userWalletsHandler.handleWalletHistory(callbackQuery);
|
||||
} else if (action === 'view_archived_wallets') {
|
||||
logDebug(action, 'handleViewArchivedWallets');
|
||||
await userWalletsHandler.handleViewArchivedWallets(callbackQuery);
|
||||
} else if (action === 'refresh_balance') {
|
||||
logDebug(action, 'handleRefreshBalance');
|
||||
await userWalletsHandler.handleRefreshBalance(callbackQuery);
|
||||
|
||||
try {
|
||||
// Profile and location management
|
||||
if (action === 'set_location') {
|
||||
logDebug(action, 'handleSetLocation');
|
||||
await userLocationHandler.handleSetLocation(callbackQuery);
|
||||
} else if (action.startsWith('set_country_')) {
|
||||
logDebug(action, 'handleSetCountry');
|
||||
await userLocationHandler.handleSetCountry(callbackQuery);
|
||||
} else if (action.startsWith('set_city_')) {
|
||||
logDebug(action, 'handleSetCity');
|
||||
await userLocationHandler.handleSetCity(callbackQuery);
|
||||
} else if (action.startsWith('set_district_')) {
|
||||
logDebug(action, 'handleSetDistrict');
|
||||
await userLocationHandler.handleSetDistrict(callbackQuery);
|
||||
} else if (action === 'back_to_profile') {
|
||||
logDebug(action, 'handleBackToProfile');
|
||||
await userHandler.handleBackToProfile(callbackQuery);
|
||||
} else if (action === 'back_to_balance') {
|
||||
logDebug(action, 'handleBackToBalance');
|
||||
await userWalletsHandler.handleBackToBalance(callbackQuery);
|
||||
}
|
||||
// Wallet management
|
||||
else if (action === 'add_wallet') {
|
||||
logDebug(action, 'handleAddWallet');
|
||||
await userWalletsHandler.handleAddWallet(callbackQuery);
|
||||
} else if (action === 'top_up_wallet') {
|
||||
logDebug(action, 'handleTopUpWallet');
|
||||
await userWalletsHandler.handleTopUpWallet(callbackQuery);
|
||||
} else if (action === 'wallet_history') {
|
||||
logDebug(action, 'handleWalletHistory');
|
||||
await userWalletsHandler.handleWalletHistory(callbackQuery);
|
||||
} else if (action === 'view_archived_wallets') {
|
||||
logDebug(action, 'handleViewArchivedWallets');
|
||||
await userWalletsHandler.handleViewArchivedWallets(callbackQuery);
|
||||
} else if (action === 'refresh_balance') {
|
||||
logDebug(action, 'handleRefreshBalance');
|
||||
await userWalletsHandler.handleRefreshBalance(callbackQuery);
|
||||
}
|
||||
// Wallet generation
|
||||
else if (action.startsWith('generate_wallet_')) {
|
||||
logDebug(action, 'handleGenerateWallet');
|
||||
await userWalletsHandler.handleGenerateWallet(callbackQuery);
|
||||
}
|
||||
// Shop navigation
|
||||
else if (action === 'shop_start') {
|
||||
logDebug(action, 'showProducts');
|
||||
await userProductHandler.showProducts(msg);
|
||||
} else if (action.startsWith('shop_country_')) {
|
||||
logDebug(action, 'handleCountrySelection');
|
||||
await userProductHandler.handleCountrySelection(callbackQuery);
|
||||
} else if (action.startsWith('shop_city_')) {
|
||||
logDebug(action, 'handleCitySelection');
|
||||
await userProductHandler.handleCitySelection(callbackQuery);
|
||||
} else if (action.startsWith('shop_district_')) {
|
||||
logDebug(action, 'handleDistrictSelection');
|
||||
await userProductHandler.handleDistrictSelection(callbackQuery);
|
||||
} else if (action.startsWith('shop_category_')) {
|
||||
logDebug(action, 'handleCategorySelection');
|
||||
await userProductHandler.handleCategorySelection(callbackQuery);
|
||||
} else if (action.startsWith('shop_subcategory_')) {
|
||||
logDebug(action, 'handleSubcategorySelection');
|
||||
await userProductHandler.handleSubcategorySelection(callbackQuery);
|
||||
} else if (action.startsWith('shop_product_')) {
|
||||
logDebug(action, 'handleProductSelection');
|
||||
await userProductHandler.handleProductSelection(callbackQuery);
|
||||
} else if (action.startsWith('increase_quantity_')) {
|
||||
logDebug(action, 'handleIncreaseQuantity');
|
||||
await userProductHandler.handleIncreaseQuantity(callbackQuery);
|
||||
} else if (action.startsWith('decrease_quantity_')) {
|
||||
logDebug(action, 'handleDecreaseQuantity');
|
||||
await userProductHandler.handleDecreaseQuantity(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);
|
||||
} else if (action.startsWith('list_purchases_')) {
|
||||
logDebug(action, 'handlePurchaseListPage');
|
||||
await userPurchaseHandler.handlePurchaseListPage(callbackQuery);
|
||||
} else if (action.startsWith('view_purchase_')) {
|
||||
logDebug(action, 'viewPurchase');
|
||||
await userPurchaseHandler.viewPurchase(callbackQuery);
|
||||
}
|
||||
// Admin location management
|
||||
else if (action === 'add_location') {
|
||||
logDebug(action, 'handleAddLocation');
|
||||
await adminLocationHandler.handleAddLocation(callbackQuery);
|
||||
} else if (action === 'view_locations') {
|
||||
logDebug(action, 'handleViewLocations');
|
||||
await adminLocationHandler.handleViewLocations(callbackQuery);
|
||||
} else if (action === 'delete_location') {
|
||||
logDebug(action, 'handleDeleteLocation');
|
||||
await adminLocationHandler.handleDeleteLocation(callbackQuery);
|
||||
} else if (action.startsWith('confirm_delete_location_')) {
|
||||
logDebug(action, 'handleConfirmDelete');
|
||||
await adminLocationHandler.handleConfirmDelete(callbackQuery);
|
||||
} else if (action === 'admin_menu') {
|
||||
logDebug(action, 'backToMenu');
|
||||
await adminLocationHandler.backToMenu(callbackQuery);
|
||||
}
|
||||
|
||||
// Admin product management
|
||||
else if (action === 'manage_products') {
|
||||
logDebug(action, 'handleProductManagement');
|
||||
await adminProductHandler.handleProductManagement(callbackQuery);
|
||||
} else if (action.startsWith('prod_country_')) {
|
||||
logDebug(action, 'handleCountrySelection');
|
||||
await adminProductHandler.handleCountrySelection(callbackQuery);
|
||||
} else if (action.startsWith('prod_city_')) {
|
||||
logDebug(action, 'handleCitySelection');
|
||||
await adminProductHandler.handleCitySelection(callbackQuery);
|
||||
} else if (action.startsWith('prod_district_')) {
|
||||
logDebug(action, 'handleDistrictSelection');
|
||||
await adminProductHandler.handleDistrictSelection(callbackQuery);
|
||||
} else if (action.startsWith('add_category_')) {
|
||||
logDebug(action, 'handleAddCategory');
|
||||
await adminProductHandler.handleAddCategory(callbackQuery);
|
||||
} else if (action.startsWith('edit_category_')) {
|
||||
logDebug(action, 'handleEditCategory');
|
||||
await adminProductHandler.handleEditCategory(callbackQuery);
|
||||
} else if (action.startsWith('prod_category_')) {
|
||||
logDebug(action, 'handleCategorySelection');
|
||||
await adminProductHandler.handleCategorySelection(callbackQuery);
|
||||
} else if (action.startsWith('add_subcategory_')) {
|
||||
logDebug(action, 'handleAddSubcategory');
|
||||
await adminProductHandler.handleAddSubcategory(callbackQuery);
|
||||
} else if (action.startsWith('prod_subcategory_')) {
|
||||
logDebug(action, 'handleSubcategorySelection');
|
||||
await adminProductHandler.handleSubcategorySelection(callbackQuery);
|
||||
} else if (action.startsWith('list_products_')) {
|
||||
logDebug(action, 'handleProductListPage');
|
||||
await adminProductHandler.handleProductListPage(callbackQuery);
|
||||
} else if (action.startsWith('add_product_')) {
|
||||
logDebug(action, 'handleAddProduct');
|
||||
await adminProductHandler.handleAddProduct(callbackQuery);
|
||||
} else if (action.startsWith('view_product_')) {
|
||||
logDebug(action, 'handleViewProduct');
|
||||
await adminProductHandler.handleViewProduct(callbackQuery);
|
||||
} else if (action.startsWith('edit_product_')) {
|
||||
logDebug(action, 'handleProductEdit');
|
||||
await adminProductHandler.handleProductEdit(callbackQuery)
|
||||
} else if (action.startsWith('delete_product_')) {
|
||||
logDebug(action, 'handleProductDelete');
|
||||
await adminProductHandler.handleProductDelete(callbackQuery);
|
||||
} else if (action.startsWith('confirm_delete_product_')) {
|
||||
logDebug(action, 'handleConfirmDelete');
|
||||
await adminProductHandler.handleConfirmDelete(callbackQuery);
|
||||
}
|
||||
// Admin user management
|
||||
else if (action.startsWith('view_user_')) {
|
||||
logDebug(action, 'handleViewUser');
|
||||
await adminUserHandler.handleViewUser(callbackQuery);
|
||||
} else if (action.startsWith('list_users_')) {
|
||||
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, 'handleBlockUser');
|
||||
await adminUserHandler.handleBlockUser(callbackQuery);
|
||||
} else if (action.startsWith('confirm_delete_user_')) {
|
||||
logDebug(action, 'handleConfirmDelete');
|
||||
await adminUserHandler.handleConfirmDelete(callbackQuery);
|
||||
} else if (action.startsWith('confirm_block_user_')) {
|
||||
logDebug(action, 'handleConfirmBlock');
|
||||
await adminUserHandler.handleConfirmBlock(callbackQuery);
|
||||
} else if (action.startsWith('edit_user_balance_')) {
|
||||
logDebug(action, 'handleEditUserBalance');
|
||||
await adminUserHandler.handleEditUserBalance(callbackQuery);
|
||||
}
|
||||
// Admin users location management
|
||||
else if (action.startsWith('edit_user_location_')) {
|
||||
logDebug(action, 'handleEditUserLocation');
|
||||
await adminUserLocationHandler.handleEditUserLocation(callbackQuery);
|
||||
} else if (action.startsWith('edit_user_country_')) {
|
||||
logDebug(action, 'handleEditUserCountry');
|
||||
await adminUserLocationHandler.handleEditUserCountry(callbackQuery);
|
||||
} else if (action.startsWith('edit_user_city_')) {
|
||||
logDebug(action, 'handleEditUserCity');
|
||||
await adminUserLocationHandler.handleEditUserCity(callbackQuery);
|
||||
} else if (action.startsWith('edit_user_district_')) {
|
||||
logDebug(action, 'handleEditUserDistrict');
|
||||
await adminUserLocationHandler.handleEditUserDistrict(callbackQuery)
|
||||
}
|
||||
// Dump manage
|
||||
else if (action === "export_database") {
|
||||
await adminDumpHandler.handleExportDatabase(callbackQuery);
|
||||
return;
|
||||
} else if (action === "import_database") {
|
||||
await adminDumpHandler.handleImportDatabase(callbackQuery);
|
||||
}
|
||||
|
||||
await bot.answerCallbackQuery(callbackQuery.id);
|
||||
} catch (error) {
|
||||
await ErrorHandler.handleError(bot, msg.chat.id, error, 'callback query');
|
||||
}
|
||||
// Wallet generation
|
||||
else if (action.startsWith('generate_wallet_')) {
|
||||
logDebug(action, 'handleGenerateWallet');
|
||||
await userWalletsHandler.handleGenerateWallet(callbackQuery);
|
||||
}
|
||||
// Shop navigation
|
||||
else if (action === 'shop_start') {
|
||||
logDebug(action, 'showProducts');
|
||||
await userProductHandler.showProducts(msg);
|
||||
} else if (action.startsWith('shop_country_')) {
|
||||
logDebug(action, 'handleCountrySelection');
|
||||
await userProductHandler.handleCountrySelection(callbackQuery);
|
||||
} else if (action.startsWith('shop_city_')) {
|
||||
logDebug(action, 'handleCitySelection');
|
||||
await userProductHandler.handleCitySelection(callbackQuery);
|
||||
} else if (action.startsWith('shop_district_')) {
|
||||
logDebug(action, 'handleDistrictSelection');
|
||||
await userProductHandler.handleDistrictSelection(callbackQuery);
|
||||
} else if (action.startsWith('shop_category_')) {
|
||||
logDebug(action, 'handleCategorySelection');
|
||||
await userProductHandler.handleCategorySelection(callbackQuery);
|
||||
} else if (action.startsWith('shop_subcategory_')) {
|
||||
logDebug(action, 'handleSubcategorySelection');
|
||||
await userProductHandler.handleSubcategorySelection(callbackQuery);
|
||||
} else if (action.startsWith('shop_product_')) {
|
||||
logDebug(action, 'handleProductSelection');
|
||||
await userProductHandler.handleProductSelection(callbackQuery);
|
||||
} else if (action.startsWith('increase_quantity_')) {
|
||||
logDebug(action, 'handleIncreaseQuantity');
|
||||
await userProductHandler.handleIncreaseQuantity(callbackQuery);
|
||||
} else if (action.startsWith('decrease_quantity_')) {
|
||||
logDebug(action, 'handleDecreaseQuantity');
|
||||
await userProductHandler.handleDecreaseQuantity(callbackQuery);
|
||||
} else if (action.startsWith('buy_product_')) {
|
||||
logDebug(action, 'handleBuyProduct');
|
||||
await userProductHandler.handleBuyProduct(callbackQuery);
|
||||
}
|
||||
// Admin location management
|
||||
else if (action === 'add_location') {
|
||||
logDebug(action, 'handleAddLocation');
|
||||
await adminLocationHandler.handleAddLocation(callbackQuery);
|
||||
} else if (action === 'view_locations') {
|
||||
logDebug(action, 'handleViewLocations');
|
||||
await adminLocationHandler.handleViewLocations(callbackQuery);
|
||||
} else if (action === 'delete_location') {
|
||||
logDebug(action, 'handleDeleteLocation');
|
||||
await adminLocationHandler.handleDeleteLocation(callbackQuery);
|
||||
} else if (action.startsWith('confirm_delete_')) {
|
||||
logDebug(action, 'handleConfirmDelete');
|
||||
await adminLocationHandler.handleConfirmDelete(callbackQuery);
|
||||
}
|
||||
// Admin product management
|
||||
else if (action === 'manage_products') {
|
||||
logDebug(action, 'handleProductManagement');
|
||||
await adminProductHandler.handleProductManagement(callbackQuery);
|
||||
} else if (action.startsWith('prod_country_')) {
|
||||
logDebug(action, 'handleCountrySelection');
|
||||
await adminProductHandler.handleCountrySelection(callbackQuery);
|
||||
} else if (action.startsWith('prod_city_')) {
|
||||
logDebug(action, 'handleCitySelection');
|
||||
await adminProductHandler.handleCitySelection(callbackQuery);
|
||||
} else if (action.startsWith('prod_district_')) {
|
||||
logDebug(action, 'handleDistrictSelection');
|
||||
await adminProductHandler.handleDistrictSelection(callbackQuery);
|
||||
} else if (action.startsWith('add_category_')) {
|
||||
logDebug(action, 'handleAddCategory');
|
||||
await adminProductHandler.handleAddCategory(callbackQuery);
|
||||
} else if (action.startsWith('edit_category_')) {
|
||||
logDebug(action, 'handleEditCategory');
|
||||
await adminProductHandler.handleEditCategory(callbackQuery);
|
||||
} else if (action.startsWith('prod_category_')) {
|
||||
logDebug(action, 'handleCategorySelection');
|
||||
await adminProductHandler.handleCategorySelection(callbackQuery);
|
||||
} else if (action.startsWith('add_subcategory_')) {
|
||||
logDebug(action, 'handleAddSubcategory');
|
||||
await adminProductHandler.handleAddSubcategory(callbackQuery);
|
||||
} else if (action.startsWith('prod_subcategory_')) {
|
||||
logDebug(action, 'handleSubcategorySelection');
|
||||
await adminProductHandler.handleSubcategorySelection(callbackQuery);
|
||||
} else if (action.startsWith('add_product_')) {
|
||||
logDebug(action, 'handleAddProduct');
|
||||
await adminProductHandler.handleAddProduct(callbackQuery);
|
||||
} else if (action.startsWith('view_product_')) {
|
||||
logDebug(action, 'handleViewProduct');
|
||||
await adminProductHandler.handleViewProduct(callbackQuery);
|
||||
}
|
||||
// Admin user management
|
||||
else if (action.startsWith('view_user_')) {
|
||||
logDebug(action, 'handleViewUser');
|
||||
await adminUserHandler.handleViewUser(callbackQuery);
|
||||
} else if (action.startsWith('delete_user_')) {
|
||||
logDebug(action, 'handleDeleteUser');
|
||||
await adminUserHandler.handleDeleteUser(callbackQuery);
|
||||
} else if (action.startsWith('confirm_delete_user_')) {
|
||||
logDebug(action, 'handleConfirmDelete');
|
||||
await adminUserHandler.handleConfirmDelete(callbackQuery);
|
||||
} else if (action.startsWith('edit_user_balance_')) {
|
||||
logDebug(action, 'handleEditUserBalance');
|
||||
await adminUserHandler.handleEditUserBalance(callbackQuery);
|
||||
}
|
||||
await bot.answerCallbackQuery(callbackQuery.id);
|
||||
} catch (error) {
|
||||
await ErrorHandler.handleError(bot, msg.chat.id, error, 'callback query');
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
bot.on('polling_error', ErrorHandler.handlePollingError);
|
||||
|
||||
process.on('unhandledRejection', (error) => {
|
||||
console.error('Unhandled promise rejection:', error);
|
||||
console.error('Unhandled promise rejection:', error);
|
||||
});
|
||||
|
||||
console.log('Bot is running...');
|
||||
@@ -1,66 +0,0 @@
|
||||
import db from '../config/database.js';
|
||||
|
||||
export default class User {
|
||||
static async create(telegramId) {
|
||||
try {
|
||||
// First check if user exists
|
||||
const existingUser = await this.getById(telegramId);
|
||||
if (existingUser) {
|
||||
return existingUser.id;
|
||||
}
|
||||
|
||||
// Begin transaction
|
||||
await db.runAsync('BEGIN TRANSACTION');
|
||||
|
||||
// Create new user
|
||||
const result = await db.runAsync(
|
||||
'INSERT INTO users (telegram_id) VALUES (?)',
|
||||
[telegramId.toString()]
|
||||
);
|
||||
|
||||
// Commit transaction
|
||||
await db.runAsync('COMMIT');
|
||||
|
||||
return result.lastID;
|
||||
} catch (error) {
|
||||
// Rollback on error
|
||||
await db.runAsync('ROLLBACK');
|
||||
console.error('Error creating user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getById(telegramId) {
|
||||
try {
|
||||
return await db.getAsync(
|
||||
'SELECT * FROM users WHERE telegram_id = ?',
|
||||
[telegramId.toString()]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error getting user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getUserStats(telegramId) {
|
||||
try {
|
||||
return await db.getAsync(`
|
||||
SELECT
|
||||
u.*,
|
||||
COUNT(DISTINCT p.id) as purchase_count,
|
||||
COALESCE(SUM(p.total_price), 0) as total_spent,
|
||||
COUNT(DISTINCT cw.id) as crypto_wallet_count,
|
||||
COUNT(DISTINCT cw2.id) as archived_wallet_count
|
||||
FROM users u
|
||||
LEFT JOIN purchases p ON u.id = p.user_id
|
||||
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id AND cw.wallet_type NOT LIKE '%_%'
|
||||
LEFT JOIN crypto_wallets cw2 ON u.id = cw2.user_id AND cw2.wallet_type LIKE '%_%'
|
||||
WHERE u.telegram_id = ?
|
||||
GROUP BY u.id
|
||||
`, [telegramId.toString()]);
|
||||
} catch (error) {
|
||||
console.error('Error getting user stats:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
27
src/services/categoryService.js
Normal file
27
src/services/categoryService.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import db from "../config/database.js";
|
||||
|
||||
class CategoryService {
|
||||
static async getCategoriesByLocationId(locationId) {
|
||||
return await db.allAsync(
|
||||
'SELECT id, name FROM categories WHERE location_id = ? ORDER BY name',
|
||||
[locationId]
|
||||
);
|
||||
}
|
||||
|
||||
static async getSubcategoriesByCategoryId(categoryId) {
|
||||
return await db.allAsync(
|
||||
'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name',
|
||||
[categoryId]
|
||||
);
|
||||
}
|
||||
|
||||
static async getCategoryById(categoryId) {
|
||||
return await db.getAsync('SELECT id, name FROM categories WHERE id = ?', [categoryId]);
|
||||
}
|
||||
|
||||
static async getSubcategoryById(subcategoryId) {
|
||||
return await db.getAsync('SELECT id, name FROM subcategories WHERE id = ?', [subcategoryId]);
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryService;
|
||||
37
src/services/locationService.js
Normal file
37
src/services/locationService.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import db from "../config/database.js";
|
||||
|
||||
class LocationService {
|
||||
static async getCountries() {
|
||||
return await db.allAsync('SELECT DISTINCT country FROM locations ORDER BY country');
|
||||
}
|
||||
|
||||
static async getCitiesByCountry(country) {
|
||||
return await db.allAsync(
|
||||
'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city',
|
||||
[country]
|
||||
);
|
||||
}
|
||||
|
||||
static async getDistrictsByCountryAndCity(country, city) {
|
||||
return await db.allAsync(
|
||||
'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district',
|
||||
[country, city]
|
||||
);
|
||||
}
|
||||
|
||||
static async getLocation(country, city, district) {
|
||||
return await db.getAsync(
|
||||
'SELECT id FROM locations WHERE country = ? AND city = ? AND district = ?',
|
||||
[country, city, district]
|
||||
);
|
||||
}
|
||||
|
||||
static async getLocationById(locationId) {
|
||||
return await db.getAsync(
|
||||
'SELECT country, city, district FROM locations WHERE id = ?',
|
||||
[locationId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LocationService;
|
||||
37
src/services/productService.js
Normal file
37
src/services/productService.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import db from "../config/database.js";
|
||||
|
||||
class ProductService {
|
||||
static async getProductById(productId) {
|
||||
try {
|
||||
return await db.getAsync(`SELECT * FROM products WHERE id = ?`, [productId]);
|
||||
} catch (error) {
|
||||
console.error('Error get product:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getDetailedProductById(productId) {
|
||||
return 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]
|
||||
);
|
||||
}
|
||||
|
||||
static async getProductsByLocationAndCategory(locationId, categoryId, subcategoryId) {
|
||||
return 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]
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductService
|
||||
59
src/services/purchaseService.js
Normal file
59
src/services/purchaseService.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import db from "../config/database.js";
|
||||
|
||||
class PurchaseService {
|
||||
static async getPurchasesByUserId(userId, limit, offset) {
|
||||
try {
|
||||
return 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 ?
|
||||
OFFSET ?
|
||||
`, [userId, limit, offset]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error get purchases:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getPurchaseById(purchaseId) {
|
||||
try {
|
||||
return await db.getAsync(`
|
||||
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.id = ?
|
||||
`, [purchaseId]);
|
||||
} catch (error) {
|
||||
console.error('Error get purchase:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static async createPurchase(userId, productId, walletType, quantity, totalPrice) {
|
||||
await db.runAsync(
|
||||
'INSERT INTO purchases (user_id, product_id, wallet_type, tx_hash, quantity, total_price) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[userId, productId, walletType, "null", quantity, totalPrice]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PurchaseService
|
||||
140
src/services/userService.js
Normal file
140
src/services/userService.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import db from "../config/database.js";
|
||||
import Wallet from "../models/Wallet.js";
|
||||
|
||||
class UserService {
|
||||
static async getUserByUserId(userId) {
|
||||
try {
|
||||
return await db.getAsync(
|
||||
'SELECT * FROM users WHERE id = ?',
|
||||
[String(userId)]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error getting user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getUserByTelegramId(telegramId) {
|
||||
try {
|
||||
return await db.getAsync(
|
||||
'SELECT * FROM users WHERE telegram_id = ?',
|
||||
[String(telegramId)]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error getting user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getDetailedUserByTelegramId(telegramId) {
|
||||
try {
|
||||
return await db.getAsync(`
|
||||
SELECT
|
||||
u.*,
|
||||
COUNT(DISTINCT p.id) as purchase_count,
|
||||
COALESCE(SUM(p.total_price), 0) as total_spent,
|
||||
COUNT(DISTINCT cw.id) as crypto_wallet_count,
|
||||
COUNT(DISTINCT cw2.id) as archived_wallet_count
|
||||
FROM users u
|
||||
LEFT JOIN purchases p ON u.id = p.user_id
|
||||
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id AND cw.wallet_type NOT LIKE '%_%'
|
||||
LEFT JOIN crypto_wallets cw2 ON u.id = cw2.user_id AND cw2.wallet_type LIKE '%_%'
|
||||
WHERE u.telegram_id = ?
|
||||
GROUP BY u.id
|
||||
`, [telegramId.toString()]);
|
||||
} catch (error) {
|
||||
console.error('Error getting user stats:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async createUser(userData) {
|
||||
try {
|
||||
const existingUser = await this.getUserByTelegramId(userData?.telegram_id);
|
||||
|
||||
if (existingUser) {
|
||||
return existingUser.id;
|
||||
}
|
||||
|
||||
const fields = Object.keys(userData);
|
||||
const values = [];
|
||||
|
||||
for (const field of fields) {
|
||||
values.push(userData[field]);
|
||||
}
|
||||
|
||||
const marks = [];
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
marks.push("?");
|
||||
}
|
||||
|
||||
const query = [
|
||||
`INSERT INTO users (${fields.join(', ')})`,
|
||||
`VALUES (${marks.join(', ')})`
|
||||
].join("");
|
||||
|
||||
await db.runAsync('BEGIN TRANSACTION');
|
||||
const result = await db.runAsync(query, [values]);
|
||||
await db.runAsync('COMMIT');
|
||||
return result.lastID;
|
||||
} catch (error) {
|
||||
await db.runAsync('ROLLBACK');
|
||||
console.error('Error creating user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async updateUser(userId, newUserData) {}
|
||||
|
||||
static async deleteUser() {}
|
||||
|
||||
static async recalculateUserBalanceByTelegramId(telegramId) {
|
||||
const user = await this.getUserByTelegramId(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]);
|
||||
}
|
||||
|
||||
static async updateUserLocation(telegramId, country, city, district) {
|
||||
await db.runAsync(
|
||||
'UPDATE users SET country = ?, city = ?, district = ? WHERE telegram_id = ?',
|
||||
[country, city, district, telegramId.toString()]
|
||||
);
|
||||
}
|
||||
|
||||
static async updateUserStatus(telegramId, status) {
|
||||
// statuses
|
||||
// 0 - active
|
||||
// 1 - deleted
|
||||
// 2 - blocked
|
||||
|
||||
try {
|
||||
await db.runAsync('BEGIN TRANSACTION');
|
||||
|
||||
// Update user status
|
||||
await db.runAsync('UPDATE users SET status = ? WHERE telegram_id = ?', [status, telegramId.toString()]);
|
||||
|
||||
// Commit transaction
|
||||
await db.runAsync('COMMIT');
|
||||
} catch (e) {
|
||||
await db.runAsync("ROLLBACK");
|
||||
console.error('Error deleting user:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UserService;
|
||||
3
src/services/walletService.js
Normal file
3
src/services/walletService.js
Normal file
@@ -0,0 +1,3 @@
|
||||
class WalletService {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user