Notifications
Push-уведомления: Redis stream → notifications-service → FCM.
Архитектура¶
sequenceDiagram
Service->>NotificationPublisher: publish(userId, channel, title, body, traceId, data)
NotificationPublisher->>NotificationPublisher: проверить isInitialized
alt Redis не инициализирован
NotificationPublisher->>Session: log WARNING — skipping
else
NotificationPublisher->>Redis: XADD stream.notifications.jobs {payload: JSON}
Note over Redis: best-effort (нет retry, нет ACK)
Redis->>NotifService: XREADGROUP consume
NotifService->>FCM: отправить push
FCM->>Device: push notification
end
Stream: stream.notifications.jobs. Payload — JSON с полями: userId, channel, title, body, traceId?, data?, badge?.
NotificationPublisher.publish() — параметры¶
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
userId |
int | да | числовой id получателя |
channel |
String | да | payments / security / kyc / system |
title |
String | да | заголовок уведомления |
body |
String | да | текст уведомления |
traceId |
String? | нет | для корреляции с payment intent |
data |
Map? | нет | произвольные данные для Flutter-обработчика |
badge |
int? | нет | счётчик badge (iOS) |
NotificationTemplates — шаблоны¶
Фаза 1: шаблоны захардкожены в коде. Фаза 2: таблица notification_template + редактор в Admin.
Локали: ru (primary), th (Thailand), en (fallback). Нормализация: ru* → ru, th* → th, иное → en.
| Шаблон | Канал | Когда | Параметры |
|---|---|---|---|
newDeviceLogin |
security |
вход с нового устройства | locale, city?, country?, at |
passwordChanged |
security |
пароль изменён или сброшен | locale |
nfcCloneDetected |
security |
клон NFC-метки — метка автоматически отозвана | locale |
nfcChargeRejectedLimit |
payments |
списание по NFC отклонено из-за превышения лимита | locale |
kycFullyVerified |
kyc |
оператор подтвердил верификацию (fully_verified) | locale |
transferReceived |
payments |
INTERNAL-перевод получен | locale, amountMinor, currency, senderName? |
transferSent |
payments |
INTERNAL-перевод отправлен | locale, amountMinor, currency, recipientName? |
Суммы форматируются из minor units (сатанги): amountMinor / 100 с 2 знаками после запятой.
NotificationEndpoint — DeviceToken¶
registerDevice¶
- Upsert: токен уже существует → UPDATE
lastSeenAt,userId,appId,appVersion,locale - Новый токен → INSERT
- Валидация:
platform∈{ios, android, web}, длина токена 100–4096,locale∈{ru, th, en} - Хранит
ipAddressизsession.request.remoteInfo
unregisterDevice¶
- Идемпотентен: токен не найден или чужой userId → silent (нет ошибки)
- Вызывается при logout перед
FirebaseMessaging.deleteToken()
appId и роутинг¶
| appId | Приложение |
|---|---|
wallet |
onewallet_base_flutter |
closeloop |
closeloop_app_flutter |
merchant |
merchant_app_flutter |
appId хранится в device_token.appId и используется notifications-service для роутинга к правильному FCM проекту.
Best-effort семантика¶
Сбой публикации в Redis (NotificationPublisher.publish()) не откатывает вызывающую операцию.
Примеры:
- finalizeVerification() — push failure логируется и игнорируется, kyc-транзакция уже зафиксирована
- KycQueueService.submitKycJob() — fire-and-forget отдельно от DB write
Если NotificationsRedisClient.isInitialized == false (сервер поднялся без Redis), publish() логирует WARNING и возвращает без ошибки.