POST /api/messenger/chats/{chatId}/messages/text — Отправить текстовое сообщение
Назначение
Участник отправляет текстовое сообщение (Kind = Text) в чат. Операция создаёт сообщение, атомарно
выдаёт per-chat Seq (INV-CHAT-5), захватывает IpAddress/UserAgent отправителя (INV-MSG-6), публикует
MessagePostedEvent (консюмер notifications) и через persist-then-push доставляет MessageReceived в хаб.
Дом полей — message.md.
| Поле | Значение |
|---|
| Сервис/контекст | messenger |
| API area | messages |
| Feature group | messages |
| Статус | draft |
| Документ | docs/06-services/messenger/api/messages/send-text-message.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Инвестор / Агент / Руководитель / Менеджер застройщика / Менеджер ПИН) |
| Auth policy / Permissions | authenticated (JWT) + permission messenger.chats.write |
| Scope (RBAC) | не применяется: доступ по членству (ADR-0055/0052) |
| Record-level | активный ParticipantEntity (ChatId,UserId = actor, RemovedAt is null) — INV-CHAT-4 |
| Tenant isolation | не tenanted: чат кросс-сторонний; изоляция — членство (chat.md) |
| Audit | create (MessageEntity); SenderUserId = actor; IpAddress/UserAgent — Sensitive/internal, в логи/события не пишутся |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/messenger/chats/{chatId}/messages/text |
| Success status | 201 (сообщение создано) |
| Idempotency header | Idempotency-Key (= ClientMessageId, Guid) — дедуп двойной отправки при retry/reconnect |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ChatId | route | Guid | Да | != Guid.Empty; чат существует; actor — активный участник; Status != Closed (INV-CHAT-8); политика (см. Алгоритм) | — |
Body | body | string | Да | 1..8000 после trim (не пустое); контроль-символы нормализуются | PII: тело в логи не пишется |
ClientMessageId | header Idempotency-Key | Guid | Да | != Guid.Empty; ключ идемпотентности | дедуп ретраев |
UserId | identity claims | Guid | Да | Required; SenderUserId, предикат членства | scope isolation |
IpAddress | server (remote/XFF) | IpAddress | Да | захват сервером (INV-MSG-6); не из тела | Sensitive/internal |
UserAgent | header User-Agent | string | Да | <= 512; захват сервером (INV-MSG-6) | Sensitive/internal |
Модель ответа MessageModel
Проекция созданного сообщения (та же union-модель, что в list-messages.md); для Text
непуста под-модель Text = { Body }. PII-Body возвращается только автору-инициатору в ответе; IP/UA/
история правок в ответ не входят.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | MessageEntity.Id | — | Идентификатор сообщения. |
Seq | long | Да | per-chat порядковый | — | Выданный порядковый. |
Kind | string (MessageKind) | Да | Kind | — | Всегда Text. |
SenderUserId | Guid | Да | SenderUserId | — | Автор (= actor). |
SenderRole | string (ParticipantRole) | Да | SenderRole | — | Роль автора на момент отправки. |
SentAt | DateTimeOffset | Да | SentAt | — | Момент отправки. |
EditableUntil | DateTimeOffset | Да | SentAt + 60s | — | Граница окна правки/удаления (INV-MSG-1). |
Text | MessageTextModel | Да | TextMessageEntity | — | { Body: string }. |
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь-участник; требуется право
messenger.chats.write
(иначе — FORBIDDEN; без JWT — UNAUTHORIZED). ChatId берётся из маршрута, Body — из тела,
ClientMessageId — из Idempotency-Key; IpAddress/UserAgent захватываются сервером. По
ClientMessageId проверяется идемпотентность: если запрос уже обработан — вернуть сохранённое сообщение
(201), не создавая дубль.
- Проверка входа и предусловий.
Body 1..8000 (trim, не пустое), ChatId/ClientMessageId непусты,
UserAgent <= 512, IpAddress задан (INV-MSG-6) — иначе VALIDATION_FAILED. Актор должен быть активным
участником — иначе MSG_CHAT_ACCESS_DENIED. Чат не Closed — иначе MSG_CHAT_CLOSED. Коммуникационная
политика для Direct-чата: ни одна сторона не в чёрном списке (blacklist.md,
contact.md, INV-CHAT-3) — иначе MSG_COMMUNICATION_BLOCKED.
- Что читаем. Чат — для монотонной выдачи
Seq (сериализация конкурентных отправок, INV-CHAT-5/
INV-MSG-3) и источника роли участника.
- Бизнес-правила и маппинг. Создаётся текстовое сообщение:
SenderUserId = actor, SenderRole — роль
участника, IsSystem = false, Body, SentAt = now, EditableUntil = now + 60s, IpAddress/
UserAgent. Чат выдаёт Seq (per-chat монотонный), обновляет LastMessageId/Seq/At и LastMessagePreview
(ограничено ≤ 256).
- Что меняется. Появляется сообщение, чат обновляет денормализацию и счётчик
Seq. Всё фиксируется в
одном согласованном изменении с публикацией события, намерением realtime-доставки и отметкой
идемпотентности; конфликт выдачи Seq → MSG_SEQ_CONFLICT (авто-retry).
- Факты и события. Публикуется интеграционное событие
MessagePostedEvent в топик
pin.messenger.message-posted.v1 (ContractVersion = 1, ключ партиционирования ChatId, payload
{ChatId, MessageId, Seq, Kind = Text, SenderUserId, SenderRole, SentAt, Preview} — без IP/UA/тела;
консюмер notifications, контракт message-posted.md). Плюс намерение
realtime-доставки для realtime-dispatch (persist-then-push → хаб MessageReceived, группа chat:{ChatId}).
- Результат.
MessageModel (Kind = Text, Seq, EditableUntil, Text), 201.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Интеграционные события | MessagePostedEvent → pin.messenger.message-posted.v1 (key ChatId, консюмер notifications) |
| Проекции / поиск | денорм last-message на ChatEntity (LastMessageId/Seq/At/Preview) |
| Уведомления | in-app realtime MessageReceived (persist-then-push); offline push/email — notifications как консюмер события |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | Body пуст/> 8000, пустой ChatId/ClientMessageId, нет IP/UA (INV-MSG-6) | «Проверьте текст сообщения» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
FORBIDDEN | 403 | нет permission messenger.chats.write | «Недостаточно прав» | no retry |
MSG_CHAT_ACCESS_DENIED | 403 | actor — не активный участник (или чат не существует) | «Нет доступа к чату» | no retry |
MSG_COMMUNICATION_BLOCKED | 409 | чёрный список любой из сторон (INV-CHAT-3) | «Отправка сообщений недоступна» | no retry |
MSG_CHAT_CLOSED | 409 | чат Closed (сделка завершена/отменена, INV-CHAT-8) | «Чат закрыт» | no retry |
MSG_SEQ_CONFLICT | 409 | гонка выдачи Seq (INV-MSG-3) | «Повторите отправку» | retry (авто) |
Acceptance Criteria
- Отправка текста. Дано активный участник, чат не
Closed, Body в 1..8000 после trim, ни одна
сторона не в чёрном списке. Когда POST. Тогда создаётся сообщение Kind = Text, выдаётся per-chat Seq
(INV-CHAT-5), публикуется MessagePostedEvent в pin.messenger.message-posted.v1 (ключ ChatId, без
тела/IP/UA), push MessageReceived (persist-then-push), ответ 201 с EditableUntil = SentAt + 60s.
- Идемпотентный повтор. Дано тот же
Idempotency-Key (ClientMessageId). Когда retry/reconnect. Тогда
возвращается ранее сохранённое сообщение (201), дубля нет.
- Пустое/длинное тело. Дано
Body пуст после trim или > 8000. Когда POST. Тогда VALIDATION_FAILED
(400).
- Чёрный список (Direct). Дано
Direct-чат и одна из сторон в чёрном списке (INV-CHAT-3). Когда POST.
Тогда MSG_COMMUNICATION_BLOCKED (409).
- Закрытый чат. Дано чат
Closed (сделка завершена/отменена, INV-CHAT-8). Когда POST. Тогда
MSG_CHAT_CLOSED (409).
- Не участник. Дано актор не активный участник. Когда
POST. Тогда MSG_CHAT_ACCESS_DENIED (403).
- Гонка выдачи
Seq. Дано конкурентные отправки в один чат. Когда Seq конфликтует. Тогда
MSG_SEQ_CONFLICT (409) с авто-retry; Seq остаётся монотонным per-chat (INV-MSG-3).
- Приватность.
Body/IP/UA не входят в событие и логи (INV-MSG-6); PII-Body возвращается только
автору-инициатору в ответе.
Совместимость и наблюдаемость
- Новые optional поля модели/payload — additive; удаление/переименование/смена типа или значения enum —
breaking (migration + rollout; событие — новый
.v2 + dual-publish). Лимит Body — конфигурация (расширение additive).
- Logs:
text-message-posted (ChatId, MessageId, Seq, SenderUserId; Body/IP/UA не пишутся);
metrics: messenger_messages_posted_total{kind,result}, persist p95 < 300 мс (SLO); traces через
traceparent; dashboard pin-messenger.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы
- Сущности: message.md (
Text, EditableUntil, INV-MSG-1/3/6),
chat.md (выдача Seq, INV-CHAT-3/5/8), participant.md,
blacklist.md, contact.md.
- Событие: message-posted.md.
- Соседние: send-attachment-message.md, edit-message.md,
delete-message.md, list-messages.md; README — ../../README.md.
- ADR: 0055,
0056.