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
@@ -0,0 +1,22 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_NotificationPreference" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"globalEnabled" BOOLEAN NOT NULL DEFAULT true,
"orderCreated" BOOLEAN NOT NULL DEFAULT true,
"orderStatusChanged" BOOLEAN NOT NULL DEFAULT true,
"orderMessageReceived" BOOLEAN NOT NULL DEFAULT true,
"paymentStatusChanged" BOOLEAN NOT NULL DEFAULT true,
"deliveryFeeAdjusted" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "NotificationPreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_NotificationPreference" ("createdAt", "globalEnabled", "id", "orderCreated", "orderMessageReceived", "orderStatusChanged", "paymentStatusChanged", "updatedAt", "userId") SELECT "createdAt", "globalEnabled", "id", "orderCreated", "orderMessageReceived", "orderStatusChanged", "paymentStatusChanged", "updatedAt", "userId" FROM "NotificationPreference";
DROP TABLE "NotificationPreference";
ALTER TABLE "new_NotificationPreference" RENAME TO "NotificationPreference";
CREATE UNIQUE INDEX "NotificationPreference_userId_key" ON "NotificationPreference"("userId");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
+1
View File
@@ -281,6 +281,7 @@ model NotificationPreference {
orderStatusChanged Boolean @default(true)
orderMessageReceived Boolean @default(true)
paymentStatusChanged Boolean @default(true)
deliveryFeeAdjusted Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+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 },