Files
shop-server/docs/superpowers/plans/2026-05-18-notifications-improvements.md
T

518 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Улучшение системы оповещений — План реализации
> **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} → <b>${labels[newStatus] || newStatus}</b>`
}
```
- [ ] **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 `📦 <b>Новый заказ</b> #${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 = `
<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**
```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 `🛒 <b>Новый заказ</b> #${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"
? '<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**
```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 `💰 <b>Стоимость доставки скорректирована</b> для заказа #${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 = `
<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**
```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`