# MVP Spec 10 — Notifications System

## Паспорт документа

- Статус документа: working spec
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: перед реализацией, при изменении domain rules или при смене source-of-truth по контракту
- Область применения: детальные feature-спеки для product backlog и реализации MVP/v1 направлений
- Связанные документы:
  - [Product roadmap и delivery checklist](../product-roadmap.md)
  - [Roadmap](../../project/roadmap.md)
  - [Карта API-маршрутов](../../architecture/api-routes.md)

> Version: MVP · Priority: P0 · Phase: A (Supply)
> Status: Draft v1
> Sync note, 28 Mar 2026:
> - live API prefix is `/api/v1/`, not `/api/`;
> - частично реализована только Telegram binding часть: `POST /api/v1/seller/telegram/verify` и `DELETE /api/v1/seller/telegram`;
> - notification settings endpoints, delivery pipeline, webhook и fallback email ещё не являются текущим production contract.

---

## 1. Контекст и цель

Уведомления — критическая часть петли ценности для продавцов: продавец должен узнавать о новой заявке мгновенно, пока покупатель ещё «горячий». Медленная реакция на лид = потерянный клиент.

**Цель модуля:** доставлять уведомления продавцу по предпочтительному каналу (Telegram как первичный, Email как резервный), давать продавцу контроль над тем, что он получает, и обеспечивать надёжность доставки даже при сбоях одного канала.

**Telegram — основной канал.** Email — только резервный/дополнительный.

**Что не входит в этот модуль:**
- Привязка Telegram-аккаунта (верификационный поток) → Spec 01 UC-06 (реализация там, здесь только reference)
- Создание лидов → Spec 06 (Buyer Flow) + Spec 09
- Управление профилем продавца → Spec 01
- Push-уведомления в браузере или мобильном приложении → вне скоупа MVP
- Уведомления для покупателей → TBD (не определено в MVP)

---

## 2. Роли пользователей

| Роль | Действие в этом модуле |
|------|----------------------|
| **Seller (Owner)** | Подключает Telegram, настраивает предпочтения уведомлений, отключает Telegram |
| **Seller Staff** | Получает уведомления только если у Staff есть собственный Telegram (TBD v1.0); в MVP уведомления идут только Owner |
| **Система (Backend)** | Отправляет уведомления в Telegram и Email при триггерных событиях |
| **Admin** | Может видеть статус Telegram-подключения продавца; не управляет уведомлениями |
| **Telegram Bot (@qadam_notify_bot)** | Принимает /start, генерирует код, доставляет уведомления |

---

## 3. Use Cases

---

### UC-01: Продавец подключает Telegram

**Актор:** Seller (Owner)
**Предусловие:** Продавец авторизован. Telegram НЕ подключён (Seller.telegram_chat_id = null)
**Триггер:** Продавец хочет получать уведомления о лидах в Telegram

> **Реализация верификационного флоу описана в Spec 01 UC-06.**
> Настоящий UC — это точка входа из раздела уведомлений и описание UX-связки.

**Полный поток:**

```
[Точка входа]
→ Продавец авторизован, переходит на /seller/settings/notifications
→ Раздел "Telegram-уведомления" показывает статус "Не подключён"
→ Кнопка [Подключить Telegram]

───────────────────────────────────────────────────────
ШАГ 1 — Запуск верификации
───────────────────────────────────────────────────────
→ Продавец нажимает [Подключить Telegram]
→ Открывается модальное окно (или inline-блок) с инструкцией:

    ┌───────────────────────────────────────────────────┐
    │  Подключение Telegram                             │
    │───────────────────────────────────────────────────│
    │  1. Откройте Telegram-бот @qadam_notify_bot        │
    │     [Открыть бота →] (ссылка t.me/qadam_notify_bot)│
    │  2. Нажмите /start в боте                         │
    │  3. Бот отправит вам 6-значный код                │
    │  4. Введите код здесь:                            │
    │     [  _ _ _ _ _ _  ]  (6-цифровое поле)         │
    │     Код действителен 10 минут                     │
    │                                                   │
    │  [Отмена]                        [Подтвердить]   │
    └───────────────────────────────────────────────────┘

───────────────────────────────────────────────────────
ШАГ 2 — Действия в Telegram-боте
───────────────────────────────────────────────────────
→ Продавец открывает бот @qadam_notify_bot в Telegram
→ Нажимает /start
→ Бот регистрирует chat_id продавца и генерирует 6-значный код
→ Бот отправляет сообщение:
    "Ваш код верификации для Qadam: 847291
     Код действителен 10 минут.
     Введите его на сайте в разделе Настройки → Уведомления."
→ Возвращается на сайт

───────────────────────────────────────────────────────
ШАГ 3 — Ввод кода
───────────────────────────────────────────────────────
→ Продавец вводит 6 цифр в поле
→ Auto-submit при вводе 6-го символа
→ POST /api/v1/seller/telegram/verify { code: '847291' }
→ Система находит TelegramVerificationCode по коду
→ Проверяет: код не истёк, не использован, chat_id не привязан к другому seller_id
→ Сохраняет: Seller.telegram_chat_id = chat_id, Seller.telegram_verified_at = now()
→ Помечает код как used = true

───────────────────────────────────────────────────────
ШАГ 4 — Подтверждение
───────────────────────────────────────────────────────
→ Модальное окно закрывается
→ Раздел "Telegram-уведомления" обновляется: статус "Подключён ✓"
→ Toast (зелёный): "Telegram подключён! Вы будете получать уведомления о заявках."
→ Бот немедленно отправляет продавцу в Telegram:
    "✅ Telegram успешно подключён к вашему аккаунту Qadam!
     Теперь вы будете получать уведомления о новых заявках здесь."
```

---

### UC-01 — Альтернативные потоки и обработка ошибок

**1a. Неверный код (CODE_INVALID):**
```
Триггер: Продавец ввёл код, которого нет в БД

UI-реакция:
→ Поле кода: красная обводка + ⚠ справа
→ Под полем: "Неверный код. Убедитесь, что вводите код именно из бота @qadam_notify_bot."
→ Поле очищается, фокус возвращается в поле
→ Кнопка [Подтвердить] разблокирована для повтора
```

**1b. Код истёк (CODE_EXPIRED):**
```
Триггер: Прошло > 10 минут с момента генерации кода в боте

UI-реакция:
→ Поле кода: красная обводка + ⚠
→ Под полем: "Код устарел. Вернитесь в бота @qadam_notify_bot и нажмите /start для получения нового кода."
→ Ссылка: "Открыть бота" (t.me/qadam_notify_bot)
→ Поле очищается
```

**1c. Этот Telegram уже привязан к другому аккаунту (TELEGRAM_ALREADY_BOUND):**
```
Триггер: chat_id уже существует в другой записи Seller

UI-реакция:
→ Toast (красный): "Этот Telegram-аккаунт уже используется другой организацией. Используйте другой аккаунт Telegram."
→ Поле очищается
→ Инструкция остаётся видимой
```

**1d. Сеть недоступна или сервер вернул 5xx при верификации кода:**
```
UI-реакция:
→ Toast (красный): "Не удалось выполнить запрос. Проверьте интернет-соединение и попробуйте снова."
→ Поле кода не очищается — продавец может повторить без повторного ввода
→ Кнопка [Подтвердить] разблокирована
```

**1e. Продавец закрыл модальное окно до завершения верификации:**
```
→ Модальное окно закрывается, код аннулируется (но в БД остаётся до истечения TTL)
→ При следующем открытии — продавец начинает заново (нажимает /start в боте)
```

**1f. Продавец уже верифицирован (Telegram подключён):**
```
→ Кнопка [Подключить Telegram] заменяется на кнопку [Отключить Telegram] и статус "Подключён ✓"
→ При попытке POST /api/v1/seller/telegram/verify когда уже подключён: 400 ALREADY_VERIFIED
```

---

### UC-02: Новый лид создан → Telegram-уведомление отправляется продавцу

**Актор:** Система (Backend), триггер — событие создания лида
**Предусловие:** Продавец имеет Seller.telegram_chat_id != null AND NotificationSettings.notify_new_lead_telegram = true
**Триггер:** Покупатель оставил заявку на курс продавца → Lead создан со статусом `new`

**Полный поток:**

```
[Точка входа]
→ Buyer Flow (Spec 06): покупатель заполнил форму заявки
→ Backend создаёт Lead { lead_status: 'new' }
→ Backend вызывает NotificationService.onNewLead(lead)

───────────────────────────────────────────────────────
ШАГ 1 — Проверка настроек уведомлений
───────────────────────────────────────────────────────
→ Запрос NotificationSettings для seller_id
→ Если notify_new_lead_telegram = true И telegram_chat_id != null:
    → Переходим к отправке Telegram (ШАГ 2)
→ Если notify_new_lead_telegram = false:
    → Пропускаем Telegram
    → Если notify_new_lead_email = true: переходим к Email (UC-06)
→ Если оба false: уведомление не отправляется (продавец отписался от всего)

───────────────────────────────────────────────────────
ШАГ 2 — Формирование и отправка Telegram-уведомления
───────────────────────────────────────────────────────
→ Backend формирует сообщение:

    🎓 Новая заявка на курс!
    Курс: {item_name}
    Имя: {lead_name}
    Телефон: {lead_phone}
    Тип: Пробное занятие / Запись на курс
    Комментарий: {lead_comment или "-"}
    [Открыть в кабинете →] (deep link: qadam.uz/seller/leads?lead={lead_id})

→ Telegram Bot API: POST https://api.telegram.org/bot{TOKEN}/sendMessage
    { chat_id: seller.telegram_chat_id, text: ..., parse_mode: 'HTML', reply_markup: { inline_keyboard: [["Открыть в кабинете", url]] } }

───────────────────────────────────────────────────────
ШАГ 3 — Результат отправки
───────────────────────────────────────────────────────
→ Telegram API вернул 200 OK:
    → Уведомление считается доставленным
    → Запись в NotificationLog { channel: 'telegram', status: 'sent' }

→ Telegram API вернул ошибку (любую):
    → Переходим к UC-03 (Email fallback)
```

---

### UC-02 — Альтернативные потоки

**2a. Telegram API временно недоступен (timeout, 5xx):**
```
→ Backend делает 2 повторных попытки с задержкой (retry: 5s, 30s)
→ Если все 3 попытки неудачны: переходим к Email fallback (UC-03)
→ Запись в NotificationLog { channel: 'telegram', status: 'failed', error: '...' }
```

**2b. Покупатель не указал комментарий (lead_comment = null):**
```
→ В сообщении: "Комментарий: -"
→ Остальные поля без изменений
```

**2c. Тип лида = 'buy' (не trial):**
```
→ В сообщении: "Тип: Запись на курс"
→ Тип лида = 'trial': "Тип: Пробное занятие"
```

**2d. lead_email указан покупателем:**
```
→ В Telegram-сообщении email не отображается (лаконичность)
→ Email доступен продавцу в деталях лида (UC-05 Spec 09)
```

---

### UC-03: Telegram недоступен → Email fallback

**Актор:** Система (Backend)
**Предусловие:** Telegram-уведомление не удалось отправить (все retry провалились) И NotificationSettings.notify_new_lead_email = true
**Триггер:** Сбой Telegram-доставки

**Полный поток:**

```
[Точка входа]
→ UC-02 завершился с ошибкой после всех retry

───────────────────────────────────────────────────────
ШАГ 1 — Проверка Email-настроек
───────────────────────────────────────────────────────
→ Проверяем NotificationSettings.notify_new_lead_email
→ Если true: отправляем Email (ШАГ 2)
→ Если false: уведомление не отправляется; пишем в лог

───────────────────────────────────────────────────────
ШАГ 2 — Отправка Email
───────────────────────────────────────────────────────
→ Email отправляется на адрес из профиля продавца (SchoolProfile.email / contributor.email)
→ Тема: "Новая заявка на курс «{item_name}» — Qadam"
→ Тело: HTML-письмо (UC-06)

───────────────────────────────────────────────────────
ШАГ 3 — Результат
───────────────────────────────────────────────────────
→ Email успешно поставлен в очередь SMTP:
    → NotificationLog { channel: 'email', status: 'sent', fallback_reason: 'telegram_failed' }

→ Email тоже провалился:
    → NotificationLog { channel: 'email', status: 'failed' }
    → Нет дальнейших попыток в MVP
    → Алерт в мониторинг (Sentry/alerting — TBD)
```

---

### UC-03 — Альтернативные потоки

**3a. Продавец отключил Email-уведомления (notify_new_lead_email = false):**
```
→ Даже при сбое Telegram — email не отправляется
→ Продавец не получает уведомление
→ Запись: NotificationLog { channel: 'none', status: 'skipped', reason: 'all_channels_disabled' }
→ Лид при этом создан в базе нормально; продавец увидит его при следующем визите в кабинет
```

**3b. Email-адрес продавца не указан или пустой:**
```
→ Пропускаем email-отправку
→ NotificationLog { channel: 'email', status: 'skipped', reason: 'no_email_address' }
```

---

### UC-04: Продавец настраивает предпочтения уведомлений

**Актор:** Seller (Owner)
**Предусловие:** Продавец авторизован
**Триггер:** Продавец хочет изменить какие уведомления получать и по какому каналу

**Полный поток:**

```
[Точка входа]
→ Продавец авторизован
→ Переходит: /seller/settings → вкладка "Уведомления"
→ Открывается /seller/settings/notifications

───────────────────────────────────────────────────────
ШАГ 1 — Страница настроек
───────────────────────────────────────────────────────
→ Страница загружает: GET /api/v1/seller/notification-settings
→ Отображается форма:

    ┌───────────────────────────────────────────────────────┐
    │  📱 Telegram                                          │
    │  Статус: Подключён ✓ (@username)   [Отключить]       │
    │  ─────────────────────────────────────────────────── │
    │  □ Уведомлять о новых заявках          [переключатель]│
    │  □ Уведомлять при смене статуса (TBD)  [переключатель]│
    │                                                       │
    │  ✉ Email                                              │
    │  Резервный канал: school@example.com                  │
    │  ─────────────────────────────────────────────────── │
    │  □ Уведомлять о новых заявках          [переключатель]│
    │                                                       │
    │  [Сохранить настройки]                                │
    └───────────────────────────────────────────────────────┘

───────────────────────────────────────────────────────
ШАГ 2 — Изменение настройки
───────────────────────────────────────────────────────
→ Продавец переключает тоггл "Уведомлять о новых заявках (Telegram)"
→ Изменение сохраняется немедленно (auto-save) или через кнопку [Сохранить]
→ PATCH /api/v1/seller/notification-settings { notify_new_lead_telegram: false }
→ Toast: "Настройки уведомлений сохранены."

───────────────────────────────────────────────────────
ШАГ 3 — Telegram не подключён
───────────────────────────────────────────────────────
→ Если Seller.telegram_chat_id = null:
    - Telegram-секция показывает статус "Не подключён"
    - Тогглы Telegram-уведомлений задизейблены (серые)
    - Баннер (синий): "Подключите Telegram чтобы получать уведомления в мессенджере. [Подключить →]"
    - Клик на "Подключить →" → запускает UC-01

───────────────────────────────────────────────────────
ШАГ 4 — Предупреждение об отключении всех каналов
───────────────────────────────────────────────────────
→ Если продавец отключает последний активный канал:
    - После переключения: баннер (жёлтый): "Вы отключили все уведомления. Вы будете видеть заявки только при входе в кабинет."
    - Кнопка [Включить обратно] в баннере
```

---

### UC-04 — Альтернативные потоки

**4a. Сеть недоступна при сохранении настроек:**
```
UI-реакция:
→ Тоггл возвращается в предыдущее состояние (оптимистичный UI откатывается)
→ Toast (красный): "Не удалось сохранить настройки. Проверьте соединение."
```

**4b. Сервер вернул 5xx:**
```
UI-реакция:
→ Тоггл возвращается в предыдущее состояние
→ Toast (красный): "Ошибка на сервере. Попробуйте через несколько минут."
```

---

### UC-05: Продавец отключает Telegram

**Актор:** Seller (Owner)
**Предусловие:** Seller.telegram_chat_id != null (Telegram подключён)
**Триггер:** Продавец хочет отвязать Telegram-аккаунт

**Полный поток:**

```
[Точка входа]
→ Продавец на /seller/settings/notifications
→ В секции Telegram: статус "Подключён ✓" + кнопка [Отключить]

───────────────────────────────────────────────────────
ШАГ 1 — Подтверждение отключения
───────────────────────────────────────────────────────
→ Продавец нажимает [Отключить]
→ Появляется диалог подтверждения:
    "Отключить уведомления в Telegram?
     Вы перестанете получать уведомления о новых заявках через Telegram."
    [Отмена]   [Отключить]

───────────────────────────────────────────────────────
ШАГ 2 — Отключение
───────────────────────────────────────────────────────
→ Продавец нажимает [Отключить] в диалоге
→ DELETE /api/v1/seller/telegram
→ Сервер: Seller.telegram_chat_id = null, Seller.telegram_verified_at = null
→ Тогглы Telegram-уведомлений задизейблены
→ Статус меняется на "Не подключён"
→ Toast: "Telegram отключён. Включите email-уведомления чтобы не пропустить заявки."
→ Если Email-уведомления отключены: баннер (жёлтый) с предупреждением (UC-04, ШАГ 4)
```

---

### UC-05 — Альтернативные потоки

**5a. Сеть недоступна при отключении:**
```
UI-реакция:
→ Toast (красный): "Не удалось отключить Telegram. Проверьте соединение и попробуйте снова."
→ Статус остаётся "Подключён ✓"
```

**5b. Продавец нажал [Отмена] в диалоге:**
```
→ Диалог закрывается
→ Telegram остаётся подключённым
→ Никаких изменений
```

---

### UC-06: Email-уведомление о новой заявке

**Актор:** Система (Backend)
**Предусловие:** NotificationSettings.notify_new_lead_email = true; email-адрес продавца заполнен
**Триггер:** Новый лид создан (и Telegram недоступен или отключён)

**Полный поток:**

```
[Точка входа]
→ UC-02 (Telegram) завершён (успешно или с ошибкой)
→ Если нужен Email: Backend вызывает EmailService.sendNewLeadNotification(lead, seller)

───────────────────────────────────────────────────────
ШАГ 1 — Формирование письма
───────────────────────────────────────────────────────
→ Backend формирует HTML-письмо:

    От кого: noreply@qadam.uz (Qadam Уведомления)
    Кому: {seller_email из профиля}
    Тема: 🎓 Новая заявка на курс «{item_name}»

    HTML тело письма (примерный контент):
    ┌──────────────────────────────────────────────────────┐
    │  [Логотип Qadam]                                     │
    │                                                      │
    │  Новая заявка на ваш курс                            │
    │  ──────────────────────────────────────────────────  │
    │  Курс:       {item_name}                             │
    │  Имя:        {lead_name}                             │
    │  Телефон:    {lead_phone}                            │
    │  Email:      {lead_email или "не указан"}            │
    │  Тип:        Пробное занятие / Запись на курс        │
    │  Комментарий:{lead_comment или "—"}                  │
    │  Дата:       25 марта 2026, 14:35                    │
    │                                                      │
    │  [Кнопка: Открыть заявку в кабинете]                 │
    │  qadam.uz/seller/leads?lead={lead_id}                │
    │                                                      │
    │  ──────────────────────────────────────────────────  │
    │  Управление уведомлениями: qadam.uz/seller/settings/ │
    │  notifications                                       │
    └──────────────────────────────────────────────────────┘

───────────────────────────────────────────────────────
ШАГ 2 — Отправка через SMTP
───────────────────────────────────────────────────────
→ EmailService отправляет через SMTP-провайдер (TBD — Postmark/SendGrid/AWS SES)
→ Письмо попадает в очередь SMTP

───────────────────────────────────────────────────────
ШАГ 3 — Получение продавцом
───────────────────────────────────────────────────────
→ Продавец видит письмо в почтовом клиенте
→ Нажимает кнопку → открывается /seller/leads с развёрнутой деталью нужного лида
```

---

### UC-06 — Альтернативные потоки

**6a. SMTP-провайдер временно недоступен:**
```
→ EmailService ставит письмо в retry-очередь (max 3 попытки: через 1м, 5м, 30м)
→ Если все попытки провалились: NotificationLog { channel: 'email', status: 'failed' }
→ Алерт в мониторинг
```

**6b. Email продавца не существует (bounce):**
```
→ SMTP-провайдер возвращает bounce
→ NotificationLog { channel: 'email', status: 'bounced' }
→ TBD: автоматически ли отключать email-уведомления после bounce? (не определено для MVP)
```

---

## 4. Бизнес-правила и валидации

### Таблица бизнес-правил

| Правило | Описание | Обработка нарушения |
|---------|---------|-------------------|
| Telegram — первичный канал | Если подключён и включён: всегда пробуем Telegram первым | — |
| Email — только fallback или дополнение | Email отправляется если: Telegram не подключён / отключён / упал | Email не дублирует Telegram при успешной доставке |
| Только Owner получает уведомления (MVP) | Staff не получает уведомления — только на аккаунт Owner | TBD в v1.0 |
| Уникальность Telegram chat_id | Один chat_id — один seller | 400 TELEGRAM_ALREADY_BOUND |
| TTL кода верификации | 10 минут с момента /start в боте | 400 CODE_EXPIRED |
| Код одноразовый | После использования: used = true | 400 CODE_INVALID |
| Retry для Telegram | 3 попытки (immediate, +5s, +30s) | После 3-й неудачи → Email fallback |
| Retry для Email | 3 попытки в очереди (1м, 5м, 30м) | После провала: NotificationLog failed + алерт |
| Уведомление только при notify=true | Если продавец отключил тип уведомлений — не отправляем | — |
| По умолчанию уведомления включены | При регистрации: notify_new_lead_telegram = true, notify_new_lead_email = true | Настраивается в UC-04 |
| Без дублирования | Один лид = одно уведомление (Telegram ИЛИ Email, не оба если Telegram успешен) | Атомарность через NotificationLog |

### Таблица валидаций API

| Поле | Правило | Ошибка |
|------|---------|--------|
| `code` (Telegram verify) | Строго 6 цифр | 422: "Код должен содержать 6 цифр" |
| `notify_new_lead_telegram` | Boolean | 422 |
| `notify_new_lead_email` | Boolean | 422 |
| `notify_status_change_telegram` | Boolean | 422 |
| Telegram chat_id | Не занят другим seller_id | 400 TELEGRAM_ALREADY_BOUND |
| Telegram код | Существует, не истёк (TTL 10 мин), not used | 400 CODE_INVALID / CODE_EXPIRED |

---

## 5. Модель данных

> Seller (telegram_chat_id, telegram_verified_at) определена в Spec 01. Здесь — только новые сущности.

### NotificationSettings

| Атрибут | Тип | Описание |
|---------|-----|---------|
| settings_id | UUID | PK |
| seller_id | UUID FK | → Seller (Spec 01), unique |
| notify_new_lead_telegram | boolean | Уведомлять о новом лиде в Telegram. Default: true |
| notify_new_lead_email | boolean | Уведомлять о новом лиде по Email. Default: true |
| notify_status_change_telegram | boolean | Уведомлять при смене статуса в Telegram. TBD MVP, default: false |
| updated_at | DateTime | |

### NotificationLog (аудит-лог всех попыток отправки)

| Атрибут | Тип | Описание |
|---------|-----|---------|
| log_id | UUID | PK |
| seller_id | UUID FK | → Seller |
| lead_id | UUID FK? | → Lead (Spec 09), если уведомление о лиде |
| event_type | NotificationEventType | new_lead / status_changed |
| channel | NotificationChannel | telegram / email |
| status | NotificationStatus | sent / failed / skipped / bounced |
| fallback_reason | string? | Причина fallback (например: 'telegram_failed') |
| error_message | string? | Текст ошибки от API/SMTP |
| attempt_number | int | Номер попытки (1, 2, 3) |
| sent_at | DateTime | Время попытки |

---

## 6. Технические контракты

### 6.1 Prisma Schema

```prisma
enum NotificationEventType {
  new_lead
  status_changed
}

enum NotificationChannel {
  telegram
  email
}

enum NotificationStatus {
  sent
  failed
  skipped
  bounced
}

model NotificationSettings {
  settings_id                    String   @id @default(uuid())
  seller_id                      String   @unique
  notify_new_lead_telegram       Boolean  @default(true)
  notify_new_lead_email          Boolean  @default(true)
  notify_status_change_telegram  Boolean  @default(false)
  updated_at                     DateTime @updatedAt

  seller Seller @relation(fields: [seller_id], references: [seller_id])
}

model NotificationLog {
  log_id           String                  @id @default(uuid())
  seller_id        String
  lead_id          String?
  event_type       NotificationEventType
  channel          NotificationChannel
  status           NotificationStatus
  fallback_reason  String?
  error_message    String?
  attempt_number   Int                     @default(1)
  sent_at          DateTime                @default(now())

  seller Seller  @relation(fields: [seller_id], references: [seller_id])
  lead   Lead?   @relation(fields: [lead_id], references: [lead_id])

  @@index([seller_id, sent_at])
  @@index([lead_id])
}
```

> `TelegramVerificationCode` определена и описана в Spec 01 (раздел 5). Здесь не дублируется.

### 6.2 TypeScript DTO

```typescript
// ─── Настройки уведомлений ────────────────────────────────────────────────

export class UpdateNotificationSettingsDto {
  @IsOptional() @IsBoolean()
  notify_new_lead_telegram?: boolean

  @IsOptional() @IsBoolean()
  notify_new_lead_email?: boolean

  @IsOptional() @IsBoolean()
  notify_status_change_telegram?: boolean
}

// ─── Telegram верификация (переиспользуется из Spec 01) ───────────────────

export class TelegramVerifyDto {
  @IsString() @Length(6, 6, { message: 'Код должен содержать 6 цифр' })
  @Matches(/^\d{6}$/, { message: 'Код должен содержать только цифры' })
  code: string
}

// ─── Ответы ───────────────────────────────────────────────────────────────

export interface NotificationSettingsResponse {
  notify_new_lead_telegram: boolean
  notify_new_lead_email: boolean
  notify_status_change_telegram: boolean
  telegram_connected: boolean
  telegram_username: string | null  // из ответа Telegram API, если доступен
  seller_email: string | null       // email для fallback уведомлений
}

export interface TelegramVerifyResponse {
  success: true
  username: string | null  // Telegram username, если у пользователя есть @
}

// ─── Внутренний интерфейс NotificationService ─────────────────────────────

export interface SendNewLeadNotificationPayload {
  lead_id: string
  seller_id: string
  item_name: string
  lead_name: string
  lead_phone: string
  lead_comment: string | null
  lead_type: 'trial' | 'buy'
}
```

### 6.3 API Endpoints

```
────────────────────────────────────────────────────────────────
НАСТРОЙКИ УВЕДОМЛЕНИЙ
────────────────────────────────────────────────────────────────

GET /api/v1/seller/notification-settings
Auth: Bearer (seller)
→ 200: NotificationSettingsResponse
→ 401: { error: 'UNAUTHORIZED' }

PATCH /api/v1/seller/notification-settings
Auth: Bearer (seller)
Body: UpdateNotificationSettingsDto
→ 200: NotificationSettingsResponse
→ 422: { errors: [{ field: string, message: string }] }
→ 500: { error: 'INTERNAL_ERROR', message: 'Что-то пошло не так. Попробуйте позже.' }

────────────────────────────────────────────────────────────────
TELEGRAM ВЕРИФИКАЦИЯ
────────────────────────────────────────────────────────────────

POST /api/v1/seller/telegram/verify
Auth: Bearer (seller)
Body: TelegramVerifyDto
→ 200: TelegramVerifyResponse
→ 400: { error: 'CODE_INVALID', message: 'Неверный код. Убедитесь, что вводите код именно из бота @qadam_notify_bot.' }
→ 400: { error: 'CODE_EXPIRED', message: 'Код устарел. Вернитесь в бота и нажмите /start для получения нового кода.' }
→ 400: { error: 'TELEGRAM_ALREADY_BOUND', message: 'Этот Telegram-аккаунт уже используется другой организацией.' }
→ 400: { error: 'ALREADY_VERIFIED', message: 'Telegram уже подключён к этому аккаунту.' }
→ 422: { errors: [{ field: 'code', message: 'Код должен содержать 6 цифр' }] }

DELETE /api/v1/seller/telegram
Auth: Bearer (seller)
→ 204
→ 400: { error: 'NOT_CONNECTED', message: 'Telegram не подключён.' }

────────────────────────────────────────────────────────────────
ВНУТРЕННИЙ: ОТПРАВКА УВЕДОМЛЕНИЯ (не public API)
────────────────────────────────────────────────────────────────

NotificationService.sendNewLeadNotification(payload: SendNewLeadNotificationPayload): Promise<void>
→ Вызывается внутри LeadService после создания лида
→ Не является HTTP-эндпоинтом, это внутренний сервисный вызов
→ Результат пишется в NotificationLog

────────────────────────────────────────────────────────────────
TELEGRAM BOT WEBHOOK (внутренний)
────────────────────────────────────────────────────────────────

POST /api/internal/telegram/webhook
Auth: Telegram secret token в заголовке X-Telegram-Bot-Api-Secret-Token
Body: Telegram Update объект
→ Обрабатывает /start команду:
    1. Извлекает chat_id из message.chat.id
    2. Генерирует 6-значный код
    3. Сохраняет TelegramVerificationCode { code, chat_id, expires_at: now()+10min }
    4. Отправляет код обратно в чат
→ 200 (всегда, чтобы Telegram не делал retry)
```

---

## 7. Edge Cases

| Сценарий | Поведение |
|----------|----------|
| Продавец написал /start боту дважды | Второй /start инвалидирует старый код (used=true) и генерирует новый |
| Продавец ввёл код, который принадлежит другому seller_id | CODE_INVALID — не раскрываем что код существует для другого аккаунта |
| Telegram заблокировал бота (bot was blocked by user) | Telegram API возвращает 403 "Forbidden: bot was blocked by user"; система фиксирует в NotificationLog.status = 'failed'; fallback на Email |
| Продавец удалил переписку с ботом | Telegram API возвращает 400 "Bad Request: chat not found"; то же поведение что выше |
| Telegram API недоступен > 24 часов | Email fallback работает; Admin видит alert (мониторинг TBD); продавец не замечает если Email работает |
| Продавец отключился от Telegram, пока уведомление уже в retry-очереди | Retry проверяет актуальный telegram_chat_id перед каждой попыткой; если null — переходим к Email |
| NotificationSettings не созданы (новый продавец) | При регистрации автоматически создаётся запись с дефолтными значениями (notify_new_lead_telegram=true, notify_new_lead_email=true) |
| Email продавца изменился с момента создания настроек | Email берётся из профиля в момент отправки, не кешируется в NotificationSettings |
| Два лида созданы одновременно для одного продавца | Два независимых уведомления; каждый лид → своё уведомление; очередь обрабатывает последовательно |
| Продавец отключил все уведомления и забыл об этом | Лиды создаются нормально; продавец видит их при входе в /seller/leads; уведомления не отправляются |
| SMTP bounce при вводе неверного email в профиле | NotificationLog bounce; автодеактивация email-уведомлений — TBD |
| Telegram API вернул успешный ответ, но сообщение не дошло | Внешняя проблема вне контроля системы; NotificationLog = sent; пользователь обращается в поддержку |

---

## 8. TBD / Сознательно опущено

| Тема | Статус | Примечание |
|------|--------|-----------|
| SMTP-провайдер (Postmark/SendGrid/AWS SES) | TBD | Не выбран. Метод `sendEmail()` — абстракция над провайдером |
| Email-шаблоны (дизайн и HTML) | TBD | Дизайнер не утвердил шаблон; в MVP — функциональный plain/simple HTML |
| Уведомление покупателя при смене статуса лида | TBD | Продукт не определился: нужно ли уведомлять buyer? Исключено из MVP |
| Уведомления для Seller Staff | Вне скоупа MVP | В MVP уведомления только на аккаунт Owner; Staff-уведомления в v1.0 |
| Push-уведомления (браузер / iOS / Android) | Вне скоупа | Требует мобильного приложения или PWA; не планируется в MVP |
| SMS-уведомления | Вне скоупа MVP | Избыточно при наличии Telegram + Email; TBD для v1.5 |
| Уведомление продавца при смене статуса покупателем (реверс) | Вне скоупа | Покупатель не меняет статусы в MVP |
| Digest-уведомления (сводка за день/неделю) | Вне скоупа MVP | Запланировано в v1.5 |
| Шаблоны уведомлений редактируемые продавцом | Вне скоупа | Продавец не может кастомизировать текст уведомлений |
| In-app уведомления (колокольчик в кабинете) | TBD v1.0 | В MVP только Telegram + Email; in-app bell — v1.0 |
| Автодеактивация email при bounce | TBD | Механизм не определён; bounce просто логируется в MVP |
| Rate limiting для Telegram webhook | Реализовать | Защита от DDoS на /api/internal/telegram/webhook через secret token + IP whitelist Telegram |
| Аналитика доставляемости уведомлений | TBD | Дашборд delivery rate для Admin — v1.5 |

---

## 9. Зависимости

| Модуль | Связь |
|--------|-------|
| **Spec 01** (Seller Onboarding) | Seller.telegram_chat_id, Seller.telegram_verified_at; TelegramVerificationCode; UC-06 (привязка Telegram) реализована там |
| **Spec 02** (Items) | Item.item_name — используется в тексте уведомления |
| **Spec 06** (Buyer Flow) | Создание лида → триггер для уведомления; LeadService вызывает NotificationService |
| **Spec 09** (Lead Management) | Lead.lead_id, Lead.lead_status — данные в уведомлении; deep link в кабинет |
| **Telegram Bot API** | Внешняя зависимость: @qadam_notify_bot должен быть создан и настроен до MVP-запуска |
| **SMTP-провайдер** | Внешняя зависимость: TBD (Postmark / SendGrid / AWS SES); нужна настройка DNS (SPF, DKIM, DMARC) |
| **Spec 15** (Billing) | Уведомления не связаны с биллингом напрямую; но новый лид = начисление $30 |
