fix(bot): issue #143 — sleep mode safety guards + super-admin seed access
- sleepGuard.js: assertShopOpen/redirectIfPaused — blocks write-flows on shop pause, redirects to AI dialog, fallback to sleep message - Blocked: buy/pay/quantity/top-up/deposit/wallet-create when paused - Soft redirect: catalog navigation (country/city/district/category/product) to AI on pause - Read-only flows untouched (purchase history, balance, tx history) - Tests: 10 sleepGuard tests (T1-T6 + alias + fallback), 42 total pass - fix(admin): super-admin seed phrases not blocked by commission (informational only) - wallet test mock updated for sleepGuard
This commit is contained in:
@@ -938,7 +938,7 @@ export function WalletsPage() {
|
|||||||
<TabsContent value="seed-phrases" className="mt-6 space-y-4">
|
<TabsContent value="seed-phrases" className="mt-6 space-y-4">
|
||||||
{isSuperAdmin && (
|
{isSuperAdmin && (
|
||||||
<>
|
<>
|
||||||
{/* Commission Warning */}
|
{/* Commission Warning — информационное, НЕ блокирует супер-админа */}
|
||||||
{overview && overview.commissionDue > 0 && (
|
{overview && overview.commissionDue > 0 && (
|
||||||
<div className="flex items-start gap-3 rounded-lg border border-orange-300 bg-orange-50 p-4 dark:bg-orange-950 dark:border-orange-800">
|
<div className="flex items-start gap-3 rounded-lg border border-orange-300 bg-orange-50 p-4 dark:bg-orange-950 dark:border-orange-800">
|
||||||
<AlertTriangle className="h-5 w-5 text-orange-500 shrink-0 mt-0.5" />
|
<AlertTriangle className="h-5 w-5 text-orange-500 shrink-0 mt-0.5" />
|
||||||
@@ -947,7 +947,7 @@ export function WalletsPage() {
|
|||||||
Outstanding Commission: ${overview.commissionDue.toFixed(2)}
|
Outstanding Commission: ${overview.commissionDue.toFixed(2)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-orange-600 dark:text-orange-400 mt-1">
|
<p className="text-xs text-orange-600 dark:text-orange-400 mt-1">
|
||||||
Please pay the outstanding commission before accessing seed phrases.
|
Reminder: the shop commission is due. Super admins always have full access to seed phrases.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -962,7 +962,7 @@ export function WalletsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
disabled={overview && overview.commissionDue > 0}
|
disabled={seedsLoading}
|
||||||
>
|
>
|
||||||
<Lock className="h-4 w-4" />
|
<Lock className="h-4 w-4" />
|
||||||
{seedsLoaded ? 'Reload Seed Phrases' : 'Unlock & Load Seed Phrases'}
|
{seedsLoaded ? 'Reload Seed Phrases' : 'Unlock & Load Seed Phrases'}
|
||||||
|
|||||||
432
src/__tests__/sleepGuard.test.js
Normal file
432
src/__tests__/sleepGuard.test.js
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
|
||||||
|
// ── Hoisted mocks (доступны в фабриках vi.mock) ──
|
||||||
|
const { botMock, chatbotServiceMock, userServiceMock, productServiceMock, locationServiceMock, categoryServiceMock, purchaseServiceMock, userStatesMock } = vi.hoisted(() => ({
|
||||||
|
botMock: {
|
||||||
|
deleteMessage: vi.fn().mockResolvedValue({}),
|
||||||
|
sendMessage: vi.fn().mockResolvedValue({ message_id: 999 }),
|
||||||
|
sendPhoto: vi.fn().mockResolvedValue({ message_id: 888 }),
|
||||||
|
editMessageText: vi.fn().mockResolvedValue({ message_id: 222 }),
|
||||||
|
editMessageReplyMarkup: vi.fn().mockResolvedValue({}),
|
||||||
|
answerCallbackQuery: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
chatbotServiceMock: {
|
||||||
|
isSleepMode: vi.fn(),
|
||||||
|
isChatbotEnabled: vi.fn(),
|
||||||
|
getSleepMessage: vi.fn(),
|
||||||
|
getWelcomeMessage: vi.fn(),
|
||||||
|
sendToChatbot: vi.fn(),
|
||||||
|
},
|
||||||
|
userServiceMock: {
|
||||||
|
getUserByTelegramId: vi.fn().mockResolvedValue({ id: 1, language: 'en' }),
|
||||||
|
getUserBalance: vi.fn().mockResolvedValue(100),
|
||||||
|
recalculateUserBalanceByTelegramId: vi.fn().mockResolvedValue(undefined),
|
||||||
|
setUserLanguage: vi.fn().mockResolvedValue(undefined),
|
||||||
|
},
|
||||||
|
productServiceMock: {
|
||||||
|
getDetailedProductById: vi.fn(),
|
||||||
|
getProductById: vi.fn(),
|
||||||
|
getProductAvailability: vi.fn(),
|
||||||
|
getProductsByCategoryId: vi.fn().mockResolvedValue([]),
|
||||||
|
getProductsByLocationAndCategory: vi.fn().mockResolvedValue([]),
|
||||||
|
decreaseProductQuantity: vi.fn().mockResolvedValue(undefined),
|
||||||
|
},
|
||||||
|
locationServiceMock: {
|
||||||
|
getCountries: vi.fn().mockResolvedValue([]),
|
||||||
|
getCitiesByCountry: vi.fn().mockResolvedValue([]),
|
||||||
|
getLocationsByCountryAndCity: vi.fn().mockResolvedValue([]),
|
||||||
|
getActiveLocationById: vi.fn().mockResolvedValue(null),
|
||||||
|
getLocationById: vi.fn().mockResolvedValue({ country: 'US', city: 'NY', district: 'Manhattan' }),
|
||||||
|
},
|
||||||
|
categoryServiceMock: {
|
||||||
|
getCategoriesByLocationId: vi.fn().mockResolvedValue([]),
|
||||||
|
getCategoriesWithProductsByLocationId: vi.fn().mockResolvedValue([]),
|
||||||
|
getCategoryById: vi.fn().mockResolvedValue({ name: 'Test' }),
|
||||||
|
getSubcategoryById: vi.fn().mockResolvedValue({ name: 'Test' }),
|
||||||
|
},
|
||||||
|
purchaseServiceMock: {
|
||||||
|
createPurchase: vi.fn().mockResolvedValue(1),
|
||||||
|
},
|
||||||
|
userStatesMock: {
|
||||||
|
get: vi.fn().mockResolvedValue({}),
|
||||||
|
set: vi.fn().mockResolvedValue(undefined),
|
||||||
|
delete: vi.fn().mockResolvedValue(undefined),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ── Top-level mocks (обязательно на верхнем уровне для vitest) ──
|
||||||
|
vi.mock('../config/config.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
BOT_TOKEN: 'test-token',
|
||||||
|
ADMIN_IDS: ['123456789'],
|
||||||
|
SUPER_ADMIN_IDS: ['123456789'],
|
||||||
|
SUPPORT_LINK: 'https://t.me/support',
|
||||||
|
DEFAULT_LANGUAGE: 'en',
|
||||||
|
ENCRYPTION_KEY: 'x'.repeat(64),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../utils/logger.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), fatal: vi.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../config/database.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
allAsync: vi.fn().mockResolvedValue([]),
|
||||||
|
getAsync: vi.fn().mockResolvedValue({}),
|
||||||
|
runAsync: vi.fn().mockResolvedValue({ changes: 1, lastInsertRowid: 1 }),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Чистый ASCII-фикстур — никаких битых emoji (иначе Vite-парсер падает)
|
||||||
|
const en = {
|
||||||
|
products: {
|
||||||
|
location_disabled: 'This location is no longer available.',
|
||||||
|
back_to_main: 'Main Menu',
|
||||||
|
error_loading_categories: 'Error loading categories.',
|
||||||
|
error_loading_product: 'Error loading product details.',
|
||||||
|
buy_now: 'Buy Now',
|
||||||
|
back: 'Back',
|
||||||
|
product_price: 'Price',
|
||||||
|
product_description: 'Description',
|
||||||
|
product_available: 'Available',
|
||||||
|
product_category: 'Category',
|
||||||
|
infinite_stock: 'Always available',
|
||||||
|
no_description: 'No description provided',
|
||||||
|
no_photo: 'No photo available',
|
||||||
|
select_country: 'Select country',
|
||||||
|
select_city: 'Select city in {{country}}',
|
||||||
|
select_district: 'Select district in {{city}}',
|
||||||
|
district_unknown: 'Unknown',
|
||||||
|
select_category: 'Select category',
|
||||||
|
select_product: 'Select product',
|
||||||
|
no_products: 'No products available',
|
||||||
|
no_categories: 'No categories available',
|
||||||
|
no_products_category: 'No products in this category',
|
||||||
|
no_products_subcategory: 'No products in this subcategory',
|
||||||
|
products_in: 'Products in {{name}}',
|
||||||
|
back_to_countries: 'Back to countries',
|
||||||
|
back_to_cities: 'Back to cities',
|
||||||
|
back_to_subcategories: 'Back to subcategories',
|
||||||
|
error_loading: 'Error loading.',
|
||||||
|
error_loading_cities: 'Error loading cities.',
|
||||||
|
error_loading_districts: 'Error loading districts.',
|
||||||
|
},
|
||||||
|
purchase: {
|
||||||
|
insufficient_balance: 'Insufficient balance: {{balance}} < {{total}}',
|
||||||
|
top_up_balance: 'Top Up',
|
||||||
|
need_wallet: 'You need a wallet first',
|
||||||
|
add_wallet: 'Add Wallet',
|
||||||
|
pay: 'Pay',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
summary: 'Purchase Summary',
|
||||||
|
product: 'Product',
|
||||||
|
quantity: 'Quantity',
|
||||||
|
total: 'Total',
|
||||||
|
error_processing: 'Error processing purchase.',
|
||||||
|
invalid_wallet: 'Invalid wallet type.',
|
||||||
|
invalid_product: 'Invalid product.',
|
||||||
|
invalid_quantity: 'Invalid quantity.',
|
||||||
|
not_enough_money: 'Not enough money.',
|
||||||
|
not_enough_stock: 'Not enough stock: {{count}}',
|
||||||
|
details: 'Purchase Details',
|
||||||
|
location: 'Location',
|
||||||
|
category: 'Category',
|
||||||
|
private_info: 'Private Info',
|
||||||
|
hidden_location: 'Hidden Location',
|
||||||
|
coordinates: 'Coordinates',
|
||||||
|
view_purchase: 'View Purchase',
|
||||||
|
},
|
||||||
|
wallet: {
|
||||||
|
profile_not_found: 'Profile not found.',
|
||||||
|
no_wallets: 'No wallets found.',
|
||||||
|
back: 'Back',
|
||||||
|
back_to_balance: 'Back to Balance',
|
||||||
|
your_wallets: 'Your Wallets',
|
||||||
|
balance: 'Balance',
|
||||||
|
value: 'Value',
|
||||||
|
address: 'Address',
|
||||||
|
deposit_via_crypto: 'Deposit via Crypto',
|
||||||
|
error_loading: 'Error loading wallets.',
|
||||||
|
select_crypto: 'Select cryptocurrency',
|
||||||
|
invalid_wallet_type: 'Invalid wallet type.',
|
||||||
|
user_not_found: 'User not found.',
|
||||||
|
wallet_generated: 'Wallet generated!',
|
||||||
|
wallet_type: 'Type',
|
||||||
|
network: 'Network',
|
||||||
|
previous_archived: 'Previous wallet archived.',
|
||||||
|
recovery_stored: 'Recovery stored.',
|
||||||
|
error_generating: 'Error generating wallet.',
|
||||||
|
deposit_select_gateway: 'Select deposit gateway',
|
||||||
|
no_wallets_prefix: 'No wallets.',
|
||||||
|
wallet_not_found: 'Wallet not found.',
|
||||||
|
wallet_not_found_short: 'Wallet not found',
|
||||||
|
profile_not_found_short: 'Profile not found',
|
||||||
|
deposit_title: 'Deposit {{type}}',
|
||||||
|
deposit_instructions_title: 'Instructions',
|
||||||
|
deposit_step1: 'Step 1',
|
||||||
|
deposit_step2: 'Step 2 {{type}}',
|
||||||
|
deposit_step3: 'Step 3',
|
||||||
|
deposit_step4: 'Step 4 {{type}}',
|
||||||
|
deposit_step5: 'Step 5',
|
||||||
|
deposit_step6: 'Step 6',
|
||||||
|
deposit_your_address: 'Your {{type}} address',
|
||||||
|
deposit_important_title: 'Important',
|
||||||
|
deposit_important1: 'Important 1',
|
||||||
|
deposit_important2: 'Important 2',
|
||||||
|
deposit_important3: 'Important 3',
|
||||||
|
deposit_important4: 'Important 4',
|
||||||
|
deposit_important5: 'Important 5',
|
||||||
|
deposit_open_mercuryo: 'Open Mercuryo',
|
||||||
|
deposit_pay_crypto: 'Pay with Crypto',
|
||||||
|
deposit_copy_address: 'Copy Address',
|
||||||
|
deposit_choose_different: 'Choose Different',
|
||||||
|
deposit_wallet_address: '{{type}} Wallet Address',
|
||||||
|
back_to_deposit: 'Back to Deposit',
|
||||||
|
deposit_address_sent: 'Address sent: {{type}}',
|
||||||
|
error_copying_address: 'Error copying address.',
|
||||||
|
error_deposit_instructions: 'Error loading deposit instructions.',
|
||||||
|
deposit_crypto_title: 'Crypto Deposit {{type}}',
|
||||||
|
deposit_crypto_scan_qr: 'Scan QR code',
|
||||||
|
deposit_crypto_address: '{{type}} Address',
|
||||||
|
deposit_crypto_instruction: 'Send {{type}} to this address',
|
||||||
|
},
|
||||||
|
bot: {
|
||||||
|
contact_support: 'Contact Support',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getNestedValue(obj, keyPath) {
|
||||||
|
return keyPath.split('.').reduce((o, k) => o?.[k], obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('../i18n/index.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
tForUser: () => (key) => getNestedValue(en, key) || key,
|
||||||
|
LANGUAGE_NAMES: { en: 'English', es: 'Espanol', de: 'Deutsch' },
|
||||||
|
AVAILABLE_LANGUAGES: ['en', 'es', 'de'],
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../context/bot.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: botMock,
|
||||||
|
botAvailable: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../context/userStates.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: userStatesMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../services/userService.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: userServiceMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../services/productService.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: productServiceMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../services/locationService.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: locationServiceMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../services/categoryService.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: categoryServiceMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../services/purchaseService.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: purchaseServiceMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../utils/validators.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
isValidWalletType: vi.fn().mockReturnValue(true),
|
||||||
|
isValidNumericId: vi.fn().mockReturnValue(true),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../services/chatbotService.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: chatbotServiceMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { assertShopOpen, redirectIfPaused } from '../utils/sleepGuard.js';
|
||||||
|
import UserProductHandler from '../handlers/userHandlers/userProductHandler.js';
|
||||||
|
|
||||||
|
describe('sleepGuard', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
chatbotServiceMock.isSleepMode.mockResolvedValue(false);
|
||||||
|
chatbotServiceMock.sendToChatbot.mockResolvedValue({ reply: 'AI: Hello! How can I help?' });
|
||||||
|
chatbotServiceMock.getSleepMessage.mockResolvedValue('Shop is paused.');
|
||||||
|
userServiceMock.getUserByTelegramId.mockResolvedValue({ id: 1, language: 'en' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// T1: shop open — no block
|
||||||
|
it('T1: returns false when isSleepMode is false, no bot calls', async () => {
|
||||||
|
const result = await assertShopOpen({
|
||||||
|
chatId: 123,
|
||||||
|
telegramId: 456,
|
||||||
|
callbackQueryId: 'cq-1',
|
||||||
|
username: 'testuser',
|
||||||
|
name: 'Test',
|
||||||
|
action: 'buy',
|
||||||
|
});
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(botMock.sendMessage).not.toHaveBeenCalled();
|
||||||
|
expect(botMock.answerCallbackQuery).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// T2: sleep mode — blocks and sends message
|
||||||
|
it('T2: returns true, answers callback, sends AI reply', async () => {
|
||||||
|
chatbotServiceMock.isSleepMode.mockResolvedValue(true);
|
||||||
|
const result = await assertShopOpen({
|
||||||
|
chatId: 123,
|
||||||
|
telegramId: 456,
|
||||||
|
callbackQueryId: 'cq-1',
|
||||||
|
username: 'testuser',
|
||||||
|
name: 'Test',
|
||||||
|
action: 'buy',
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(botMock.answerCallbackQuery).toHaveBeenCalledWith('cq-1');
|
||||||
|
expect(botMock.sendMessage).toHaveBeenCalledWith(123, 'AI: Hello! How can I help?', {
|
||||||
|
reply_markup: { remove_keyboard: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// T3: sends AI welcome with user language
|
||||||
|
it('T3: sends AI welcome with correct language from user', async () => {
|
||||||
|
chatbotServiceMock.isSleepMode.mockResolvedValue(true);
|
||||||
|
userServiceMock.getUserByTelegramId.mockResolvedValue({ id: 1, language: 'de' });
|
||||||
|
await assertShopOpen({
|
||||||
|
chatId: 123,
|
||||||
|
telegramId: 456,
|
||||||
|
callbackQueryId: 'cq-1',
|
||||||
|
username: 'testuser',
|
||||||
|
name: 'Test',
|
||||||
|
action: 'buy',
|
||||||
|
});
|
||||||
|
expect(chatbotServiceMock.sendToChatbot).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
sessionId: '456',
|
||||||
|
message: '/start',
|
||||||
|
telegramId: '456',
|
||||||
|
language: 'de',
|
||||||
|
username: 'testuser',
|
||||||
|
name: 'Test',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// T4: handleBuyProduct blocked in sleep mode
|
||||||
|
it('T4: handleBuyProduct returns early in sleep mode, no ProductService call', async () => {
|
||||||
|
chatbotServiceMock.isSleepMode.mockResolvedValue(true);
|
||||||
|
const callbackQuery = {
|
||||||
|
id: 'cq-buy',
|
||||||
|
from: { id: 12345, first_name: 'Test', username: 'testuser' },
|
||||||
|
message: { chat: { id: 67890 }, message_id: 111, reply_markup: { inline_keyboard: [] } },
|
||||||
|
data: 'buy_product_42',
|
||||||
|
};
|
||||||
|
await UserProductHandler.handleBuyProduct(callbackQuery);
|
||||||
|
expect(productServiceMock.getProductById).not.toHaveBeenCalled();
|
||||||
|
expect(botMock.sendMessage).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// T5: read-only flows untouched (source-level)
|
||||||
|
it('T5: userPurchaseHandler does not import sleepGuard', async () => {
|
||||||
|
const fs = await import('fs');
|
||||||
|
const content = fs.readFileSync(
|
||||||
|
new URL('../handlers/userHandlers/userPurchaseHandler.js', import.meta.url).pathname,
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
expect(content).not.toContain('sleepGuard');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('T5: balanceHandler does not import sleepGuard', async () => {
|
||||||
|
const fs = await import('fs');
|
||||||
|
const content = fs.readFileSync(
|
||||||
|
new URL('../handlers/userHandlers/wallet/balanceHandler.js', import.meta.url).pathname,
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
expect(content).not.toContain('sleepGuard');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('T5: historyHandler does not import sleepGuard', async () => {
|
||||||
|
const fs = await import('fs');
|
||||||
|
const content = fs.readFileSync(
|
||||||
|
new URL('../handlers/userHandlers/wallet/historyHandler.js', import.meta.url).pathname,
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
expect(content).not.toContain('sleepGuard');
|
||||||
|
});
|
||||||
|
|
||||||
|
// T6: regression — buy works when shop is open
|
||||||
|
it('T6: handleBuyProduct calls getProductById when shop is open', async () => {
|
||||||
|
chatbotServiceMock.isSleepMode.mockResolvedValue(false);
|
||||||
|
productServiceMock.getProductById.mockResolvedValue({
|
||||||
|
id: 42,
|
||||||
|
name: 'Test Product',
|
||||||
|
price: 10,
|
||||||
|
is_mono: true,
|
||||||
|
location_id: 1,
|
||||||
|
category_id: 1,
|
||||||
|
photo_url: null,
|
||||||
|
hidden_photo_url: null,
|
||||||
|
private_data: 'secret',
|
||||||
|
hidden_description: 'hidden',
|
||||||
|
hidden_coordinates: '0,0',
|
||||||
|
});
|
||||||
|
productServiceMock.getProductAvailability.mockResolvedValue({ loc_active: true, cat_active: true });
|
||||||
|
const callbackQuery = {
|
||||||
|
id: 'cq-buy-open',
|
||||||
|
from: { id: 12345, first_name: 'Test', username: 'testuser' },
|
||||||
|
message: { chat: { id: 67890 }, message_id: 111, reply_markup: { inline_keyboard: [] } },
|
||||||
|
data: 'buy_product_42',
|
||||||
|
};
|
||||||
|
await UserProductHandler.handleBuyProduct(callbackQuery);
|
||||||
|
expect(productServiceMock.getProductById).toHaveBeenCalledWith('42');
|
||||||
|
});
|
||||||
|
|
||||||
|
// redirectIfPaused alias
|
||||||
|
it('redirectIfPaused delegates to assertShopOpen', async () => {
|
||||||
|
chatbotServiceMock.isSleepMode.mockResolvedValue(true);
|
||||||
|
const result = await redirectIfPaused({
|
||||||
|
chatId: 123,
|
||||||
|
telegramId: 456,
|
||||||
|
callbackQueryId: 'cq-1',
|
||||||
|
username: 'testuser',
|
||||||
|
name: 'Test',
|
||||||
|
action: 'navigate',
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(botMock.sendMessage).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback: sendToChatbot fails → still blocks with sleep message
|
||||||
|
it('fallback: still blocks and shows sleep message when AI is unreachable', async () => {
|
||||||
|
chatbotServiceMock.isSleepMode.mockResolvedValue(true);
|
||||||
|
chatbotServiceMock.sendToChatbot.mockRejectedValue(new Error('Network error'));
|
||||||
|
const result = await assertShopOpen({
|
||||||
|
chatId: 123,
|
||||||
|
telegramId: 456,
|
||||||
|
callbackQueryId: 'cq-1',
|
||||||
|
username: 'testuser',
|
||||||
|
name: 'Test',
|
||||||
|
action: 'buy',
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(botMock.sendMessage).toHaveBeenCalledWith(123, 'Shop is paused.', {
|
||||||
|
reply_markup: { remove_keyboard: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -69,6 +69,12 @@ vi.mock('../utils/logger.js', () => ({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../utils/sleepGuard.js', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
redirectIfPaused: vi.fn().mockResolvedValue(false), // магазин открыт — не блокируем
|
||||||
|
assertShopOpen: vi.fn().mockResolvedValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../config/config.js', () => ({
|
vi.mock('../config/config.js', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import UserService from "../../services/userService.js";
|
|||||||
import PurchaseService from '../../services/purchaseService.js';
|
import PurchaseService from '../../services/purchaseService.js';
|
||||||
import Validators from '../../utils/validators.js';
|
import Validators from '../../utils/validators.js';
|
||||||
import { editOrSendCallback, deleteAndSend } from '../../utils/messageUtils.js';
|
import { editOrSendCallback, deleteAndSend } from '../../utils/messageUtils.js';
|
||||||
|
import { redirectIfPaused } from '../../utils/sleepGuard.js';
|
||||||
import { tForUser } from '../../i18n/index.js';
|
import { tForUser } from '../../i18n/index.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -99,6 +100,15 @@ export default class UserProductHandler {
|
|||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
const country = decodeURIComponent(callbackQuery.data.replace('shop_country_', ''));
|
const country = decodeURIComponent(callbackQuery.data.replace('shop_country_', ''));
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId: callbackQuery.from.id,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'country_selection',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
@@ -141,6 +151,15 @@ export default class UserProductHandler {
|
|||||||
const payload = callbackQuery.data.replace('shop_city_', '');
|
const payload = callbackQuery.data.replace('shop_city_', '');
|
||||||
const [country, city] = payload.split('|').map(decodeURIComponent);
|
const [country, city] = payload.split('|').map(decodeURIComponent);
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId: callbackQuery.from.id,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'city_selection',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
@@ -182,6 +201,15 @@ export default class UserProductHandler {
|
|||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
const locationId = parseInt(callbackQuery.data.replace('shop_loc_', ''), 10);
|
const locationId = parseInt(callbackQuery.data.replace('shop_loc_', ''), 10);
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId: callbackQuery.from.id,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'district_selection',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
@@ -264,6 +292,15 @@ export default class UserProductHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId: callbackQuery.from.id,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'district_back',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
@@ -315,6 +352,15 @@ export default class UserProductHandler {
|
|||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
const [locationId, categoryId] = callbackQuery.data.replace('shop_category_', '').split('_');
|
const [locationId, categoryId] = callbackQuery.data.replace('shop_category_', '').split('_');
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId: callbackQuery.from.id,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'category_selection',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
@@ -400,6 +446,15 @@ export default class UserProductHandler {
|
|||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
const [locationId, categoryId, subcategoryId, photoMessageId] = callbackQuery.data.replace('shop_subcategory_', '').split('_');
|
const [locationId, categoryId, subcategoryId, photoMessageId] = callbackQuery.data.replace('shop_subcategory_', '').split('_');
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId: callbackQuery.from.id,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'subcategory_selection',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
@@ -470,6 +525,15 @@ export default class UserProductHandler {
|
|||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
const productId = callbackQuery.data.replace('shop_product_', '');
|
const productId = callbackQuery.data.replace('shop_product_', '');
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId: callbackQuery.from.id,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'product_selection',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
@@ -588,6 +652,15 @@ export default class UserProductHandler {
|
|||||||
const productId = callbackQuery.data.replace('increase_quantity_', '');
|
const productId = callbackQuery.data.replace('increase_quantity_', '');
|
||||||
const state = await userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId: callbackQuery.from.id,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'increase_quantity',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const product = await ProductService.getProductById(productId);
|
const product = await ProductService.getProductById(productId);
|
||||||
|
|
||||||
@@ -648,6 +721,15 @@ export default class UserProductHandler {
|
|||||||
const productId = callbackQuery.data.replace('decrease_quantity_', '');
|
const productId = callbackQuery.data.replace('decrease_quantity_', '');
|
||||||
const state = await userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId: callbackQuery.from.id,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'decrease_quantity',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const product = await ProductService.getProductById(productId)
|
const product = await ProductService.getProductById(productId)
|
||||||
|
|
||||||
@@ -707,6 +789,15 @@ export default class UserProductHandler {
|
|||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const productId = callbackQuery.data.replace('buy_product_', '');
|
const productId = callbackQuery.data.replace('buy_product_', '');
|
||||||
const state = await userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'buy',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
@@ -833,6 +924,15 @@ export default class UserProductHandler {
|
|||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
|
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
|
||||||
const state = await userStates.get(chatId);
|
const state = await userStates.get(chatId);
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'pay',
|
||||||
|
})) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
|
|||||||
@@ -6,12 +6,23 @@ import UserService from '../../../services/userService.js';
|
|||||||
import logger from '../../../utils/logger.js';
|
import logger from '../../../utils/logger.js';
|
||||||
import WalletHelpers from './helpers.js';
|
import WalletHelpers from './helpers.js';
|
||||||
import { editOrSendCallback } from '../../../utils/messageUtils.js';
|
import { editOrSendCallback } from '../../../utils/messageUtils.js';
|
||||||
|
import { redirectIfPaused } from '../../../utils/sleepGuard.js';
|
||||||
import { tForUser } from '../../../i18n/index.js';
|
import { tForUser } from '../../../i18n/index.js';
|
||||||
|
|
||||||
export default class CreateHandler {
|
export default class CreateHandler {
|
||||||
static async handleAddWallet(callbackQuery) {
|
static async handleAddWallet(callbackQuery) {
|
||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'add_wallet',
|
||||||
|
})) return;
|
||||||
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
const lang = user?.language || 'en';
|
const lang = user?.language || 'en';
|
||||||
const t = tForUser(lang);
|
const t = tForUser(lang);
|
||||||
@@ -41,6 +52,15 @@ export default class CreateHandler {
|
|||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
|
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'generate_wallet',
|
||||||
|
})) return;
|
||||||
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
const lang = user?.language || 'en';
|
const lang = user?.language || 'en';
|
||||||
const t = tForUser(lang);
|
const t = tForUser(lang);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import UserService from '../../../services/userService.js';
|
|||||||
import bot from '../../../context/bot.js';
|
import bot from '../../../context/bot.js';
|
||||||
import logger from '../../../utils/logger.js';
|
import logger from '../../../utils/logger.js';
|
||||||
import { editOrSendCallback } from '../../../utils/messageUtils.js';
|
import { editOrSendCallback } from '../../../utils/messageUtils.js';
|
||||||
|
import { redirectIfPaused } from '../../../utils/sleepGuard.js';
|
||||||
import { tForUser } from '../../../i18n/index.js';
|
import { tForUser } from '../../../i18n/index.js';
|
||||||
|
|
||||||
const CRYPTO_SYMBOLS = {
|
const CRYPTO_SYMBOLS = {
|
||||||
@@ -19,6 +20,15 @@ export default class DepositHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'deposit_select',
|
||||||
|
})) return;
|
||||||
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
const lang = user?.language || 'en';
|
const lang = user?.language || 'en';
|
||||||
const t = tForUser(lang);
|
const t = tForUser(lang);
|
||||||
@@ -71,6 +81,15 @@ export default class DepositHandler {
|
|||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const walletType = callbackQuery.data.replace('deposit_wallet_', '');
|
const walletType = callbackQuery.data.replace('deposit_wallet_', '');
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'deposit_instruction',
|
||||||
|
})) return;
|
||||||
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
const lang = user?.language || 'en';
|
const lang = user?.language || 'en';
|
||||||
const t = tForUser(lang);
|
const t = tForUser(lang);
|
||||||
@@ -205,6 +224,15 @@ export default class DepositHandler {
|
|||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const walletType = callbackQuery.data.replace('deposit_crypto_', '');
|
const walletType = callbackQuery.data.replace('deposit_crypto_', '');
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'deposit_crypto_qr',
|
||||||
|
})) return;
|
||||||
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
const lang = user?.language || 'en';
|
const lang = user?.language || 'en';
|
||||||
const t = tForUser(lang);
|
const t = tForUser(lang);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import UserService from '../../../services/userService.js';
|
|||||||
import bot from '../../../context/bot.js';
|
import bot from '../../../context/bot.js';
|
||||||
import logger from '../../../utils/logger.js';
|
import logger from '../../../utils/logger.js';
|
||||||
import { editOrSendCallback } from '../../../utils/messageUtils.js';
|
import { editOrSendCallback } from '../../../utils/messageUtils.js';
|
||||||
|
import { redirectIfPaused } from '../../../utils/sleepGuard.js';
|
||||||
import { tForUser } from '../../../i18n/index.js';
|
import { tForUser } from '../../../i18n/index.js';
|
||||||
|
|
||||||
export default class TopUpHandler {
|
export default class TopUpHandler {
|
||||||
@@ -11,6 +12,15 @@ export default class TopUpHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
|
|
||||||
|
if (await redirectIfPaused({
|
||||||
|
chatId,
|
||||||
|
telegramId,
|
||||||
|
callbackQueryId: callbackQuery.id,
|
||||||
|
username: callbackQuery.from?.username,
|
||||||
|
name: callbackQuery.from?.first_name,
|
||||||
|
action: 'top_up',
|
||||||
|
})) return;
|
||||||
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
const lang = user?.language || 'en';
|
const lang = user?.language || 'en';
|
||||||
const t = tForUser(lang);
|
const t = tForUser(lang);
|
||||||
|
|||||||
56
src/utils/sleepGuard.js
Normal file
56
src/utils/sleepGuard.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import bot from '../context/bot.js';
|
||||||
|
import logger from '../utils/logger.js';
|
||||||
|
import chatbotService from '../services/chatbotService.js';
|
||||||
|
import UserService from '../services/userService.js';
|
||||||
|
import { tForUser } from '../i18n/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Блокирует запись-флоу при паузе магазина.
|
||||||
|
* Возвращает true, если магазин на паузе и действие заблокировано (handler должен return).
|
||||||
|
*/
|
||||||
|
export async function assertShopOpen({ chatId, telegramId, callbackQueryId, username, name, action }) {
|
||||||
|
if (!(await chatbotService.isSleepMode())) return false;
|
||||||
|
const sleepMsg = await chatbotService.getSleepMessage();
|
||||||
|
try {
|
||||||
|
const user = telegramId ? await UserService.getUserByTelegramId(telegramId) : null;
|
||||||
|
const lang = user?.language || 'en';
|
||||||
|
const t = tForUser(lang);
|
||||||
|
if (callbackQueryId) {
|
||||||
|
await bot.answerCallbackQuery(callbackQueryId).catch(() => {});
|
||||||
|
}
|
||||||
|
let welcomeReply = sleepMsg; // Default to sleep message
|
||||||
|
try {
|
||||||
|
const welcome = await chatbotService.sendToChatbot({
|
||||||
|
sessionId: telegramId != null ? String(telegramId) : undefined,
|
||||||
|
message: '/start',
|
||||||
|
telegramId: telegramId != null ? String(telegramId) : undefined,
|
||||||
|
language: lang,
|
||||||
|
username,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
welcomeReply = welcome.reply || sleepMsg;
|
||||||
|
} catch (welcomeErr) {
|
||||||
|
// Fallback to sleep message if AI is unreachable
|
||||||
|
logger.warn({ welcomeErr }, 'Failed to get AI welcome message, falling back to sleep message');
|
||||||
|
}
|
||||||
|
await bot.sendMessage(chatId, welcomeReply, {
|
||||||
|
reply_markup: { remove_keyboard: true },
|
||||||
|
});
|
||||||
|
logger.info({ action, telegramId }, 'Sleep mode blocked action');
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err, action }, 'sleepGuard error');
|
||||||
|
// In case of any other error, still send sleep message
|
||||||
|
await bot.sendMessage(chatId, sleepMsg, {
|
||||||
|
reply_markup: { remove_keyboard: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Мягкое перенаправление для навигации каталога: при паузе показывает ИИ-диалог.
|
||||||
|
* Алиас для assertShopOpen — та же логика.
|
||||||
|
*/
|
||||||
|
export async function redirectIfPaused(params) {
|
||||||
|
return assertShopOpen(params);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user