POST /api/pin-manager/crm/deals/{dealId}/timeline-note — Ручная запись в таймлайн
Назначение
Даёт Менеджеру ПИН и Службе заботы добавить ручную запись во вкладку «Ход сделки» (US-MGR-05):
пояснение при ручном подтверждении этапов, арбитражная заметка, комментарий постфактум. Тип строки —
ManualNote с полезной нагрузкой TimelineManualNotePayload { Text }; это единственный тип таймлайна,
который создаётся прямой командой оператора, а не автоматически как отметка о произошедшей мутации сделки.
Лента событий append-only (deal-timeline-event.md, INV-TLE-1): запись
не редактируется и не удаляется. Добавлять ручную запись вправе только Менеджер ПИН / Служба заботы
(INV-TLE-5).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | pin-manager |
| Feature group | deals |
| Статус | draft |
| Документ | docs/06-services/crm/api/pin-manager/add-timeline-note.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User — Менеджер ПИН или Служба заботы (арбитраж) |
| Auth policy / Permissions | permission crm.deals.annotate в platform-scope (identity/domain/permission.md); выдаётся Менеджеру ПИН и Службе заботы |
| Scope (RBAC) | платформа (сквозной доступ ко всем сделкам); record-level по агентству/застройщику не применяется |
| Record-level | не применяется — platform-scope обходит record-level (ADR-0052: кросс-tenant только у Менеджера ПИН / Службы заботы) |
| Tenant isolation | не tenanted: Deal.TenantId = DeveloperCompanyId, а platform-scope актор в этот tenant не входит и работает сквозным доступом |
| Audit | create: append-only строка ленты — бизнес-аудит действия (ActorUserId/ActorRole фиксируются в строке). Текст записи может содержать арбитражные детали/PII — в логи не пишется; сама сделка не мутируется |
Добавить ручную запись вправе только Менеджер ПИН или Служба заботы (INV-TLE-5) — обычные участники сделки
(агент, инвестор, застройщик) этой операцией не пользуются. Permission crm.deals.annotate выдаётся в
platform-scope: доступ сквозной, без tenant-фильтра и без привязки к record-level сделки.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Success status | 201 (создана строка таймлайна) |
| Route | /api/pin-manager/crm/deals/{dealId}/timeline-note |
| Idempotency header | не применяется — append-only: каждый вызов создаёт новую строку-факт (нет естественного dedup-ключа) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
DealId | route {dealId} | Guid | Да | != Guid.Empty; существующая сделка (шаг 2) | — |
Text | body | string | Да | 1..2000 после trim, не пустое (INV-TLE-5, payload varchar(2000)) | контент заметки — в логи не пишется |
ActorUserId | identity claims | Guid | Да | из JWT | — |
IsPinManager | identity claims | bool | Да | флаг «Менеджер ПИН» (авто-участник всех сделок, INV-DEAL-2) | INV-TLE-5 gate |
IsCareService | identity claims | bool | Да | флаг «Служба заботы» (арбитраж, не участник сделки) | INV-TLE-5 gate |
Модель ответа TimelineItemModel
Та же проекция строки ленты, что в get-deal-timeline.md; дом всех полей —
deal-timeline-event.md. Ниже — значения для созданной строки
ManualNote:
| Поле | Тип | Значение для ManualNote | Описание |
|---|---|---|---|
Id | Guid | новый идентификатор | Идентификатор строки таймлайна. |
Type | string (enum DealTimelineEventType) | "ManualNote" | Тип строки ленты. |
Status | string (enum TimelineItemStatus) | "Done" | Факт-запись без целевого действия (не InProgress). |
Title | string | «Ручная запись» | Генерируется по типу. |
OccurredAt | DateTimeOffset | момент внесения записи | Ключ сортировки ленты (asc). |
DueAt | DateTimeOffset? | null | Дедлайна нет (не целевое действие). |
CompletedAt | DateTimeOffset? | null | — |
ActorRole | string? (enum ParticipantRole) | "PinManager" или null | PinManager — для Менеджера ПИН (участник, INV-DEAL-2); null — Служба заботы (не участник). ActorUserId наружу не проецируется. |
Payload | TimelineManualNotePayload { Text: string } | { Text } | ContractVersion = 1; дом подтипа — deal-timeline-event.md. |
Алгоритм
- Контекст и доступ. Операцию выполняет Менеджер ПИН или Служба заботы; требуется permission
crm.deals.annotateв platform-scope. Platform-scope даёт сквозной кросс-tenant доступ ко всем сделкам. - Проверка входа и предусловий.
- формат:
DealId != Guid.Empty,Textне пустой после trim и1..2000символов → иначеVALIDATION_FAILED; - актор (INV-TLE-5): доступно только Менеджеру ПИН / Службе заботы (
IsPinManager || IsCareService); иначе —CRM_TIMELINE_MANUAL_FORBIDDEN(403); - существование сделки: сделки с таким
DealIdнет →CRM_DEAL_NOT_FOUND(404); запрос platform-scope, tenant-фильтр не применяется; - допустимость состояния: ручную запись можно добавить к сделке в любом
DealStatus, включая терминальныеCompleted/Cancelled(пояснение/арбитраж постфактум); append-only ленту это не нарушает (INV-TLE-1).
- формат:
- Что читаем. Сделку по
DealId— для подтверждения существования и еёTenantId, который наследует создаваемая строка. - Бизнес-правила. Статус и поля самой сделки не меняются. Создаётся append-only строка
ManualNote:Type = ManualNote;Status = Done(факт-запись,DueAt/CompletedAt = null);Title = «Ручная запись»;OccurredAt = now;TenantIdнаследуется от сделки;ActorRole = PinManager, если запись вносит Менеджер ПИН (авто-участник, INV-DEAL-2), иначеnull(Служба заботы — не участник);Payload = TimelineManualNotePayload { Text }(ContractVersion = 1). - Что меняется. В ленте таймлайна сделки появляется новая append-only строка
ManualNote; сама сделка не мутируется. - Уведомления. Интеграционных Kafka-событий таймлайн не публикует
(deal-timeline-event.md). Для
ManualNoteуведомление US-INV-11 «новое событие в таймлайне» доставляется по факту добавления строки → сервисnotifications; у прочих типов ленты этот путь идёт через их собственные интеграционные события. - Результат. Проекция созданной строки (
Id,Type = "ManualNote",Status = "Done",Title,OccurredAt,Payload { Text },ActorRole) → HTTP201.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | нет — таймлайн Kafka-событий не публикует (deal-timeline-event.md) |
| Внешние вызовы | нет |
| Проекции / поиск | append-only строка в ленте таймлайна (индекс по сделке и OccurredAt) |
| Уведомления | US-INV-11 «новое событие в таймлайне» → notifications. Для ManualNote это единственный путь доставки (нет порождающего интеграционного события) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | DealId = Guid.Empty; Text пустой или длиннее 2000 | «Проверьте текст записи» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
FORBIDDEN | 403 | нет permission crm.deals.annotate | «Доступ запрещён» | no retry |
CRM_TIMELINE_MANUAL_FORBIDDEN | 403 | актор не Менеджер ПИН / Служба заботы (INV-TLE-5) | «Ручную запись может добавить только Менеджер ПИН или Служба заботы» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделка не существует | «Сделка не найдена» | no retry |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор — Менеджер ПИН или Служба заботы с permission
crm.deals.annotateв platform-scope (INV-TLE-5); platform-scope даёт сквозной кросс-tenant доступ; - сделка существует (любого tenant, любого статуса — включая терминальные
Completed/Cancelled: пояснение/арбитраж постфактум допустимы).
Ручную запись можно добавить к сделке в любом DealStatus, включая терминальные Completed/Cancelled —
это единственная операция таймлайна, не заблокированная терминальностью сделки (арбитраж/пояснение
постфактум); append-only гарантия ленты (INV-TLE-1) при этом не нарушается.
Постусловия (гарантированы после успеха):
- в ленту таймлайна добавлена append-only строка
ManualNote(Status = Done,Title = «Ручная запись»,Payload = { Text }); сама сделка не мутируется (статус/поля без изменений); - Kafka-события таймлайн не публикует; уведомление US-INV-11 «новое событие в таймлайне» доставляет
notificationsпо факту добавления строки (дляManualNote— единственный путь доставки).
Acceptance Criteria
- Happy path (Менеджер ПИН). Дано: существующая сделка, актор — Менеджер ПИН. Когда:
POST …/timeline-note {text}(1..2000). Тогда:201, строкаManualNote,Status = Done,ActorRole = PinManager; сделка не изменена. - Happy path (Служба заботы). Дано: актор — Служба заботы (не участник сделки). Когда:
POST. Тогда:201,ActorRole = null(ActorUserIdнаружу не проецируется). - Терминальная сделка. Дано: сделка
Completed/Cancelled. Когда:POST. Тогда:201(арбитраж/пояснение постфактум; append-only не нарушается). - Актор без права аннотирования. Дано: у актора нет
crm.deals.annotate. Когда:POST. Тогда:403 FORBIDDEN. - Актор с правом, но не Менеджер ПИН/Служба заботы. Когда:
POST. Тогда:403 CRM_TIMELINE_MANUAL_FORBIDDEN(INV-TLE-5). - Пустой/слишком длинный текст. Когда:
Textпуст после trim или длиннее 2000. Тогда:400 VALIDATION_FAILED; текст записи в логи не пишется. - Несуществующая сделка. Когда:
DealIdне найден. Тогда:404 CRM_DEAL_NOT_FOUND.
Совместимость и наблюдаемость
- Тело — только
Text; новые optional-поля — additive; enum строкой; клиент обязан игнорировать неизвестныеType/Status/подтипPayload(forward-compatible, дом — deal-timeline-event.md). Удаление/переименование поля или смена семантики маршрута — breaking (migration + rollout). - Logs:
timeline-manual-note added(dealId,tenantId,actorUserId,noteId; текст записи в логи не пишется — арбитражные детали/PII); metrics:crm_pin_manager_operations_total{operation="add-timeline-note"}, latency p95 < 500 мс (SLO команд); traces — сквозной trace черезtraceparent; dashboardpin-crm.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): POST /api/pin-manager/crm/deals/{dealId}/override-status — Ручная смена статуса сделки
- Use Cases (1): UC-CRM-006. Вмешательство Менеджера ПИН
- registries (1): Реестр API-методов PIN
Связанные документы
- Дом ленты (поля/типы/индексы, INV-TLE-1/3/5,
TimelineManualNotePayload,TimelineItemStatus): domain/deal-timeline-event.md; родитель (ParticipantRole, авто-PinManagerINV-DEAL-2): domain/deal.md. - Модель ответа
TimelineItemModel(проекция строки ленты): get-deal-timeline.md. - Permission (дом): identity/domain/permission.md.
- Уведомление US-INV-11 доставляет
notifications; для прочих типов ленты — как консюмер их интеграционных событий: ../../events/deal-created.md, ../../events/deal-stage-changed.md, ../../events/booking-created.md, ../../events/document-requested.md. - Карта области pin-manager и событий: ../../README.md. Решения: ADR-0052 (platform-scope кросс-tenant), ADR-0056.