Files
shop-server/docs/superpowers/specs/2026-05-18-notification-system-design.md
T
2026-05-18 11:45:51 +05:00

222 lines
9.3 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.
# Design: Notification System
**Date:** 2026-05-18
**Status:** Draft — awaiting review
## 1. Overview
Система оповещений для craftshop: email для пользователей, email + Telegram для админа.
Архитектура — event-driven с in-memory очередью и retry. Задел на подключение новых каналов (WhatsApp, Viber, push).
## 2. Architecture
### 2.1 Components
```
server/src/
lib/
email.js ← расширяется: sendNotificationEmail()
notifications/
event-bus.js ← EventEmitter, центральный хаб
queue.js ← in-memory очередь + воркер
channels/
email-channel.js ← отправка email через nodemailer
telegram-channel.js ← отправка через Telegram Bot API
templates/
email-templates.js ← HTML-шаблоны писем
telegram-templates.js ← форматированные сообщения TG
preferences.js ← CRUD настроек оповещений
routes/
api/
admin/
notifications.js ← GET/PUT настройки админа
user/
notifications.js ← GET/PUT настройки пользователя
```
### 2.2 Database (Prisma)
#### NotificationPreference (настройки пользователя)
| Field | Type | Description |
|---|---|---|
| id | String (cuid) | Primary key |
| userId | String (cuid) | FK → User (unique) |
| globalEnabled | Boolean | Главный переключатель |
| orderCreated | Boolean | Заказ создан |
| orderStatusChanged | Boolean | Статус заказа изменён |
| orderMessageReceived | Boolean | Новое сообщение в чате заказа |
| paymentStatusChanged | Boolean | Статус оплаты изменён |
| createdAt | DateTime | |
| updatedAt | DateTime | |
#### AdminNotificationSettings (настройки админа)
| Field | Type | Description |
|---|---|---|
| id | String (cuid) | Primary key |
| emailEnabled | Boolean | Email вкл/выкл |
| telegramEnabled | Boolean | Telegram вкл/выкл |
| telegramChatId | String? | ID чата админа с ботом |
| newOrder | Boolean | Новый заказ |
| newOrderMessage | Boolean | Новое сообщение в заказе |
| newReview | Boolean | Новый отзыв |
| authCodeDuplicate | Boolean | Дублировать код входа в TG |
| createdAt | DateTime | |
| updatedAt | DateTime | |
#### NotificationLog (лог отправки)
| Field | Type | Description |
|---|---|---|
| id | String (cuid) | Primary key |
| userId | String? | FK → User (null для админа) |
| eventType | String | Тип события |
| channel | String | 'email' | 'telegram' |
| status | String | 'pending' | 'sent' | 'failed' |
| error | String? | Текст ошибки |
| payload | Json | Данные события |
| attempts | Int | Количество попыток |
| createdAt | DateTime | |
| updatedAt | DateTime | |
### 2.3 Queue
- In-memory массив задач
- Воркер: `setInterval` каждые 2 секунды, максимум 5 параллельных отправок
- Retry: 3 попытки с задержкой 5с, 30с, 120с
- При рестарте сервера: все `pending` записи помечаются как `failed`
## 3. Events
| Event | Triggered in | Payload | Recipients |
|---|---|---|---|
| `order:created` | user-orders.js | orderId, userId, orderData | User (orderCreated), Admin (newOrder) |
| `order:statusChanged` | admin-orders.js | orderId, userId, oldStatus, newStatus | User (orderStatusChanged) |
| `orderMessage:sent` | user-messages.js | orderId, authorType, messageId | Admin (newOrderMessage) |
| `orderMessage:adminReply` | admin-orders.js | orderId, userId, messageId | User (orderMessageReceived) |
| `payment:statusChanged` | user-payments.js | orderId, userId, paymentStatus | User (paymentStatusChanged) |
| `auth:codeRequested` | auth.js | email, code, isAdmin | User (email), Admin (authCodeDuplicate if isAdmin) |
## 4. Data Flow
```
Роут → eventBus.emit(eventType, payload)
→ preferences.resolveRecipients(eventType, payload)
→ для каждого получателя:
→ NotificationLog.create({ status: 'pending' })
→ queue.enqueue({ recipient, channel, eventType, payload })
→ ответ API (без ожидания отправки)
Воркер (каждые 2с, до 5 параллельно):
→ queue.dequeue()
→ channel.send(job)
→ NotificationLog.update({ status: 'sent' | 'failed', attempts++ })
→ если failed и attempts < 3 → re-enqueue с delay
```
## 5. Channel Interface
Каждый канал реализует:
```js
{
name: 'email' | 'telegram',
send(job: { recipient, payload, template }): Promise<{ success: boolean, error?: string }>
}
```
### 5.1 Email Channel
- Использует существующий nodemailer transporter из `email.js`
- `sendNotificationEmail({ to, subject, html })`
- HTML-шаблоны в `email-templates.js`
### 5.2 Telegram Channel
- Telegram Bot API: `POST https://api.telegram.org/bot<TOKEN>/sendMessage`
- `node-telegram-bot-api` или прямой fetch
- Форматирование: HTML parse mode
- Шаблоны в `telegram-templates.js`
## 6. Client-Side
### 6.1 User Notification Settings Page
- Route: `/me/notifications`
- MUI переключатели:
- Главный toggle "Получать оповещения" (globalEnabled)
- При включённом: 4 toggles для каждого типа события
- При выключенном: все остальные toggles disabled
- Сохранение через `apiClient` + `@tanstack/react-query` mutation + invalidate
### 6.2 Admin Notification Settings
- Встраивается в существующую админку
- Toggle email, toggle telegram
- Если telegram включён — поле telegramChatId (заполняется автоматически при /start бота)
- Toggle для каждого типа события + toggle дублирования кода входа
## 7. Error Handling
| Scenario | Behavior |
|---|---|
| SMTP/Telegram недоступен | Retry 3 раза (5с → 30с → 120с), затем failed |
| Невалидный email / chatId | Сразу failed, без retry |
| Ошибка рендера шаблона | failed, лог в NotificationLog.error |
| Сервер рестарт | pending → failed при старте воркера |
## 8. Security
- `TELEGRAM_BOT_TOKEN` — только в `.dev_env`, не коммитится
- Telegram chatId запоминается при `/start` от админа
- Настройки пользователя — только через `fastify.authenticate`
- Настройки админа — только через `fastify.verifyAdmin`
## 9. Extensibility
### Adding a new channel (WhatsApp, Viber, push)
1. Новый файл в `channels/` с интерфейсом `{ name, send(job) }`
2. Регистрация в `queue.js`
3. Никакие другие файлы не меняются
### Adding a new event type
1. Добавить в константы типов событий
2. Добавить поле в `NotificationPreference` / `AdminNotificationSettings`
3. Эмитить через `eventBus.emit()` в нужном роуте
### Adding new recipients (broadcasts)
- `NotificationLog.userId` nullable — поддерживает системные события
- Очередь поддерживает batch-задачи
## 10. Environment Variables
| Variable | Description | Required |
|---|---|---|
| SMTP_HOST | SMTP сервер | Да (для email) |
| SMTP_PORT | SMTP порт | Да |
| SMTP_SECURE | SSL/TLS | Да |
| SMTP_USER | SMTP логин | Да |
| SMTP_PASS | SMTP пароль | Да |
| MAIL_FROM | From address | Да |
| TELEGRAM_BOT_TOKEN | Токен Telegram бота | Для Telegram канала |
## 11. Implementation Notes
- `eventBus` декорируется на fastify instance (как `slugify`, `parseMaterialsInput`)
- `bootstrap-admin.js` создаёт `AdminNotificationSettings` при создании админа
- При создании пользователя — создаётся `NotificationPreference` с defaults (всё включено)
- Существующий `sendLoginCodeEmail` остаётся, добавляется `sendNotificationEmail`
## 12. Telegram Bot — Setup Flow
1. Админ запускает бота командой `/start`
2. Бот проверяет, что sender — админ (сверка email через webhook или ручной ввод `TELEGRAM_ADMIN_CHAT_ID` в `.dev_env`)
3. Если совпадает — сохраняет `chatId` в `AdminNotificationSettings.telegramChatId`
4. Если `telegramChatId` уже установлен — бот просто подтверждает подписку
**Fallback:** если webhook не настроен, админ вручную вписывает свой chatId в настройки админки.