test: add notification preferences tests

This commit is contained in:
Kirill
2026-05-18 11:44:49 +05:00
parent 1d36f6a31b
commit 912724082e
7 changed files with 3746 additions and 2 deletions
@@ -0,0 +1,221 @@
# 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 в настройки админки.