feat: improve notifications - fix auth code tg duplicate, double order notify, add PAID label, expand text, add deliveryFeeAdjusted event

This commit is contained in:
Kirill
2026-05-18 14:48:54 +05:00
parent 2f67c37502
commit d0b3c97803
17 changed files with 729 additions and 8 deletions
+18
View File
@@ -113,9 +113,26 @@ const {
ORDER_MESSAGE_ADMIN_REPLY,
PAYMENT_STATUS_CHANGED,
AUTH_CODE_REQUESTED,
DELIVERY_FEE_ADJUSTED,
} = NOTIFICATION_EVENTS;
async function dispatchNotification(eventType, payload) {
if (eventType === AUTH_CODE_REQUESTED) {
const targets = await resolveAuthCodeTargets(eventType, payload);
for (const target of targets.filter((t) => t.channel === 'telegram')) {
const log = await prisma.notificationLog.create({
data: {
eventType,
channel: target.channel,
status: 'pending',
payload: JSON.stringify(payload),
},
});
notificationQueue.enqueue({ ...target, eventType, payload, logId: log.id });
}
return;
}
const userTargets = await resolveUserNotificationTargets(eventType, payload);
for (const target of userTargets) {
const log = await prisma.notificationLog.create({
@@ -157,6 +174,7 @@ eventBus.on(PAYMENT_STATUS_CHANGED, (payload) => dispatchNotification(PAYMENT_ST
eventBus.on(AUTH_CODE_REQUESTED, (payload) => dispatchNotification(AUTH_CODE_REQUESTED, payload));
eventBus.on("order:created:admin", (payload) => dispatchNotification("order:created:admin", payload));
eventBus.on("review:created", (payload) => dispatchNotification("review:created", payload));
eventBus.on(DELIVERY_FEE_ADJUSTED, (payload) => dispatchNotification(DELIVERY_FEE_ADJUSTED, payload));
async function shutdown() {
notificationQueue.stop();
@@ -8,6 +8,7 @@ import {
renderAdminOrderCreatedEmail,
renderAdminNewReviewEmail,
renderAuthCodeEmail,
renderDeliveryFeeAdjustedEmail,
} from '../templates/email-templates.js'
const templateRenderers = {
@@ -19,6 +20,7 @@ const templateRenderers = {
'orderMessage:sent': renderOrderMessageEmail,
'review:created': renderAdminNewReviewEmail,
'auth:codeRequested': renderAuthCodeEmail,
'order:deliveryFeeAdjusted': renderDeliveryFeeAdjustedEmail,
}
export const emailChannel = {
@@ -6,6 +6,7 @@ import {
renderAdminOrderCreatedTg,
renderAdminNewReviewTg,
renderAuthCodeTg,
renderDeliveryFeeAdjustedTg,
} from '../templates/telegram-templates.js'
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || ''
@@ -19,6 +20,7 @@ const templateRenderers = {
'orderMessage:sent': renderOrderMessageTg,
'review:created': renderAdminNewReviewTg,
'auth:codeRequested': renderAuthCodeTg,
'order:deliveryFeeAdjusted': renderDeliveryFeeAdjustedTg,
}
async function postToTelegram(chatId, text) {
+2 -1
View File
@@ -8,6 +8,7 @@ const {
ORDER_MESSAGE_ADMIN_REPLY,
PAYMENT_STATUS_CHANGED,
AUTH_CODE_REQUESTED,
DELIVERY_FEE_ADJUSTED,
} = NOTIFICATION_EVENTS;
const userEventFieldMap = {
@@ -15,10 +16,10 @@ const userEventFieldMap = {
[ORDER_STATUS_CHANGED]: "orderStatusChanged",
[ORDER_MESSAGE_ADMIN_REPLY]: "orderMessageReceived",
[PAYMENT_STATUS_CHANGED]: "paymentStatusChanged",
[DELIVERY_FEE_ADJUSTED]: "deliveryFeeAdjusted",
};
const adminEventFieldMap = {
[ORDER_CREATED]: "newOrder",
[ORDER_MESSAGE_SENT]: "newOrderMessage",
"review:created": "newReview",
};
@@ -14,12 +14,15 @@ function baseLayout(title, body) {
</html>`;
}
export function renderOrderCreatedEmail({ orderId, totalCents, itemsCount }) {
export function renderOrderCreatedEmail({ orderId, totalCents, itemsCount, deliveryType }) {
const total = (totalCents / 100).toLocaleString("ru-RU");
const nextAction = deliveryType === "delivery"
? "Оплата будет доступна после уточнения стоимости доставки."
: "Ожидает оплаты.";
const body = `
<p>Ваш заказ <b>#${orderId.slice(0, 8)}</b> успешно создан.</p>
<p>Товаров: ${itemsCount} | Сумма: <b>${total} ₽</b></p>
<p>Мы сообщим вам об изменениях статуса.</p>
<p>${nextAction}</p>
`;
return { subject: "Заказ создан", html: baseLayout("Заказ создан", body) };
}
@@ -32,6 +35,7 @@ export function renderOrderStatusChangedEmail({
const statusLabels = {
DRAFT: "Черновик",
PENDING_PAYMENT: "Ожидает оплаты",
PAID: "Оплачен",
IN_PROGRESS: "В работе",
READY_FOR_PICKUP: "Готов к выдаче",
SHIPPED: "Отправлен",
@@ -87,11 +91,16 @@ export function renderAdminOrderCreatedEmail({
userEmail,
totalCents,
itemsCount,
deliveryType,
}) {
const total = (totalCents / 100).toLocaleString("ru-RU");
const note = deliveryType === "delivery"
? '<p>⚠️ <b>Скорректируйте стоимость доставки</b> в админ-панели.</p>'
: "";
const body = `
<p>Новый заказ <b>#${orderId.slice(0, 8)}</b> от <b>${userEmail}</b>.</p>
<p>Товаров: ${itemsCount} | Сумма: <b>${total} ₽</b></p>
${note}
`;
return { subject: "Новый заказ", html: baseLayout("Новый заказ", body) };
}
@@ -118,3 +127,16 @@ export function renderAuthCodeEmail({ code }) {
`;
return { subject: "Код входа", html: baseLayout("Код входа", body) };
}
export function renderDeliveryFeeAdjustedEmail({ orderId, totalCents }) {
const total = (totalCents / 100).toLocaleString("ru-RU");
const body = `
<p>Стоимость доставки заказа <b>#${orderId.slice(0, 8)}</b> скорректирована.</p>
<p>Новая сумма: <b>${total} ₽</b></p>
<p>Ожидает оплаты. Проверьте статус заказа в личном кабинете.</p>
`;
return {
subject: "Стоимость доставки скорректирована",
html: baseLayout("Стоимость доставки скорректирована", body),
};
}
@@ -1,11 +1,14 @@
export function renderOrderCreatedTg({ orderId, totalCents, itemsCount }) {
export function renderOrderCreatedTg({ orderId, totalCents, itemsCount, deliveryType }) {
const total = (totalCents / 100).toLocaleString('ru-RU')
return `📦 <b>Новый заказ</b> #${orderId.slice(0, 8)}\nТоваров: ${itemsCount} | Сумма: ${total}`
const nextAction = deliveryType === 'delivery'
? 'Оплата будет доступна после уточнения стоимости доставки.'
: 'Ожидает оплаты.'
return `📦 <b>Новый заказ</b> #${orderId.slice(0, 8)}\nТоваров: ${itemsCount} | Сумма: ${total}\n${nextAction}`
}
export function renderOrderStatusChangedTg({ orderId, oldStatus, newStatus }) {
const labels = {
DRAFT: 'Черновик', PENDING_PAYMENT: 'Ожидает оплаты', IN_PROGRESS: 'В работе',
DRAFT: 'Черновик', PENDING_PAYMENT: 'Ожидает оплаты', PAID: 'Оплачен', IN_PROGRESS: 'В работе',
READY_FOR_PICKUP: 'Готов к выдаче', SHIPPED: 'Отправлен', DONE: 'Выполнен', CANCELLED: 'Отменён',
}
return `🔄 Заказ #${orderId.slice(0, 8)}\n${labels[oldStatus] || oldStatus} → <b>${labels[newStatus] || newStatus}</b>`
@@ -21,9 +24,10 @@ export function renderPaymentStatusChangedTg({ orderId, paymentStatus }) {
return `💳 Оплата заказа #${orderId.slice(0, 8)}: <b>${labels[paymentStatus] || paymentStatus}</b>`
}
export function renderAdminOrderCreatedTg({ orderId, userEmail, totalCents, itemsCount }) {
export function renderAdminOrderCreatedTg({ orderId, userEmail, totalCents, itemsCount, deliveryType }) {
const total = (totalCents / 100).toLocaleString('ru-RU')
return `🛒 <b>Новый заказ</b> #${orderId.slice(0, 8)}\nОт: ${userEmail}\nТоваров: ${itemsCount} | Сумма: ${total}`
const note = deliveryType === 'delivery' ? '\n\n⚠️ Скорректируйте стоимость доставки' : ''
return `🛒 <b>Новый заказ</b> #${orderId.slice(0, 8)}\nОт: ${userEmail}\nТоваров: ${itemsCount} | Сумма: ${total}${note}`
}
export function renderAdminNewReviewTg({ rating, text, productTitle, userName }) {
@@ -34,3 +38,8 @@ export function renderAdminNewReviewTg({ rating, text, productTitle, userName })
export function renderAuthCodeTg({ code }) {
return `🔐 Код входа: <b>${code}</b>`
}
export function renderDeliveryFeeAdjustedTg({ orderId, totalCents }) {
const total = (totalCents / 100).toLocaleString('ru-RU')
return `💰 <b>Стоимость доставки скорректирована</b> для заказа #${orderId.slice(0, 8)}\nНовая сумма: ${total}\n\nОжидает оплаты.`
}
+7
View File
@@ -192,6 +192,13 @@ export async function registerAdminOrderRoutes(fastify) {
deliveryFeeLocked: true,
},
});
request.server.eventBus.emit(NOTIFICATION_EVENTS.DELIVERY_FEE_ADJUSTED, {
orderId: updated.id,
userId: existing.userId,
totalCents: updated.totalCents,
});
return { item: updated };
},
);
+2
View File
@@ -190,6 +190,7 @@ export async function registerUserOrderRoutes(fastify) {
userId,
totalCents: created.totalCents,
itemsCount: cartItems.length,
deliveryType: created.deliveryType,
});
// Also emit admin notification
@@ -199,6 +200,7 @@ export async function registerUserOrderRoutes(fastify) {
userEmail: request.user.email || "",
totalCents: created.totalCents,
itemsCount: cartItems.length,
deliveryType: created.deliveryType,
});
return reply.code(201).send({ orderId: created.id });
+1
View File
@@ -25,6 +25,7 @@ export async function registerUserNotificationRoutes(fastify) {
if ('orderStatusChanged' in body) data.orderStatusChanged = Boolean(body.orderStatusChanged)
if ('orderMessageReceived' in body) data.orderMessageReceived = Boolean(body.orderMessageReceived)
if ('paymentStatusChanged' in body) data.paymentStatusChanged = Boolean(body.paymentStatusChanged)
if ('deliveryFeeAdjusted' in body) data.deliveryFeeAdjusted = Boolean(body.deliveryFeeAdjusted)
const prefs = await prisma.notificationPreference.upsert({
where: { userId },