Перейти к основному содержимому

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 areapin-manager
Feature groupdeals
Статусdraft
Документdocs/06-services/crm/api/pin-manager/add-timeline-note.md

Актор и доступ

ПроверкаЗначение
Actor typeUser — Менеджер ПИН или Служба заботы (арбитраж)
Auth policy / Permissionspermission 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 не входит и работает сквозным доступом
Auditcreate: append-only строка ленты — бизнес-аудит действия (ActorUserId/ActorRole фиксируются в строке). Текст записи может содержать арбитражные детали/PII — в логи не пишется; сама сделка не мутируется
Доступ

Добавить ручную запись вправе только Менеджер ПИН или Служба заботы (INV-TLE-5) — обычные участники сделки (агент, инвестор, застройщик) этой операцией не пользуются. Permission crm.deals.annotate выдаётся в platform-scope: доступ сквозной, без tenant-фильтра и без привязки к record-level сделки.

HTTP-контракт

ПолеЗначение
MethodPOST
Success status201 (создана строка таймлайна)
Route/api/pin-manager/crm/deals/{dealId}/timeline-note
Idempotency headerне применяется — append-only: каждый вызов создаёт новую строку-факт (нет естественного dedup-ключа)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DealIdroute {dealId}GuidДа!= Guid.Empty; существующая сделка (шаг 2)
TextbodystringДа1..2000 после trim, не пустое (INV-TLE-5, payload varchar(2000))контент заметки — в логи не пишется
ActorUserIdidentity claimsGuidДаиз JWT
IsPinManageridentity claimsboolДафлаг «Менеджер ПИН» (авто-участник всех сделок, INV-DEAL-2)INV-TLE-5 gate
IsCareServiceidentity claimsboolДафлаг «Служба заботы» (арбитраж, не участник сделки)INV-TLE-5 gate

Модель ответа TimelineItemModel

Та же проекция строки ленты, что в get-deal-timeline.md; дом всех полей — deal-timeline-event.md. Ниже — значения для созданной строки ManualNote:

ПолеТипЗначение для ManualNoteОписание
IdGuidновый идентификаторИдентификатор строки таймлайна.
Typestring (enum DealTimelineEventType)"ManualNote"Тип строки ленты.
Statusstring (enum TimelineItemStatus)"Done"Факт-запись без целевого действия (не InProgress).
Titlestring«Ручная запись»Генерируется по типу.
OccurredAtDateTimeOffsetмомент внесения записиКлюч сортировки ленты (asc).
DueAtDateTimeOffset?nullДедлайна нет (не целевое действие).
CompletedAtDateTimeOffset?null
ActorRolestring? (enum ParticipantRole)"PinManager" или nullPinManager — для Менеджера ПИН (участник, INV-DEAL-2); null — Служба заботы (не участник). ActorUserId наружу не проецируется.
PayloadTimelineManualNotePayload { Text: string }{ Text }ContractVersion = 1; дом подтипа — deal-timeline-event.md.

Алгоритм

  1. Контекст и доступ. Операцию выполняет Менеджер ПИН или Служба заботы; требуется permission crm.deals.annotate в platform-scope. Platform-scope даёт сквозной кросс-tenant доступ ко всем сделкам.
  2. Проверка входа и предусловий.
    • формат: 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).
  3. Что читаем. Сделку по DealId — для подтверждения существования и её TenantId, который наследует создаваемая строка.
  4. Бизнес-правила. Статус и поля самой сделки не меняются. Создаётся 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).
  5. Что меняется. В ленте таймлайна сделки появляется новая append-only строка ManualNote; сама сделка не мутируется.
  6. Уведомления. Интеграционных Kafka-событий таймлайн не публикует (deal-timeline-event.md). Для ManualNote уведомление US-INV-11 «новое событие в таймлайне» доставляется по факту добавления строки → сервис notifications; у прочих типов ленты этот путь идёт через их собственные интеграционные события.
  7. Результат. Проекция созданной строки (Id, Type = "ManualNote", Status = "Done", Title, OccurredAt, Payload { Text }, ActorRole) → HTTP 201.

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событиянет — таймлайн Kafka-событий не публикует (deal-timeline-event.md)
Внешние вызовынет
Проекции / поискappend-only строка в ленте таймлайна (индекс по сделке и OccurredAt)
УведомленияUS-INV-11 «новое событие в таймлайне» → notifications. Для ManualNote это единственный путь доставки (нет порождающего интеграционного события)

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400DealId = Guid.Empty; Text пустой или длиннее 2000«Проверьте текст записи»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
FORBIDDEN403нет permission crm.deals.annotate«Доступ запрещён»no retry
CRM_TIMELINE_MANUAL_FORBIDDEN403актор не Менеджер ПИН / Служба заботы (INV-TLE-5)«Ручную запись может добавить только Менеджер ПИН или Служба заботы»no retry
CRM_DEAL_NOT_FOUND404сделка не существует«Сделка не найдена»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; dashboard pin-crm.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы