Files
telegram-shop/src/handlers/userHandlers/userHandler.js
NW e90dcb60c9
All checks were successful
Release: multi-arch Docker images / build-push (push) Successful in 11m28s
feat: shop activation + DB commission wallets + onion display (deploy pipeline)
- src/services/commissionService.js: read commission wallets and shop_activated from site_settings (DB) with env fallback, 30s cache
- src/handlers/adminHandlers/adminWalletsHandler.js: commission wallets from CommissionService instead of static config
- src/handlers/userHandlers/userHandler.js: canUseBot blocks all users when shop not activated
- src/migrations/015_shop_activation.js: seed shop_activated + commission_wallet_* into site_settings; register in runner
- admin-next: /api/system/info (onion hosts), /api/commission-wallets GET/PUT (super admin), /api/activation GET/PUT (super admin); lib/commission-wallets.ts shared helper; onion badge in header; shop activation card in settings; wallet editor in wallets page
- test: commissionService.test.js (10 cases: DB/env fallback, caching)
2026-08-11 18:59:18 +01:00

271 lines
11 KiB
JavaScript

// userHandler.js
import config from "../../config/config.js";
import bot from "../../context/bot.js";
import UserService from "../../services/userService.js";
import WalletService from "../../services/walletService.js";
import logger from "../../utils/logger.js";
import { resetUserContext } from "../../utils/messageUtils.js";
import userStates from "../../context/userStates.js";
import chatbotService from "../../services/chatbotService.js";
import CommissionService from "../../services/commissionService.js";
import leadService from "../../services/leadService.js";
import { tForUser, LANGUAGE_NAMES, AVAILABLE_LANGUAGES } from '../../i18n/index.js';
export default class UserHandler {
static async canUseBot(msg) {
const telegramId = msg.from.id;
const user = await UserService.getUserByTelegramId(telegramId);
msg.__user = user; // Cache user for downstream handlers
const lang = user?.language || 'en';
const t = tForUser(lang);
if (!(await CommissionService.getShopActivated())) {
await bot.sendMessage(telegramId, '⛔ Shop is not activated yet. Please contact support.');
return false;
}
const keyboard = {
inline_keyboard: [
[{text: t('bot.contact_support'), url: config.SUPPORT_LINK}]
]
};
switch (user?.status) {
case 0:
return true;
case 1:
await bot.sendMessage(telegramId, t('bot.account_deleted'), {reply_markup: keyboard});
return false;
case 2:
await bot.sendMessage(telegramId, t('bot.account_blocked'), {reply_markup: keyboard});
return false;
default:
return true;
}
}
static async showProfile(msg) {
const chatId = msg.chat.id;
const telegramId = msg.from.id;
const user = await UserService.getUserByTelegramId(telegramId);
const lang = user?.language || 'en';
const t = tForUser(lang);
try {
await UserService.recalculateUserBalanceByTelegramId(telegramId);
const userStats = await UserService.getDetailedUserByTelegramId(telegramId);
if (!userStats) {
await bot.sendMessage(chatId, t('profile.not_found'));
return;
}
const activeWalletsBalance = await WalletService.getActiveWalletsBalance(userStats.id);
const archivedWalletsBalance = await WalletService.getArchivedWalletsBalance(userStats.id);
const availableBalance = userStats.bonus_balance + (userStats.total_balance || 0);
const locationText = userStats.country && userStats.city && userStats.district
? `${userStats.country}, ${userStats.city}, ${userStats.district}`
: t('profile.location_not_set');
const text = `
${t('profile.title')}
${t('profile.telegram_id')}: \`${telegramId}\`
${t('profile.location')}: ${locationText}
${t('profile.stats')}
${t('profile.total_purchases')}: ${userStats.purchase_count || 0}
${t('profile.total_spent')}: $${userStats.total_spent || 0}
${t('profile.active_wallets')}: ${userStats.crypto_wallet_count || 0} ($${activeWalletsBalance.toFixed(2)})
${t('profile.archived_wallets')}: ${userStats.archived_wallet_count || 0} ($${archivedWalletsBalance.toFixed(2)})
${t('profile.bonus_balance')}: $${userStats.bonus_balance || 0}
${t('profile.available_balance')}: $${availableBalance.toFixed(2)}
${t('profile.member_since')}: ${new Date(userStats.created_at).toLocaleDateString()}
`;
const keyboard = {
inline_keyboard: [
[{text: t('profile.set_location'), callback_data: 'set_location'}],
[{text: t('profile.change_language'), callback_data: 'change_language'}],
[{text: t('profile.delete_account'), callback_data: 'delete_account'}]
]
};
const result = await bot.sendMessage(chatId, text, {
parse_mode: 'Markdown',
reply_markup: keyboard
});
await userStates.set(chatId, { ...(await userStates.get(chatId) || {}), lastInlineMessageId: result.message_id });
} catch (error) {
logger.error({ err: error }, 'Error in showProfile');
await bot.sendMessage(chatId, t('profile.error_loading'));
}
}
static async handleStart(msg) {
const chatId = msg.chat.id;
const telegramId = msg.from.id;
const username = msg.chat.username;
try {
await resetUserContext(chatId);
await UserService.createUser({
telegram_id: telegramId,
username: username
});
// Фиксация лида при активации чата (/start)
const lead = await leadService.getOrCreateLead({
telegramId,
username,
name: msg.from?.first_name || null,
});
if (lead?.id) {
await leadService.logInteraction({
telegramId,
leadId: lead.id,
action: 'bot_start',
details: { username: username || null, chatType: msg.chat?.type || 'private' },
});
}
// Sleep mode: если магазин на паузе — живой ИИ-диалог вместо каталога
if (await chatbotService.isSleepMode()) {
const user = await UserService.getUserByTelegramId(telegramId);
// Если язык ещё не выбран (первый старт) — сначала выбор языка
if (!user?.language_set) {
const keyboard = {
inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{
text: LANGUAGE_NAMES[code],
callback_data: `set_language_${code}`
}])
};
await bot.sendMessage(chatId, tForUser('en')('bot.language_select'), { reply_markup: keyboard });
return;
}
const lang = user?.language || 'en';
// Приветствие от ИИ-бота (живой диалог, не заглушка)
const welcome = await chatbotService.sendToChatbot({
sessionId: telegramId,
message: '/start',
telegramId,
language: lang,
username,
name: msg.from?.first_name,
});
if (welcome.reply) {
await bot.sendMessage(chatId, welcome.reply);
} else {
await bot.sendMessage(chatId, await chatbotService.getWelcomeMessage());
}
return;
}
const keyboard = {
inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{
text: LANGUAGE_NAMES[code],
callback_data: `set_language_${code}`
}])
};
await bot.sendMessage(chatId, tForUser('en')('bot.language_select'), { reply_markup: keyboard });
} catch (error) {
logger.error({ err: error }, 'Error in handleStart');
const fallbackT = tForUser('en');
await bot.sendMessage(chatId, fallbackT('bot.error_generic'));
}
}
static async handleSetLanguage(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
const lang = callbackQuery.data.replace('set_language_', '');
if (!AVAILABLE_LANGUAGES.includes(lang)) {
await bot.answerCallbackQuery(callbackQuery.id);
return;
}
try {
await UserService.setUserLanguage(telegramId, lang);
const t = tForUser(lang);
await bot.answerCallbackQuery(callbackQuery.id);
// Если магазин на паузе — после выбора языка сразу ИИ-диалог на выбранном языке
if (await chatbotService.isSleepMode()) {
try { await bot.deleteMessage(chatId, callbackQuery.message.message_id); } catch {}
const welcome = await chatbotService.sendToChatbot({
sessionId: telegramId,
message: '/start',
telegramId,
language: lang,
username: callbackQuery.from?.username,
name: callbackQuery.from?.first_name,
});
await bot.sendMessage(chatId, welcome.reply || await chatbotService.getWelcomeMessage(), {
reply_markup: { remove_keyboard: true },
});
return;
}
const keyboard = {
reply_markup: {
keyboard: [
[t('keyboard.products'), t('keyboard.profile')],
[t('keyboard.purchases'), t('keyboard.wallets')]
],
resize_keyboard: true
}
};
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
await bot.sendMessage(chatId, t('bot.language_changed', { language: LANGUAGE_NAMES[lang] }), keyboard);
} catch (error) {
logger.error({ err: error }, 'Error in handleSetLanguage');
await bot.answerCallbackQuery(callbackQuery.id);
}
}
static async handleChangeLanguage(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const user = callbackQuery.message.__user || await UserService.getUserByTelegramId(callbackQuery.from.id);
const currentLang = user?.language || 'en';
const keyboard = {
inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{
text: LANGUAGE_NAMES[code],
callback_data: `set_language_${code}`
}])
};
try {
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
} catch {}
await bot.sendMessage(chatId, tForUser(currentLang)('bot.language_select'), { reply_markup: keyboard });
}
static async handleLanguageCommand(msg) {
const chatId = msg.chat.id;
const keyboard = {
inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{
text: LANGUAGE_NAMES[code],
callback_data: `set_language_${code}`
}])
};
await bot.sendMessage(chatId, tForUser('en')('bot.language_select'), { reply_markup: keyboard });
}
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);
}
}