feat: improve notifications - fix auth code tg duplicate, double order notify, add PAID label, expand text, add deliveryFeeAdjusted event
This commit is contained in:
@@ -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;
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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Ожидает оплаты.`
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user