ADR-0072: Увольнение/исключение агента и каскад доступа
Статус
Accepted
Дата
2026-07-16
Контекст
Агент перестаёт работать в агентстве двумя разными путями, и платформа их не различает и не отрабатывает до конца:
- Добровольный уход vs принудительное исключение — один сценарий. ADR-0061 ввёл отвязку членства
(
leave), но только как действие самого агента. Кейс «руководитель/админ агентства исключает агента» отдельной операции не имеет, а причина ухода нигде не фиксируется — увольнение и добровольный выход в событиях неразличимы. Для отчётности агентства, реферальных разборов и последующего каскада важно, кто инициатор и с какого момента членство недействительно. AgentMembershipEntityне должен обрастать статусами (ADR-0070). Членство инвариантно ограниченоSingleActivePerCompany(unique(agent_user_id, company_id) where status=Active). Событиеpin.identity.agent-membership-changed.v1уже консюмитсяrewardsдля user-centric метрик; его консюмер деградирует на неизвестном значении в независимого агента (Independent). Введение нового значенияStatusпод увольнение сломало бы этот консюмер и потребовало бы бампа контракта — цена несоразмерна.- Каскад доступа не описан. Когда агент теряет членство, он остаётся
ExecutorAgentUserId/ResponsibleAgentUserIdна активных лидах/сделках/фиксациях, участником чатов сделок и лидов, держателем открытыхBookingDeadlineи встреч в календаре, а его финансовая история вrewardsпривязана к иммутабельнымOriginAgencyCompanyId/OriginAgentUserId. Требование «ровно один активныйExecutor, процесс нельзя оставить безExecutor» (ADR-0071) при увольнении не обеспечено ничем.
Решение
1. Два разных действия: self-leave и terminate
- Добровольный уход (
self-leave) — действие самого агента (endpointleaveиз ADR-0061), инициатор — владелец учётки. - Принудительное исключение (
terminate) — новый endpointterminate-agent-membership, актор — руководитель или админ агентства, permissionidentity.users.manage, record-level по членству своего агентства. Обе операции приводят членство к неактивному состоянию и запускают один и тот же каскад; различаются инициатором и причиной.
2. Причина и момент — обогащение события, БЕЗ нового Status
- Событие
pin.identity.agent-membership-changed.v1обогащается двумя additive-полями:Reason— enumLeaveReason:SelfLeave(добровольный) /ExcludedByAgency(принудительный);EffectiveAt— момент, с которого членство недействительно (для отложенного увольнения ≠ время события).
ContractVersionНЕ бампим: поля additive, старые консюмеры их игнорируют. Новое значениеStatusсознательно не вводится — это сломало быrewards-консюмер, деградирующий на unknown вIndependent; факт увольнения переносится причиной, а не новым состоянием членства.
3. Guard: нельзя уволить активного единственного Executor без reassign
terminate(иself-leave) запрещены, пока агент остаётся единственным активнымExecutorхотя бы на одном активном лиде, сделке или фиксации этого агентства без назначенного преемника. Инвариант ADR-0071 «процесс нельзя оставить безExecutor» первичен: сначала руководитель переназначаетExecutorAgentUserIdна процессах агента, затем членство снимается.- Отказ возвращает список процессов, требующих reassign, чтобы руководитель разрулил их до увольнения.
4. Каскад по контекстам
crm— по событию членства массовый soft-remove участий агента (ролиExecutor/Responsible/CoAgent/Assistant/Lawyer) в процессах этого агентства; там, где агент былExecutorи преемник ещё не назначен, процесс помечается требующим reassignExecutor(guard п.3 гарантирует, что таких на момент увольнения нет). Иммутабельная атрибуцияOriginAgencyCompanyId/OriginAgentUserIdне трогается — история владения сохраняется. Изменения состава публикуются штатнымиdeal-members-changed/pin.crm.lead-participants-changed.v1.messenger— штатный soft-remove участника поdeal-members-changed/pin.crm.lead-participants-changed.v1: доступ к чатам сделок/лидов и realtime-подписка сняты, история сохраняется команде. Отдельных readonly-состояний чата не вводим — решение «проще»: уволенный просто перестаёт быть участником.calendar— открытыеBookingDeadlineи встречи (Appointment) уволенного агента переносятся на преемника/руководителя, чтобы дедлайны и назначенные консультации не повисли.rewards— начисления в статусах earned/paid и Origin-snapshot неизменны (гарантия ADR-0061/ ADR-0066:AgencyCompanyId/AgentUserIdначислений не пересчитываются); история в ЛК агента становится read-only, а интерактивный доступ к финансовым разделам агентства revoke через снятиеRoleAssignment. Право получить уже заработанное сохраняется.
Область Действия
| Область | Входит | Не входит |
|---|---|---|
| Сервисы | identity (endpoint terminate-agent-membership, обогащение события членства), crm (soft-remove участий + reassign), messenger (soft-remove участника), calendar (перенос дедлайнов/встреч), rewards (read-only история + revoke доступа) | chessboard, catalog, wallet (счета/остатки не трогаются) |
| Контракты | pin.identity.agent-membership-changed.v1 +Reason/EffectiveAt; endpoint terminate-agent-membership | новое значение AgentMembershipEntity.Status; бамп ContractVersion события членства |
| Данные | soft-remove участий в crm; перенос BookingDeadline/Appointment; read-only-флаг истории агента в rewards | иммутабельные OriginAgencyCompanyId/OriginAgentUserId; earned/paid-начисления; Origin-snapshot |
| Эксплуатация | guard «единственный активный Executor»; снятие RoleAssignment уволенного | пересчёт исторических метрик/начислений |
Варианты
| Вариант | Описание | Причина решения |
|---|---|---|
Новое значение Status под увольнение | Отдельный статус членства Terminated/Excluded | Отклонено: ломает rewards-консюмер (деградирует на unknown в Independent) и требует бампа ContractVersion; факт увольнения полностью выражается additive-полями Reason/EffectiveAt без слома совместимости. |
| Единая операция ухода без различения инициатора | Один leave и для агента, и для руководителя | Отклонено: теряется отчётная разница «ушёл сам» vs «исключён», нужная агентству и разборам; разные акторы и permissions (identity.users.manage для принудительного). |
| Readonly-состояния чатов для уволенного | messenger вводит спец-режим «только чтение» для бывшего участника | Отклонено как усложнение (ADR-0056): штатный soft-remove участника проще, история и так остаётся команде. |
| Разрешить увольнение без reassign, чинить процессы потом | Снять членство сразу, Executor-дыры закрывать асинхронно | Отклонено: нарушает инвариант ADR-0071 «процесс нельзя оставить без Executor»; guard дешевле, чем разбор осиротевших процессов. |
| Back-fill истории агента на новое/пустое агентство | Переписать привязку начислений после ухода | Отклонено (ADR-0061): искажает историю сделок и основания комиссий; Origin-снапшоты иммутабельны. |
Последствия
| Контекст | Последствие |
|---|---|
identity | +endpoint terminate-agent-membership (identity.users.manage, актор руководитель/админ агентства); событие pin.identity.agent-membership-changed.v1 несёт Reason (LeaveReason) и EffectiveAt; guard «единственный активный Executor» перед снятием членства; снятие RoleAssignment уволенного. |
crm | Массовый soft-remove участий агента в процессах агентства и требование reassign ExecutorAgentUserId/ResponsibleAgentUserId; атрибуция OriginAgencyCompanyId/OriginAgentUserId неизменна; публикация deal-members-changed / pin.crm.lead-participants-changed.v1. |
messenger | Soft-remove участника чатов сделок/лидов по событиям состава: доступ и realtime-подписка сняты, история сохранена; readonly-состояний нет. |
calendar | Перенос открытых BookingDeadline и встреч уволенного на преемника/руководителя. |
rewards | Earned/paid-начисления и Origin-snapshot неизменны; ЛК-история агента read-only; интерактивный доступ к финансам агентства revoke через RoleAssignment. |
Затронутые контракты и совместимость
| Контракт | Версия | Совместимость | Миграция |
|---|---|---|---|
pin.identity.agent-membership-changed.v1 (+Reason/EffectiveAt) | 1 | additive, ContractVersion не меняется | новые поля nullable/enum с дефолтом; старые консюмеры игнорируют |
LeaveReason (SelfLeave / ExcludedByAgency) | — | новый enum в контексте события | additive |
endpoint terminate-agent-membership (identity.users.manage) | 1 | новый эндпоинт | — |
deal-members-changed / pin.crm.lead-participants-changed.v1 | 1 | не меняются | +триггер от каскада увольнения |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Entity / Aggregate: AgentMembershipEntity, Entity / Aggregate: SignInConfirmationRequest + RefreshSession, Entity / Projection: AgentPerformanceEntity
- Use Cases (6): UC-CRM-012. Управление составом участников лида (добавление/снятие: второй агент, супруг/юрист инвестора), UC-CRM-013. Увольнение агента: каскад отвязки от процессов CRM, UC-MSG-008. Добавленный участник появляется в чате сделки/лида, UC-RWD-016. Застройщик подтверждает начисление после смены исполнителя, UC-RWD-017. Уволенный агент: судьба заработанных комиссий, US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- События (3): Event Contract: pin.calendar.event-owner-changed (CalendarEventOwnerChangedEvent), Event Contract: pin.crm.lead-participants-changed (LeadParticipantsChangedEvent), Event Contract: pin.identity.agent-membership-changed (AgentMembershipChangedKafkaEvent)
- Консюмеры (3): Consumer: CRM Agent Offboarding (CrmAgentOffboardingConsumer), Consumer: Identity Agent Membership Changed, Consumer: Lead Chat Participants Sync (MessengerLeadParticipantsChangedConsumer)
- index.md (1): Messenger Service
Связанные документы
identity: domain/user.md, domain/role.md.crm: domain/deal.md, domain/lead.md, domain/fixation-request.md.- ADR: ADR-0061 (независимый агент,
leave, иммутабельная привязка, запрет back-fill), ADR-0070 (AgentMembershipEntity,SingleActivePerCompany), ADR-0071 (ParticipantRole, единственный активныйExecutor, атрибуция).