Centralize notification templates
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
|
import { buildLoginCodeEmailTemplate } from './notification-templates.js';
|
||||||
|
|
||||||
let cachedTransporter = null;
|
let cachedTransporter = null;
|
||||||
|
|
||||||
@@ -57,13 +58,13 @@ function getTransporter() {
|
|||||||
export async function sendLoginCodeEmail({ to, code, expiresAt }) {
|
export async function sendLoginCodeEmail({ to, code, expiresAt }) {
|
||||||
const { from } = getSmtpConfig();
|
const { from } = getSmtpConfig();
|
||||||
const transporter = getTransporter();
|
const transporter = getTransporter();
|
||||||
const expiresText = new Date(expiresAt).toLocaleString('ru-RU', { hour12: false });
|
const template = buildLoginCodeEmailTemplate({ code, expiresAt });
|
||||||
|
|
||||||
await transporter.sendMail({
|
await transporter.sendMail({
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
subject: 'Код входа в личный кабинет Fregat',
|
subject: template.subject,
|
||||||
text: `Код входа: ${code}\nДействует до: ${expiresText}`,
|
text: template.text,
|
||||||
html: `<p>Код входа: <b>${code}</b></p><p>Действует до: ${expiresText}</p>`,
|
html: template.html,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
255
src/notification-templates.js
Normal file
255
src/notification-templates.js
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import { isManagerRole } from './access.js';
|
||||||
|
|
||||||
|
const DELIVERY_CHANNELS = ['EMAIL', 'TELEGRAM', 'MAX'];
|
||||||
|
|
||||||
|
function splitBody(text) {
|
||||||
|
return String(text ?? '')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChannelPreview(channel, fields = {}) {
|
||||||
|
return {
|
||||||
|
channel,
|
||||||
|
implemented: true,
|
||||||
|
subject: fields.subject ?? null,
|
||||||
|
body: fields.body ?? [],
|
||||||
|
buttonText: fields.buttonText ?? null,
|
||||||
|
buttonUrl: fields.buttonUrl ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ||
|
||||||
|
process.env.WEB_FRONTEND_URL ||
|
||||||
|
process.env.NUXT_PUBLIC_SITE_URL ||
|
||||||
|
'',
|
||||||
|
).trim().replace(/\/$/, '');
|
||||||
|
|
||||||
|
const normalizedPath = String(path || '').trim();
|
||||||
|
if (!baseUrl || !normalizedPath.startsWith('/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseUrl}${normalizedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildUserOrderPath(orderId, role) {
|
||||||
|
const normalizedOrderId = String(orderId || '').trim();
|
||||||
|
if (!normalizedOrderId) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return isManagerRole(role)
|
||||||
|
? `/client-orders/${normalizedOrderId}`
|
||||||
|
: `/orders/${normalizedOrderId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBonusProgramPath(entry = 'bonus-message') {
|
||||||
|
const normalizedEntry = String(entry || '').trim();
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (normalizedEntry) {
|
||||||
|
params.set('entry', normalizedEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = params.toString();
|
||||||
|
return query ? `/bonus-program?${query}` : '/bonus-program';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildLoginCodeEmailTemplate({ code, expiresAt }) {
|
||||||
|
const expiresText = new Date(expiresAt).toLocaleString('ru-RU', { hour12: false });
|
||||||
|
const body = [
|
||||||
|
`Код входа: ${code}`,
|
||||||
|
`Действует до: ${expiresText}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
subject: 'Код входа в личный кабинет Fregat',
|
||||||
|
body,
|
||||||
|
text: body.join('\n'),
|
||||||
|
html: `<p>Код входа: <b>${code}</b></p><p>Действует до: ${expiresText}</p>`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMessengerLoginTemplate({ buttonUrl }) {
|
||||||
|
return {
|
||||||
|
message: 'Вход подтвержден. Нажмите кнопку, чтобы открыть личный кабинет.',
|
||||||
|
buttonText: 'Открыть кабинет',
|
||||||
|
buttonUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildOrderStatusNotificationTemplate({ orderId, orderCode, status, note, role }) {
|
||||||
|
const message = `Заказ ${orderCode} изменил статус: ${status}.${note ? `\nКомментарий: ${note}` : ''}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
buttonText: 'Открыть заказ',
|
||||||
|
buttonUrl: buildFrontendAppUrl(buildUserOrderPath(orderId, role)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAutoBonusNotificationTemplate({ amount, orderCode }) {
|
||||||
|
return {
|
||||||
|
message: `Начислен бонус: ${amount} за заказ ${orderCode}.`,
|
||||||
|
buttonText: 'Открыть бонусную программу',
|
||||||
|
buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('auto-balance')),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildManualBonusNotificationTemplate({ amount, reason }) {
|
||||||
|
return {
|
||||||
|
message: `Начислен бонус: ${amount}. Причина: ${reason}`,
|
||||||
|
buttonText: 'Открыть бонусную программу',
|
||||||
|
buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('manual-balance')),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWithdrawalReviewNotificationTemplate({ status, reviewComment }) {
|
||||||
|
return {
|
||||||
|
message: `Заявка на вывод вознаграждения обновлена: ${status}.${reviewComment ? ` Комментарий: ${reviewComment}` : ''}`,
|
||||||
|
buttonText: 'Проверить бонусную программу',
|
||||||
|
buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('withdrawal-review')),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNotificationTemplatesCatalog() {
|
||||||
|
const loginTemplate = buildLoginCodeEmailTemplate({
|
||||||
|
code: '123456',
|
||||||
|
expiresAt: '2026-04-06T15:30:00.000Z',
|
||||||
|
});
|
||||||
|
const messengerLoginTemplate = buildMessengerLoginTemplate({
|
||||||
|
buttonUrl: 'https://fregat.dsrptlab.com/login?login_token=demo-token',
|
||||||
|
});
|
||||||
|
const orderStatusTemplate = buildOrderStatusNotificationTemplate({
|
||||||
|
orderId: 'demo-order-id',
|
||||||
|
orderCode: 'FRG-2401',
|
||||||
|
status: 'IN_PROGRESS',
|
||||||
|
note: 'заказ передан в производство',
|
||||||
|
role: 'CLIENT',
|
||||||
|
});
|
||||||
|
const autoBonusTemplate = buildAutoBonusNotificationTemplate({
|
||||||
|
amount: 1250,
|
||||||
|
orderCode: 'FRG-2401',
|
||||||
|
});
|
||||||
|
const manualBonusTemplate = buildManualBonusNotificationTemplate({
|
||||||
|
amount: 500,
|
||||||
|
reason: 'ручное начисление менеджером',
|
||||||
|
});
|
||||||
|
const withdrawalReviewTemplate = buildWithdrawalReviewNotificationTemplate({
|
||||||
|
status: 'APPROVED',
|
||||||
|
reviewComment: 'выплата передана в обработку',
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'login-code-email',
|
||||||
|
title: 'Код входа по email',
|
||||||
|
channels: buildChannelMatrix([
|
||||||
|
createChannelPreview('EMAIL', {
|
||||||
|
subject: loginTemplate.subject,
|
||||||
|
body: loginTemplate.body,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'messenger-login-confirmed',
|
||||||
|
title: 'Подтверждение входа через мессенджер',
|
||||||
|
channels: buildChannelMatrix([
|
||||||
|
createChannelPreview('TELEGRAM', {
|
||||||
|
body: splitBody(messengerLoginTemplate.message),
|
||||||
|
buttonText: messengerLoginTemplate.buttonText,
|
||||||
|
buttonUrl: messengerLoginTemplate.buttonUrl,
|
||||||
|
}),
|
||||||
|
createChannelPreview('MAX', {
|
||||||
|
body: splitBody(messengerLoginTemplate.message),
|
||||||
|
buttonText: messengerLoginTemplate.buttonText,
|
||||||
|
buttonUrl: messengerLoginTemplate.buttonUrl,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'order-status-update',
|
||||||
|
title: 'Изменение статуса заказа',
|
||||||
|
channels: buildChannelMatrix([
|
||||||
|
createChannelPreview('TELEGRAM', {
|
||||||
|
body: splitBody(orderStatusTemplate.message),
|
||||||
|
buttonText: orderStatusTemplate.buttonText,
|
||||||
|
buttonUrl: orderStatusTemplate.buttonUrl,
|
||||||
|
}),
|
||||||
|
createChannelPreview('MAX', {
|
||||||
|
body: splitBody(orderStatusTemplate.message),
|
||||||
|
buttonText: orderStatusTemplate.buttonText,
|
||||||
|
buttonUrl: orderStatusTemplate.buttonUrl,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'auto-referral-bonus',
|
||||||
|
title: 'Автоматическое начисление реферального бонуса',
|
||||||
|
channels: buildChannelMatrix([
|
||||||
|
createChannelPreview('TELEGRAM', {
|
||||||
|
body: splitBody(autoBonusTemplate.message),
|
||||||
|
buttonText: autoBonusTemplate.buttonText,
|
||||||
|
buttonUrl: autoBonusTemplate.buttonUrl,
|
||||||
|
}),
|
||||||
|
createChannelPreview('MAX', {
|
||||||
|
body: splitBody(autoBonusTemplate.message),
|
||||||
|
buttonText: autoBonusTemplate.buttonText,
|
||||||
|
buttonUrl: autoBonusTemplate.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([
|
||||||
|
createChannelPreview('TELEGRAM', {
|
||||||
|
body: splitBody(withdrawalReviewTemplate.message),
|
||||||
|
buttonText: withdrawalReviewTemplate.buttonText,
|
||||||
|
buttonUrl: withdrawalReviewTemplate.buttonUrl,
|
||||||
|
}),
|
||||||
|
createChannelPreview('MAX', {
|
||||||
|
body: splitBody(withdrawalReviewTemplate.message),
|
||||||
|
buttonText: withdrawalReviewTemplate.buttonText,
|
||||||
|
buttonUrl: withdrawalReviewTemplate.buttonUrl,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
113
src/resolvers.js
113
src/resolvers.js
@@ -15,6 +15,13 @@ import {
|
|||||||
isManagerRole,
|
isManagerRole,
|
||||||
} from './access.js';
|
} from './access.js';
|
||||||
import { sendLoginCodeEmail } from './mailer.js';
|
import { sendLoginCodeEmail } from './mailer.js';
|
||||||
|
import {
|
||||||
|
buildAutoBonusNotificationTemplate,
|
||||||
|
buildManualBonusNotificationTemplate,
|
||||||
|
buildOrderStatusNotificationTemplate,
|
||||||
|
buildWithdrawalReviewNotificationTemplate,
|
||||||
|
getNotificationTemplatesCatalog,
|
||||||
|
} from './notification-templates.js';
|
||||||
import { upsertActiveMessengerConnection } from './messenger-connections.js';
|
import { upsertActiveMessengerConnection } from './messenger-connections.js';
|
||||||
import { dispatchToUserConnections, sendMessengerMessage } from './messenger.js';
|
import { dispatchToUserConnections, sendMessengerMessage } from './messenger.js';
|
||||||
import { dateTimeScalar, jsonScalar } from './scalars.js';
|
import { dateTimeScalar, jsonScalar } from './scalars.js';
|
||||||
@@ -230,13 +237,17 @@ async function applyManualOrderStatus(context, order, manager, status) {
|
|||||||
await notifyOrderStakeholders(context, updated, status, `Manager set status to ${status}`);
|
await notifyOrderStakeholders(context, updated, status, `Manager set status to ${status}`);
|
||||||
|
|
||||||
if (referralBonus?.isNew) {
|
if (referralBonus?.isNew) {
|
||||||
|
const template = buildAutoBonusNotificationTemplate({
|
||||||
|
amount: toFloat(referralBonus.transaction.amount),
|
||||||
|
orderCode: updated.code,
|
||||||
|
});
|
||||||
await dispatchToUserConnections(
|
await dispatchToUserConnections(
|
||||||
context.prisma,
|
context.prisma,
|
||||||
referralBonus.transaction.userId,
|
referralBonus.transaction.userId,
|
||||||
`Начислен бонус: ${toFloat(referralBonus.transaction.amount)} за заказ ${updated.code}.`,
|
template.message,
|
||||||
{
|
{
|
||||||
buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('auto-balance')),
|
buttonUrl: template.buttonUrl,
|
||||||
buttonText: 'Открыть бонусную программу',
|
buttonText: template.buttonText,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -452,57 +463,12 @@ async function resolveOrderRequirements(context, user, deliveryAddressId) {
|
|||||||
return resolveSelectedDeliveryAddress(context, user.id, deliveryAddressId);
|
return resolveSelectedDeliveryAddress(context, user.id, deliveryAddressId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOrderStatusMessage(order, status, note) {
|
|
||||||
const suffix = note ? `\nКомментарий: ${note}` : '';
|
|
||||||
return `Заказ ${order.code} изменил статус: ${status}.${suffix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildFrontendAppUrl(path) {
|
|
||||||
const baseUrl = String(
|
|
||||||
process.env.TELEGRAM_MINI_APP_URL ||
|
|
||||||
process.env.WEB_FRONTEND_URL ||
|
|
||||||
process.env.NUXT_PUBLIC_SITE_URL ||
|
|
||||||
'',
|
|
||||||
).trim().replace(/\/$/, '');
|
|
||||||
|
|
||||||
const normalizedPath = String(path || '').trim();
|
|
||||||
if (!baseUrl || !normalizedPath.startsWith('/')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${baseUrl}${normalizedPath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildUserOrderPath(orderId, role) {
|
|
||||||
const normalizedOrderId = String(orderId || '').trim();
|
|
||||||
if (!normalizedOrderId) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return isManagerRole(role)
|
|
||||||
? `/client-orders/${normalizedOrderId}`
|
|
||||||
: `/orders/${normalizedOrderId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildBonusProgramPath(entry = 'bonus-message') {
|
|
||||||
const normalizedEntry = String(entry || '').trim();
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if (normalizedEntry) {
|
|
||||||
params.set('entry', normalizedEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = params.toString();
|
|
||||||
return query ? `/bonus-program?${query}` : '/bonus-program';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function notifyOrderStakeholders(context, order, status, note) {
|
async function notifyOrderStakeholders(context, order, status, note) {
|
||||||
const recipients = [order.customerId, order.managerId].filter(Boolean);
|
const recipients = [order.customerId, order.managerId].filter(Boolean);
|
||||||
if (!recipients.length) {
|
if (!recipients.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = formatOrderStatusMessage(order, status, note);
|
|
||||||
const uniqueRecipients = [...new Set(recipients)];
|
const uniqueRecipients = [...new Set(recipients)];
|
||||||
const users = await context.prisma.user.findMany({
|
const users = await context.prisma.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -516,10 +482,20 @@ async function notifyOrderStakeholders(context, order, status, note) {
|
|||||||
const userRoleMap = new Map(users.map((user) => [user.id, user.role]));
|
const userRoleMap = new Map(users.map((user) => [user.id, user.role]));
|
||||||
|
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
uniqueRecipients.map((userId) => dispatchToUserConnections(context.prisma, userId, message, {
|
uniqueRecipients.map((userId) => {
|
||||||
buttonUrl: buildFrontendAppUrl(buildUserOrderPath(order.id, userRoleMap.get(userId))),
|
const template = buildOrderStatusNotificationTemplate({
|
||||||
buttonText: 'Открыть заказ',
|
orderId: order.id,
|
||||||
})),
|
orderCode: order.code,
|
||||||
|
status,
|
||||||
|
note,
|
||||||
|
role: userRoleMap.get(userId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return dispatchToUserConnections(context.prisma, userId, template.message, {
|
||||||
|
buttonUrl: template.buttonUrl,
|
||||||
|
buttonText: template.buttonText,
|
||||||
|
});
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,7 +538,13 @@ async function collectNotificationHistory(context, userId, channel, limit) {
|
|||||||
id: `ORDER_${event.id}_${channel}`,
|
id: `ORDER_${event.id}_${channel}`,
|
||||||
channel,
|
channel,
|
||||||
title: `Статус заказа ${event.order.code}`,
|
title: `Статус заказа ${event.order.code}`,
|
||||||
message: formatOrderStatusMessage(event.order, event.status, event.note),
|
message: buildOrderStatusNotificationTemplate({
|
||||||
|
orderId: event.orderId,
|
||||||
|
orderCode: event.order.code,
|
||||||
|
status: event.status,
|
||||||
|
note: event.note,
|
||||||
|
role: 'CLIENT',
|
||||||
|
}).message,
|
||||||
createdAt: event.createdAt,
|
createdAt: event.createdAt,
|
||||||
orderId: event.orderId,
|
orderId: event.orderId,
|
||||||
}));
|
}));
|
||||||
@@ -679,6 +661,11 @@ export const resolvers = {
|
|||||||
return collectNotificationHistory(context, user.id, channel, normalizedLimit);
|
return collectNotificationHistory(context, user.id, channel, normalizedLimit);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
notificationTemplates: (_, __, context) => {
|
||||||
|
requireManagerAccess(context);
|
||||||
|
return getNotificationTemplatesCatalog();
|
||||||
|
},
|
||||||
|
|
||||||
managerNotificationHistory: async (_, { userId, channel, limit }, context) => {
|
managerNotificationHistory: async (_, { userId, channel, limit }, context) => {
|
||||||
const manager = requireManagerAccess(context);
|
const manager = requireManagerAccess(context);
|
||||||
await assertManagerCanAccessUser(context.prisma, manager, userId);
|
await assertManagerCanAccessUser(context.prisma, manager, userId);
|
||||||
@@ -1906,13 +1893,17 @@ export const resolvers = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const template = buildManualBonusNotificationTemplate({
|
||||||
|
amount: toFloat(transaction.amount),
|
||||||
|
reason: transaction.reason,
|
||||||
|
});
|
||||||
await dispatchToUserConnections(
|
await dispatchToUserConnections(
|
||||||
context.prisma,
|
context.prisma,
|
||||||
transaction.userId,
|
transaction.userId,
|
||||||
`Начислен бонус: ${toFloat(transaction.amount)}. Причина: ${transaction.reason}`,
|
template.message,
|
||||||
{
|
{
|
||||||
buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('manual-balance')),
|
buttonUrl: template.buttonUrl,
|
||||||
buttonText: 'Открыть бонусную программу',
|
buttonText: template.buttonText,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1954,13 +1945,17 @@ export const resolvers = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const template = buildWithdrawalReviewNotificationTemplate({
|
||||||
|
status: withdrawal.status,
|
||||||
|
reviewComment: withdrawal.reviewComment,
|
||||||
|
});
|
||||||
await dispatchToUserConnections(
|
await dispatchToUserConnections(
|
||||||
context.prisma,
|
context.prisma,
|
||||||
withdrawal.requesterId,
|
withdrawal.requesterId,
|
||||||
`Заявка на вывод вознаграждения обновлена: ${withdrawal.status}.${withdrawal.reviewComment ? ` Комментарий: ${withdrawal.reviewComment}` : ''}`,
|
template.message,
|
||||||
{
|
{
|
||||||
buttonUrl: buildFrontendAppUrl(buildBonusProgramPath('withdrawal-review')),
|
buttonUrl: template.buttonUrl,
|
||||||
buttonText: 'Проверить бонусную программу',
|
buttonText: template.buttonText,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,21 @@ type NotificationHistoryItem {
|
|||||||
orderId: ID
|
orderId: ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotificationTemplateChannel {
|
||||||
|
channel: LoginChannel!
|
||||||
|
implemented: Boolean!
|
||||||
|
subject: String
|
||||||
|
body: [String!]!
|
||||||
|
buttonText: String
|
||||||
|
buttonUrl: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationTemplate {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
channels: [NotificationTemplateChannel!]!
|
||||||
|
}
|
||||||
|
|
||||||
type Warehouse {
|
type Warehouse {
|
||||||
id: ID!
|
id: ID!
|
||||||
code: String!
|
code: String!
|
||||||
@@ -364,6 +379,7 @@ type Query {
|
|||||||
myDeliveryAddresses: [DeliveryAddress!]!
|
myDeliveryAddresses: [DeliveryAddress!]!
|
||||||
myMessengerConnections: [MessengerConnection!]!
|
myMessengerConnections: [MessengerConnection!]!
|
||||||
myNotificationHistory(channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
myNotificationHistory(channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
||||||
|
notificationTemplates: [NotificationTemplate!]!
|
||||||
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
||||||
clientProducts: [Product!]!
|
clientProducts: [Product!]!
|
||||||
order(id: ID!): Order
|
order(id: ID!): Order
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
} from './messenger-connections.js';
|
} from './messenger-connections.js';
|
||||||
import { validateMaxMiniAppInitData } from './max-mini-app.js';
|
import { validateMaxMiniAppInitData } from './max-mini-app.js';
|
||||||
import { sendMessengerMessage } from './messenger.js';
|
import { sendMessengerMessage } from './messenger.js';
|
||||||
|
import { buildMessengerLoginTemplate } from './notification-templates.js';
|
||||||
import { prisma } from './prisma-client.js';
|
import { prisma } from './prisma-client.js';
|
||||||
import { resolvers } from './resolvers.js';
|
import { resolvers } from './resolvers.js';
|
||||||
import { telegramApi, telegramFileUrl } from './telegram.js';
|
import { telegramApi, telegramFileUrl } from './telegram.js';
|
||||||
@@ -427,12 +428,15 @@ app.post('/bot/messenger-login', async (req, res) => {
|
|||||||
const loginUrl = `${frontendUrl}/login?${loginQuery.toString()}`;
|
const loginUrl = `${frontendUrl}/login?${loginQuery.toString()}`;
|
||||||
|
|
||||||
if (!skipDispatch) {
|
if (!skipDispatch) {
|
||||||
|
const template = buildMessengerLoginTemplate({
|
||||||
|
buttonUrl: loginUrl,
|
||||||
|
});
|
||||||
const dispatch = await sendMessengerMessage({
|
const dispatch = await sendMessengerMessage({
|
||||||
type: channel,
|
type: channel,
|
||||||
channelId,
|
channelId,
|
||||||
message: 'Вход подтвержден. Нажмите кнопку, чтобы открыть личный кабинет.',
|
message: template.message,
|
||||||
buttonUrl: loginUrl,
|
buttonUrl: template.buttonUrl,
|
||||||
buttonText: 'Открыть кабинет',
|
buttonText: template.buttonText,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!dispatch.success) {
|
if (!dispatch.success) {
|
||||||
|
|||||||
Reference in New Issue
Block a user