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';
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,
}),
]),
],
},
];
}

View File

@@ -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)

View File

@@ -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,