Refine notification template copy

This commit is contained in:
Ruslan Bakiev
2026-04-06 20:50:17 +07:00
parent c6a515803b
commit db2e05bbf4
3 changed files with 120 additions and 91 deletions

View File

@@ -1,7 +1,5 @@
import { isManagerRole } from './access.js'; import { isManagerRole } from './access.js';
const DELIVERY_CHANNELS = ['EMAIL', 'TELEGRAM', 'MAX'];
function splitBody(text) { function splitBody(text) {
return String(text ?? '') return String(text ?? '')
.split('\n') .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) { export function buildFrontendAppUrl(path) {
const baseUrl = String( const baseUrl = String(
process.env.TELEGRAM_MINI_APP_URL || 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 { return {
message: 'Вход подтвержден. Нажмите кнопку, чтобы открыть личный кабинет.', message: body.join('\n'),
buttonText: 'Открыть кабинет', buttonText: 'Открыть кабинет',
buttonUrl, buttonUrl,
}; };
} }
export function buildOrderStatusNotificationTemplate({ orderId, orderCode, status, note, role }) { export function buildOrderStatusNotificationTemplate({ orderId, orderCode, status, note, role }) {
const message = `Заказ ${orderCode} изменил статус: ${status}.${note ? `\nКомментарий: ${note}` : ''}`; const body = [
`Статус заказа ${orderCode}: ${formatOrderStatusLabel(status)}.`,
...(note ? [`Комментарий: ${note}`] : []),
];
return { return {
message, subject: `Изменение статуса заказа ${orderCode}`,
body,
message: body.join('\n'),
buttonText: 'Открыть заказ', buttonText: 'Открыть заказ',
buttonUrl: buildFrontendAppUrl(buildUserOrderPath(orderId, role)), buttonUrl: buildFrontendAppUrl(buildUserOrderPath(orderId, role)),
}; };
} }
export function buildAutoBonusNotificationTemplate({ amount, orderCode }) { export function buildBonusCreditTemplate({ amount }) {
const normalizedAmount = Number(amount);
return { return {
message: `Начислен бонус: ${amount} за заказ ${orderCode}.`, message: `Начислен бонус: ${Number.isFinite(normalizedAmount) ? normalizedAmount : amount}.`,
buttonText: 'Открыть бонусную программу', buttonText: 'Открыть бонусную программу',
buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('auto-balance')), buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('balance')),
}; };
} }
export function buildManualBonusNotificationTemplate({ amount, reason }) { export function buildAutoBonusNotificationTemplate({ amount }) {
return { return buildBonusCreditTemplate({ amount });
message: `Начислен бонус: ${amount}. Причина: ${reason}`, }
buttonText: 'Открыть бонусную программу',
buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('manual-balance')), export function buildManualBonusNotificationTemplate({ amount }) {
}; return buildBonusCreditTemplate({ amount });
} }
export function buildWithdrawalReviewNotificationTemplate({ status, reviewComment }) { export function buildWithdrawalReviewNotificationTemplate({ status, reviewComment }) {
const body = [
`Ваша заявка на вывод вознаграждения ${formatWithdrawalStatusLabel(status)}.`,
...(reviewComment ? [`Комментарий: ${reviewComment}`] : []),
];
return { return {
message: `Заявка на вывод вознаграждения обновлена: ${status}.${reviewComment ? ` Комментарий: ${reviewComment}` : ''}`, message: body.join('\n'),
buttonText: 'Проверить бонусную программу', buttonText: 'Открыть бонусную программу',
buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('withdrawal-review')), buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('withdrawal-review')),
}; };
} }
@@ -139,6 +170,7 @@ export function getNotificationTemplatesCatalog() {
}); });
const messengerLoginTemplate = buildMessengerLoginTemplate({ const messengerLoginTemplate = buildMessengerLoginTemplate({
buttonUrl: 'https://fregat.dsrptlab.com/login?login_token=demo-token', buttonUrl: 'https://fregat.dsrptlab.com/login?login_token=demo-token',
expiresAt: '2026-04-06T15:35:00.000Z',
}); });
const orderStatusTemplate = buildOrderStatusNotificationTemplate({ const orderStatusTemplate = buildOrderStatusNotificationTemplate({
orderId: 'demo-order-id', orderId: 'demo-order-id',
@@ -147,13 +179,8 @@ export function getNotificationTemplatesCatalog() {
note: 'заказ передан в производство', note: 'заказ передан в производство',
role: 'CLIENT', role: 'CLIENT',
}); });
const autoBonusTemplate = buildAutoBonusNotificationTemplate({ const bonusTemplate = buildBonusCreditTemplate({
amount: 1250, amount: 1250,
orderCode: 'FRG-2401',
});
const manualBonusTemplate = buildManualBonusNotificationTemplate({
amount: 500,
reason: 'ручное начисление менеджером',
}); });
const withdrawalReviewTemplate = buildWithdrawalReviewNotificationTemplate({ const withdrawalReviewTemplate = buildWithdrawalReviewNotificationTemplate({
status: 'APPROVED', status: 'APPROVED',
@@ -164,17 +191,17 @@ export function getNotificationTemplatesCatalog() {
{ {
id: 'login-code-email', id: 'login-code-email',
title: 'Код входа по email', title: 'Код входа по email',
channels: buildChannelMatrix([ channels: [
createChannelPreview('EMAIL', { createChannelPreview('EMAIL', {
subject: loginTemplate.subject, subject: loginTemplate.subject,
body: loginTemplate.body, body: loginTemplate.body,
}), }),
]), ],
}, },
{ {
id: 'messenger-login-confirmed', id: 'messenger-login-confirmed',
title: одтверждение входа через мессенджер', title: ривязка мессенджера',
channels: buildChannelMatrix([ channels: [
createChannelPreview('TELEGRAM', { createChannelPreview('TELEGRAM', {
body: splitBody(messengerLoginTemplate.message), body: splitBody(messengerLoginTemplate.message),
buttonText: messengerLoginTemplate.buttonText, buttonText: messengerLoginTemplate.buttonText,
@@ -185,12 +212,16 @@ export function getNotificationTemplatesCatalog() {
buttonText: messengerLoginTemplate.buttonText, buttonText: messengerLoginTemplate.buttonText,
buttonUrl: messengerLoginTemplate.buttonUrl, buttonUrl: messengerLoginTemplate.buttonUrl,
}), }),
]), ],
}, },
{ {
id: 'order-status-update', id: 'order-status-update',
title: 'Изменение статуса заказа', title: 'Изменение статуса заказа',
channels: buildChannelMatrix([ channels: [
createChannelPreview('EMAIL', {
subject: orderStatusTemplate.subject,
body: orderStatusTemplate.body,
}),
createChannelPreview('TELEGRAM', { createChannelPreview('TELEGRAM', {
body: splitBody(orderStatusTemplate.message), body: splitBody(orderStatusTemplate.message),
buttonText: orderStatusTemplate.buttonText, buttonText: orderStatusTemplate.buttonText,
@@ -201,44 +232,28 @@ export function getNotificationTemplatesCatalog() {
buttonText: orderStatusTemplate.buttonText, buttonText: orderStatusTemplate.buttonText,
buttonUrl: orderStatusTemplate.buttonUrl, buttonUrl: orderStatusTemplate.buttonUrl,
}), }),
]), ],
}, },
{ {
id: 'auto-referral-bonus', id: 'bonus-credit',
title: 'Автоматическое начисление реферального бонуса', title: 'Начислен бонус',
channels: buildChannelMatrix([ channels: [
createChannelPreview('TELEGRAM', { createChannelPreview('TELEGRAM', {
body: splitBody(autoBonusTemplate.message), body: splitBody(bonusTemplate.message),
buttonText: autoBonusTemplate.buttonText, buttonText: bonusTemplate.buttonText,
buttonUrl: autoBonusTemplate.buttonUrl, buttonUrl: bonusTemplate.buttonUrl,
}), }),
createChannelPreview('MAX', { createChannelPreview('MAX', {
body: splitBody(autoBonusTemplate.message), body: splitBody(bonusTemplate.message),
buttonText: autoBonusTemplate.buttonText, buttonText: bonusTemplate.buttonText,
buttonUrl: autoBonusTemplate.buttonUrl, 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', id: 'reward-withdrawal-review',
title: 'Решение по заявке на вывод бонусов', title: 'Заявка на вывод вознаграждения',
channels: buildChannelMatrix([ channels: [
createChannelPreview('TELEGRAM', { createChannelPreview('TELEGRAM', {
body: splitBody(withdrawalReviewTemplate.message), body: splitBody(withdrawalReviewTemplate.message),
buttonText: withdrawalReviewTemplate.buttonText, buttonText: withdrawalReviewTemplate.buttonText,
@@ -249,7 +264,7 @@ export function getNotificationTemplatesCatalog() {
buttonText: withdrawalReviewTemplate.buttonText, buttonText: withdrawalReviewTemplate.buttonText,
buttonUrl: withdrawalReviewTemplate.buttonUrl, buttonUrl: withdrawalReviewTemplate.buttonUrl,
}), }),
]), ],
}, },
]; ];
} }

View File

@@ -549,23 +549,36 @@ async function collectNotificationHistory(context, userId, channel, limit) {
orderId: event.orderId, orderId: event.orderId,
})); }));
const bonusHistory = bonuses.map((bonus) => ({ const bonusHistory = bonuses.map((bonus) => {
id: `BONUS_${bonus.id}_${channel}`, const template = buildManualBonusNotificationTemplate({
channel, amount: toFloat(bonus.amount),
title: 'Реферальный бонус', });
message: `Начисление ${toFloat(bonus.amount)}. Причина: ${bonus.reason}`,
createdAt: bonus.createdAt,
orderId: bonus.orderId,
}));
const withdrawalHistory = withdrawals.map((withdrawal) => ({ return {
id: `WITHDRAW_${withdrawal.id}_${channel}`, id: `BONUS_${bonus.id}_${channel}`,
channel, channel,
title: 'Заявка на вывод вознаграждения', title: 'Начислен бонус',
message: `Статус: ${withdrawal.status}.${withdrawal.reviewComment ? ` Комментарий: ${withdrawal.reviewComment}` : ''}`, message: template.message,
createdAt: withdrawal.updatedAt, createdAt: bonus.createdAt,
orderId: null, 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] return [...eventHistory, ...bonusHistory, ...withdrawalHistory]
.sort(byCreatedAtDesc) .sort(byCreatedAtDesc)

View File

@@ -430,6 +430,7 @@ app.post('/bot/messenger-login', async (req, res) => {
if (!skipDispatch) { if (!skipDispatch) {
const template = buildMessengerLoginTemplate({ const template = buildMessengerLoginTemplate({
buttonUrl: loginUrl, buttonUrl: loginUrl,
expiresAt: login.expiresAt,
}); });
const dispatch = await sendMessengerMessage({ const dispatch = await sendMessengerMessage({
type: channel, type: channel,