# Улучшение системы оповещений — План реализации > **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):** ```bash 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`: ```js 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** ```js 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** ```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} → ${labels[newStatus] || newStatus}` } ``` - [ ] **Step 2: Добавить PAID в email-templates.js** ```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** ```js request.server.eventBus.emit(NOTIFICATION_EVENTS.ORDER_CREATED, { orderId: created.id, userId, totalCents: created.totalCents, itemsCount: cartItems.length, deliveryType: created.deliveryType, }); ``` - [ ] **Step 2: Обновить renderOrderCreatedTg** ```js export function renderOrderCreatedTg({ orderId, totalCents, itemsCount, deliveryType }) { const total = (totalCents / 100).toLocaleString('ru-RU') const nextAction = deliveryType === 'delivery' ? 'Оплата будет доступна после уточнения стоимости доставки.' : 'Ожидает оплаты.' return `📦 Новый заказ #${orderId.slice(0, 8)}\nТоваров: ${itemsCount} | Сумма: ${total} ₽\n${nextAction}` } ``` - [ ] **Step 3: Обновить renderOrderCreatedEmail** ```js export function renderOrderCreatedEmail({ orderId, totalCents, itemsCount, deliveryType }) { const total = (totalCents / 100).toLocaleString("ru-RU"); const nextAction = deliveryType === "delivery" ? "Оплата будет доступна после уточнения стоимости доставки." : "Ожидает оплаты."; const body = `

Ваш заказ #${orderId.slice(0, 8)} успешно создан.

Товаров: ${itemsCount} | Сумма: ${total} ₽

${nextAction}

`; 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** ```js 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** ```js export function renderAdminOrderCreatedTg({ orderId, userEmail, totalCents, itemsCount, deliveryType }) { const total = (totalCents / 100).toLocaleString('ru-RU') const note = deliveryType === 'delivery' ? '\n\n⚠️ Скорректируйте стоимость доставки' : '' return `🛒 Новый заказ #${orderId.slice(0, 8)}\nОт: ${userEmail}\nТоваров: ${itemsCount} | Сумма: ${total} ₽${note}` } ``` - [ ] **Step 3: Обновить renderAdminOrderCreatedEmail** ```js export function renderAdminOrderCreatedEmail({ orderId, userEmail, totalCents, itemsCount, deliveryType, }) { const total = (totalCents / 100).toLocaleString("ru-RU"); const note = deliveryType === "delivery" ? '

⚠️ Скорректируйте стоимость доставки в админ-панели.

' : ""; const body = `

Новый заказ #${orderId.slice(0, 8)} от ${userEmail}.

Товаров: ${itemsCount} | Сумма: ${total} ₽

${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** ```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** ```ts export type NotificationEventType = | 'order:created' | 'order:statusChanged' | 'orderMessage:sent' | 'orderMessage:adminReply' | 'payment:statusChanged' | 'auth:codeRequested' | 'order:deliveryFeeAdjusted' ``` И добавить в объект: ```ts 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** ```prisma 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** ```js const userEventFieldMap = { [ORDER_CREATED]: 'orderCreated', [ORDER_STATUS_CHANGED]: 'orderStatusChanged', [ORDER_MESSAGE_ADMIN_REPLY]: 'orderMessageReceived', [PAYMENT_STATUS_CHANGED]: 'paymentStatusChanged', [DELIVERY_FEE_ADJUSTED]: 'deliveryFeeAdjusted', }; ``` Импорт в `preferences.js`: ```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`: ```js if ('deliveryFeeAdjusted' in body) data.deliveryFeeAdjusted = Boolean(body.deliveryFeeAdjusted) ``` - [ ] **Step 7: Добавить эмит события при корректировке стоимости доставки** В `server/src/routes/api/admin-orders.js`, в конце `PATCH /:id/delivery-fee`: ```js 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: ```js eventBus.on(DELIVERY_FEE_ADJUSTED, (payload) => dispatchNotification(DELIVERY_FEE_ADJUSTED, payload)); ``` Добавить в деструктуризацию: ```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 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`: ```js export function renderDeliveryFeeAdjustedTg({ orderId, totalCents }) { const total = (totalCents / 100).toLocaleString('ru-RU') return `💰 Стоимость доставки скорректирована для заказа #${orderId.slice(0, 8)}\nНовая сумма: ${total} ₽\n\nОжидает оплаты.` } ``` - [ ] **Step 2: Зарегистрировать в telegram-channel.js** ```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`: ```js export function renderDeliveryFeeAdjustedEmail({ orderId, totalCents }) { const total = (totalCents / 100).toLocaleString("ru-RU"); const body = `

Стоимость доставки заказа #${orderId.slice(0, 8)} скорректирована.

Новая сумма: ${total} ₽

Ожидает оплаты. Проверьте статус заказа в личном кабинете.

`; return { subject: "Стоимость доставки скорректирована", html: baseLayout("Стоимость доставки скорректирована", body), }; } ``` - [ ] **Step 4: Зарегистрировать в email-channel.js** ```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** ```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`