17 KiB
Улучшение системы оповещений — План реализации
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Исправить дублирование, добавить недостающие лейблы, расширить текст оповещений и починить авторизационный код в Telegram.
Architecture: Изменения затрагивают shared-константы, серверные шаблоны и роуты, а также клиентский UI. Новая миграция Prisma. Новое событие order:deliveryFeeAdjusted.
Tech Stack: Fastify + Prisma + SQLite, React + Vite + MUI, shared-константы.
Build order (из AGENTS.md):
cd server && npm run db:migrate # если schema.prisma изменён
cd server && npm test # серверные тесты сначала
cd client && npm run lint && npm run format:check && npm test # потом клиент
cd client && npm run build # полная проверка типов + сборка
Task 1: Починить dispatchNotification для AUTH_CODE_REQUESTED
Файлы:
-
Modify:
server/src/index.js -
Step 1: Изменить dispatchNotification для AUTH_CODE_REQUESTED
В server/src/index.js, внутри dispatchNotification, добавить отдельную ветку для AUTH_CODE_REQUESTED, использующую resolveAuthCodeTargets:
async function dispatchNotification(eventType, payload) {
if (eventType === AUTH_CODE_REQUESTED) {
const targets = await resolveAuthCodeTargets(eventType, payload);
for (const target of targets) {
const log = await prisma.notificationLog.create({
data: {
userId: payload.userId,
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);
// ... остальной код без изменений
- Step 2: Запустить серверные тесты
Run: cd server && npm test
Expected: все тесты проходят.
- Step 3: Проверить линтер
Run: cd server && npm run lint
Expected: no errors (линтера на сервере может не быть, просто проверить что файл валидный JS)
Task 2: Убрать дублирование new order для админа
Файлы:
-
Modify:
server/src/lib/notifications/preferences.js -
Step 1: Удалить ORDER_CREATED из adminEventFieldMap
const adminEventFieldMap = {
[ORDER_MESSAGE_SENT]: 'newOrderMessage',
'review:created': 'newReview',
};
- Step 2: Запустить тесты
Run: cd server && npm test
Task 3: Добавить PAID в лейблы статусов
Файлы:
-
Modify:
server/src/lib/notifications/templates/telegram-templates.js -
Modify:
server/src/lib/notifications/templates/email-templates.js -
Step 1: Добавить PAID в telegram-templates.js
export function renderOrderStatusChangedTg({ orderId, oldStatus, newStatus }) {
const labels = {
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>`
}
- Step 2: Добавить PAID в email-templates.js
const statusLabels = {
DRAFT: "Черновик",
PENDING_PAYMENT: "Ожидает оплаты",
PAID: "Оплачен",
IN_PROGRESS: "В работе",
READY_FOR_PICKUP: "Готов к выдаче",
SHIPPED: "Отправлен",
DONE: "Выполнен",
CANCELLED: "Отменён",
};
- Step 3: Запустить тесты
Run: cd server && npm test
Task 4: Добавить deliveryType в ORDER_CREATED payload и обновить шаблоны пользователя
Файлы:
-
Modify:
server/src/routes/user-orders.js -
Modify:
server/src/lib/notifications/templates/telegram-templates.js -
Modify:
server/src/lib/notifications/templates/email-templates.js -
Step 1: Добавить deliveryType в ORDER_CREATED payload
request.server.eventBus.emit(NOTIFICATION_EVENTS.ORDER_CREATED, {
orderId: created.id,
userId,
totalCents: created.totalCents,
itemsCount: cartItems.length,
deliveryType: created.deliveryType,
});
- Step 2: Обновить renderOrderCreatedTg
export function renderOrderCreatedTg({ orderId, totalCents, itemsCount, deliveryType }) {
const total = (totalCents / 100).toLocaleString('ru-RU')
const nextAction = deliveryType === 'delivery'
? 'Оплата будет доступна после уточнения стоимости доставки.'
: 'Ожидает оплаты.'
return `📦 <b>Новый заказ</b> #${orderId.slice(0, 8)}\nТоваров: ${itemsCount} | Сумма: ${total} ₽\n${nextAction}`
}
- Step 3: Обновить renderOrderCreatedEmail
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>${nextAction}</p>
`;
return { subject: "Заказ создан", html: baseLayout("Заказ создан", body) };
}
- Step 4: Запустить тесты
Run: cd server && npm test
Task 5: Добавить deliveryType в order:created:admin payload и обновить шаблоны админа
Файлы:
-
Modify:
server/src/routes/user-orders.js -
Modify:
server/src/lib/notifications/templates/telegram-templates.js -
Modify:
server/src/lib/notifications/templates/email-templates.js -
Step 1: Добавить deliveryType в order:created:admin payload
request.server.eventBus.emit('order:created:admin', {
orderId: created.id,
userId,
userEmail: request.user.email || '',
totalCents: created.totalCents,
itemsCount: cartItems.length,
deliveryType: created.deliveryType,
});
- Step 2: Обновить renderAdminOrderCreatedTg
export function renderAdminOrderCreatedTg({ orderId, userEmail, totalCents, itemsCount, deliveryType }) {
const total = (totalCents / 100).toLocaleString('ru-RU')
const note = deliveryType === 'delivery' ? '\n\n⚠️ Скорректируйте стоимость доставки' : ''
return `🛒 <b>Новый заказ</b> #${orderId.slice(0, 8)}\nОт: ${userEmail}\nТоваров: ${itemsCount} | Сумма: ${total} ₽${note}`
}
- Step 3: Обновить renderAdminOrderCreatedEmail
export function renderAdminOrderCreatedEmail({
orderId,
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) };
}
- Step 4: Запустить тесты
Run: cd server && npm test
Task 6: Добавить новое событие order:deliveryFeeAdjusted — Shared и Prisma
Файлы:
-
Modify:
shared/constants/notification-events.js -
Modify:
shared/constants/notification-events.d.ts -
Modify:
server/prisma/schema.prisma -
Modify:
server/src/lib/notifications/preferences.js -
Modify:
server/src/routes/user/notifications.js -
Modify:
server/src/index.js -
Modify:
server/src/routes/api/admin-orders.js -
Step 1: Добавить DELIVERY_FEE_ADJUSTED в shared/constants/notification-events.js
export const NOTIFICATION_EVENTS = {
ORDER_CREATED: 'order:created',
ORDER_STATUS_CHANGED: 'order:statusChanged',
ORDER_MESSAGE_SENT: 'orderMessage:sent',
ORDER_MESSAGE_ADMIN_REPLY: 'orderMessage:adminReply',
PAYMENT_STATUS_CHANGED: 'payment:statusChanged',
AUTH_CODE_REQUESTED: 'auth:codeRequested',
DELIVERY_FEE_ADJUSTED: 'order:deliveryFeeAdjusted',
}
- Step 2: Добавить тип в shared/constants/notification-events.d.ts
export type NotificationEventType =
| 'order:created'
| 'order:statusChanged'
| 'orderMessage:sent'
| 'orderMessage:adminReply'
| 'payment:statusChanged'
| 'auth:codeRequested'
| 'order:deliveryFeeAdjusted'
И добавить в объект:
export const NOTIFICATION_EVENTS: {
ORDER_CREATED: NotificationEventType
ORDER_STATUS_CHANGED: NotificationEventType
ORDER_MESSAGE_SENT: NotificationEventType
ORDER_MESSAGE_ADMIN_REPLY: NotificationEventType
PAYMENT_STATUS_CHANGED: NotificationEventType
AUTH_CODE_REQUESTED: NotificationEventType
DELIVERY_FEE_ADJUSTED: NotificationEventType
}
- Step 3: Добавить поле deliveryFeeAdjusted в Prisma schema
model NotificationPreference {
id String @id @default(cuid())
userId String @unique
globalEnabled Boolean @default(true)
orderCreated Boolean @default(true)
orderStatusChanged Boolean @default(true)
orderMessageReceived Boolean @default(true)
paymentStatusChanged Boolean @default(true)
deliveryFeeAdjusted Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
- Step 4: Создать миграцию Prisma
Run: cd server && npx prisma migrate dev --name add_delivery_fee_adjusted --create-only
Затем применить: cd server && npx prisma migrate deploy
- Step 5: Добавить DELIVERY_FEE_ADJUSTED в userEventFieldMap
const userEventFieldMap = {
[ORDER_CREATED]: 'orderCreated',
[ORDER_STATUS_CHANGED]: 'orderStatusChanged',
[ORDER_MESSAGE_ADMIN_REPLY]: 'orderMessageReceived',
[PAYMENT_STATUS_CHANGED]: 'paymentStatusChanged',
[DELIVERY_FEE_ADJUSTED]: 'deliveryFeeAdjusted',
};
Импорт в preferences.js:
const {
ORDER_CREATED,
ORDER_STATUS_CHANGED,
ORDER_MESSAGE_SENT,
ORDER_MESSAGE_ADMIN_REPLY,
PAYMENT_STATUS_CHANGED,
AUTH_CODE_REQUESTED,
DELIVERY_FEE_ADJUSTED,
} = NOTIFICATION_EVENTS;
- Step 6: Добавить поле deliveryFeeAdjusted в API пользователя
В server/src/routes/user/notifications.js:
if ('deliveryFeeAdjusted' in body) data.deliveryFeeAdjusted = Boolean(body.deliveryFeeAdjusted)
- Step 7: Добавить эмит события при корректировке стоимости доставки
В server/src/routes/api/admin-orders.js, в конце PATCH /:id/delivery-fee:
request.server.eventBus.emit(NOTIFICATION_EVENTS.DELIVERY_FEE_ADJUSTED, {
orderId: updated.id,
userId: existing.userId,
totalCents: updated.totalCents,
});
Добавить импорт NOTIFICATION_EVENTS (уже есть в файле).
- Step 8: Зарегистрировать событие в dispatchNotification
В server/src/index.js, в секцию eventBus.on:
eventBus.on(DELIVERY_FEE_ADJUSTED, (payload) => dispatchNotification(DELIVERY_FEE_ADJUSTED, payload));
Добавить в деструктуризацию:
const {
ORDER_CREATED,
ORDER_STATUS_CHANGED,
ORDER_MESSAGE_SENT,
ORDER_MESSAGE_ADMIN_REPLY,
PAYMENT_STATUS_CHANGED,
AUTH_CODE_REQUESTED,
DELIVERY_FEE_ADJUSTED,
} = NOTIFICATION_EVENTS;
- Step 9: Запустить тесты
Run: cd server && npm test
Task 7: Добавить рендереры для deliveryFeeAdjusted
Файлы:
-
Modify:
server/src/lib/notifications/templates/telegram-templates.js -
Modify:
server/src/lib/notifications/templates/email-templates.js -
Modify:
server/src/lib/notifications/channels/telegram-channel.js -
Modify:
server/src/lib/notifications/channels/email-channel.js -
Step 1: Добавить renderDeliveryFeeAdjustedTg
В telegram-templates.js:
export function renderDeliveryFeeAdjustedTg({ orderId, totalCents }) {
const total = (totalCents / 100).toLocaleString('ru-RU')
return `💰 <b>Стоимость доставки скорректирована</b> для заказа #${orderId.slice(0, 8)}\nНовая сумма: ${total} ₽\n\nОжидает оплаты.`
}
- Step 2: Зарегистрировать в telegram-channel.js
import {
renderOrderCreatedTg,
renderOrderStatusChangedTg,
renderOrderMessageTg,
renderPaymentStatusChangedTg,
renderAdminOrderCreatedTg,
renderAdminNewReviewTg,
renderAuthCodeTg,
renderDeliveryFeeAdjustedTg,
} from '../templates/telegram-templates.js'
const templateRenderers = {
'order:created': renderOrderCreatedTg,
'order:statusChanged': renderOrderStatusChangedTg,
'orderMessage:adminReply': renderOrderMessageTg,
'payment:statusChanged': renderPaymentStatusChangedTg,
'order:created:admin': renderAdminOrderCreatedTg,
'orderMessage:sent': renderOrderMessageTg,
'review:created': renderAdminNewReviewTg,
'auth:codeRequested': renderAuthCodeTg,
'order:deliveryFeeAdjusted': renderDeliveryFeeAdjustedTg,
}
- Step 3: Добавить renderDeliveryFeeAdjustedEmail
В email-templates.js:
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),
};
}
- Step 4: Зарегистрировать в email-channel.js
import {
renderOrderCreatedEmail,
renderOrderStatusChangedEmail,
renderOrderMessageEmail,
renderPaymentStatusChangedEmail,
renderAdminOrderCreatedEmail,
renderAdminNewReviewEmail,
renderAuthCodeEmail,
renderDeliveryFeeAdjustedEmail,
} from '../templates/email-templates.js'
const templateRenderers = {
'order:created': renderOrderCreatedEmail,
'order:statusChanged': renderOrderStatusChangedEmail,
'orderMessage:adminReply': renderOrderMessageEmail,
'payment:statusChanged': renderPaymentStatusChangedEmail,
'order:created:admin': renderAdminOrderCreatedEmail,
'orderMessage:sent': renderOrderMessageEmail,
'review:created': renderAdminNewReviewEmail,
'auth:codeRequested': renderAuthCodeEmail,
'order:deliveryFeeAdjusted': renderDeliveryFeeAdjustedEmail,
}
- Step 5: Запустить тесты
Run: cd server && npm test
Task 8: Добавить переключатель deliveryFeeAdjusted в UI пользователя
Файлы:
-
Modify:
client/src/pages/me/ui/sections/NotificationsPage.tsx -
Step 1: Добавить переключатель в NotificationsPage.tsx
const eventFields = [
{ key: 'orderCreated' as const, label: 'Заказ создан' },
{ key: 'orderStatusChanged' as const, label: 'Изменение статуса заказа' },
{ key: 'orderMessageReceived' as const, label: 'Сообщение в чате заказа' },
{ key: 'paymentStatusChanged' as const, label: 'Изменение статуса оплаты' },
{ key: 'deliveryFeeAdjusted' as const, label: 'Корректировка стоимости доставки' },
]
- Step 2: Проверить линтер и формат
Run: cd client && npm run lint && npm run format:check
Task 9: Финальная проверка
- Step 1: Запустить все серверные тесты
Run: cd server && npm test
- Step 2: Запустить все клиентские проверки
Run: cd client && npm run lint && npm run format:check && npm test
- Step 3: Полная сборка клиента
Run: cd client && npm run build