10. Уведомления: как устроено в legacy и что меняется¶
Разбор всех каналов уведомлений: ../site (источник поводов и текстов), ../user (доставка
клиенту: Telegram, MAX, SMS), ../notification (служебные сообщения в MAX), ../frontend
(уведомления на витрине и отправка ошибок).
10.1. Карта¶
┌──────────────── site (Laravel) ────────────────┐
события заказа ────▶│ OrderNotifier ──┐ │
стоп-листы ────▶│ NotificationSvc ─┼──┐ │
ожидания ────▶│ │ │ │
health-check ────▶│ │ │ │
ошибки ────▶│ SmsReporting / EmailReporting │
└──────────────────┼──┼───────────────────────────┘
│ │
POST /api/user/notify/send (Bearer) │
│ │ POST /api/notification/system (Bearer)
▼ ▼
┌──── user (Go) ────┐ ┌─── notification (Node) ───┐
│ tg-бот ──▶ клиент│ │ MAX-бот ──▶ рабочий чат │
│ MAX-бот ──▶ клиент│ │ 4 типа → 3 чата из env │
│ smsc.ru ──▶ клиент│ └───────────────────────────┘
└───────────────────┘
▲
│ прямая отправка в Telegram
│ (токен бота в бандле)
┌──────────── frontend ────────────┐
│ ошибки JS, bug-logger │
│ + свои уведомления в интерфейсе │
└──────────────────────────────────┘
Два независимых контура:
| Контур | Кому | Через что | Кто решает, что отправить |
|---|---|---|---|
| клиентский | покупателю | user-сервис: Telegram-бот, MAX-бот, SMS (smsc.ru) |
site, OrderNotifier |
| служебный | сотрудникам | notification-сервис: MAX-бот в рабочие чаты |
site, NotificationService |
Плюс два «полуофициальных» канала: почта разработчикам/менеджерам напрямую из site и отправка
ошибок из браузера прямо в Telegram.
10.2. Клиентские уведомления¶
Поводы¶
Всего пять, все — про заказ (App\Services\Notification\OrderNotifier):
| Метод | Когда | Текст |
|---|---|---|
accepted |
заказ принят (OrderAcceptEvent) |
шаблон из БД, выбирается из 8 вариантов |
ready |
самовывоз готов (OrderReadyEvent) |
шаблон order_ready_pickup |
onWay |
курьер забрал (OnWay из iiko) |
строка в коде: «Курьер забрал ваш заказ и уже спешит к вам» |
delivered |
доставлен | строка в коде: «{Имя}, приятного аппетита! До новых встреч» |
deliveredToDoor |
доставлен к двери | строка в коде + шаблон order_delivered_to_door для SMS-отката |
Шаблон «заказ принят» выбирается перебором трёх флагов — самовывоз/доставка, ко времени/сейчас,
оплачен/не оплачен (SmsOrderGenerator::resolveTemplateName), итого 8 имён в SmsTemplateEnum.
Тексты лежат в таблице sms_messages (name, value, active) и правятся в админке Orchid.
Переменные: {client_name}, {order_sum}, {time}, {time_end} (= {time} + 30 минут),
{pizzeria_address}, {pizzeria_description}.
Событие «заказ принят» для наличной оплаты происходит при создании, для картой — в момент подтверждения оплаты; отдельного уведомления «оплата прошла» нет.
Чего клиенту не сообщают вообще¶
- заказ отменён или принудительно закрыт (см. §9.6) — уведомление идёт только менеджерам;
- оплата не прошла (
PAID_ERROR); - заказ отклонён антифродом или ждёт ручного подтверждения (
WAIT_CONFIRM); - позиция снята из-за стоп-листа (сейчас проверка отключена, но и уведомления нет);
- изменилось время доставки.
Каскад каналов¶
OrderNotifier::dispatch():
- Собирается текст для мессенджера: либо строка из кода, либо рендер SMS-шаблона (активность шаблона на этом шаге не проверяется).
- Вызов
user-сервиса с приоритетом[max, tg]. - Если исключение — SMS-откат: тот же
user-сервис с каналом[sms], но только если шаблон существует иactive = true.
Следствия:
- SMS-откат не спасает от недоступности
user-сервиса: и мессенджеры, и SMS идут через один и тот же HTTP-эндпоинт с таймаутом 5 секунд. Падениеuser= ни одного уведомления. - Приоритет каналов задан в двух местах и по-разному:
siteпросит[max, tg], аuserпо умолчанию использует[tg, max, sms]. - Ретраев нет: обработчики событий стоят в очереди (
ShouldQueue), но неудачная отправка просто логируется. - Журнала доставки нет: результат («доставлено каналом tg») пишется только в лог.
Дедупликация¶
Таблица order_status_notifications (order_id, status) — одна запись на пару «заказ + статус
iiko», проверяется в SendOrderDeliveryStatusNotification перед отправкой. Работает только для
уведомлений о доставке; для «заказ принят» и «готов к выдаче» защиты от повтора нет — она
держится на том, что событие диспатчится один раз.
10.3. Доставка клиенту: user-сервис¶
Единственная точка входа — POST /api/user/notify/send, авторизация Bearer <INTEGRATION_TOKEN>
(общий секрет с legacy):
{ "phone": "+79001234567", "text": "…", "channels": ["max", "tg"] }
→ 200 { "channel": "max" } // 404, если ни одного пригодного канала
NotifyClientUseCase нормализует телефон, ищет пользователя и идёт по каналам в порядке запроса,
возвращая первый успешный:
| Канал | Условие | Реализация |
|---|---|---|
tg |
у пользователя есть tg_chat_id |
Telegram Bot API |
max |
у пользователя есть max_chat_id |
MAX Bot API |
sms |
всегда (пользователь может быть даже не найден) | smsc.ru, https://smsc.ru/sys/send.php |
Привязка каналов. Клиент пишет боту (Telegram или MAX), бот получает контакт с телефоном →
LinkTelegramHandler / LinkMaxHandler сохраняют bot_contacts (телефон, источник, chat_id,
username) и, если пользователь с таким телефоном есть, проставляют ему tg_chat_id / max_chat_id.
Если пользователя ещё нет — контакт остаётся и привязывается при регистрации. Те же боты выдают
коды авторизации по кнопке «Получить код» — то есть бот одновременно канал уведомлений и второй
фактор входа.
Флаг согласия не работает. У пользователя есть поле allow_notification (по умолчанию true),
оно отдаётся в профиле как allow_notifications, но NotifyClientUseCase его не проверяет —
отключить уведомления через профиль нельзя.
Чего нет вообще: push (веб и мобильные), e-mail клиенту, отложенной отправки, тихих часов, разделения транзакционных и маркетинговых сообщений, отписки.
10.4. Служебные уведомления: notification-сервис¶
Тонкий Fastify-сервис (~270 строк), единственный эндпоинт
POST /api/notification/system, авторизация Bearer или заголовок x-webhook-token:
{ "type": "orders|stop_list|expectations|system", "text": "…",
"format": "markdown|html", "button_url": "https://…", "notify": true }
type определяет чат MAX из переменных окружения: CHAT_ID_STOP_LISTS,
CHAT_ID_EXPECTATIONS, CHAT_ID_ORDERS (он же для system). button_url превращается в
inline-кнопку «Открыть» — обычно ссылку на карточку заказа в админке.
Кто и что отправляет:
| Тип | Повод | Источник |
|---|---|---|
orders |
заказ требует ручного подтверждения (антифрод) | OrderNeedsConfirmation\SendNotify + письмо |
orders |
заказ закрыт принудительно | OrderForceClosed\SendTelegramNotify + письмо |
stop_list |
изменился стоп-лист (добавлено/убрано/осталось) | TelegramStopListNotificationHandler |
stop_list |
отчёт по стоп-листам, 5 раз в день (10:00, 12:00, 16:00, 20:00, 01:20) | notify:stop-lists |
expectations |
включено/изменено повышенное ожидание, в т.ч. по категориям | PizzeriaExpectationTime, ежечасно notify:increased-expectation |
expectations |
включены/отключены заказы ко времени | OrderToTimeHandler |
system |
health-check: «Проверьте админку сайта, возможно проблема с заказами» | app:system:check, каждую минуту |
Тексты — blade-шаблоны в resources/views/telegram/**.
app:system:check — единственная автоматическая диагностика: работает с 11 до 19 часов, шлёт
сигнал, если за 10 минут больше двух принудительно закрытых заказов, либо за 30 минут не создано
ни одного заказа, либо заказ висит в WAIT_CRM дольше 5 минут; дедупликация — ключ в кеше на
5 минут.
Ограничения контура: один чат на тип для всей сети — нет адресации по точкам и ролям, сотрудник конкретной пиццерии получает сообщения по всем. Нет подтверждения прочтения и эскалации.
10.5. Почта и уведомления об ошибках¶
| Канал | Что | Куда |
|---|---|---|
| Письмо | OrderConfirmEmail — заказ ждёт подтверждения |
список из конфигурации |
| Письмо | OrderCreateErrorMail — заказ закрыт принудительно |
mail.order_crm_error (prod) / mail.devs |
| Письмо | RefundCodeMail, Error |
разработчикам |
| SMS | SmsReporting — критичные ошибки приложения |
захардкоженный личный номер в Services/Notify/Error/Config.php |
| Письмо | EmailReporting — те же ошибки |
захардкоженные адреса, включая личные ящики на gmail |
Маршрутизация ошибок (Config::ERROR_RECIVERS) задана константами в коде: default,
DISH_OR_MODIFIER_NOT_FOUND, IIKO_ORDER_CREATE_FAILED, PAYMENT_FAILED. В SmsReporting
логирование инвертировано: сообщение «Неудалось отправить смс уведомление» пишется при
SendResult::OK.
Отдельно: у site остался собственный клиент smsc.ru (Services\Integrations\Sms\SmsService)
— он используется только для этих ошибок и тестовой команды, тогда как клиентские SMS идут через
user-сервис. То есть два независимых интеграционных пути к одному провайдеру.
Ошибки с фронтенда¶
- Эндпоинт
POST /api/send-errorсуществует, но его тело закомментировано — обработчик ничего не делает. - Фактически фронт шлёт ошибки напрямую в Telegram:
services/utils/send-to-telegram.jsс токеном бота и id чата, зашитыми в клиентский бандл. Дедупликация — по совпадению заголовка с предыдущей ошибкой в рамках сессии. bug-logger.jsпишет вlocalStorageпоследовательность действий пользователя (открытие сайта, переходы, события, id заказа) и публикует её тем же способом.
Токен бота, доступный в бандле, — это доступ к чату на запись для любого, кто открыл сайт.
10.6. Уведомления на витрине¶
Отдельная система, к серверу отношения не имеет: модуль Timer во Vuex + компонент
Notifications.vue. Событие описывается объектом {id, type, ico, text, time, timeIgnoreAfterClose,
once, replace}, живёт заданное число секунд, тикающий таймер раз в секунду убирает просроченные.
Закрытие вручную кладёт id в ignoreEvents до Date.now() + timeIgnoreAfterClose — типично
5 минут. Флаг once не даёт продублировать сообщение, replace обновляет текст существующего.
Через этот механизм показываются: «ресторан закрыт», «скоро закрывается», «много заказов» (см. §8.5 A–C), «добавьте товаров на N ₽», «выберите время», «товар добавлен в корзину», «личная скидка применена». Все тексты и правила показа — в коде фронта.
10.7. Сводка проблем¶
- Нет журнала доставки. Ни одна отправка не сохраняется: что ушло, кому, каким каналом, с каким результатом — только строки в логах. Разбор жалобы «мне ничего не пришло» невозможен.
- Нет ретраев и очереди доставки. Синхронный HTTP с таймаутом 5 секунд; неудача = потеря.
- Единая точка отказа. SMS-откат идёт через тот же сервис, что и мессенджеры.
- Тексты в трёх местах: таблица
sms_messages, строки в PHP-коде, blade-шаблоны; плюс тексты витринных уведомлений в бандле фронта. - Согласие клиента не учитывается (
allow_notificationне проверяется), отписки нет. - Пять поводов на весь жизненный цикл заказа; про отмену, неоплату и ожидание подтверждения клиент не узнаёт ничего.
- Служебный контур без адресации: один чат на тип для всей сети, без ролей и точек.
- Секреты в коде: токен Telegram-бота в клиентском бандле, личные телефоны и почты в конфигурации ошибок.
- Дублирующая интеграция с smsc.ru в двух сервисах.
- Дедупликация частичная — только для статусов доставки.
10.8. Целевая модель: модуль notifications¶
Принцип¶
Уведомление — это реакция на доменное событие, а не вызов из обработчика. Домен публикует
событие (OrderAccepted, OrderCancelled, PaymentFailed, OrderReadyForPickup,
StopListChanged, …) через транзакционный outbox (Event Publication Registry, ADR-001), модуль
notifications подписан на них и отвечает за всё остальное: кому, по какому правилу, каким
текстом, в какой канал, с какими повторами.
Ни один модуль домена не должен знать про Telegram, SMS и шаблоны — сейчас site знает про всё
это напрямую.
Данные (эскиз)¶
notifications.event_subscription — событие → правило: аудитория (CLIENT | STAFF),
набор каналов с приоритетом, окно отправки, дедуп-ключ
notifications.template — текст на (правило, канал, язык), переменные, версия,
признак активности; правится в админке
notifications.contact — контакт клиента: канал (TG | MAX | SMS | EMAIL | PUSH),
идентификатор (chat_id/телефон/почта), подтверждён, отписан
notifications.consent — согласия: транзакционные / сервисные / маркетинговые
notifications.message — журнал: событие, адресат, канал, шаблон+версия, текст,
статус (QUEUED|SENT|DELIVERED|FAILED|SKIPPED), попытки,
ошибка, id у провайдера, дедуп-ключ (unique)
notifications.staff_route — служебная адресация: тип, организация, роль → чат/канал
notifications.message.dedup_key обобщает нынешнюю order_status_notifications: уникальный ключ
вида order:{id}:event:{ready} делает повторную отправку невозможной независимо от того, сколько
раз пришло событие.
Доставка¶
- Каскад каналов — политика правила, а не
try/catch: порядок задаётся в одном месте, с учётом подтверждённых контактов клиента и его отписок. - Ретраи с экспоненциальной задержкой и терминальным исходом; провайдеры за интерфейсом с таймаутами и circuit breaker (Resilience4j, как для iiko).
- Транзакционные уведомления (статус заказа) отправляются всегда; сервисные и маркетинговые — по согласию, с отпиской.
- SMS остаются последним каналом по стоимости; их количество — метрика.
Поводы, которые должны появиться¶
К пяти существующим добавляются: оплата не прошла, заказ отменён (клиентом, рестораном, автоматикой), заказ ждёт подтверждения, позиция снята из-за стоп-листа, изменилось время исполнения. Полный список — предмет ОВ-25.
Служебный контур¶
Остаётся отдельным, но получает адресацию: тип события × организация × роль → канал и чат. Сообщение хранится в том же журнале, что и клиентские, — один механизм, разные аудитории. Health-check переезжает в наблюдаемость (Prometheus + алерты), а не в самописную команду, шлющую сообщение в чат.
Ошибки и телеметрия фронта¶
Отправка ошибок из браузера — на собственный эндпоинт с rate limit, без секретов в бандле; дальше — в систему ошибок (Sentry уже используется на бэкенде), а не в мессенджер.
10.9. Открытые вопросы¶
ОВ-25. Полный список поводов для уведомления клиента. Нужно согласовать: отмена (кем), неуспешная оплата, ожидание подтверждения, снятие позиции по стоп-листу, изменение времени, запрос оценки после заказа. Каждый повод = текст + канал + правило дедупликации.
ОВ-26. Управление каналами клиентом. Даём ли выбор канала и отписку в профиле? Флаг
allow_notification уже есть, но не работает. Что считаем транзакционным (отписка невозможна),
что сервисным.
ОВ-27. Push-уведомления. Сейчас их нет нигде. Нужны ли веб-пуши на сайте и пуши в мобильном
приложении — от ответа зависит, закладываем ли контакты типа PUSH и хранение токенов устройств.
ОВ-28. E-mail клиенту. Сейчас почта уходит только сотрудникам. Нужны ли клиенту чек, детали заказа, письмо о регистрации?
ОВ-29. Адресация служебных уведомлений. Разделяем ли чаты по точкам и ролям (стоп-листы — шеф-повару точки, заказы — менеджеру смены), или сохраняем общий чат на сеть?
ОВ-30. Маркетинговые рассылки. Планируются ли (акции, брошенная корзина, реактивация)? Если да — согласия, отписка и частотные ограничения нужны в модели с самого начала, а не потом.
ОВ-31. SMS-провайдер. Остаётся ли smsc.ru, какое имя отправителя, есть ли лимиты и бюджет. Сейчас интеграция продублирована в двух сервисах.
ОВ-32. Судьба ботов как второго фактора. Telegram/MAX-боты сейчас и канал уведомлений, и источник кодов авторизации. Сохраняем связку или разделяем.
10.10. Влияние на план работ¶
Продолжение таблиц §8.10 и §9.12:
| # | Работа | Оценка |
|---|---|---|
| 34 | Модуль notifications: модель (правила, шаблоны, контакты, согласия, журнал), подписка на доменные события через outbox |
5 дн |
| 35 | Каналы: Telegram, MAX, SMS за общим интерфейсом; ретраи, таймауты, circuit breaker, метрики | 4 дн |
| 36 | Админка: редактор шаблонов с предпросмотром и переменными, правила по событиям, журнал отправок с фильтрами | 4 дн |
| 37 | Служебный контур: адресация по типу × организации × роли, перенос текущих поводов, вынос health-check в алерты | 3 дн |
Пункт 33 из §9.12 («уведомления по событиям заказа») поглощается пунктами 34–36 — отдельной работой он был оценкой того же объёма со стороны модуля заказа.