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

UC-CRM-013. Увольнение агента: каскад отвязки от процессов CRM

Назначение

Руководитель агентства исключает агента из компании. Членство агента развязывается (Active → Left), а все активные CRM-процессы агентства реагируют каскадом: вспомогательные участия агента снимаются, доступ к чужим теперь лидам/сделкам и их чатам прекращается, а процессы, где агент был исполнителем (Executor), не могут быть оставлены без преемника — их передача обязательна до увольнения. Учётка агента, его метрики/достижения и уже заработанные комиссии сохраняются. Сценарий связывает операцию увольнения (identity) с offboarding-каскадом (crm); дом членства — agent-membership.md, дом каскада — ../consumers/agent-offboarding.md.

Metadata

ПолеЗначение
IDUC-CRM-013
Контекстcrm (каскад) + identity (увольнение) (см. docs/06-services/crm/, docs/06-services/identity/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольРуководитель / администратор агентства (исключает агента); альтернатива — сам агент (добровольный уход)
Permissions / scopeidentity.users.manage в scope Organization(AgencyCompanyId); передача ведения перед увольнением — crm.deals.manage / crm.leads.manage
Tenantувольнение — в scope прежнего Organization (AgencyCompanyId); каскад в crm — system-actor, platform-scope (лиды по TenantId = AgencyCompanyId, сделки/фиксации — санкционированный кросс-tenant по OriginAgencyCompanyId)
Record-levelактивное AgentMembership пары (AgentUserId, AgencyCompanyId) в ростере компании актора

Предусловие

  • У агента есть активное AgentMembership (Status = Active) в агентстве актора (agent-membership.md, INV-AM1); иначе увольнять нечего.
  • Учётка агента Active (UserType = Agent); метрики/достижения агрегируются по AgentUserId (user-centric).
  • Известны активные процессы агентства, где ExecutorAgentUserId = AgentUserId (лиды, сделки, Pending-фиксации) — их набор определяет обязательность передачи ведения.

Триггер

ПолеЗначение
ИсточникUI-действие → вызов API
Триггерящий элемент«Исключить агента» в ростере агентства → POST /api/v1/companies/{companyId}/agents/{agentUserId}/terminate (terminate-agent-membership.md)

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

  1. Контекст и доступ. Руководитель открывает ростер агентства и инициирует исключение агента; проверяется identity.users.manage в scope компании — иначе IDENTITY_FORBIDDEN.
  2. Guard активных процессов (обязательный reassign). Перед снятием членства identity координируется с crm: собирает активные лиды/сделки/Pending-фиксации агентства, где агент — единственный активный Executor без назначенного преемника (инвариант «процесс нельзя оставить без исполнителя», INV-DEAL-3 / INV-LEAD-8). Если такие процессы есть — увольнение блокируется IDENTITY_AGENT_HAS_ACTIVE_EXECUTOR_PROCESSES со списком процессов под передачу ведения.
  3. Передача ведения (если требуется). Руководитель по каждому процессу из списка передаёт исполнителя другому активному агенту агентства (UC-CRM-015, reassign-executor): ExecutorAgentUserId меняется атомарно, процесс ни на миг не остаётся без Executor. После этого guard проходит.
  4. Развязка членства. identity в одном согласованном изменении: AgentMembership.Status = Active → Left, LeftAt = EffectiveAt, LeaveReason = ExcludedByAgency (INV-AM3); зеркало UserEntity.CompanyId сбрасывается, если это было выбранное активное агентство; роль agent перевыдаётся Organization → Platform, только если не осталось ни одной Active-записи (агент состоит в других агентствах — scope сохраняется). Историческая привязка сделок/лидов/фиксаций не пересчитывается (Origin-снимки иммутабельны).
  5. Публикация факта. Публикуется AgentMembershipChangedKafkaEvent (Status = Left, Reason = ExcludedByAgency, EffectiveAt, key = AgentUserId) в pin.identity.agent-membership-changed.v1 (agent-membership-changed.md); при смене зеркала/scope — pin.identity.users.v1 и pin.identity.access.v1.
  6. Каскад отвязки в CRM (async). Консюмер offboarding (../consumers/agent-offboarding.md) по событию Left:
    • находит вспомогательные участия агента (Responsible/CoAgent/Assistant/Lawyer) в активных лидах/сделках агентства и снимает их (RemovedAt = EffectiveAt, система); при снятии Responsible обнуляется ResponsibleAgentUserId;
    • публикует pin.crm.deal-members-changed.v1 / pin.crm.lead-participants-changed.v1 с полным активным составом и дельтой (ChangeType = Removed / ResponsibleChanged);
    • процессы, где агент остался Executor (защитная ветка, если guard шага 2 был обойдён), не снимает, а помечает ExecutorReassignRequiredAt = EffectiveAt и уведомляет руководителя.
  7. Потеря доступа к чатам. По событиям состава messenger снимает агента из чатов затронутых лидов/сделок (доступ и realtime-подписка убраны, история переписки сохраняется команде, ADR-0055/0072).
  8. Комиссии и атрибуция сохранены. rewards не пересчитывает earned/paid-начисления; иммутабельные OriginAgentUserId/OriginAgencyCompanyId на сделках/лидах/фиксациях остаются — прежнее агентство сохраняет основания комиссий, а уже подтверждённые начисления закреплены за получателем. Руководитель видит убытие агента из ростера; агент видит статус «Независимый агент» (или переключение на оставшееся агентство).

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

A1. Добровольный уход агента (self-leave)

  1. Инициатор — сам агент (leave-agency / become-investor), LeaveReason = SelfLeave. Тот же guard «единственный активный Executor» и тот же offboarding-каскад; отличие — только Reason (влияет на формулировку уведомления, каскад идентичен, ../consumers/agent-offboarding.md AC-10).
Возврат к основному потоку

Продолжение — шаг 5.

A2. Guard блокирует увольнение (нужен reassign)

  1. Шаг 2 находит процессы, где агент — единственный Executor без преемника → IDENTITY_AGENT_HAS_ACTIVE_EXECUTOR_PROCESSES (409).
  2. Руководитель передаёт ведение (шаг 3) и повторяет исключение — теперь guard пуст, поток продолжается.
Блокируется до передачи ведения

409 IDENTITY_AGENT_HAS_ACTIVE_EXECUTOR_PROCESSES — увольнение не проходит, пока по каждому процессу из списка не выполнена передача ведения (шаг 3).

A3. Агент состоит в нескольких агентствах

  1. Исключение закрывает только членство пары (AgentUserId, AgencyCompanyId); членства в других агентствах и их scope роли сохраняются (ADR-0070). Зеркало UserEntity.CompanyId трогается, только если исключаемое агентство было выбранным (тогда — перевыбор через UC-CRM-014).
  2. Offboarding-каскад отвязывает агента только от процессов этого агентства; процессы других агентств не затрагиваются.

A4. Нет активного членства (повтор/идемпотентность)

  1. Шаг 3 (identity) не находит активного членства пары → IDENTITY_MEMBERSHIP_NOT_ACTIVE (404); повтор безопасен (идемпотентный исход).
Завершается ошибкой

404 IDENTITY_MEMBERSHIP_NOT_ACTIVE — состояние не меняется; повторный вызов безопасен (идемпотентный исход).

A5. Битое/устаревшее событие каскада

  1. Событие Left без EffectiveAt → консюмер offboarding отбраковывает CRM_OFFBOARDING_INVALID_EVENT (non-retriable → DLQ + алерт). Устаревший (out-of-order) переход отбрасывается Version-guard членства.
Завершается ошибкой

CRM_OFFBOARDING_INVALID_EVENT — non-retriable, DLQ + алерт (не клиентская ошибка); устаревший переход членства отбрасывается Version-guard.

Постусловие

  • AgentMembership.Status = Left, LeftAt = EffectiveAt, LeaveReason = ExcludedByAgency (или SelfLeave); запись сохранена (история найма, INV-AM2).
  • Вспомогательные участия агента в активных лидах/сделках агентства сняты (RemovedAt = EffectiveAt); доступ к этим процессам и их чатам у агента прекращён (история команде сохранена).
  • Процессы, где агент остался Executor (защитная ветка), помечены ExecutorReassignRequiredAt с уведомлением руководителю — состав не менялся.
  • Метрики/достижения агента (user-centric) и earned/paid-комиссии rewards сохранены; Origin-атрибуция сделок/лидов/фиксаций иммутабельна (back-fill запрещён).
  • Опубликованы agent-membership-changed (Left) и, при затронутом составе, deal-members-changed / lead-participants-changed.

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

КодHTTPУсловиеПоведение клиента
IDENTITY_FORBIDDEN403Нет identity.users.manage в scope агентства / чужая компанияСкрыть действие
IDENTITY_MEMBERSHIP_NOT_ACTIVE404Нет активного членства пары (AgentUserId, CompanyId) (A4)«Агент не состоит в агентстве»
IDENTITY_AGENT_HAS_ACTIVE_EXECUTOR_PROCESSES409Агент — единственный активный Executor без преемника (A2)Показать список процессов под передачу ведения
IDENTITY_TERMINATION_EFFECTIVE_AT_INVALID400EffectiveAt в прошлом«Дата вступления в силу не может быть в прошлом»
CONCURRENCY_CONFLICT409Гонка terminate/leave/accept по паре«Повторите попытку»
CRM_OFFBOARDING_INVALID_EVENT— (консюмер)Left-событие без обязательных полей (A5)DLQ + алерт (не клиентская ошибка)

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

СобытиеКогда поднимаетсяSubscribers
AgentLeftAgencyEventActive → Left (terminate / self-leave)in-proc аудит identity
DealMemberRemoved / LeadParticipantRemovedКаскад снял вспомогательное участиетаймлайн/лента процесса; диспетчеризуются при коммите каскада

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

СобытиеTopicКогда публикуетсяConsumers
AgentMembershipChangedKafkaEvent.v1pin.identity.agent-membership-changed.v1Active → Left (terminate/self-leave)crm (offboarding-каскад + ростер), rewards (read-only история, revoke RoleAssignment), notifications — контракт agent-membership-changed.md
DealMembersChangedEvent.v1pin.crm.deal-members-changed.v1Каскад снял участие в сделкеmessenger (снять из чата), notifications — контракт deal-members-changed.md
LeadParticipantsChangedEvent.v1pin.crm.lead-participants-changed.v1Каскад снял участие в лидеmessenger, notifications — контракт lead-participants-changed.md

Увольнение не пересчитывает rewards-начисления и не публикует событий отмены комиссий: earned/paid закреплены за получателем, аннулирование основания — только при отмене самой сделки (deal.md, DealCancelledEvent).

Acceptance Criteria

#КритерийПроверка
AC1Исключение переводит членство Active → Left (LeaveReason = ExcludedByAgency), запись сохраненаТест terminate → membership с LeftAt, история присутствует
AC2Guard блокирует увольнение, если агент — единственный Executor без преемника; после reassign проходитНегативный тест IDENTITY_AGENT_HAS_ACTIVE_EXECUTOR_PROCESSES → reassign → повтор 200
AC3Каскад снимает вспомогательные участия (CoAgent/Assistant/Lawyer/Responsible) в процессах агентства; публикуются события составаТест консюмера offboarding + deal-members-changed/lead-participants-changed
AC4messenger снимает агента из чатов затронутых процессов, история сохранена командеКонсюмер-контракт messenger
AC5Earned/paid-комиссии и Origin-атрибуция не меняются; метрики агента (user-centric) сохраненыТест: снапшоты OriginAgentUserId/OriginAgencyCompanyId и начисления rewards неизменны
AC6Исключение из одного агентства не трогает членства в других (A3); зеркало сбрасывается только если агентство было выбраннымТест мульти-членства
AC7Повторное исключение без активного членства → 404 (A4), идемпотентноДвойной вызов
AC8Защитная ветка: агент остался Executor → участие не снято, ExecutorReassignRequiredAt выставлен, уведомление руководителюТест обхода guard

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

Увольнение — синхронная операция в identity (членство + роль + зеркало атомарно), но каскад в CRM и потеря доступа к чатам — eventually consistent: после коммита identity доступ к клиентской базе и процессам фактически прекращается по инвалидации прав (pin.identity.access.v1, окно ≤ TTL, UC-IDN-008) и по обработке agent-membership-changed (Left) консюмером offboarding + messenger (SLA консюмеров < 1 мин). В этом окне агент может кратко видеть данные агентства из кэша. Guard активных процессов (шаг 2) гарантирует, что ни один процесс не остаётся без исполнителя: передача ведения — предусловие увольнения, а не следствие. Порядок переходов членства per-agent гарантирован ключом AgentUserId.

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

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

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