From db2e05bbf4d971b50ec2f29bfdfb16de3b2d5359 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:50:17 +0700 Subject: [PATCH] Refine notification template copy --- src/notification-templates.js | 165 ++++++++++++++++++---------------- src/resolvers.js | 45 ++++++---- src/server.js | 1 + 3 files changed, 120 insertions(+), 91 deletions(-) diff --git a/src/notification-templates.js b/src/notification-templates.js index 406da22..4e004cb 100644 --- a/src/notification-templates.js +++ b/src/notification-templates.js @@ -1,7 +1,5 @@ import { isManagerRole } from './access.js'; -const DELIVERY_CHANNELS = ['EMAIL', 'TELEGRAM', 'MAX']; - function splitBody(text) { return String(text ?? '') .split('\n') @@ -20,22 +18,6 @@ function createChannelPreview(channel, fields = {}) { }; } -function createMissingChannelPreview(channel) { - return { - channel, - implemented: false, - subject: null, - body: ['В коде не реализовано.'], - buttonText: null, - buttonUrl: null, - }; -} - -function buildChannelMatrix(implementedChannels) { - const implementedMap = new Map(implementedChannels.map((item) => [item.channel, item])); - return DELIVERY_CHANNELS.map((channel) => implementedMap.get(channel) ?? createMissingChannelPreview(channel)); -} - export function buildFrontendAppUrl(path) { const baseUrl = String( process.env.TELEGRAM_MINI_APP_URL || @@ -90,44 +72,93 @@ export function buildLoginCodeEmailTemplate({ code, expiresAt }) { }; } -export function buildMessengerLoginTemplate({ buttonUrl }) { +function formatOrderStatusLabel(status) { + if (status === 'NEW' || status === 'MANAGER_PROCESSING') { + return 'Заявка'; + } + if (status === 'WAITING_DOUBLE_CONFIRM' || status === 'CONFIRMED') { + return 'Предложение'; + } + if (status === 'IN_PROGRESS') { + return 'В работе'; + } + if (status === 'COMPLETED') { + return 'Завершен'; + } + if (status === 'CLIENT_REJECTED' || status === 'MANAGER_REJECTED') { + return 'Отклонен'; + } + if (status === 'MANAGER_BLOCKED') { + return 'Пауза'; + } + return String(status || '').trim() || 'Статус обновлен'; +} + +function formatWithdrawalStatusLabel(status) { + if (status === 'APPROVED') { + return 'одобрена'; + } + if (status === 'REJECTED') { + return 'отклонена'; + } + return 'обновлена'; +} + +export function buildMessengerLoginTemplate({ buttonUrl, expiresAt = null }) { + const body = ['Для входа в личный кабинет перейдите по ссылке.']; + if (expiresAt) { + const expiresText = new Date(expiresAt).toLocaleString('ru-RU', { hour12: false }); + body.push(`Ссылка действует до: ${expiresText}`); + } + return { - message: 'Вход подтвержден. Нажмите кнопку, чтобы открыть личный кабинет.', + message: body.join('\n'), buttonText: 'Открыть кабинет', buttonUrl, }; } export function buildOrderStatusNotificationTemplate({ orderId, orderCode, status, note, role }) { - const message = `Заказ ${orderCode} изменил статус: ${status}.${note ? `\nКомментарий: ${note}` : ''}`; + const body = [ + `Статус заказа ${orderCode}: ${formatOrderStatusLabel(status)}.`, + ...(note ? [`Комментарий: ${note}`] : []), + ]; return { - message, + subject: `Изменение статуса заказа ${orderCode}`, + body, + message: body.join('\n'), buttonText: 'Открыть заказ', buttonUrl: buildFrontendAppUrl(buildUserOrderPath(orderId, role)), }; } -export function buildAutoBonusNotificationTemplate({ amount, orderCode }) { +export function buildBonusCreditTemplate({ amount }) { + const normalizedAmount = Number(amount); return { - message: `Начислен бонус: ${amount} за заказ ${orderCode}.`, + message: `Начислен бонус: ${Number.isFinite(normalizedAmount) ? normalizedAmount : amount}.`, buttonText: 'Открыть бонусную программу', - buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('auto-balance')), + buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('balance')), }; } -export function buildManualBonusNotificationTemplate({ amount, reason }) { - return { - message: `Начислен бонус: ${amount}. Причина: ${reason}`, - buttonText: 'Открыть бонусную программу', - buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('manual-balance')), - }; +export function buildAutoBonusNotificationTemplate({ amount }) { + return buildBonusCreditTemplate({ amount }); +} + +export function buildManualBonusNotificationTemplate({ amount }) { + return buildBonusCreditTemplate({ amount }); } export function buildWithdrawalReviewNotificationTemplate({ status, reviewComment }) { + const body = [ + `Ваша заявка на вывод вознаграждения ${formatWithdrawalStatusLabel(status)}.`, + ...(reviewComment ? [`Комментарий: ${reviewComment}`] : []), + ]; + return { - message: `Заявка на вывод вознаграждения обновлена: ${status}.${reviewComment ? ` Комментарий: ${reviewComment}` : ''}`, - buttonText: 'Проверить бонусную программу', + message: body.join('\n'), + buttonText: 'Открыть бонусную программу', buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('withdrawal-review')), }; } @@ -139,6 +170,7 @@ export function getNotificationTemplatesCatalog() { }); const messengerLoginTemplate = buildMessengerLoginTemplate({ buttonUrl: 'https://fregat.dsrptlab.com/login?login_token=demo-token', + expiresAt: '2026-04-06T15:35:00.000Z', }); const orderStatusTemplate = buildOrderStatusNotificationTemplate({ orderId: 'demo-order-id', @@ -147,13 +179,8 @@ export function getNotificationTemplatesCatalog() { note: 'заказ передан в производство', role: 'CLIENT', }); - const autoBonusTemplate = buildAutoBonusNotificationTemplate({ + const bonusTemplate = buildBonusCreditTemplate({ amount: 1250, - orderCode: 'FRG-2401', - }); - const manualBonusTemplate = buildManualBonusNotificationTemplate({ - amount: 500, - reason: 'ручное начисление менеджером', }); const withdrawalReviewTemplate = buildWithdrawalReviewNotificationTemplate({ status: 'APPROVED', @@ -164,17 +191,17 @@ export function getNotificationTemplatesCatalog() { { id: 'login-code-email', title: 'Код входа по email', - channels: buildChannelMatrix([ + channels: [ createChannelPreview('EMAIL', { subject: loginTemplate.subject, body: loginTemplate.body, }), - ]), + ], }, { id: 'messenger-login-confirmed', - title: 'Подтверждение входа через мессенджер', - channels: buildChannelMatrix([ + title: 'Привязка мессенджера', + channels: [ createChannelPreview('TELEGRAM', { body: splitBody(messengerLoginTemplate.message), buttonText: messengerLoginTemplate.buttonText, @@ -185,12 +212,16 @@ export function getNotificationTemplatesCatalog() { buttonText: messengerLoginTemplate.buttonText, buttonUrl: messengerLoginTemplate.buttonUrl, }), - ]), + ], }, { id: 'order-status-update', title: 'Изменение статуса заказа', - channels: buildChannelMatrix([ + channels: [ + createChannelPreview('EMAIL', { + subject: orderStatusTemplate.subject, + body: orderStatusTemplate.body, + }), createChannelPreview('TELEGRAM', { body: splitBody(orderStatusTemplate.message), buttonText: orderStatusTemplate.buttonText, @@ -201,44 +232,28 @@ export function getNotificationTemplatesCatalog() { buttonText: orderStatusTemplate.buttonText, buttonUrl: orderStatusTemplate.buttonUrl, }), - ]), + ], }, { - id: 'auto-referral-bonus', - title: 'Автоматическое начисление реферального бонуса', - channels: buildChannelMatrix([ + id: 'bonus-credit', + title: 'Начислен бонус', + channels: [ createChannelPreview('TELEGRAM', { - body: splitBody(autoBonusTemplate.message), - buttonText: autoBonusTemplate.buttonText, - buttonUrl: autoBonusTemplate.buttonUrl, + body: splitBody(bonusTemplate.message), + buttonText: bonusTemplate.buttonText, + buttonUrl: bonusTemplate.buttonUrl, }), createChannelPreview('MAX', { - body: splitBody(autoBonusTemplate.message), - buttonText: autoBonusTemplate.buttonText, - buttonUrl: autoBonusTemplate.buttonUrl, + body: splitBody(bonusTemplate.message), + buttonText: bonusTemplate.buttonText, + buttonUrl: bonusTemplate.buttonUrl, }), - ]), - }, - { - id: 'manual-bonus-credit', - title: 'Ручное начисление бонуса', - channels: buildChannelMatrix([ - createChannelPreview('TELEGRAM', { - body: splitBody(manualBonusTemplate.message), - buttonText: manualBonusTemplate.buttonText, - buttonUrl: manualBonusTemplate.buttonUrl, - }), - createChannelPreview('MAX', { - body: splitBody(manualBonusTemplate.message), - buttonText: manualBonusTemplate.buttonText, - buttonUrl: manualBonusTemplate.buttonUrl, - }), - ]), + ], }, { id: 'reward-withdrawal-review', - title: 'Решение по заявке на вывод бонусов', - channels: buildChannelMatrix([ + title: 'Заявка на вывод вознаграждения', + channels: [ createChannelPreview('TELEGRAM', { body: splitBody(withdrawalReviewTemplate.message), buttonText: withdrawalReviewTemplate.buttonText, @@ -249,7 +264,7 @@ export function getNotificationTemplatesCatalog() { buttonText: withdrawalReviewTemplate.buttonText, buttonUrl: withdrawalReviewTemplate.buttonUrl, }), - ]), + ], }, ]; } diff --git a/src/resolvers.js b/src/resolvers.js index f78b18b..2c77a4d 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -549,23 +549,36 @@ async function collectNotificationHistory(context, userId, channel, limit) { orderId: event.orderId, })); - const bonusHistory = bonuses.map((bonus) => ({ - id: `BONUS_${bonus.id}_${channel}`, - channel, - title: 'Реферальный бонус', - message: `Начисление ${toFloat(bonus.amount)}. Причина: ${bonus.reason}`, - createdAt: bonus.createdAt, - orderId: bonus.orderId, - })); + const bonusHistory = bonuses.map((bonus) => { + const template = buildManualBonusNotificationTemplate({ + amount: toFloat(bonus.amount), + }); - const withdrawalHistory = withdrawals.map((withdrawal) => ({ - id: `WITHDRAW_${withdrawal.id}_${channel}`, - channel, - title: 'Заявка на вывод вознаграждения', - message: `Статус: ${withdrawal.status}.${withdrawal.reviewComment ? ` Комментарий: ${withdrawal.reviewComment}` : ''}`, - createdAt: withdrawal.updatedAt, - orderId: null, - })); + return { + id: `BONUS_${bonus.id}_${channel}`, + channel, + title: 'Начислен бонус', + message: template.message, + createdAt: bonus.createdAt, + orderId: bonus.orderId, + }; + }); + + const withdrawalHistory = withdrawals.map((withdrawal) => { + const template = buildWithdrawalReviewNotificationTemplate({ + status: withdrawal.status, + reviewComment: withdrawal.reviewComment, + }); + + return { + id: `WITHDRAW_${withdrawal.id}_${channel}`, + channel, + title: 'Заявка на вывод вознаграждения', + message: template.message, + createdAt: withdrawal.updatedAt, + orderId: null, + }; + }); return [...eventHistory, ...bonusHistory, ...withdrawalHistory] .sort(byCreatedAtDesc) diff --git a/src/server.js b/src/server.js index 4fae6d8..ee8040f 100644 --- a/src/server.js +++ b/src/server.js @@ -430,6 +430,7 @@ app.post('/bot/messenger-login', async (req, res) => { if (!skipDispatch) { const template = buildMessengerLoginTemplate({ buttonUrl: loginUrl, + expiresAt: login.expiresAt, }); const dispatch = await sendMessengerMessage({ type: channel,