fix(bot): improve utilities reminder template flow

This commit is contained in:
2026-03-12 02:54:42 +04:00
parent ce8f0b5b93
commit e56f1194e9
4 changed files with 69 additions and 19 deletions

View File

@@ -270,7 +270,8 @@ export const enBotTranslations: BotTranslationCatalog = {
`I could not read that amount for ${categoryName}. Reply with a number in ${currency}, or send 0 / "skip".`, `I could not read that amount for ${categoryName}. Reply with a number in ${currency}, or send 0 / "skip".`,
templateIntro: (currency) => templateIntro: (currency) =>
`Fill in the utility amounts below in ${currency}, then send the completed message back in this topic.`, `Fill in the utility amounts below in ${currency}, then send the completed message back in this topic.`,
templateInstruction: 'Use 0 or skip for any category you want to leave empty.', templateInstruction:
'For any category you do not want to add, leave it blank, remove the line entirely, or send 0 / "skip".',
templateInvalid: templateInvalid:
'I could not read any utility amounts from that template. Send the filled template back with at least one amount.', 'I could not read any utility amounts from that template. Send the filled template back with at least one amount.',
summaryTitle: (period) => `Utility charges for ${period}`, summaryTitle: (period) => `Utility charges for ${period}`,

View File

@@ -275,7 +275,7 @@ export const ruBotTranslations: BotTranslationCatalog = {
templateIntro: (currency) => templateIntro: (currency) =>
`Заполните суммы по коммуналке ниже в ${currency}, затем отправьте заполненное сообщение обратно в этот топик.`, `Заполните суммы по коммуналке ниже в ${currency}, затем отправьте заполненное сообщение обратно в этот топик.`,
templateInstruction: templateInstruction:
'Для любой категории, которую не нужно добавлять, укажите 0 или слово «пропуск».', 'Для любой категории, которую не нужно добавлять, оставьте поле пустым, удалите строку целиком или укажите 0 / «пропуск».',
templateInvalid: templateInvalid:
'Не удалось распознать ни одной суммы в этом шаблоне. Отправьте заполненный шаблон хотя бы с одной суммой.', 'Не удалось распознать ни одной суммы в этом шаблоне. Отправьте заполненный шаблон хотя бы с одной суммой.',
summaryTitle: (period) => `Коммунальные начисления за ${period}`, summaryTitle: (period) => `Коммунальные начисления за ${period}`,

View File

@@ -375,7 +375,8 @@ describe('registerReminderTopicUtilities', () => {
expect(calls[1]).toMatchObject({ expect(calls[1]).toMatchObject({
method: 'sendMessage', method: 'sendMessage',
payload: { payload: {
text: expect.stringContaining('Electricity:'), text: expect.stringContaining('<pre>Electricity: \nWater: </pre>'),
parse_mode: 'HTML',
message_thread_id: 555 message_thread_id: 555
} }
}) })
@@ -391,6 +392,34 @@ describe('registerReminderTopicUtilities', () => {
}) })
}) })
test('treats blank or removed template lines as skipped categories', async () => {
const { bot, calls } = setupBot()
await bot.handleUpdate(reminderCallbackUpdate(REMINDER_UTILITY_TEMPLATE_CALLBACK) as never)
calls.length = 0
await bot.handleUpdate(reminderMessageUpdate('Electricity: 22\nWater: ') as never)
expect(calls[0]).toMatchObject({
method: 'sendMessage',
payload: {
text: expect.stringContaining('- Electricity: 22.00 GEL')
}
})
calls.length = 0
await bot.handleUpdate(reminderCallbackUpdate(REMINDER_UTILITY_TEMPLATE_CALLBACK) as never)
calls.length = 0
await bot.handleUpdate(reminderMessageUpdate('Electricity: 22') as never)
expect(calls[0]).toMatchObject({
method: 'sendMessage',
payload: {
text: expect.stringContaining('- Electricity: 22.00 GEL')
}
})
})
test('treats expired pending reminder submissions as unavailable', async () => { test('treats expired pending reminder submissions as unavailable', async () => {
const { bot, calls, promptRepository } = setupBot() const { bot, calls, promptRepository } = setupBot()

View File

@@ -182,20 +182,32 @@ function parseTemplateEntries(
return entries.length > 0 ? entries : null return entries.length > 0 ? entries : null
} }
function escapeHtml(raw: string): string {
return raw.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
}
function buildTemplateText( function buildTemplateText(
locale: BotLocale, locale: BotLocale,
currency: 'GEL' | 'USD', currency: 'GEL' | 'USD',
categories: readonly string[] categories: readonly string[]
): string { ): {
text: string
parseMode: 'HTML'
} {
const t = getBotTranslations(locale).reminders const t = getBotTranslations(locale).reminders
return [ const templateLines = categories.map((category) => `${category}: `).join('\n')
t.templateIntro(currency),
'', return {
...categories.map((category) => `${category}: `), text: [
'', escapeHtml(t.templateIntro(currency)),
t.templateInstruction '',
].join('\n') `<pre>${escapeHtml(templateLines)}</pre>`,
'',
escapeHtml(t.templateInstruction)
].join('\n'),
parseMode: 'HTML'
}
} }
function reminderUtilitySummaryText( function reminderUtilitySummaryText(
@@ -261,7 +273,10 @@ function buildReminderConfirmationPayload(input: {
async function replyInTopic( async function replyInTopic(
ctx: Context, ctx: Context,
text: string, text: string,
replyMarkup?: InlineKeyboardMarkup replyMarkup?: InlineKeyboardMarkup,
options?: {
parseMode?: 'HTML'
}
): Promise<void> { ): Promise<void> {
const message = ctx.msg const message = ctx.msg
if (!ctx.chat || !message) { if (!ctx.chat || !message) {
@@ -286,6 +301,11 @@ async function replyInTopic(
? { ? {
reply_markup: replyMarkup as InlineKeyboardMarkup reply_markup: replyMarkup as InlineKeyboardMarkup
} }
: {}),
...(options?.parseMode
? {
parse_mode: options.parseMode
}
: {}) : {})
}) })
} }
@@ -481,14 +501,14 @@ export function registerReminderTopicUtilities(options: {
await ctx.answerCallbackQuery({ await ctx.answerCallbackQuery({
text: t.templateToast text: t.templateToast
}) })
await replyInTopic( const template = buildTemplateText(
ctx, reminderContext.locale,
buildTemplateText( reminderContext.currency,
reminderContext.locale, reminderContext.categories
reminderContext.currency,
reminderContext.categories
)
) )
await replyInTopic(ctx, template.text, undefined, {
parseMode: template.parseMode
})
} }
options.bot.callbackQuery(REMINDER_UTILITY_GUIDED_CALLBACK, async (ctx) => { options.bot.callbackQuery(REMINDER_UTILITY_GUIDED_CALLBACK, async (ctx) => {