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

ADR-0071: Владение процессами CRM компанией и единая модель участников

Статус

Accepted

Дата

2026-07-16

Контекст

CRM-процессы (LeadEntity, DealEntity, FixationRequestEntity, ClientEntity) моделируются как собственность конкретного агента: владелец и вся видимость держатся на скалярном AgentUserId, а на сделке роль исполнителя задублирована — она есть и как скаляр DealEntity.AgentUserId, и как участник DealMemberEntity с ролью Agent (INV-DEAL-3). Это ломается на реальных сценариях платформы:

  • Скаляр AgentUserId смешивает две разные роли поля. Он одновременно (а) историческая атрибуция «кто привёл клиента» — основа воронки агентства, оснований комиссий и антифрода, поэтому иммутабелен (INV-DEAL-10 / INV-LEAD-6 / INV-FIX-7, ADR-0061 п.4); и (б) текущий исполнитель процесса, который по жизни обязан меняться (агент в отпуске, уволился, руководитель переназначил ведение). Совместить «никогда не меняется» и «должен меняться» в одном поле нельзя.
  • Ровно один агент и один инвестор — слишком узко. INV-DEAL-3 фиксирует «ровно один активный Agent, один Investor»; INV-CRM-001 строит уникальность лида на паре (AgentUserId, InvestorUserId). Но у лида/сделки бывает несколько инвесторов (созаёмщики, супруги, соинвесторы), а вести процесс могут несколько агентских участников (со-агент, ассистент, юрист) — при одном ответственном исполнителе.
  • Двойной источник истины исполнителя на сделке. DealEntity.AgentUserId (скаляр) и активный DealMemberEntity(Role = Agent) описывают одно и то же и могут разойтись — какое из них «настоящий исполнитель» для комиссии, чата, календаря, ни модель, ни код однозначно не отвечают.
  • Видимость завязана на скаляр AgentUserId == actor. Из-за этого руководитель, со-агент или менеджер ПИН, реально участвующие в процессе, не проходят record-level проверку, а при передаче ведения доступ «перескакивает» вместе со скаляром — но история должна остаться у прежней стороны.

ADR-0061 уже зафиксировал: историческая привязка сделок/лидов/фиксаций держится снапшотами AgencyCompanyId/AgentUserId и не пересчитывается, back-fill запрещён. ADR-0052 задал владельца процесса на уровне tenant: агентство — для лида/клиента, застройщик — для сделки/фиксации. Требуется отделить иммутабельную атрибуцию от мутабельного текущего исполнителя, снять ограничение «один агент/один инвестор» и убрать двойной источник истины.

Решение

1. Процессы CRM принадлежат компании, агент — участник, а не владелец

  • Владелец лида/сделки/фиксации/клиента — компания-tenant (ADR-0052): агентство для LeadEntity и ClientEntity, застройщик для DealEntity и FixationRequestEntity. TenantId компании-владельца остаётся границей данных.
  • Агент понижается с record-level владельца до участника процесса. Его связь с процессом выражается не скаляром «владелец = агент», а членством в составе участников (см. п. 3). Это устраняет неявное «процесс принадлежит агенту» и делает передачу ведения штатной операцией, а не переписыванием владельца.

2. Атрибуция — иммутабельный Origin-снапшот, отдельный от текущего исполнителя

Скалярное AgentUserId расщепляется на два независимых по смыслу набора полей:

  • Атрибуция (иммутабельна, set-once): OriginAgencyCompanyId, OriginAgentUserId — «кто и от какого агентства привёл клиента и создал процесс». Фиксируется при создании, не переписывается и не back-fill-ится при смене агентом агентства, передаче ведения или увольнении. Это основа исторической воронки агентства, оснований комиссий (rewards) и антифрода (ADR-0061 п.4). Именно на Origin теперь указывают инварианты иммутабельности INV-DEAL-10 / INV-LEAD-6 / INV-FIX-7.
  • Текущий исполнитель (мутабельные производные состава участников): ExecutorAgentUserId, ResponsibleAgentUserId — денормализованные проекции активных участников с ролями Executor и Responsible (см. п. 3). Меняются при переназначении ведения; служат быстрым фильтром «мои текущие процессы» и адресатом операций без обхода состава участников на каждый запрос.

Origin ⟂ Executor: при создании они совпадают (создатель — первый исполнитель), дальше расходятся свободно.

3. Единая модель участников — ParticipantRole

Состав участников процесса описывается перечислением ролей (единым для лида и сделки; на сделке доступны дополнительные роли сторон):

ParticipantRole = {
Executor, // текущий исполнитель — ровно один активный
Responsible, // ответственный/куратор ведения
CoAgent, // со-агент (нефинансовый в v1, ADR-0069)
Assistant, // ассистент (нефинансовый)
Lawyer, // юрист сопровождения (нефинансовый)
InvestorPrimary, // основной инвестор — ровно один активный
InvestorSecondary, // созаёмщики/соинвесторы — сколько угодно
SellManager, // менеджер застройщика (только сделка)
PinManager // менеджер ПИН (только сделка)
}
  • Additive: перечисление расширяемо; неизвестное потребителю значение роли трактуется как строка + warning (не падение), чтобы добавление роли не ломало консюмеров.
  • Инварианты состава (среди активных участников, RemovedAt is null):
    • ровно один активный Executor — единственный источник истины исполнителя;
    • ровно один активный InvestorPrimary; InvestorSecondary — произвольное число;
    • процесс нельзя оставить без Executor: снятие/замена исполнителя выполняется только атомарной передачей ведения (новый Executor назначается в той же операции). Отдельный запрет остаться без исполнителя при потере членства — предмет ADR-0072.
  • Несколько инвесторов у процесса. Уникальность лида больше не строится на паре (AgentUserId, InvestorUserId) — она переопределяется в терминах (Executor, InvestorPrimary) (см. п. 5).

4. Единственный источник истины исполнителя и получатель комиссии

  • Скаляр DealEntity.AgentUserId как «текущий исполнитель» упраздняется. Исполнитель — это активный участник с ролью Executor; ExecutorAgentUserId — лишь его денормализованная проекция, производная от состава, а не самостоятельная истина. Двойной источник (скаляр vs член Role = Agent) устранён: прежняя роль участника Agent заменяется ролью Executor, второго носителя «исполнителя» на агрегате нет.
  • Получатель комиссии = Executor на момент завершения процесса. Событие завершения несёт CommissionRecipientUserId — снимок ExecutorAgentUserId на момент deal-completed / fixation-approved. Порядок назначения получателя руководителем, границы его смены и компенсирующие корректировки — предмет доменной модели rewards; здесь фиксируется лишь, что источник получателя — единственный активный Executor, а не скаляр и не Origin.

5. Видимость по членству, а не по скаляру

Record-level доступ к процессу переопределяется с «AgentUserId == actor» на членство/полномочие:

  • актор — активный участник процесса (любая роль состава), или
  • актор — администратор/руководитель компании-владельца (по AgencyCompanyId для лида/клиента; по стороне агентства сделки) в рамках scope-иерархии (ADR-0025), или
  • актор — менеджер ПИН (кросс-tenant permission уровня платформы, ADR-0052).

Передача ведения меняет состав участников, а не переписывает владельца: доступ уходящего исполнителя снимается вместе с его членством, историческая атрибуция (Origin) при этом не трогается.

6. Явно переопределяемые инварианты

ИнвариантБылоСтало (ADR-0071)
INV-DEAL-3Ровно один активный Investor и один Agent на сделкеРовно один активный Executor и один InvestorPrimary; InvestorSecondary — сколько угодно; прочие агентские роли (CoAgent/Assistant/Lawyer) — по составу
INV-DEAL-10 / INV-LEAD-6 / INV-FIX-7Иммутабелен AgentUserId (+ AgencyCompanyId)Иммутабелен только Origin (OriginAgentUserId / OriginAgencyCompanyId); ExecutorAgentUserId / ResponsibleAgentUserId — мутабельные производные состава
INV-CRM-001Единственность активного лида на пару (AgentUserId, InvestorUserId)Единственность на пару (Executor, InvestorPrimary) активного лида

7. Смежные контексты

  • messenger: чат лида — новый вид ChatKind.Lead (у сделки чат уже есть, ADR-0055); состав участников чата синхронизируется с составом участников процесса (Executor + инвесторы + агентские роли), а не со скаляром AgentUserId.
  • rewards: получатель комиссии берётся из CommissionRecipientUserId события завершения (снимок Executor), а не из Origin; основания начислений по-прежнему опираются на иммутабельный Origin.
  • calendar / notifications: адресация событий и уведомлений — по составу участников (активные Executor/Responsible/инвесторы), а не по скаляру исполнителя.

Область Действия

ОбластьВходитНе входит
Сервисыcrm (владение процессами, состав участников, Origin/Executor), rewards (получатель = Executor), messenger (ChatKind.Lead, синк состава), calendar/notifications (адресация по составу)chessboard, catalog, identity-модель ролей (scope/permissions не меняются)
КонтрактыParticipantRole; поля OriginAgencyCompanyId/OriginAgentUserId/ExecutorAgentUserId/ResponsibleAgentUserId; CommissionRecipientUserId в событиях завершения; топик pin.crm.lead-participants-changed.v1; ChatKind.Leadконтракты выбора активного агентства (ADR-0070), увольнения (ADR-0072)
ДанныеСостав участников лида/сделки/фиксации; Origin-снапшот; денормализованные Executor/Responsible-проекциипересчёт/back-fill исторической атрибуции (запрещён, ADR-0061 п.4)
Эксплуатациямиграция расщепления AgentUserId → Origin + Executor; переоформление INV-DEAL-3 / INV-DEAL-10 / INV-LEAD-6 / INV-FIX-7 / INV-CRM-001мультистрочный агент (AgentShares[]) — v-next (ADR-0069)

Варианты

ВариантОписаниеПлюсыМинусыПричина решения
Компания-владелец + Origin/Executor + состав участников (принято)Процесс принадлежит tenant-компании; атрибуция — иммутабельный Origin; исполнитель — единственный активный Executor из состава; видимость по членствуРазводит «никогда не меняется» и «должен меняться»; один источник исполнителя; несколько инвесторов/агентов; штатная передача ведения без искажения историиМиграция расщепления скаляра, переоформление пяти инвариантовЕдинственный вариант, снимающий и двойной источник истины, и конфликт иммутабельности с переназначением
Оставить скаляр AgentUserId, добавить мутабельный CurrentAgentUserIdВторое скалярное поле «текущий исполнитель» рядом со снапшотомМалая миграцияСкаляр по-прежнему конкурирует с членом Role = Agent (двойной источник не снят); нет модели нескольких инвесторов/со-агентовОтклонено: не устраняет корневую проблему двойного источника истины
Только состав участников, без денормализованных Executor/ResponsibleНикаких проекций — исполнитель всегда вычисляется из составаНет риска рассинхрона проекцииКаждый список/фильтр «мои процессы» разворачивает состав; тяжёлые запросы на горячем путиОтклонено: проекция дешева и поддерживается атомарно со сменой состава
Back-fill Origin на новую компанию при передаче веденияПереписывать атрибуцию вслед за исполнителем«Свежая» принадлежностьИскажает историческую воронку, основания комиссий, антифродОтклонено прямо ADR-0061 п.4 (back-fill запрещён)

Последствия

ТипПоследствиеКомпенсация
АрхитектураЕдиный источник истины исполнителя (Executor); процессы принадлежат компании, агент — участник; передача ведения — операция над составомДенормализованные ExecutorAgentUserId/ResponsibleAgentUserId поддерживаются атомарно со сменой состава
ДанныеAgentUserId расщеплён на иммутабельный Origin + мутабельные проекции; INV-DEAL-3/-10, INV-LEAD-6, INV-FIX-7, INV-CRM-001 переоформляютсяМиграция: старый AgentUserIdOriginAgentUserId (снапшот) и стартовый ExecutorAgentUserId; уникальность лида — на (Executor, InvestorPrimary)
БезопасностьВидимость по членству/полномочию (участник ∨ админ компании ∨ менеджер ПИН), а не по скаляру AgentUserId == actorRecord-level проверки переписываются на состав участников + scope-иерархию (ADR-0025)
ЭксплуатацияСмежные консюмеры (messenger/rewards/calendar/notifications) переходят на состав/CommissionRecipientUserIdParticipantRole additive (unknown → строка + warning); события несут получателя явным полем

Затрагиваемые сервисы

КонтекстПоследствие
crmРасщепление AgentUserIdOriginAgencyCompanyId/OriginAgentUserId (иммутабельны) + ExecutorAgentUserId/ResponsibleAgentUserId (производные); единый ParticipantRole; упразднение скаляра-исполнителя (единственный источник — Executor); несколько инвесторов; видимость по составу; переоформление INV-DEAL-3, INV-DEAL-10, INV-LEAD-6, INV-FIX-7, INV-CRM-001; топик pin.crm.lead-participants-changed.v1
rewardsПолучатель комиссии — Executor на момент завершения (CommissionRecipientUserId в pin.crm.deal-completed.v1 / fixation-approved), а не Origin; основания начислений — по иммутабельному Origin
messengerНовый ChatKind.Lead; участники чата лида/сделки синхронизируются с составом участников процесса
calendar / notificationsАдресация событий и уведомлений — по активному составу участников, а не по скаляру исполнителя

Связанные ADR

  • ADR-0052 — tenancy: владелец процесса = компания (агентство для лида/клиента, застройщик для сделки/фиксации).
  • ADR-0061 — независимый агент; иммутабельная историческая привязка и запрет back-fill Origin (п. 4) — основа расщепления AgentUserId.
  • ADR-0068 — резолюция ставок на asOf = CompletedAt использует получателя-Executor и Origin как измерения.
  • ADR-0069 — комиссию получает только Executor; прочие агентские роли состава (CoAgent/Assistant/Lawyer) нефинансовые в v1.
  • ADR-0072 — увольнение агента и каскад: передача Executor до потери членства, запрет оставить процесс без исполнителя.

Затрагиваемые доки

crm/domain/deal.md, crm/domain/lead.md, crm/domain/fixation-request.md, crm/domain/client.md, crm/events/deal-members-changed.md.

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

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