Перейти к содержанию

Модуль notifications

Назначение и границы

Модуль notifications отвечает за доставку сообщений через внешних провайдеров (SMS через smsc.ru, Telegram и MAX через их Bot API). Это инфраструктурный слой: он не знает, кто и почему отправляет сообщение — клиент запрашивает код входа, сотруднику ФЧ нужно системное уведомление или что-то ещё. Эти решения принимают модули-потребители через публичный контракт.

Модуль не является целевой моделью уведомлений, описанной в docs/technical/legacy/10-notifications.md §10.8 (событийная подписка, шаблоны, согласия, журнал доставки, адресация по ролям/точкам) — это отдельная задача, которую нужно делать явно, когда появится конкретный потребитель (например, служебные уведомления сотрудникам ФЧ). Сейчас notifications — это только провайдеры за общим интерфейсом, без домена правил и без собственной схемы БД.

Модуль владеет:

  • HTTP-взаимодействием с smsc.ru, Telegram Bot API и MAX Bot API;
  • учётными данными провайдеров (логин/пароль smsc.ru; токен и секрет вебхука у каждого бота).

Модуль не отвечает за:

  • кому, когда и по какому поводу отправлять сообщение — это решает модуль-потребитель (customer — для кода входа);
  • кто такой получатель: для SMS это телефон (сам по себе адрес), а для Telegram/MAX — chat_id, который ещё нужно узнать по телефону. Привязку телефона к chat_id хранит и отдаёт customer (customer.telegram_contact/customer.max_contact, см. модуль customer) — notifications о привязках не знает, только умеет слать по уже готовому chat_id;
  • шаблоны текста, повторные попытки, журнал доставки, согласия и отписку — этого пока нет нигде, см. открытые вопросы ОВ-25–ОВ-32 в 10-notifications.md.

Единственный текущий потребитель — customer (доставка одноразового кода по SMS, Telegram и MAX, ProviderCustomerCodeSender, см. модуль customer).

Структура

ru.panampizza.monolith.notifications
├── api
│   ├── SmsSender                    публичный контракт отправки SMS
│   ├── TelegramSender, TelegramWebhookAuthentication   Telegram по chat_id + проверка вебхука
│   └── MaxSender, MaxWebhookAuthentication             MAX по chat_id + проверка вебхука
├── Secrets.kt                       resolveSecret — общая логика value/file для секретов
├── SmscProperties, SmscClient       smsc.ru
├── TelegramProperties, TelegramClient  Telegram Bot API
└── MaxProperties, MaxClient         MAX Bot API

Корневой пакет объявлен модулем Spring Modulith, allowedDependencies = {} — модуль ни от кого не зависит. Пакет notifications.api объявлен named interface api.

Публичный контракт

interface SmsSender {
    fun send(phone: String, text: String)
}

interface TelegramSender {
    fun send(chatId: Long, text: String)
    fun requestContact(chatId: Long, text: String)
}

interface TelegramWebhookAuthentication {
    fun verify(secretHeader: String?): Boolean
}

MaxSender/MaxWebhookAuthentication — те же сигнатуры, что TelegramSender/ TelegramWebhookAuthentication, отдельный интерфейс на канал, а не общий MessengerSender: у Telegram и MAX разные провайдеры под капотом (разный HTTP, разная схема ошибок), общий контракт без пользы преждевременно объединял бы их.

Вызовы синхронные. Ошибка (сетевая, код/описание ошибки от Telegram, HTTP-статус с телом {code, message} от MAX) выбрасывается исключением — решение, что с этим делать (повторить, откатить транзакцию, залогировать), остаётся за вызывающей стороной. Так, customer.ProviderCustomerCodeSender вызывает send() внутри @Transactional-метода CustomerAuthChallengeService.request(): исключение откатывает уже созданный challenge, клиент видит ошибку и может повторить запрос.

requestContact — отдельный от send метод, а не общий параметр «с клавиатурой»: это одноразовое сообщение с кнопкой «поделиться номером», используемое только на шаге привязки контакта (см. StorefrontTelegramWebhookController/StorefrontMaxWebhookController в storefront), а не часть контракта доставки кода.

Конфигурация

smsc.ru — panam.notifications.smsc

panam:
  notifications:
    smsc:
      login: ${SMSC_LOGIN:}
      password: ${PANAM_SMSC_PASSWORD_SECRET:}
      password-file: ${PANAM_SMSC_PASSWORD_SECRET_FILE:}
      sender: ${SMSC_SENDER:}
Параметр Назначение Значение по умолчанию
base-url Адрес API smsc.ru https://smsc.ru/sys/send.php
login Логин аккаунта smsc.ru —
password / password-file Пароль аккаунта — ровно один из двух, секрет —
sender Имя отправителя (буквенно-цифровое, до 11 символов); пусто — номер аккаунта по умолчанию —

Telegram — panam.notifications.telegram

panam:
  notifications:
    telegram:
      bot-token: ${PANAM_TELEGRAM_BOT_TOKEN_SECRET:}
      bot-token-file: ${PANAM_TELEGRAM_BOT_TOKEN_SECRET_FILE:}
      webhook-secret: ${PANAM_TELEGRAM_WEBHOOK_SECRET:}
      webhook-secret-file: ${PANAM_TELEGRAM_WEBHOOK_SECRET_FILE:}
      webhook-url: ${PANAM_TELEGRAM_WEBHOOK_URL:}
Параметр Назначение Значение по умолчанию
api-base-url Адрес Telegram Bot API https://api.telegram.org
bot-token / bot-token-file Токен бота — ровно один из двух, секрет —
webhook-secret / webhook-secret-file Секрет X-Telegram-Bot-Api-Secret-Token — ровно один из двух, секрет —
webhook-url Публичный HTTPS-адрес вебхука; пусто — регистрация при старте пропускается —

MAX — panam.notifications.max

panam:
  notifications:
    max:
      bot-token: ${PANAM_MAX_BOT_TOKEN_SECRET:}
      bot-token-file: ${PANAM_MAX_BOT_TOKEN_SECRET_FILE:}
      webhook-secret: ${PANAM_MAX_WEBHOOK_SECRET:}
      webhook-secret-file: ${PANAM_MAX_WEBHOOK_SECRET_FILE:}
      webhook-url: ${PANAM_MAX_WEBHOOK_URL:}
Параметр Назначение Значение по умолчанию
api-base-url Адрес MAX Bot API https://platform-api.max.ru
bot-token / bot-token-file Токен бота — ровно один из двух, секрет —
webhook-secret / webhook-secret-file Секрет X-Max-Bot-Api-Secret — ровно один из двух, секрет —
webhook-url Публичный HTTPS-адрес вебхука; пусто — регистрация при старте пропускается —

Все секреты (пароль smsc.ru, токены и секреты вебхуков Telegram/MAX) — production-секреты, задаются по той же схеме, что JWT-секреты и pepper покупателя (docs/technical/modules/customer.md): инлайн-значение для разработки, _FILE (путь к файлу, например из Kubernetes Secret) — в общих средах и production. Отсутствие ровно одного значения или обоих сразу не даёт приложению стартовать (SmscClient.validatePassword, TelegramClient.init, MaxClient.init, @PostConstruct).

Запрос к smsc.ru

POST /sys/send.php, application/x-www-form-urlencoded, fmt=3 (JSON-ответ):

login=...&psw=...&phones=79001234567&mes=...&fmt=3&charset=utf-8&sender=...

Телефон передаётся без знака + — так ожидает smsc.ru. Ответ с error_code != 0 превращается в исключение с расшифровкой кода (коды 1–9 замаплены на читаемые сообщения; неизвестный код — исходная строка error от провайдера).

Telegram Bot API

POST /bot<token>/sendMessage (JSON-тело {chat_id, text}, опционально reply_markup с одной кнопкой «Поделиться номером телефона» для requestContact). Ответ Telegram — {"ok": true|false, "description": "..."}; ok: false превращается в исключение с текстом description.

При старте, если задан webhook-url, TelegramClient идемпотентно регистрирует его через POST /bot<token>/setWebhook {url, secret_token}. Без webhook-url (типично для локальной разработки без публичного HTTPS) регистрация пропускается без ошибки — это не мешает приложению стартовать и не мешает send/requestContact работать, если вебхук уже зарегистрирован вручную.

Входящие сообщения от Telegram и MAX обрабатывает не notifications, а вебхуки в storefront (StorefrontTelegramWebhookController/StorefrontMaxWebhookController, POST /api/v1/auth/telegram/webhook и POST /api/v1/auth/max/webhook) — они вызывают TelegramWebhookAuthentication.verify/MaxWebhookAuthentication.verify для проверки секрета и customer.api.CustomerTelegramContacts/CustomerMaxContacts для привязки телефона к chat_id. Разбор ботов не входит в notifications, потому что привязка телефона — customer-домен, а не деталь провайдера; см. модуль customer.

MAX Bot API

POST /messages?chat_id=<id>&v=1.2.5 (JSON-тело {text, attachments}; attachments — пустой массив либо одна inline_keyboard-клавиатура с кнопкой request_contact для requestContact), заголовок Authorization: <токен> (без Bearer). Ошибки сигналятся HTTP-статусом (не полем в теле, как у Telegram) с JSON {"code": "...", "message": "..."} — текст ответа целиком попадает в исключение.

При старте, если задан webhook-url, MaxClient идемпотентно регистрирует его через POST /subscriptions?v=1.2.5 {url, secret}. Без webhook-url регистрация пропускается без ошибки — как и у Telegram.

Контакт в вебхуке MAX приходит не отдельным полем phone_number (как у Telegram), а VCF-блоком (attachment.payload.vcf_info, многострочный текст вида TEL:+79001234567) — StorefrontMaxWebhookController.extractPhoneFromVcf находит строку, начинающуюся на TEL, и берёт текст после :.

Тесты

  • SmscClientTests — успешная отправка (проверка полей запроса), маппинг кода ошибки в сообщение, отказ старта при отсутствующем пароле.
  • TelegramClientTests — отправка текста и запроса контакта, маппинг ok: false в исключение, регистрация/пропуск вебхука при старте в зависимости от webhook-url, проверка секрета вебхука, отказ старта при отсутствующем токене.
  • MaxClientTests — то же самое для MAX: авторизационный заголовок и ?v=, отправка текста и запроса контакта, маппинг HTTP-ошибки с телом в исключение, регистрация/пропуск вебхука, проверка секрета, отказ старта при отсутствующем токене.
  • ModularityTests проверяет границы Spring Modulith.

Полный запуск:

task test

Следующие этапы

Целевая модель уведомлений (10-notifications.md §10.8) — событийная подписка, шаблоны, контакты, согласия, журнал доставки, адресация служебных уведомлений — отдельная задача, когда появится конкретный потребитель за пределами кода входа покупателя (три канала кода — SMS, Telegram, MAX — уже подключены).