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 переоформляются | Миграция: старый AgentUserId → OriginAgentUserId (снапшот) и стартовый ExecutorAgentUserId; уникальность лида — на (Executor, InvestorPrimary) |
| Безопасность | Видимость по членству/полномочию (участник ∨ админ компании ∨ менеджер ПИН), а не по скаляру AgentUserId == actor | Record-level проверки переписываются на состав участников + scope-иерархию (ADR-0025) |
| Эксплуатация | Смежные консюмеры (messenger/rewards/calendar/notifications) переходят на состав/CommissionRecipientUserId | ParticipantRole additive (unknown → строка + warning); события несут получателя явным полем |
Затрагиваемые сервисы
| Контекст | Последствие |
|---|---|
crm | Расщепление AgentUserId → OriginAgencyCompanyId/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.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: LeadEntity, Entity: ParticipantEntity (owned-child ChatEntity)
- API (11): DELETE /api/agent/crm/deals/{dealId}/members/{memberId} — Удалить участника сделки, DELETE /api/investor/crm/leads/{leadId}/booking-intents/{intentId} — Инвестор снимает объект-намерение, GET /api/agent/crm/leads/{leadId}/booking-intents — Список намерений лида, POST /api/agent/crm/deals/{dealId}/engage-developer — Вовлечь застройщика (Draft → Application), POST /api/agent/crm/deals/{dealId}/members — Добавить участника сделки, POST /api/agent/crm/leads/{leadId}/booking-intents — Агент предлагает объект клиенту, POST /api/investor/crm/leads/{leadId}/booking-intents — Инвестор добавляет объект-намерение, POST /api/investor/crm/leads/{leadId}/booking-intents/{intentId}/confirm — Инвестор подтверждает предложенное намерение, PUT /api/agent/crm/{deals|leads}/{id}/executor — Передать ведение (сменить исполнителя), PUT /api/agent/crm/{deals|leads}/{id}/responsible — Назначить/снять ответственного (куратора), PUT /api/agent/crm/deals/{dealId}/members/{memberId}/role — Сменить роль участника сделки
- Use Cases (16): UC-CRM-002. Агент создаёт сделку из лида (выбор объекта, гейт вовлечения застройщика), UC-CRM-007. Добавление участника в сделку, UC-CRM-008. Снятие участника со сделки, UC-CRM-009. Передача ведения сделки (смена исполнителя), UC-CRM-010. Назначение/снятие ответственного (куратора) процесса — ответственный ≠ исполнитель, UC-CRM-011. Лид с несколькими инвесторами: назначение основного инвестора, UC-CRM-012. Управление составом участников лида (добавление/снятие: второй агент, супруг/юрист инвестора), UC-CRM-013. Увольнение агента: каскад отвязки от процессов CRM, UC-CRM-015. Явное назначение исполнителя для разрешения спора о комиссии, UC-CRM-016. Инвестор добавляет намерения бронирования (много объектов), UC-CRM-017. Flow B: агент предлагает объект → Draft-сделка → подтверждение инвестором → вовлечение застройщика, UC-CRM-018. Несколько сделок из одной одобренной фиксации (по разным объектам застройщика) (+4)
- События (2): Event Contract: pin.crm.lead-booking-intent-changed (LeadBookingIntentChangedEvent), Event Contract: pin.crm.lead-participants-changed (LeadParticipantsChangedEvent)
- Консюмеры (5): Consumer: CRM Agent Offboarding (CrmAgentOffboardingConsumer), Consumer: CRM Deal Completed, Consumer: CRM Fixation Approved, Consumer: Deal Chat Members Sync (MessengerDealMembersChangedConsumer), Consumer: Lead Chat Participants Sync (MessengerLeadParticipantsChangedConsumer)
- index.md (1): Messenger Service