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