UC-CRM-013. Увольнение агента: каскад отвязки от процессов CRM
Назначение
Руководитель агентства исключает агента из компании. Членство агента развязывается (Active → Left), а
все активные CRM-процессы агентства реагируют каскадом: вспомогательные участия агента снимаются, доступ к
чужим теперь лидам/сделкам и их чатам прекращается, а процессы, где агент был исполнителем (Executor),
не могут быть оставлены без преемника — их передача обязательна до увольнения. Учётка агента, его
метрики/достижения и уже заработанные комиссии сохраняются. Сценарий связывает операцию увольнения
(identity) с offboarding-каскадом (crm); дом членства — agent-membership.md,
дом каскада — ../consumers/agent-offboarding.md.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CRM-013 |
| Контекст | crm (каскад) + identity (увольнение) (см. docs/06-services/crm/, docs/06-services/identity/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Руководитель / администратор агентства (исключает агента); альтернатива — сам агент (добровольный уход) |
| Permissions / scope | identity.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) |
Основной Поток
- Контекст и доступ. Руководитель открывает ростер агентства и инициирует исключение агента; проверяется
identity.users.manageв scope компании — иначеIDENTITY_FORBIDDEN. - Guard активных процессов (обязательный reassign). Перед снятием членства
identityкоординируется сcrm: собирает активные лиды/сделки/Pending-фиксации агентства, где агент — единственный активныйExecutorбез назначенного преемника (инвариант «процесс нельзя оставить без исполнителя», INV-DEAL-3 / INV-LEAD-8). Если такие процессы есть — увольнение блокируетсяIDENTITY_AGENT_HAS_ACTIVE_EXECUTOR_PROCESSESсо списком процессов под передачу ведения. - Передача ведения (если требуется). Руководитель по каждому процессу из списка передаёт исполнителя
другому активному агенту агентства (UC-CRM-015,
reassign-executor):ExecutorAgentUserIdменяется атомарно, процесс ни на миг не остаётся безExecutor. После этого guard проходит. - Развязка членства.
identityв одном согласованном изменении:AgentMembership.Status = Active → Left,LeftAt = EffectiveAt,LeaveReason = ExcludedByAgency(INV-AM3); зеркалоUserEntity.CompanyIdсбрасывается, если это было выбранное активное агентство; рольagentперевыдаётсяOrganization → Platform, только если не осталось ни однойActive-записи (агент состоит в других агентствах — scope сохраняется). Историческая привязка сделок/лидов/фиксаций не пересчитывается (Origin-снимки иммутабельны). - Публикация факта. Публикуется
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. - Каскад отвязки в 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и уведомляет руководителя.
- находит вспомогательные участия агента (
- Потеря доступа к чатам. По событиям состава
messengerснимает агента из чатов затронутых лидов/сделок (доступ и realtime-подписка убраны, история переписки сохраняется команде, ADR-0055/0072). - Комиссии и атрибуция сохранены.
rewardsне пересчитывает earned/paid-начисления; иммутабельныеOriginAgentUserId/OriginAgencyCompanyIdна сделках/лидах/фиксациях остаются — прежнее агентство сохраняет основания комиссий, а уже подтверждённые начисления закреплены за получателем. Руководитель видит убытие агента из ростера; агент видит статус «Независимый агент» (или переключение на оставшееся агентство).
Альтернативные Потоки
A1. Добровольный уход агента (self-leave)
- Инициатор — сам агент (
leave-agency/become-investor),LeaveReason = SelfLeave. Тот же guard «единственный активныйExecutor» и тот же offboarding-каскад; отличие — толькоReason(влияет на формулировку уведомления, каскад идентичен, ../consumers/agent-offboarding.md AC-10).
Продолжение — шаг 5.
A2. Guard блокирует увольнение (нужен reassign)
- Шаг 2 находит процессы, где агент — единственный
Executorбез преемника →IDENTITY_AGENT_HAS_ACTIVE_EXECUTOR_PROCESSES(409). - Руководитель передаёт ведение (шаг 3) и повторяет исключение — теперь guard пуст, поток продолжается.
409 IDENTITY_AGENT_HAS_ACTIVE_EXECUTOR_PROCESSES — увольнение не проходит, пока по каждому процессу из
списка не выполнена передача ведения (шаг 3).
A3. Агент состоит в нескольких агентствах
- Исключение закрывает только членство пары
(AgentUserId, AgencyCompanyId); членства в других агентствах и их scope роли сохраняются (ADR-0070). ЗеркалоUserEntity.CompanyIdтрогается, только если исключаемое агентство было выбранным (тогда — перевыбор через UC-CRM-014). - Offboarding-каскад отвязывает агента только от процессов этого агентства; процессы других агентств не затрагиваются.
A4. Нет активного членства (повтор/идемпотентность)
- Шаг 3 (identity) не находит активного членства пары →
IDENTITY_MEMBERSHIP_NOT_ACTIVE(404); повтор безопасен (идемпотентный исход).
404 IDENTITY_MEMBERSHIP_NOT_ACTIVE — состояние не меняется; повторный вызов безопасен (идемпотентный исход).
A5. Битое/устаревшее событие каскада
- Событие
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_FORBIDDEN | 403 | Нет identity.users.manage в scope агентства / чужая компания | Скрыть действие |
IDENTITY_MEMBERSHIP_NOT_ACTIVE | 404 | Нет активного членства пары (AgentUserId, CompanyId) (A4) | «Агент не состоит в агентстве» |
IDENTITY_AGENT_HAS_ACTIVE_EXECUTOR_PROCESSES | 409 | Агент — единственный активный Executor без преемника (A2) | Показать список процессов под передачу ведения |
IDENTITY_TERMINATION_EFFECTIVE_AT_INVALID | 400 | EffectiveAt в прошлом | «Дата вступления в силу не может быть в прошлом» |
CONCURRENCY_CONFLICT | 409 | Гонка terminate/leave/accept по паре | «Повторите попытку» |
CRM_OFFBOARDING_INVALID_EVENT | — (консюмер) | Left-событие без обязательных полей (A5) | DLQ + алерт (не клиентская ошибка) |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
AgentLeftAgencyEvent | Active → Left (terminate / self-leave) | in-proc аудит identity |
DealMemberRemoved / LeadParticipantRemoved | Каскад снял вспомогательное участие | таймлайн/лента процесса; диспетчеризуются при коммите каскада |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
AgentMembershipChangedKafkaEvent.v1 | pin.identity.agent-membership-changed.v1 | Active → Left (terminate/self-leave) | crm (offboarding-каскад + ростер), rewards (read-only история, revoke RoleAssignment), notifications — контракт agent-membership-changed.md |
DealMembersChangedEvent.v1 | pin.crm.deal-members-changed.v1 | Каскад снял участие в сделке | messenger (снять из чата), notifications — контракт deal-members-changed.md |
LeadParticipantsChangedEvent.v1 | pin.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, история присутствует |
| AC2 | Guard блокирует увольнение, если агент — единственный Executor без преемника; после reassign проходит | Негативный тест IDENTITY_AGENT_HAS_ACTIVE_EXECUTOR_PROCESSES → reassign → повтор 200 |
| AC3 | Каскад снимает вспомогательные участия (CoAgent/Assistant/Lawyer/Responsible) в процессах агентства; публикуются события состава | Тест консюмера offboarding + deal-members-changed/lead-participants-changed |
| AC4 | messenger снимает агента из чатов затронутых процессов, история сохранена команде | Консюмер-контракт messenger |
| AC5 | Earned/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.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-CRM-014. Агент выбирает активное агентство перед созданием лида/сделки, UC-CRM-015. Явное назначение исполнителя для разрешения спора о комиссии
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- Увольнение (identity): terminate-agent-membership.md; добровольный уход: leave-agency.md — сценарий UC-IDN-010.
- Каскад CRM: ../consumers/agent-offboarding.md.
- Передача ведения (обязательный reassign): reassign-executor.md, сценарий UC-CRM-015.
- Domain: agent-membership.md (INV-AM2/AM3/AM6,
LeaveReason), ../domain/deal.md (INV-DEAL-3/10,ExecutorReassignRequiredAt), ../domain/lead.md (INV-LEAD-8), ../domain/fixation-request.md. - События: agent-membership-changed.md, deal-members-changed.md, lead-participants-changed.md.
- Мульти-агентство и активное агентство: UC-CRM-014.
- ADR: 0072 termination-cascade, 0071 crm-ownership-and-participant-model, 0070 multi-agency, 0061 independent-agent, 0052 tenancy.