fix(bot): remove redundant deposit amount step, add Visa/MC label, Mercuryo auth note, enforce product completeness
- Remove deposit amount-selection step; deposit_wallet_<TYPE> now routes directly to instruction - Add VISA / Mastercard text to Mercuryo card button (i18n en/es/de) - Add deposit_important5: Mercuryo 1-payment-without-auth + top-up-reserve warning - Admin validation: description + photo required on product create/update (products.js, catalogProducts.js) - Cleanup orphan uploaded files on validation failure (catalogProducts.js) - Bot guards: fallback to products.no_description / products.no_photo for incomplete products - Add vitest + 7 purchase edge-case tests (src/__tests__/userProductHandler.test.js) - Bump version to v1.2.1
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,3 +20,6 @@ db/*.db-shm
|
||||
/*.mjs
|
||||
|
||||
/*.mjs
|
||||
|
||||
# Production backups (contain secrets + DB snapshots — never commit)
|
||||
production-backup/
|
||||
|
||||
11
VERSION.md
11
VERSION.md
@@ -9,10 +9,19 @@
|
||||
|
||||
## Current Version
|
||||
|
||||
**v1.2.0** — 2026-07-08
|
||||
**v1.2.1** — 2026-07-18
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.2.1 — 2026-07-18
|
||||
- **refactor**: Removed deposit amount-selection step (redundant); deposit_wallet_ now goes directly to Mercuryo instructions
|
||||
- **feat**: Updated Mercuryo button text to include VISA/Mastercard branding in all locales
|
||||
- **feat**: Added deposit_important5 note about Mercuryo authorization limits and top-up reserve
|
||||
- **fix**: Enforced description + photo required in admin product create/update routes (products.js, catalogProducts.js)
|
||||
- **fix**: Added defensive guards in bot purchase display — description fallback and no-photo placeholder (userProductHandler.js)
|
||||
- **feat**: Added i18n keys `products.no_description` and `products.no_photo` in en/es/de
|
||||
- **fix**: Marked description and photo fields as required in admin forms (product-edit.ejs, products.ejs, catalog modal)
|
||||
|
||||
### v1.2.0 — 2026-07-08
|
||||
- **fix**: Disabled CSRF checks in admin panel for Tor / onion zone compatibility
|
||||
- **fix**: Fixed "Invalid wallet type" error in Telegram bot purchase flow (`main`/`bonus` types added to validator)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js"
|
||||
},
|
||||
@@ -31,6 +33,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"playwright": "^1.61.1"
|
||||
"playwright": "^1.61.1",
|
||||
"vitest": "^2.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
223
src/__tests__/userProductHandler.test.js
Normal file
223
src/__tests__/userProductHandler.test.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import UserProductHandler from '../handlers/userHandlers/userProductHandler.js';
|
||||
|
||||
const en = {
|
||||
products: {
|
||||
product_price: '💰 Price',
|
||||
product_description: '📝 Description',
|
||||
product_available: '📦 Available',
|
||||
product_category: 'Category',
|
||||
buy_now: '🛒 Buy Now',
|
||||
back: '« Back',
|
||||
infinite_stock: '∞ Always available',
|
||||
no_description: 'No description provided',
|
||||
no_photo: 'No photo available',
|
||||
error_loading_product: 'Error loading product details. Please try again.'
|
||||
}
|
||||
};
|
||||
|
||||
vi.mock('../context/bot.js', () => {
|
||||
const bot = {
|
||||
deleteMessage: vi.fn(),
|
||||
sendMessage: vi.fn().mockResolvedValue({ message_id: 999 }),
|
||||
sendPhoto: vi.fn().mockResolvedValue({ message_id: 888 })
|
||||
};
|
||||
return { __esModule: true, default: bot, botAvailable: true };
|
||||
});
|
||||
|
||||
vi.mock('../context/userStates.js', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
get: vi.fn().mockResolvedValue({}),
|
||||
set: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../services/userService.js', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getUserByTelegramId: vi.fn().mockResolvedValue({ language: 'en' })
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../i18n/index.js', () => ({
|
||||
__esModule: true,
|
||||
tForUser: (lang) => (key) => getNestedValue(en, key) || key
|
||||
}));
|
||||
|
||||
vi.mock('../services/productService.js', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getDetailedProductById: vi.fn(),
|
||||
getProductById: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
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/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)
|
||||
}
|
||||
}));
|
||||
|
||||
function getNestedValue(obj, keyPath) {
|
||||
return keyPath.split('.').reduce((o, k) => o?.[k], obj);
|
||||
}
|
||||
|
||||
import bot from '../context/bot.js';
|
||||
import userStates from '../context/userStates.js';
|
||||
import ProductService from '../services/productService.js';
|
||||
|
||||
function makeCallbackQuery(productId, overrides = {}) {
|
||||
return {
|
||||
id: 'callback-id',
|
||||
from: { id: 12345, first_name: 'Test' },
|
||||
message: {
|
||||
chat: { id: 67890 },
|
||||
message_id: 111
|
||||
},
|
||||
data: `shop_product_${productId}`,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function baseProduct(overrides = {}) {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Test Product',
|
||||
price: 10,
|
||||
description: 'A nice product',
|
||||
quantity_in_stock: 5,
|
||||
photo_url: 'http://example.com/photo.jpg',
|
||||
is_mono: 0,
|
||||
category_name: 'Test Category',
|
||||
category_id: 2,
|
||||
location_id: 3,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('UserProductHandler.handleProductSelection edge cases', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders fallback text when description and photo are missing, sends message, does not send photo', async () => {
|
||||
ProductService.getDetailedProductById.mockResolvedValue(
|
||||
baseProduct({ description: '', photo_url: '' })
|
||||
);
|
||||
|
||||
await expect(
|
||||
UserProductHandler.handleProductSelection(makeCallbackQuery(1))
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(bot.deleteMessage).toHaveBeenCalledWith(67890, 111);
|
||||
expect(bot.sendPhoto).not.toHaveBeenCalled();
|
||||
expect(bot.sendMessage).toHaveBeenCalledTimes(1);
|
||||
const sentText = bot.sendMessage.mock.calls[0][1];
|
||||
expect(sentText).toContain(en.products.no_description);
|
||||
expect(sentText).toContain(en.products.no_photo);
|
||||
});
|
||||
|
||||
it('renders no-photo line and does not call sendPhoto when photo_url is missing', async () => {
|
||||
ProductService.getDetailedProductById.mockResolvedValue(
|
||||
baseProduct({ photo_url: '' })
|
||||
);
|
||||
|
||||
await expect(
|
||||
UserProductHandler.handleProductSelection(makeCallbackQuery(1))
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(bot.sendPhoto).not.toHaveBeenCalled();
|
||||
const sentText = bot.sendMessage.mock.calls[0][1];
|
||||
expect(sentText).toContain(en.products.no_photo);
|
||||
expect(sentText).toContain('A nice product');
|
||||
});
|
||||
|
||||
it('renders description fallback and calls sendPhoto when description is missing', async () => {
|
||||
ProductService.getDetailedProductById.mockResolvedValue(
|
||||
baseProduct({ description: '' })
|
||||
);
|
||||
|
||||
await expect(
|
||||
UserProductHandler.handleProductSelection(makeCallbackQuery(1))
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(bot.sendPhoto).toHaveBeenCalledWith(67890, 'http://example.com/photo.jpg', { caption: 'Public photo' });
|
||||
const sentText = bot.sendMessage.mock.calls[0][1];
|
||||
expect(sentText).toContain(en.products.no_description);
|
||||
expect(sentText).not.toContain(en.products.no_photo);
|
||||
});
|
||||
|
||||
it('takes normal path with both fields present, calls sendPhoto, omits fallback text', async () => {
|
||||
ProductService.getDetailedProductById.mockResolvedValue(baseProduct());
|
||||
|
||||
await expect(
|
||||
UserProductHandler.handleProductSelection(makeCallbackQuery(1))
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(bot.sendPhoto).toHaveBeenCalledWith(67890, 'http://example.com/photo.jpg', { caption: 'Public photo' });
|
||||
const sentText = bot.sendMessage.mock.calls[0][1];
|
||||
expect(sentText).toContain('A nice product');
|
||||
expect(sentText).not.toContain(en.products.no_description);
|
||||
expect(sentText).not.toContain(en.products.no_photo);
|
||||
});
|
||||
|
||||
it('does not crash when ProductService returns null, sends error message', async () => {
|
||||
ProductService.getDetailedProductById.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
UserProductHandler.handleProductSelection(makeCallbackQuery(1))
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(bot.sendMessage).toHaveBeenCalledWith(67890, en.products.error_loading_product);
|
||||
expect(bot.sendPhoto).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders infinite_stock line for mono product with missing description', async () => {
|
||||
ProductService.getDetailedProductById.mockResolvedValue(
|
||||
baseProduct({ is_mono: 1, description: '', photo_url: '' })
|
||||
);
|
||||
|
||||
await expect(
|
||||
UserProductHandler.handleProductSelection(makeCallbackQuery(1))
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
const sentText = bot.sendMessage.mock.calls[0][1];
|
||||
expect(sentText).toContain(en.products.no_description);
|
||||
expect(sentText).toContain(en.products.infinite_stock);
|
||||
expect(sentText).toContain(en.products.no_photo);
|
||||
expect(bot.sendPhoto).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders non-mono product with quantity_in_stock=0 without crashing', async () => {
|
||||
ProductService.getDetailedProductById.mockResolvedValue(
|
||||
baseProduct({ quantity_in_stock: 0, photo_url: '' })
|
||||
);
|
||||
|
||||
await expect(
|
||||
UserProductHandler.handleProductSelection(makeCallbackQuery(1))
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
const sentText = bot.sendMessage.mock.calls[0][1];
|
||||
expect(sentText).toContain('0 pcs');
|
||||
expect(bot.sendPhoto).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -230,13 +230,13 @@ function buildProductFormHtml(action, catOptions, locations) {
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-control form-control-sm" rows="3" placeholder="Public description"></textarea>
|
||||
<label class="form-label">Description <span class="text-danger">*</span></label>
|
||||
<textarea name="description" class="form-control form-control-sm" rows="3" placeholder="Public description" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<hr class="my-2">
|
||||
<h6 class="text-muted mb-2">Public Photo</h6>
|
||||
<h6 class="text-muted mb-2">Public Photo <span class="text-danger">*</span></h6>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Photo URL</label>
|
||||
@@ -245,6 +245,7 @@ function buildProductFormHtml(action, catOptions, locations) {
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Or Upload Photo</label>
|
||||
<input type="file" name="photo_file" accept="image/*" class="form-control form-control-sm">
|
||||
<small class="text-danger">Photo is required — upload a file or paste a URL.</small>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
|
||||
@@ -3,10 +3,18 @@ import multer from 'multer';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import db from '../../config/database.js';
|
||||
import { asyncHandler } from '../errorHandler.js';
|
||||
import { validateCsrfFromBody } from '../csrf.js';
|
||||
|
||||
function cleanupUploadedFiles(req) {
|
||||
for (const field of ['photo_file', 'hidden_photo_file']) {
|
||||
const p = req.files?.[field]?.[0]?.path;
|
||||
if (p) fs.unlink(p, () => {});
|
||||
}
|
||||
}
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const uploadsDir = join(__dirname, '..', '..', '..', 'uploads');
|
||||
const storage = multer.diskStorage({
|
||||
@@ -34,10 +42,13 @@ router.post('/products', upload.fields([{ name: 'photo_file' }, { name: 'hidden_
|
||||
if (!validateCsrfFromBody(req, res)) return;
|
||||
const { name, price, quantity_in_stock, description, photo_url, hidden_photo_url,
|
||||
hidden_coordinates, hidden_description, private_data, category_id, subcategory_id, location_id, is_mono } = req.body;
|
||||
if (!name || !price || !category_id) return res.redirect('/catalog?msg=Name+price+category+required&msg_type=error');
|
||||
if (!name || !price || !category_id) { cleanupUploadedFiles(req); return res.redirect('/catalog?msg=Name+price+category+required&msg_type=error'); }
|
||||
const priceNum = parseFloat(price);
|
||||
if (isNaN(priceNum) || priceNum <= 0) return res.redirect('/catalog?msg=Price+must+be+greater+than+0&msg_type=error');
|
||||
if (isNaN(priceNum) || priceNum <= 0) { cleanupUploadedFiles(req); return res.redirect('/catalog?msg=Price+must+be+greater+than+0&msg_type=error'); }
|
||||
const desc = (description || '').trim();
|
||||
if (!desc) { cleanupUploadedFiles(req); return res.redirect('/catalog?msg=Description+is+required&msg_type=error'); }
|
||||
const pu = req.files?.photo_file?.[0] ? `/uploads/${req.files.photo_file[0].filename}` : (photo_url || '');
|
||||
if (!pu.trim()) { cleanupUploadedFiles(req); return res.redirect('/catalog?msg=Photo+is+required&msg_type=error'); }
|
||||
const hu = req.files?.hidden_photo_file?.[0] ? `/uploads/${req.files.hidden_photo_file[0].filename}` : (hidden_photo_url || '');
|
||||
const locId = location_id || (await db.getAsync('SELECT location_id FROM categories WHERE id=?', [category_id]))?.location_id || null;
|
||||
const monoFlag = is_mono === '1' ? 1 : 0;
|
||||
@@ -45,7 +56,7 @@ router.post('/products', upload.fields([{ name: 'photo_file' }, { name: 'hidden_
|
||||
await db.runAsync(`INSERT INTO products (name,price,quantity_in_stock,description,photo_url,hidden_photo_url,
|
||||
hidden_coordinates,hidden_description,private_data,category_id,subcategory_id,location_id,is_mono)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[name.trim(), priceNum, stockQty, description || '', pu, hu,
|
||||
[name.trim(), priceNum, stockQty, desc, pu, hu,
|
||||
hidden_coordinates || '', hidden_description || '', private_data || '', category_id, subcategory_id || null, locId, monoFlag]);
|
||||
res.redirect('/catalog?msg=Product+added&msg_type=success');
|
||||
}));
|
||||
@@ -54,17 +65,20 @@ router.post('/products/:id/edit', upload.fields([{ name: 'photo_file' }, { name:
|
||||
if (!validateCsrfFromBody(req, res)) return;
|
||||
const { name, price, quantity_in_stock, description, photo_url, hidden_photo_url,
|
||||
hidden_coordinates, hidden_description, private_data, category_id, subcategory_id, location_id, is_mono } = req.body;
|
||||
if (!name || !price || !category_id) return res.redirect('/catalog?msg=Name+price+category+required&msg_type=error');
|
||||
if (!name || !price || !category_id) { cleanupUploadedFiles(req); return res.redirect('/catalog?msg=Name+price+category+required&msg_type=error'); }
|
||||
const priceNum = parseFloat(price);
|
||||
if (isNaN(priceNum) || priceNum <= 0) return res.redirect('/catalog?msg=Price+must+be+greater+than+0&msg_type=error');
|
||||
if (isNaN(priceNum) || priceNum <= 0) { cleanupUploadedFiles(req); return res.redirect('/catalog?msg=Price+must+be+greater+than+0&msg_type=error'); }
|
||||
const desc = (description || '').trim();
|
||||
if (!desc) { cleanupUploadedFiles(req); return res.redirect('/catalog?msg=Description+is+required&msg_type=error'); }
|
||||
const pu = req.files?.photo_file?.[0] ? `/uploads/${req.files.photo_file[0].filename}` : (photo_url || '');
|
||||
if (!pu.trim()) { cleanupUploadedFiles(req); return res.redirect('/catalog?msg=Photo+is+required&msg_type=error'); }
|
||||
const hu = req.files?.hidden_photo_file?.[0] ? `/uploads/${req.files.hidden_photo_file[0].filename}` : (hidden_photo_url || '');
|
||||
const locId = location_id || (await db.getAsync('SELECT location_id FROM categories WHERE id=?', [category_id]))?.location_id || null;
|
||||
const monoFlag = is_mono === '1' ? 1 : 0;
|
||||
const stockQty = monoFlag ? 999999 : (parseInt(quantity_in_stock, 10) || 0);
|
||||
await db.runAsync(`UPDATE products SET name=?,price=?,quantity_in_stock=?,description=?,photo_url=?,hidden_photo_url=?,
|
||||
hidden_coordinates=?,hidden_description=?,private_data=?,category_id=?,subcategory_id=?,location_id=?,is_mono=? WHERE id=?`,
|
||||
[name.trim(), priceNum, stockQty, description || '', pu, hu,
|
||||
[name.trim(), priceNum, stockQty, desc, pu, hu,
|
||||
hidden_coordinates || '', hidden_description || '', private_data || '', category_id, subcategory_id || null, locId, monoFlag, req.params.id]);
|
||||
res.redirect('/catalog?msg=Product+updated&msg_type=success');
|
||||
}));
|
||||
|
||||
@@ -26,12 +26,20 @@ router.post('/', asyncHandler(async (req, res) => {
|
||||
if (isNaN(priceNum) || priceNum <= 0) {
|
||||
return res.redirect('/products?error=Price+must+be+greater+than+0');
|
||||
}
|
||||
const desc = (description || '').trim();
|
||||
if (!desc) {
|
||||
return res.redirect('/products?error=Description+is+required');
|
||||
}
|
||||
const photo = (photo_url || '').trim();
|
||||
if (!photo) {
|
||||
return res.redirect('/products?error=Photo+is+required');
|
||||
}
|
||||
const monoFlag = is_mono === '1' ? 1 : 0;
|
||||
const stockQty = monoFlag ? 999999 : (quantity_in_stock || 0);
|
||||
await db.runAsync(
|
||||
`INSERT INTO products (name, price, quantity_in_stock, description, photo_url, category_id, subcategory_id, location_id, is_mono)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, (SELECT location_id FROM categories WHERE id = ?), ?)`,
|
||||
[name, priceNum, stockQty, description || '', photo_url || '', category_id, subcategory_id || null, category_id, monoFlag]
|
||||
[name, priceNum, stockQty, desc, photo, category_id, subcategory_id || null, category_id, monoFlag]
|
||||
);
|
||||
res.redirect('/products');
|
||||
}));
|
||||
@@ -56,12 +64,20 @@ router.post('/:id/update', asyncHandler(async (req, res) => {
|
||||
if (isNaN(priceNum) || priceNum <= 0) {
|
||||
return res.redirect('/products?error=Price+must+be+greater+than+0');
|
||||
}
|
||||
const desc = (description || '').trim();
|
||||
if (!desc) {
|
||||
return res.redirect('/products?error=Description+is+required');
|
||||
}
|
||||
const photo = (photo_url || '').trim();
|
||||
if (!photo) {
|
||||
return res.redirect('/products?error=Photo+is+required');
|
||||
}
|
||||
const monoFlag = is_mono === '1' ? 1 : 0;
|
||||
const stockQty = monoFlag ? 999999 : (quantity_in_stock || 0);
|
||||
await db.runAsync(
|
||||
`UPDATE products SET name=?, price=?, quantity_in_stock=?, description=?, photo_url=?, category_id=?, subcategory_id=?, location_id=?, is_mono=?
|
||||
WHERE id=?`,
|
||||
[name, priceNum, stockQty, description || '', photo_url || '', category_id, subcategory_id || null, location_id || null, monoFlag, req.params.id]
|
||||
[name, priceNum, stockQty, desc, photo, category_id, subcategory_id || null, location_id || null, monoFlag, req.params.id]
|
||||
);
|
||||
res.redirect('/products');
|
||||
}));
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<svg class="sa-icon sa-thin">
|
||||
<use href="/icons/sprite.svg#tag"></use>
|
||||
</svg>
|
||||
<span class="fs-xs opacity-70">v1.2.0</span>
|
||||
<span class="fs-xs opacity-70">v1.2.1</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -43,9 +43,16 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info mb-3">
|
||||
<strong>Current:</strong> v1.2.0 · 2026-07-08
|
||||
<strong>Current:</strong> v1.2.1 · 2026-07-18
|
||||
</div>
|
||||
|
||||
<h6 class="fw-bold mb-2">v1.2.1 <span class="text-muted fs-sm">— 2026-07-18</span></h6>
|
||||
<ul class="small mb-3">
|
||||
<li><span class="badge bg-info">refactor</span> Removed deposit amount-selection step; deposit_wallet_ now goes directly to Mercuryo instructions</li>
|
||||
<li><span class="badge bg-primary">feat</span> Updated Mercuryo button text to include VISA/Mastercard branding in all locales</li>
|
||||
<li><span class="badge bg-primary">feat</span> Added deposit_important5 note about Mercuryo authorization limits and top-up reserve</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="fw-bold mb-2">v1.2.0 <span class="text-muted fs-sm">— 2026-07-08</span></h6>
|
||||
<ul class="small mb-3">
|
||||
<li><span class="badge bg-warning text-dark">fix</span> Disabled CSRF checks in admin panel for Tor / onion zone compatibility</li>
|
||||
|
||||
@@ -65,12 +65,12 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit-photo" class="form-label">Photo URL</label>
|
||||
<input type="url" class="form-control" id="edit-photo" name="photo_url" value="<%= product.photo_url || '' %>" placeholder="https://example.com/image.jpg">
|
||||
<label for="edit-photo" class="form-label">Photo URL <span class="text-danger">*</span></label>
|
||||
<input type="url" class="form-control" id="edit-photo" name="photo_url" value="<%= product.photo_url || '' %>" placeholder="https://example.com/image.jpg" required>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="edit-description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="edit-description" name="description" rows="4" placeholder="Enter product description"><%= product.description || '' %></textarea>
|
||||
<label for="edit-description" class="form-label">Description <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="edit-description" name="description" rows="4" placeholder="Enter product description" required><%= product.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
|
||||
@@ -46,12 +46,12 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="add-photo" class="form-label">Photo URL</label>
|
||||
<input type="url" class="form-control" id="add-photo" name="photo_url" placeholder="https://example.com/image.jpg">
|
||||
<label for="add-photo" class="form-label">Photo URL <span class="text-danger">*</span></label>
|
||||
<input type="url" class="form-control" id="add-photo" name="photo_url" placeholder="https://example.com/image.jpg" required>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="add-description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="add-description" name="description" rows="3" placeholder="Enter product description"></textarea>
|
||||
<label for="add-description" class="form-label">Description <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="add-description" name="description" rows="3" placeholder="Enter product description" required></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
|
||||
@@ -431,7 +431,7 @@ export default class UserProductHandler {
|
||||
📦 ${product.name}
|
||||
|
||||
${t('products.product_price')}: $${product.price}
|
||||
${t('products.product_description')}: ${product.description}
|
||||
${t('products.product_description')}: ${product.description || t('products.no_description')}
|
||||
${product.is_mono ? t('products.infinite_stock') : `${t('products.product_available')}: ${product.quantity_in_stock} pcs`}
|
||||
|
||||
${t('products.product_category')}: ${product.category_name}
|
||||
@@ -443,6 +443,9 @@ export default class UserProductHandler {
|
||||
photoMessage = await sendProductPhoto(chatId, product.photo_url, 'Public photo');
|
||||
}
|
||||
|
||||
// Append no-photo line to message if no photo
|
||||
const displayMessage = product.photo_url ? message : message + `📷 ${t('products.no_photo')}\n`;
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: product.is_mono ? [
|
||||
[{ text: t('products.buy_now'), callback_data: `buy_product_${productId}` }],
|
||||
@@ -467,7 +470,7 @@ export default class UserProductHandler {
|
||||
};
|
||||
|
||||
// Отправляем сообщение с кнопками
|
||||
const productMessage = await bot.sendMessage(chatId, message, {
|
||||
const productMessage = await bot.sendMessage(chatId, displayMessage, {
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'HTML'
|
||||
});
|
||||
|
||||
@@ -5,9 +5,6 @@ import bot from '../../../context/bot.js';
|
||||
import logger from '../../../utils/logger.js';
|
||||
import { editOrSendCallback } from '../../../utils/messageUtils.js';
|
||||
import { tForUser } from '../../../i18n/index.js';
|
||||
import WalletUtils from '../../../utils/walletUtils.js';
|
||||
|
||||
const DEPOSIT_AMOUNTS = [25, 50, 100, 250, 500];
|
||||
|
||||
const CRYPTO_SYMBOLS = {
|
||||
BTC: '₿',
|
||||
@@ -79,39 +76,10 @@ export default class DepositHandler {
|
||||
}
|
||||
}
|
||||
|
||||
static async handleDepositSelectAmount(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const walletType = callbackQuery.data.replace('deposit_wallet_', '');
|
||||
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
const lang = user?.language || 'en';
|
||||
const t = tForUser(lang);
|
||||
|
||||
const amountButtons = DEPOSIT_AMOUNTS.map(amount => ([{
|
||||
text: `$${amount}`,
|
||||
callback_data: `deposit_amount_${walletType}_${amount}`
|
||||
}]));
|
||||
|
||||
amountButtons.push([{ text: t('wallet.back'), callback_data: 'top_up_wallet' }]);
|
||||
|
||||
await bot.editMessageText(
|
||||
t('wallet.deposit_select_amount', { type: walletType }),
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: { inline_keyboard: amountButtons }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static async handleDepositInstruction(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const parts = callbackQuery.data.replace('deposit_amount_', '').split('_');
|
||||
const walletType = parts[0];
|
||||
const amount = parts[1];
|
||||
const walletType = callbackQuery.data.replace('deposit_wallet_', '');
|
||||
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
const lang = user?.language || 'en';
|
||||
@@ -135,7 +103,7 @@ export default class DepositHandler {
|
||||
|
||||
const mercuryoUrl = 'https://mercuryo.io/';
|
||||
|
||||
let message = `${t('wallet.deposit_title', { type: walletType, amount })}
|
||||
let message = `${t('wallet.deposit_title', { type: walletType })}
|
||||
|
||||
`;
|
||||
message += `${t('wallet.deposit_instructions_title')}
|
||||
@@ -143,11 +111,11 @@ export default class DepositHandler {
|
||||
`;
|
||||
message += `${t('wallet.deposit_step1')}
|
||||
`;
|
||||
message += `${t('wallet.deposit_step2', { amount, type: walletType })}
|
||||
message += `${t('wallet.deposit_step2', { type: walletType })}
|
||||
`;
|
||||
message += `${t('wallet.deposit_step3')}
|
||||
`;
|
||||
message += `${t('wallet.deposit_step4')}
|
||||
message += `${t('wallet.deposit_step4', { type: walletType })}
|
||||
`;
|
||||
message += `${t('wallet.deposit_step5')}
|
||||
`;
|
||||
@@ -167,17 +135,18 @@ export default class DepositHandler {
|
||||
`;
|
||||
message += `${t('wallet.deposit_important3')}
|
||||
`;
|
||||
message += `${t('wallet.deposit_important4')}`;
|
||||
message += `${t('wallet.deposit_important4')}
|
||||
`;
|
||||
message += `${t('wallet.deposit_important5')}`;
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: t('wallet.deposit_open_mercuryo'), url: mercuryoUrl },
|
||||
{ text: t('wallet.deposit_pay_crypto'), callback_data: `deposit_crypto_${walletType}_${amount}` }
|
||||
{ text: t('wallet.deposit_pay_crypto'), callback_data: `deposit_crypto_${walletType}` }
|
||||
],
|
||||
[
|
||||
{ text: t('wallet.deposit_copy_address'), callback_data: `deposit_copy_${walletType}` },
|
||||
{ text: t('wallet.deposit_change_amount'), callback_data: `deposit_wallet_${walletType}` }
|
||||
{ text: t('wallet.deposit_copy_address'), callback_data: `deposit_copy_${walletType}` }
|
||||
],
|
||||
[
|
||||
{ text: t('wallet.deposit_choose_different'), callback_data: 'top_up_wallet' },
|
||||
@@ -246,9 +215,7 @@ export default class DepositHandler {
|
||||
static async handleDepositCryptoQR(callbackQuery) {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const parts = callbackQuery.data.replace('deposit_crypto_', '').split('_');
|
||||
const walletType = parts[0];
|
||||
const amount = parts[1];
|
||||
const walletType = callbackQuery.data.replace('deposit_crypto_', '');
|
||||
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
const lang = user?.language || 'en';
|
||||
@@ -274,10 +241,7 @@ export default class DepositHandler {
|
||||
const upperType = walletType.toUpperCase();
|
||||
|
||||
if (upperType === 'BTC') {
|
||||
const prices = await WalletUtils.getCryptoPrices();
|
||||
const btcPrice = prices.btc || 0;
|
||||
const btcAmount = btcPrice > 0 ? (parseFloat(amount) / btcPrice).toFixed(8) : '0';
|
||||
paymentUri = `bitcoin:${wallet.address}?amount=${btcAmount}`;
|
||||
paymentUri = `bitcoin:${wallet.address}`;
|
||||
} else if (upperType === 'ETH' || upperType === 'USDT' || upperType === 'USDC') {
|
||||
paymentUri = `ethereum:${wallet.address}`;
|
||||
} else if (upperType === 'LTC') {
|
||||
@@ -297,7 +261,7 @@ export default class DepositHandler {
|
||||
caption += `${t('wallet.deposit_crypto_scan_qr')}\n\n`;
|
||||
caption += `${t('wallet.deposit_crypto_address', { type: walletType })}\n`;
|
||||
caption += `\`${wallet.address}\`\n\n`;
|
||||
caption += `${t('wallet.deposit_crypto_instruction', { amount, type: walletType })}`;
|
||||
caption += `${t('wallet.deposit_crypto_instruction', { type: walletType })}`;
|
||||
|
||||
await bot.sendPhoto(chatId, qrBuffer, {
|
||||
caption,
|
||||
@@ -306,7 +270,7 @@ export default class DepositHandler {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: t('wallet.deposit_copy_address'), callback_data: `deposit_copy_${walletType}` },
|
||||
{ text: t('wallet.back_to_deposit'), callback_data: `deposit_amount_${walletType}_${amount}` }
|
||||
{ text: t('wallet.back_to_deposit'), callback_data: `deposit_wallet_${walletType}` }
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ export default {
|
||||
handleViewArchivedWallets: ArchiveHandler.handleViewArchivedWallets,
|
||||
handleBackToBalance: BalanceHandler.handleBackToBalance,
|
||||
handleDepositSelectWallet: DepositHandler.handleDepositSelectWallet,
|
||||
handleDepositSelectAmount: DepositHandler.handleDepositSelectAmount,
|
||||
handleDepositInstruction: DepositHandler.handleDepositInstruction,
|
||||
handleDepositCopyAddress: DepositHandler.handleDepositCopyAddress,
|
||||
getNetworkName: WalletHelpers.getNetworkName,
|
||||
|
||||
@@ -55,7 +55,9 @@
|
||||
"error_loading_product": "Fehler beim Laden der Produktdetails. Bitte versuche es erneut.",
|
||||
"not_found": "Standort nicht gefunden. Zurück zum vorherigen Menü.",
|
||||
"mono_product": "📦 Digitales Produkt",
|
||||
"infinite_stock": "∞ Immer verfügbar"
|
||||
"infinite_stock": "∞ Immer verfügbar",
|
||||
"no_description": "Keine Beschreibung",
|
||||
"no_photo": "Kein Foto verfügbar"
|
||||
},
|
||||
"purchase": {
|
||||
"summary": "🛒 Kaufübersicht:",
|
||||
@@ -156,13 +158,12 @@
|
||||
"wallet_not_found": "Wallet nicht gefunden. Bitte versuche es erneut.",
|
||||
"wallet_not_found_short": "Wallet nicht gefunden.",
|
||||
"deposit_select_gateway": "💳 *Einzahlung über Krypto*\n\nWähle das Wallet zum Aufladen:",
|
||||
"deposit_select_amount": "💳 *{{type}} aufladen*\n\nWähle den Betrag (USD) zum Aufladen:",
|
||||
"deposit_title": "💳 *{{type}} aufladen — €{{amount}}*",
|
||||
"deposit_title": "💳 *{{type}} aufladen*",
|
||||
"deposit_instructions_title": "📋 *Schritt-für-Schritt-Anleitung:*",
|
||||
"deposit_step1": "1️⃣ Tippe auf *Adresse kopieren* unten, um deine Wallet-Adresse zu kopieren",
|
||||
"deposit_step2": "2️⃣ Öffne Mercuryo über den Button unten",
|
||||
"deposit_step2": "2️⃣ Öffne Mercuryo über den Button unten und gib den gewünschten Betrag ein",
|
||||
"deposit_step3": "3️⃣ Füge die kopierte Adresse als Empfangswallet ein",
|
||||
"deposit_step4": "4️⃣ Wähle die richtige Währung: *{{type}}* und den Betrag €{{amount}}",
|
||||
"deposit_step4": "4️⃣ Wähle die richtige Währung: *{{type}}* und gib den gewünschten Betrag ein",
|
||||
"deposit_step5": "5️⃣ Bezahle mit deiner Bankkarte",
|
||||
"deposit_step6": "6️⃣ Die Kryptowährung wird innerhalb von 5\\-30 Minuten in deinem Wallet eintreffen",
|
||||
"deposit_your_address": "🔐 *Deine {{type}} Wallet-Adresse:*",
|
||||
@@ -171,14 +172,14 @@
|
||||
"deposit_important2": "• Stelle sicher\\, dass du die richtige Währung in Mercuryo auswählst",
|
||||
"deposit_important3": "• E-Mail \\+ Telefonnummer können von Mercuryo für die Verifizierung verlangt werden",
|
||||
"deposit_important4": "• Wenn die Kryptowährung nicht innerhalb von 30 Minuten eintrifft \\— kontaktiere den Mercuryo-Support",
|
||||
"deposit_open_mercuryo": "💳 Mercuryo",
|
||||
"deposit_important5": "• Ohne Autorisierung erlaubt Mercuryo nur 1 Zahlung. Danach fordert Mercuryo Autorisierung & Identitätsverifikation. Lade dein Guthaben einmalig mit Reserve auf.",
|
||||
"deposit_open_mercuryo": "💳 Mit Karte zahlen (VISA / Mastercard)",
|
||||
"deposit_pay_crypto": "💰 Mit Krypto zahlen",
|
||||
"deposit_crypto_title": "💳 *{{type}} über Krypto einzahlen*",
|
||||
"deposit_crypto_address": "🔐 *Deine {{type}} Wallet-Adresse:*",
|
||||
"deposit_crypto_instruction": "Sende {{amount}} USD in {{type}} an die Adresse oben. Der QR-Code enthält die Zahlungsadresse.",
|
||||
"deposit_crypto_instruction": "Sende den gewünschten Betrag in USD in {{type}} an die Adresse oben. Der QR-Code enthält die Zahlungsadresse.",
|
||||
"deposit_crypto_scan_qr": "📱 Scanne den QR-Code unten, um die Wallet-Adresse zu erhalten",
|
||||
"deposit_copy_address": "📋 Adresse kopieren",
|
||||
"deposit_change_amount": "🔄 Betrag ändern",
|
||||
"deposit_choose_different": "💸 Anderes Wallet wählen",
|
||||
"deposit_wallet_address": "{{type}} Wallet-Adresse:",
|
||||
"back_to_deposit": "« Zurück zur Einzahlung",
|
||||
|
||||
@@ -55,7 +55,9 @@
|
||||
"error_loading_product": "Error loading product details. Please try again.",
|
||||
"not_found": "Location not found. Returning to previous menu.",
|
||||
"mono_product": "📦 Digital Product",
|
||||
"infinite_stock": "∞ Always available"
|
||||
"infinite_stock": "∞ Always available",
|
||||
"no_description": "No description provided",
|
||||
"no_photo": "No photo available"
|
||||
},
|
||||
"purchase": {
|
||||
"summary": "🛒 Purchase Summary:",
|
||||
@@ -156,13 +158,12 @@
|
||||
"wallet_not_found": "Wallet not found. Please try again.",
|
||||
"wallet_not_found_short": "Wallet not found.",
|
||||
"deposit_select_gateway": "💳 *Deposit via Crypto*\n\nSelect the wallet you want to top up:",
|
||||
"deposit_select_amount": "💳 *Deposit {{type}}*\n\nSelect the amount (USD) you want to deposit:",
|
||||
"deposit_title": "💳 *Deposit {{type}} — €{{amount}}*",
|
||||
"deposit_title": "💳 *Deposit {{type}}*",
|
||||
"deposit_instructions_title": "📋 *Step\\-by\\-step instructions:*",
|
||||
"deposit_step1": "1️⃣ Tap *Copy Address* below to copy your wallet address",
|
||||
"deposit_step2": "2️⃣ Open Mercuryo via the button below",
|
||||
"deposit_step2": "2️⃣ Open Mercuryo via the button below and enter your desired amount",
|
||||
"deposit_step3": "3️⃣ Paste the copied address as the receiving wallet",
|
||||
"deposit_step4": "4️⃣ Select the correct currency: *{{type}}* and the amount €{{amount}}",
|
||||
"deposit_step4": "4️⃣ Select the correct currency: *{{type}}* and enter your desired amount",
|
||||
"deposit_step5": "5️⃣ Complete the payment with your bank card",
|
||||
"deposit_step6": "6️⃣ Crypto will arrive in your wallet within 5\\-30 minutes",
|
||||
"deposit_your_address": "🔐 *Your {{type}} wallet address:*",
|
||||
@@ -171,14 +172,14 @@
|
||||
"deposit_important2": "• Make sure you select the correct currency in Mercuryo",
|
||||
"deposit_important3": "• Email \\+ phone may be required by Mercuryo for verification",
|
||||
"deposit_important4": "• If crypto doesn't arrive within 30 min \\— contact Mercuryo support",
|
||||
"deposit_open_mercuryo": "💳 Mercuryo",
|
||||
"deposit_important5": "• Without authorization, Mercuryo allows only 1 payment. Then Mercuryo will request authorization & identity verification. Top up your balance with a reserve in one go.",
|
||||
"deposit_open_mercuryo": "💳 Pay with Card (VISA / Mastercard)",
|
||||
"deposit_pay_crypto": "💰 Pay with Crypto",
|
||||
"deposit_crypto_title": "💳 *Deposit {{type}} via Crypto*",
|
||||
"deposit_crypto_address": "🔐 *Your {{type}} wallet address:*",
|
||||
"deposit_crypto_instruction": "Send {{amount}} USD in {{type}} to the address above. The QR code contains the payment address.",
|
||||
"deposit_crypto_instruction": "Send the desired amount in USD in {{type}} to the address above. The QR code contains the payment address.",
|
||||
"deposit_crypto_scan_qr": "📱 Scan the QR code below to get the wallet address",
|
||||
"deposit_copy_address": "📋 Copy Address",
|
||||
"deposit_change_amount": "🔄 Change Amount",
|
||||
"deposit_choose_different": "💸 Choose Different Wallet",
|
||||
"deposit_wallet_address": "{{type}} wallet address:",
|
||||
"back_to_deposit": "« Back to Deposit",
|
||||
|
||||
@@ -55,7 +55,9 @@
|
||||
"error_loading_product": "Error al cargar detalles del producto. Inténtalo de nuevo.",
|
||||
"not_found": "Ubicación no encontrada. Volviendo al menú anterior.",
|
||||
"mono_product": "📦 Producto Digital",
|
||||
"infinite_stock": "∞ Siempre disponible"
|
||||
"infinite_stock": "∞ Siempre disponible",
|
||||
"no_description": "Sin descripción",
|
||||
"no_photo": "Sin foto disponible"
|
||||
},
|
||||
"purchase": {
|
||||
"summary": "🛒 Resumen de compra:",
|
||||
@@ -156,13 +158,12 @@
|
||||
"wallet_not_found": "Billetera no encontrada. Inténtalo de nuevo.",
|
||||
"wallet_not_found_short": "Billetera no encontrada.",
|
||||
"deposit_select_gateway": "💳 *Depositar vía Cripto*\n\nSelecciona la billetera que quieres recargar:",
|
||||
"deposit_select_amount": "💳 *Depositar {{type}}*\n\nSelecciona la cantidad (USD) que quieres depositar:",
|
||||
"deposit_title": "💳 *Depositar {{type}} — €{{amount}}*",
|
||||
"deposit_title": "💳 *Depositar {{type}}*",
|
||||
"deposit_instructions_title": "📋 *Instrucciones paso a paso:*",
|
||||
"deposit_step1": "1️⃣ Toca *Copiar dirección* abajo para copiar tu dirección de billetera",
|
||||
"deposit_step2": "2️⃣ Abre Mercuryo con el botón de abajo",
|
||||
"deposit_step2": "2️⃣ Abre Mercuryo con el botón de abajo e ingresa la cantidad deseada",
|
||||
"deposit_step3": "3️⃣ Pega la dirección copiada como billetera receptora",
|
||||
"deposit_step4": "4️⃣ Selecciona la moneda correcta: *{{type}}* y la cantidad €{{amount}}",
|
||||
"deposit_step4": "4️⃣ Selecciona la moneda correcta: *{{type}}* e ingresa la cantidad deseada",
|
||||
"deposit_step5": "5️⃣ Paga con tu tarjeta bancaria",
|
||||
"deposit_step6": "6️⃣ Las criptomonedas llegarán a tu billetera en 5\\-30 minutos",
|
||||
"deposit_your_address": "🔐 *Tu dirección de billetera {{type}}:*",
|
||||
@@ -171,14 +172,14 @@
|
||||
"deposit_important2": "• Asegúrate de seleccionar la moneda correcta en Mercuryo",
|
||||
"deposit_important3": "• Correo \\+ teléfono pueden ser requeridos por Mercuryo para verificación",
|
||||
"deposit_important4": "• Si las criptomonedas no llegan en 30 min \\— contacta al soporte de Mercuryo",
|
||||
"deposit_open_mercuryo": "💳 Mercuryo",
|
||||
"deposit_important5": "• Sin autorización, Mercuryo permite solo 1 pago. Luego Mercuryo pedirá autorización y verificación de identidad. Recarga tu saldo con reserva de una sola vez.",
|
||||
"deposit_open_mercuryo": "💳 Pagar con Tarjeta (VISA / Mastercard)",
|
||||
"deposit_pay_crypto": "💰 Pagar con Cripto",
|
||||
"deposit_crypto_title": "💳 *Depositar {{type}} vía Cripto*",
|
||||
"deposit_crypto_address": "🔐 *Tu dirección de billetera {{type}}:*",
|
||||
"deposit_crypto_instruction": "Envía {{amount}} USD en {{type}} a la dirección de arriba. El código QR contiene la dirección de pago.",
|
||||
"deposit_crypto_instruction": "Envía la cantidad deseada en USD en {{type}} a la dirección de arriba. El código QR contiene la dirección de pago.",
|
||||
"deposit_crypto_scan_qr": "📱 Escanea el código QR de abajo para obtener la dirección de la billetera",
|
||||
"deposit_copy_address": "📋 Copiar dirección",
|
||||
"deposit_change_amount": "🔄 Cambiar cantidad",
|
||||
"deposit_choose_different": "💸 Elegir otra billetera",
|
||||
"deposit_wallet_address": "Dirección de billetera {{type}}:",
|
||||
"back_to_deposit": "« Volver al depósito",
|
||||
|
||||
@@ -119,10 +119,6 @@ export function registerRoutes() {
|
||||
await DepositHandler.handleDepositSelectWallet(cq);
|
||||
});
|
||||
callbackRouter.registerPrefix('deposit_wallet_', async (cq) => {
|
||||
logDebug(cq.data, 'handleDepositSelectAmount');
|
||||
await DepositHandler.handleDepositSelectAmount(cq);
|
||||
});
|
||||
callbackRouter.registerPrefix('deposit_amount_', async (cq) => {
|
||||
logDebug(cq.data, 'handleDepositInstruction');
|
||||
await DepositHandler.handleDepositInstruction(cq);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user