Qadam Roadmap
проектdocs/Agents/specs/2026-03-24-mvp-spec-10-notifications.md

MVP Spec 10 — Notifications System

Обновлён 1 апр. 2026 г., 12:41 · 0 комментариев

MVP Spec 10 — Notifications System

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

  • Статус документа: working spec
  • Актуально на: 28 марта 2026 года
  • Владелец: backend/platform-команда
  • Пересмотр: перед реализацией, при изменении domain rules или при смене source-of-truth по контракту
  • Область применения: детальные feature-спеки для product backlog и реализации MVP/v1 направлений
  • Связанные документы:

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 не получает уведомления — только на аккаунт OwnerTBD в v1.0
Уникальность Telegram chat_idОдин chat_id — один seller400 TELEGRAM_ALREADY_BOUND
TTL кода верификации10 минут с момента /start в боте400 CODE_EXPIRED
Код одноразовыйПосле использования: used = true400 CODE_INVALID
Retry для Telegram3 попытки (immediate, +5s, +30s)После 3-й неудачи → Email fallback
Retry для Email3 попытки в очереди (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_telegramBoolean422
notify_new_lead_emailBoolean422
notify_status_change_telegramBoolean422
Telegram chat_idНе занят другим seller_id400 TELEGRAM_ALREADY_BOUND
Telegram кодСуществует, не истёк (TTL 10 мин), not used400 CODE_INVALID / CODE_EXPIRED

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

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

NotificationSettings

АтрибутТипОписание
settings_idUUIDPK
seller_idUUID FK→ Seller (Spec 01), unique
notify_new_lead_telegrambooleanУведомлять о новом лиде в Telegram. Default: true
notify_new_lead_emailbooleanУведомлять о новом лиде по Email. Default: true
notify_status_change_telegrambooleanУведомлять при смене статуса в Telegram. TBD MVP, default: false
updated_atDateTime

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

АтрибутТипОписание
log_idUUIDPK
seller_idUUID FK→ Seller
lead_idUUID FK?→ Lead (Spec 09), если уведомление о лиде
event_typeNotificationEventTypenew_lead / status_changed
channelNotificationChanneltelegram / email
statusNotificationStatussent / failed / skipped / bounced
fallback_reasonstring?Причина fallback (например: 'telegram_failed')
error_messagestring?Текст ошибки от API/SMTP
attempt_numberintНомер попытки (1, 2, 3)
sent_atDateTimeВремя попытки

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

6.1 Prisma Schema

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

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

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_idCODE_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 при bounceTBDМеханизм не определён; 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