222 lines
9.3 KiB
Markdown
222 lines
9.3 KiB
Markdown
# 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 в настройки админки.
|