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

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) — действие самого агента (endpoint leave из ADR-0061), инициатор — владелец учётки.
  • Принудительное исключение (terminate) — новый endpoint terminate-agent-membership, актор — руководитель или админ агентства, permission identity.users.manage, record-level по членству своего агентства. Обе операции приводят членство к неактивному состоянию и запускают один и тот же каскад; различаются инициатором и причиной.

2. Причина и момент — обогащение события, БЕЗ нового Status

  • Событие pin.identity.agent-membership-changed.v1 обогащается двумя additive-полями:
    • Reason — enum LeaveReason: SelfLeave (добровольный) / ExcludedByAgency (принудительный);
    • EffectiveAt — момент, с которого членство недействительно (для отложенного увольнения ≠ время события).
  • ContractVersion НЕ бампим: поля additive, старые консюмеры их игнорируют. Новое значение Status сознательно не вводится — это сломало бы rewards-консюмер, деградирующий на unknown в Independent; факт увольнения переносится причиной, а не новым состоянием членства.

3. Guard: нельзя уволить активного единственного Executor без reassign

  • terminateself-leave) запрещены, пока агент остаётся единственным активным Executor хотя бы на одном активном лиде, сделке или фиксации этого агентства без назначенного преемника. Инвариант ADR-0071 «процесс нельзя оставить без Executor» первичен: сначала руководитель переназначает ExecutorAgentUserId на процессах агента, затем членство снимается.
  • Отказ возвращает список процессов, требующих reassign, чтобы руководитель разрулил их до увольнения.

4. Каскад по контекстам

  • crm — по событию членства массовый soft-remove участий агента (роли Executor/Responsible/ CoAgent/Assistant/Lawyer) в процессах этого агентства; там, где агент был Executor и преемник ещё не назначен, процесс помечается требующим reassign Executor (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.
messengerSoft-remove участника чатов сделок/лидов по событиям состава: доступ и realtime-подписка сняты, история сохранена; readonly-состояний нет.
calendarПеренос открытых BookingDeadline и встреч уволенного на преемника/руководителя.
rewardsEarned/paid-начисления и Origin-snapshot неизменны; ЛК-история агента read-only; интерактивный доступ к финансам агентства revoke через RoleAssignment.

Затронутые контракты и совместимость

КонтрактВерсияСовместимостьМиграция
pin.identity.agent-membership-changed.v1 (+Reason/EffectiveAt)1additive, ContractVersion не меняетсяновые поля nullable/enum с дефолтом; старые консюмеры игнорируют
LeaveReason (SelfLeave / ExcludedByAgency)новый enum в контексте событияadditive
endpoint terminate-agent-membership (identity.users.manage)1новый эндпоинт
deal-members-changed / pin.crm.lead-participants-changed.v11не меняются+триггер от каскада увольнения

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

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

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