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

UC-CRM-006. Вмешательство Менеджера ПИН

Назначение

Ручное вмешательство Менеджера ПИН в сделку любого застройщика платформы: сквозной просмотр сделок/лидов/фиксаций, ручная смена статуса и этапа при конфликтах, ручное подтверждение этапа с записью в таймлайн, проверка и скачивание всех документов (включая PII-типы) и участие в чате сделки. Агрегат — DealEntity. Покрывает US-MGR-01..06.

Metadata

ПолеЗначение
IDUC-CRM-006
Контекстcrm (см. docs/06-services/crm/)
Типосновной (+ альтернативные потоки A1–A3)
Статусdraft

Актор

ПолеЗначение
РольМенеджер ПИН (ParticipantRole.PinManager — авто-участник всех сделок, INV-DEAL-2)
Permissions / scopecrm.deals.view/crm.deals.manage/crm.deals.decide, crm.deal-documents.view, crm.pipeline.manageplatform-scope (кросс-tenant, ADR-0052); ручной таймлайн — INV-TLE-5
Tenantnull (сквозной доступ ко всем TenantId; ADR-0052 «кросс-tenant — только platform-scope»)
Record-levelне применяется (сквозная видимость поверх record-level предикатов сторон)

Предусловие

  • Actor аутентифицирован; несёт platform-scope permission Менеджера ПИН (identity).
  • Целевая сделка существует (DealEntity); для ручного статуса/этапа — не терминальная (Status ∉ {Completed, Cancelled}, INV-DEAL-9).
  • Менеджер ПИН — активный участник сделки (DealMemberEntity с ролью PinManager, RemovedAt is null).

Триггер

ПолеЗначение
ИсточникUI-действие / вызов API
Триггерящий элементКабинет Менеджера ПИН: сквозной список/канбан и карточка сделки (/pin-manager/deals, /pin-manager/deals/[id])

Основной Поток

  1. Менеджер открывает сквозной список сделок платформы (фильтры: агент/девелопер/статус/объект) → GET /api/pin-manager/crm/deals (pin-manager-list-deals.md); выборка — без tenant-предиката (platform-scope).
  2. Открывает карточку конфликтной сделки → GET /api/pin-manager/crm/deals/{id} (pin-manager-get-deal.md) с составом участников, таймлайном, документами, бронью.
  3. Ручная смена статуса (US-MGR-03). Менеджер выбирает целевой статус (Заявка/Бронь/Продажа/Отказ)
    • Проверка входа: статус ∈ {Application, Booking, Sale, Cancelled}; Comment не пуст; при Cancelled — задан CancelReason.
    • Сделка загружается; guard INV-DEAL-9 (не терминальна) — иначе CRM_DEAL_ALREADY_TERMINAL.
    • Целевой DealStatus маппится в PipelineStepGroup (pipeline-step.md), выбирается живой этап группы (default по Order, INV-STEP-3); проставляются Status/StepId/StepGroup; событие DealStatusChanged.
    • Подписчик пишет таймлайн StatusChanged (Reason = comment).
    • Интеграционное DealStageChangedEvent (FromStatus/ToStatus, FromStepId/ToStepId, ChangedByUserId = actor) — в outbox.
    • Агрегат + таймлайн + outbox фиксируются атомарно в одном согласованном изменении.
  4. Перевод этапа (US-MGR-05). Менеджер перетаскивает карточку → POST /api/pin-manager/crm/deals/{id}/move-step (pin-manager-move-deal-step.md); перевод на этап (INV-DEAL-5: целевой этап того же резолвнутого TenantId, не архивирован; денормализует StepGroup); таймлайн StageChanged; тот же DealStageChangedEvent (FromStepId → ToStepId).
  5. Ручное подтверждение этапа + запись таймлайна (US-MGR-05). Менеджер добавляет заметку («Документы приняты вручную») → POST /api/pin-manager/crm/deals/{id}/timeline-notes (pin-manager-add-timeline-note.md); создаётся DealTimelineEventEntity{ Type = ManualNote, Payload = TimelineManualNotePayload{Text}, Status = Done, ActorRole = PinManager } (INV-TLE-5); поднимается TimelineItemAdded → push/email через notifications (US-INV-11). Kafka-события таймлайн не публикует.
  6. Проверка и скачивание документов (US-MGR-04). Вкладка «Документы» карточки (шаг 2) отдаёт весь список включая PII-типы (ClientPassport и др.); скачивание → GET /api/pin-manager/crm/deals/{id}/documents/{documentId}/download (pin-manager-download-deal-document.md, будущий документ) — пресайн-ссылка storage. Эскроу в таймлайне остаётся маской ****NNNN.
  7. Чат сделки (US-MGR-06). Менеджер уже авто-участник (PinManager); чат живёт в messenger (ADR-0055), CRM только поставляет события состава (deal-members-changed.md).
  8. Фронт инвалидирует данные карточки/списка; участники получают уведомление и системную карточку в чате.

Альтернативные Потоки

примечание
A1. Отказ по сделке (статус Отказ)
  1. Менеджер выбирает Отказ и указывает CancelReason (обязателен, INV-DEAL-8).
  2. Отказ: * → Cancelled, активная бронь отменяется тем же изменением; параллельно публикуется deal-cancelled.md (объект недвижимости освобождает консюмер chessboard) вместе с DealStageChangedEvent.

Завершается терминальным статусом.

A2. Добавление участия Менеджера ПИН в состав сделки
  1. Если у сделки нет активного участника PinManager, участие устанавливается операцией добавления участника (Role = PinManager, actor = system; INV-DEAL-3).
  2. Публикуется deal-members-changed.mdmessenger синхронизирует состав чата.

Возвращается к основному потоку (шаг 1).

A3. Целевой этап недоступен
  1. При move-step целевой этап архивирован/чужого tenant → CRM_DEAL_STEP_FORBIDDEN (INV-DEAL-5), мутаций нет.

Клиент показывает ошибку, состояние сделки не меняется.

Постусловие

  • DealEntity.Status/StepId/StepGroup обновлены; UpdatedAt/UpdatedByUserId = момент/actor.
  • В crm.deal_timeline_events добавлены строки (StatusChanged с комментарием, StageChanged, ManualNote) — append-only (INV-TLE-1).
  • В outbox — DealStageChangedEventDealCancelledEvent при отказе); участники уведомлены, карточка в чате обновлена.
  • Скачанные документы/эскроу наружу не логируются в открытом виде (Sensitive — маска).

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
FORBIDDEN403Actor без platform-scope permissionСкрыть действия вмешательства
CRM_DEAL_NOT_FOUND404Сделка по {id} не найденаОбновить список
CRM_DEAL_ALREADY_TERMINAL409Override/move на Completed/Cancelled (INV-DEAL-9)Показать «сделка завершена/отменена»
CRM_DEAL_CANCEL_REASON_REQUIRED422Статус Отказ без CancelReason (INV-DEAL-8)Запросить причину
CRM_DEAL_STEP_FORBIDDEN409Этап архивирован/чужой tenant (INV-DEAL-5)Ошибка, не менять этап
CRM_TIMELINE_MANUAL_FORBIDDEN403ManualNote не от PinManager/Службы заботы (INV-TLE-5)Скрыть добавление заметки
CONCURRENCY_CONFLICT409Гонка конкурентной правки (оптимистичная блокировка)Перечитать карточку и повторить

Доменные События

СобытиеКогда поднимаетсяSubscribers
DealStatusChangedРучная смена статуса / отказподписчик таймлайна (StatusChanged), сборщик DealStageChangedEvent
DealMovedToStepПеревод на этапподписчик таймлайна (StageChanged), сборщик DealStageChangedEvent
TimelineItemAddedДобавление строки ManualNote/любой строкиnotifications (push/email «новое событие», US-INV-11)
DealMemberAddedДобавление участника (A2)сборщик DealMembersChangedEvent

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
DealStageChangedEventpin.crm.deal-stage-changed.v1Ручная смена статуса/этапа (шаги 3–4) — контрактmessenger (карточка в чат), notifications (участникам)
DealCancelledEventpin.crm.deal-cancelled.v1Отказ (A1) — контрактchessboard (освобождение объекта недвижимости), notifications, rewards
DealMembersChangedEventpin.crm.deal-members-changed.v1Добавление участия (A2) — контрактmessenger (синк состава чата)

Acceptance Criteria

#КритерийПроверка
AC1Менеджер видит сделку чужого TenantId через list/get (кросс-tenant)Integration: сделка tenant A видна актору без tenant → 200
AC2Override Бронь → Заявка с комментарием: Status обновлён, таймлайн StatusChanged{Reason}, DealStageChangedEvent в outbox, участники уведомленыТест операции + проверка outbox/таймлайна
AC3Override Отказ без CancelReasonCRM_DEAL_CANCEL_REASON_REQUIREDВалидатор-тест
AC4Move-step в этап другой группы: StepGroup денормализован, событие несёт From/To этап и группуПроверка payload DealStageChangedEvent
AC5add-timeline-note создаёт строку Type = ManualNote, поднимает TimelineItemAdded, Kafka не публикуетсяПроверка типа строки + отсутствия outbox-записи
AC6Менеджер скачивает PII-документ (ClientPassport); эскроу в таймлайне — маска ****NNNNТест доступа + проверка маскирования
AC7Override/move терминальной сделки → CRM_DEAL_ALREADY_TERMINAL, мутаций нетGuard-тест INV-DEAL-9
AC8Менеджер — авто-участник чата; добавление участия PinManagerDealMembersChangedEventТест A2 + проверка события

Кросс-сервисные ожидания и гарантии

Вмешательство — синхронные HTTP-команды платформенного актора; эффекты для чата/уведомлений и освобождения объекта — асинхронные событийные цепочки.

Что crm предоставляет (исходящие гарантии):

КонтрактКлюч / версияКогдаКому
pin.crm.deal-stage-changed.v1key = DealId, ContractVersion = 1ручная смена статуса (шаг 3) / перевод этапа (шаг 4)messenger (карточка в чат), notifications
pin.crm.deal-cancelled.v1key = DealId, v1отказ A1, один раз на * → Cancelledchessboard (освобождение объекта), notifications, rewards
pin.crm.deal-members-changed.v1key = DealId, v1добавление участия PinManager (A2)messenger (синк состава чата)

Ручная запись таймлайна (ManualNote, шаг 5) собственного Kafka-события не публикует — поднимает только доменное TimelineItemAdded, которое доставляет push/email консюмер notifications.

Что crm ожидает от соседей:

  • identity — platform-scope permission Менеджера ПИН (кросс-tenant видимость, ADR-0052); без него — 403.
  • messenger — владелец чата (ADR-0055): Менеджер ПИН уже авто-участник; при deal-members-changed/ deal-created синхронизирует состав. crm поставляет только события состава.
  • chessboard — по deal-cancelled освобождает объект недвижимости (first-seen по DealId).
  • storage — пресайн-ссылки на скачивание документов (шаг 6), включая PII-типы; ссылка выдаётся синхронно.

Промежуточное состояние: после override-status/move-step менеджер видит обновлённые Status/StepId сразу; карточка в чате и push/email участникам приходят с задержкой (минуты); при отказе объект освобождается консюмером chessboard асинхронно (окно — см. НФТ). Фронт инвалидирует список/карточку немедленно.

Идемпотентность, конкурентность и восстановление

  • Идемпотентность. override-status/move-step не идемпотентны по ключу; повтор на терминальную сделку — CRM_DEAL_ALREADY_TERMINAL (INV-DEAL-9); повтор перевода на тот же этап — фактический no-op. ManualNote append-only: повторная отправка добавит новую строку (дублирование заметок — на усмотрение актора, не ошибка).
  • Конкурентность. Ручная правка Менеджера ПИН конкурирует с решениями сторон (агент/застройщик/инвестор): оптимистичная блокировка, проигравший получает CONCURRENCY_CONFLICT (409) и перечитывает карточку. Кросс- tenant доступ не снимает guard-ы INV-DEAL-5/8/9.
  • Доставка downstream (retry/DLQ). deal-stage-changed/deal-cancelled/deal-members-changed идут через outbox (at-least-once); консюмеры идемпотентны (MessageId, для терминальных — first-seen DealId). Транзиентная ошибка — ретрай; после исчерпания — DLQ и ручной разбор. Само вмешательство в crm из-за падения downstream не откатывается.

Наблюдаемость и НФТ

  • Логи (аудит вмешательства): каждое ручное действие — DealId, TenantId целевой сделки, actor (Менеджер ПИН), тип операции, From → To статус/этап и комментарий/причина; факт скачивания PII-документа (documentId, тип, actor) логируется как доступ. Содержимое документов, полный EscrowAccountNumber и raw- PII в логи не пишутся (эскроу — маска ****NNNN).
  • Метрики: число ручных override/move-step (в разрезе TenantId/менеджера); доля CRM_DEAL_ALREADY_TERMINAL/CRM_DEAL_STEP_FORBIDDEN/CONCURRENCY_CONFLICT; число скачиваний PII-документов (контроль доступа); лаг синхронизации состава чата.
  • Латентность/нагрузка: интерактивные команды — P95 в пределах сотен мс (ADR-0056); список платформы (кросс-tenant) — пагинированная выборка, целевая латентность как у обычных списков.
  • Доступ/безопасность: только platform-scope permission (crm.deals.*, crm.deal-documents.view, crm.pipeline.manage); ручной таймлайн — только PinManager/Служба заботы (INV-TLE-5); эскроу наружу — всегда маска.

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

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

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