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

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():

  1. Собирается текст для мессенджера: либо строка из кода, либо рендер SMS-шаблона (активность шаблона на этом шаге не проверяется).
  2. Вызов user-сервиса с приоритетом [max, tg].
  3. Если исключение — 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. Сводка проблем

  1. Нет журнала доставки. Ни одна отправка не сохраняется: что ушло, кому, каким каналом, с каким результатом — только строки в логах. Разбор жалобы «мне ничего не пришло» невозможен.
  2. Нет ретраев и очереди доставки. Синхронный HTTP с таймаутом 5 секунд; неудача = потеря.
  3. Единая точка отказа. SMS-откат идёт через тот же сервис, что и мессенджеры.
  4. Тексты в трёх местах: таблица sms_messages, строки в PHP-коде, blade-шаблоны; плюс тексты витринных уведомлений в бандле фронта.
  5. Согласие клиента не учитывается (allow_notification не проверяется), отписки нет.
  6. Пять поводов на весь жизненный цикл заказа; про отмену, неоплату и ожидание подтверждения клиент не узнаёт ничего.
  7. Служебный контур без адресации: один чат на тип для всей сети, без ролей и точек.
  8. Секреты в коде: токен Telegram-бота в клиентском бандле, личные телефоны и почты в конфигурации ошибок.
  9. Дублирующая интеграция с smsc.ru в двух сервисах.
  10. Дедупликация частичная — только для статусов доставки.

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 — отдельной работой он был оценкой того же объёма со стороны модуля заказа.