fix(bot): issue #129 — missing shop_district_/shop_subcategory_ handlers, answerCallbackQuery timing, state.location pipe format

- Register shop_district_ (handleDistrictBack) + shop_subcategory_ (handleSubcategorySelection) in routes.js — Back button in empty categories now works
- state.location pipe-delimited with encodeURIComponent — multi-word names (Saint Petersburg) no longer break nav
- answerCallbackQuery moved to start of callback handling — no stuck spinner
- Empty city fallback (district_unknown) in Select district
- Guard against stale underscore-format state.location
- 48 tests pass; bump v1.2.4
This commit is contained in:
NW
2026-08-05 10:52:18 +01:00
parent 12c4cefc4c
commit 7a97d561a5
8 changed files with 78 additions and 15 deletions

View File

@@ -9,10 +9,17 @@
## Current Version
**v1.2.3** — 2026-08-04
**v1.2.4** — 2026-08-05
## Changelog
### v1.2.4 — 2026-08-05
- **fix**: missing `shop_district_` / `shop_subcategory_` callback handlers — Back button in empty categories now works (handleDistrictBack)
- **fix**: state.location pipe-delimited (encodeURIComponent) — multi-word names (Saint Petersburg) no longer break navigation
- **fix**: answerCallbackQuery moved to start of callback handling — no stuck "clock" spinner on buttons
- **fix**: empty city fallback in "Select district in :" (district_unknown)
- **fix**: guard against stale underscore-format state.location in handleDistrictBack
### v1.2.3 — 2026-08-04
- **fix**: empty category menus — districts with no in-stock products show "No products available in this district" (getCategoriesWithProductsByLocationId filters by real stock/mono)
- **fix**: duplicate "Select your country" messages on rapid Products tap — 1200ms main-menu debounce in routes.js

View File

@@ -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.3</span>
<span class="fs-xs opacity-70">v1.2.4</span>
</div>
</div>
</aside>
@@ -43,10 +43,10 @@
</div>
<div class="modal-body">
<div class="alert alert-info mb-3">
<strong>Current:</strong> v1.2.3 &middot; 2026-08-04
<strong>Current:</strong> v1.2.4 &middot; 2026-08-05
</div>
<h6 class="fw-bold mb-2">v1.2.3 <span class="text-muted fs-sm">&mdash; 2026-08-04</span></h6>
<h6 class="fw-bold mb-2">v1.2.4 <span class="text-muted fs-sm">&mdash; 2026-08-05</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>

View File

@@ -159,7 +159,7 @@ export default class UserProductHandler {
};
await bot.editMessageText(
t('products.select_district', { city }),
t('products.select_district', { city: city || t('products.district_unknown') }),
{
chat_id: chatId,
message_id: messageId,
@@ -208,7 +208,7 @@ export default class UserProductHandler {
}
await userStates.set(chatId, {
location: `${location.country}_${location.city}_${location.district}`,
location: `${encodeURIComponent(location.country)}|${encodeURIComponent(location.city)}|${encodeURIComponent(location.district)}`,
locationId: location.id
});
@@ -259,6 +259,56 @@ export default class UserProductHandler {
}
}
static async handleDistrictBack(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
try {
const telegramId = callbackQuery.from.id;
const user = await UserService.getUserByTelegramId(telegramId);
const lang = user?.language || 'en';
const t = tForUser(lang);
const state = await userStates.get(chatId);
if (!state?.location || !state.location.includes('|')) {
await bot.editMessageText(t('products.error_loading_districts'), {
chat_id: chatId,
message_id: messageId
});
return;
}
const [country, city] = state.location.split('|').map(decodeURIComponent);
const locations = await LocationService.getLocationsByCountryAndCity(country, city);
const keyboard = {
inline_keyboard: [
...locations.map(loc => [{
text: loc.district || loc.city,
callback_data: `shop_loc_${loc.id}`
}]),
[{ text: t('products.back_to_cities'), callback_data: `shop_country_${encodeURIComponent(country)}` }]
]
};
await bot.editMessageText(
t('products.select_district', { city: city || t('products.district_unknown') }),
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
logger.error({ err: error }, 'Error in handleDistrictBack');
const telegramId = callbackQuery.from.id;
const user = await UserService.getUserByTelegramId(telegramId).catch(() => null);
const lang = user?.language || 'en';
const t = tForUser(lang);
await bot.sendMessage(chatId, t('products.error_loading_districts'));
}
}
static async handleCategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;

View File

@@ -60,7 +60,8 @@
"mono_product": "📦 Digitales Produkt",
"infinite_stock": "∞ Immer verfügbar",
"no_description": "Keine Beschreibung",
"no_photo": "Kein Foto verfügbar"
"no_photo": "Kein Foto verfügbar",
"district_unknown": "Unbekannt"
},
"purchase": {
"summary": "🛒 Kaufübersicht:",

View File

@@ -60,7 +60,8 @@
"mono_product": "📦 Digital Product",
"infinite_stock": "∞ Always available",
"no_description": "No description provided",
"no_photo": "No photo available"
"no_photo": "No photo available",
"district_unknown": "Unknown"
},
"purchase": {
"summary": "🛒 Purchase Summary:",

View File

@@ -60,7 +60,8 @@
"mono_product": "📦 Producto Digital",
"infinite_stock": "∞ Siempre disponible",
"no_description": "Sin descripción",
"no_photo": "Sin foto disponible"
"no_photo": "Sin foto disponible",
"district_unknown": "Desconocido"
},
"purchase": {
"summary": "🛒 Resumen de compra:",

View File

@@ -72,15 +72,10 @@ if (bot && botAvailable) {
await bot.answerCallbackQuery(callbackQuery.id, { text: '⏳' }).catch(() => {});
return;
}
let answered = false;
await bot.answerCallbackQuery(callbackQuery.id).catch(() => {});
try {
await callbackRouter.dispatch(callbackQuery);
await bot.answerCallbackQuery(callbackQuery.id);
answered = true;
} catch (error) {
if (!answered) {
await bot.answerCallbackQuery(callbackQuery.id).catch(() => {});
}
await ErrorHandler.handleError(bot, callbackQuery.message.chat.id, error, 'callback query');
}
});

View File

@@ -237,10 +237,18 @@ export function registerRoutes() {
logDebug(cq.data, 'handleDistrictSelection');
await userProductHandler.handleDistrictSelection(cq);
});
callbackRouter.registerPrefix('shop_district_', async (cq) => {
logDebug(cq.data, 'handleDistrictBack');
await userProductHandler.handleDistrictBack(cq);
});
callbackRouter.registerPrefix('shop_category_', async (cq) => {
logDebug(cq.data, 'handleCategorySelection');
await userProductHandler.handleCategorySelection(cq);
});
callbackRouter.registerPrefix('shop_subcategory_', async (cq) => {
logDebug(cq.data, 'handleSubcategorySelection');
await userProductHandler.handleSubcategorySelection(cq);
});
callbackRouter.registerPrefix('shop_product_', async (cq) => {
logDebug(cq.data, 'handleProductSelection');
await userProductHandler.handleProductSelection(cq);