Batele · Чаты и поддержка обзор модели + демо 5 поверхностей
Демо · эпик batele-microservices#481 · issue batele-turbo#18
Эпик · чаты · realtime · поддержка · модель v3

Чаты между сторонами заказа и служба поддержки

Дизайн-демо к эпику batele-microservices#481. Модель v3 (скетч 10.07): чат — это долгоживущий канал пары участников, не привязанный к заказу; контекст (заказ / доставка / тикет) живёт на сообщении и работает как фильтр. Realtime: typing / read / presence / непрочитанное, офлайн → push. Все пять поверхностей используют сквозную легенду: заказ #4821 · доставка D-7301 · ресторан «Тандыр» · клиентка Айгерим А. · курьерская служба «Boomerang Go» · курьер Бакыт Т. · тикеты VS-102 / TS-88 / INC-311 — одна и та же история видна глазами каждой роли.

Статус: модель v3 отражает скетч от 10.07 («Чат → Заказ» зачёркнут) и ждёт апрува. После апрува обновляем план эпика #481 (схема chat/scopeRef → канал пары + контекст на сообщении).

Демо Пять поверхностей

Каждая карточка — самостоятельный прототип в дизайн-системе своего приложения. Мобильные демо уже разобраны на задачи Flutter-командам: vendor → batele_vendor_flutter#6, курьер → boomerang_go_flutter#1, b2c → boomerang-b2c#1.

🛵
Курьер — мобилка
Чаты назначенной доставки: клиент, вендор, свой диспетчер. Quick-replies, фото-подтверждение, офлайн-очередь, push, архив после завершения.
3 пары курьераbatele_pro_flutter
🏪
Вендор — web
Раздел «Чаты» в batele-vendor: инбокс чатов заказов (three-pane), ответ от имени организации с подписью сотрудника, консоль vendor_support: тикеты, SLA, статусы.
чаты заказовvendor_supportMantine
📱
Вендор — мобилка
Те же сценарии в дизайне batele_vendor_flutter: инбокс «Заказы | Поддержка», тред с подписью автора, контекст заказа bottom-sheet, тикет и смена статуса, право «Чаты».
инбокс + тикетыFlutter · монохром
🍽️
Клиент B2C — мобилка
Чаты из трекинга заказа: ресторан, курьер, служба доставки (виртуальные пары, блокировка до назначения) + поддержка платформы: FAQ, обращения, тред тикета.
3 пары клиентаplatform_supportbatele_b2c_flutter
🚚
Курьерская служба — web (batele-go)
Переписки службы по доставкам: курьер, вендор, клиент (three-pane, ответ от имени службы) + инциденты платформы, где служба — сторона (INC-311).
3 пары службыинциденты по намMantine
🛡️
Root admin — консоль поддержки
Инциденты платформы + общая поддержка: очереди, SLA-таймеры и просрочки, назначение агентов, беседа с внутренними заметками, авто-инциденты, эскалация тикета в инцидент.
platform_incidentplatform_supportweb/root

Модель v3 Канал пары + контекст на сообщении

Участник
  • тип: Курьер · КурСлужба · Вендор · Клиент · SuperAdmin · SuperOrg
  • идентификаторы: courierId / orgId / memberId / userId
  • орг-стороны пишут от имени организации, автор — senderMemberId
Чат
  • долгоживущий канал пары участников — не привязан к заказу
  • одна пара — один чат: вся история отношений (много заказов/доставок)
  • создаётся лениво при первом сообщении; до этого — «виртуальная» строка
  • не закрывается при завершении заказа
Сообщение
  • id, chatId + опциональный контекст:
  • orderId? (vendor order) + orderShortId?
  • deliveryId? (go delivery)
  • ticketId? (обращение/инцидент)
  • контекст рисуется чипом на сообщении: «#4821» · «D-7301» · «TS-88»
Два вида одного чата: ① вся история пары — сообщения с чипами контекста (filter: chatId); ② фильтр-режим — тот же канал, сужённый до одного заказа/доставки/тикета (filter: chatId AND deliveryId): чип-фильтр в шапке «D-7301 ✕», композер автоматически тегирует новые сообщения активным контекстом. Клик по чипу сообщения включает фильтр, «✕» снимает. Вход из карточки заказа/доставки открывает всю историю канала (сообщения других заказов видны) — в шапке чип активного контекста, композер авто-тегирует новые сообщения; фильтр «только этот заказ» — опциональный вид. Там же табы собеседников «Клиент / Заведение / Курьер»: быстрое переключение вендор→курьер одним тапом; непрочитанное на табе — точка (unread считается на канал).

Пары остаются те же (каждая сторона видит свои 3 + поддержка): доступность пар с курьером/службой по-прежнему появляется после назначения доставки — но это влияет только на «когда можно написать первым в контексте», а не на существование канала.

Пара (канал)Когда доступна для первого сообщенияТиповой сценарийГде в демо
Клиент ↔ Вендор с момента создания заказа «Можно без лука?», уточнения по составу и брони c2c · vendor-web · vendor-mobile
Клиент ↔ Служба после назначения доставки вопросы по доставке в целом, до/вместо курьера c2c · go-web
Клиент ↔ Курьер после назначения курьера «Я у подъезда, код 45#», фото передачи c2c · courier-mobile
Вендор ↔ Служба после назначения доставки «Курьер задерживается?», готовность заказа vendor-web · go-web
Вендор ↔ Курьер после назначения курьера «Какой из двух пакетов #4821?» vendor-web · courier-mobile
Служба ↔ Курьер после назначения курьера эскалация: «клиент не отвечает 10 минут, что делать?» go-web · courier-mobile
Правило UI: в контексте заказа пары с курьером/службой до назначения доставки показываются заблокированными («станет доступен после назначения курьера») — пользователь понимает, что канал появится. Уже существующие каналы пар (из прошлых заказов) видны в инбоксе всегда.

Идентичность Организация пишет, сотрудник подписан

Для вендора и курьерской службы участник чата — организация (actorId = orgId/deptId): любой сотрудник с правом чата читает и пишет от её имени. В каждом сообщении фиксируется реальный автор — senderMemberId. Клиент и курьер — индивидуальные акторы.

Поддержка Три контура поверх чатов

Тикет (support_ticket) = карточка со статусом/SLA/назначением; лента тикета — это фильтр канала по ticketId: у пользователя один долгоживущий канал с поддержкой (SuperAdmin↔Клиент, SuperAdmin↔Служба…), сообщения обращений TS-88 / TS-71 тегированы и фильтруются в нём. Закрытие тикета не закрывает канал. Статусная модель: openpendingresolvedclosed.

КонтурАгентыСкоупКто открываетКонсоль в демо
platform_incident Инциденты платформы админы города / страны маршрутизация по cityId; severity пользователи и авто-система (просрочка доставки, отмена после оплаты, спор оплаты) root-admin (агент) · go-web (сторона)
platform_support Общая поддержка админы платформы любой пользователь платформы клиент / курьер / сотрудник — из своего приложения root-admin (агент) · c2c-mobile (клиент)
vendor_support Поддержка вендора сотрудники вендора строго в рамках orgId клиенты вендора vendor-web / vendor-mobile (агент) · c2c (клиент)
SLA в демо: первый ответ — 15 минут, решение — 4 часа. Таймер виден в списке и в карточке тикета; просрочка подсвечивается красным и поднимается в дашборд root-консоли (сквозной пример — неназначенный TS-87, просрочен на 12 мин).

Состояния Сообщение и тред

Статусы сообщения
  • 🕓 в очереди / отправка — есть tempId, нет ack; при офлайне копится в очереди
  • отправлено — сервер персистировал, ack вернул реальный id
  • ✓✓ прочитано — получатель прислал chat:read (по lastReadMessageId)
  • ⚠︎ ошибка — retry-кнопка, сообщение остаётся в композере/очереди
  • изменено (editedAt) · 🗑 удалено (deletedAt, «Сообщение удалено»)
  • системное — по центру серым: «Курьер назначен», «Заказ завершён — чат в архиве», «Инцидент создан автоматически»
Состояния треда
  • виртуальный — пара доступна, канала в БД нет; пустое состояние «Напишите первым»
  • 🔒 заблокирован — контекст ещё не дал пару (курьер/доставка не назначены)
  • активный — канал живёт; unread-бейдж, typing, presence
  • фильтр — тот же канал, сужен до контекста: чип «#4821 ✕» в шапке, композер тегирует
  • закрытый тикет — фильтр-вид closed-тикета read-only; сам канал не закрывается

Edge-cases Обязательные к покрытию в каждой поверхности

КейсПоведение UI
Нет сетибаннер «Нет соединения — сообщения отправятся автоматически»; исходящие копятся в очереди с 🕓; после reconnect — авто-отправка по tempId (идемпотентно, без дублей)
Офлайн-участникоркестратор уведомлений смотрит presence: онлайн → доставит realtime (push подавлен), офлайн → push «новое сообщение»; бейдж unread синхронизируется
Канал не материализованвиртуальная строка пары: пустое состояние + композер; первое сообщение идемпотентно создаёт канал (двойной тап ≠ два канала)
Сторона не назначенав контексте заказа пара с курьером/службой показана с замком «после назначения курьера»; системное «Курьер назначен» появляется в канале с меткой контекста
Чип контекстаклик по чипу «#4821»/«D-7301»/«TS-88» на сообщении → включает фильтр-режим канала; «✕» на чипе в шапке — снимает
Сообщение без контекстаразрешено (обычная реплика в канале); в фильтр-режиме композер тегирует автоматически — «отправится с меткой D-7301»
Вложенияфото (грид), файл (чек PDF); загрузка через S3 presign; состояние «отправка…» на медиа; ограничения размера — TBD
Typing / read«печатает…» — эфемерно, без персиста; read — разделитель «Непрочитанные» + ✓✓ у отправителя
Непрочитанноебейджи на трёх уровнях: пункт меню/таб → тред в списке → разделитель в ленте; счётчик приходит с ListChats и обновляется по WS
Эскалация в поддержкукурьер → чат со своей службой; клиент → «Помощь» (platform_support); агент root — кнопка «Эскалировать в инцидент» на тикете; авто-инциденты по доменным событиям
Нет правасотрудник без права «Чаты» видит заглушку с подсказкой попросить администратора роли
Заказ завершёнканал пары не закрывается — история остаётся, писать можно; завершение видно системным сообщением в контексте; ретеншн сообщений — открытый вопрос (TBD)

Протокол Путь сообщения (сквозной)

Клиент шлёт chat:send {chatId|pair, body, tempId, ctx:{orderId?, deliveryId?, ticketId?}}

WebSocket → apis/realtime-gateway; UI сразу рисует пузырь со статусом 🕓 по tempId; в фильтр-режиме ctx подставляется автоматически.

Шлюз проверяет участие и передаёт в сервис

chat.CanAccess (с кешем) → chat.SendMessage; для виртуальной пары — идемпотентная материализация канала.

apps/chat персистит и публикует

Событие chat.message.created в Redis (chatId + получатели).

Fan-out в комнату

Шлюз эмитит chat:message в chat:{chatId}; отправителю — ack с реальным id вместо tempId → статус ✓.

Офлайн-получателям — push

apps/notification слушает событие, сверяет presence в Redis, офлайн-участникам шлёт push «новое сообщение».

Read / typing

chat:read → chat.MarkRead → эмит остальным (✓✓); chat:typing — эфемерно, не персистится.

Детализация UX-акценты по фазам эпика

ФазаЧто получает пользовательUX-требования / риски
0 · #475
apps/notification
те же push/SMS, но из одного сервиса поведение и тексты 1:1 с docs/NOTIFICATIONS.md; фундамент для «push только офлайн» и бейджа GetUnreadBadge
1 · #480
realtime-gateway
единое WS-подключение, стабильный reconnect UX реконнекта: баннер офлайна, очередь отправки, ресинк непрочитанного после reconnect; presence с TTL+heartbeat — не «мигать» статусом при кратких обрывах
2 · #476
ядро чатов
первая переписка Клиент↔Вендор статусы 🕓/✓/✓✓, unread-разделитель, подпись сотрудника у орг-стороны, вложения (если войдут в MVP), архив по завершении заказа
3 · #477
каналы пар + контексты
все пары у всех сторон, фильтры по контексту виртуальные пары, замки до назначения, чипы контекста на сообщениях, фильтр-режим (chatId + orderId/deliveryId), сегмент собеседников в контексте заказа, идемпотентность первого сообщения
4 · #478
apps/support
тикеты и инциденты в трёх контурах SLA-таймеры и просрочки, очередь «не назначено», «взять в работу», внутренние заметки vs ответ, эскалация тикета в инцидент, авто-инциденты с деталями события
5 · #479
фронтенды
пять поверхностей этого демо сквозная согласованность: одна легенда, одинаковые статусы/бейджи/пустые состояния во всех приложениях; mobile-реализации по issue во Flutter-репо

TBD Открытые вопросы (в план после апрува v3)