Entity / Aggregate: DealEntity
:::note Дом полей
Дом полей агрегата «Сделка» контекста crm; покрывает US-реестр «Сделка» (US-INV-01..23, US-AGT-01..08,
US-DEV-01..08, US-MGR-01..06). Все проекции (канбан /crm, карточка /crm/deals/[id], «Мои сделки»
инвестора /profile/deals, payload событий, статистика ЛК) ссылаются сюда и описывают только дельту.
:::
Назначение
DealEntity — классическая сделка: связь «инвестор + агент + застройщик + объект недвижимости».
Создаётся только из лида (lead.md) кнопкой «Создать сделку» агента после выбора объекта.
При создании сделка попадает в один из двух статусов в зависимости от гейта вовлечения застройщика: если для
пары «инвестор × застройщик» есть одобренная фиксация (fixation-request.md) и
объект — подтверждённое инвестором намерение (lead.md) — сразу
Application (застройщик вовлечён, SellManager в составе); иначе — Draft (сделка агента с инвестором,
застройщик ещё не вовлечён). Одобрение фиксации не создаёт сделку, а авторизует заводить сделки по
объектам застройщика (фиксация→сделки 1:N). У одного лида — много одновременных сделок к одному/разным
застройщикам.
Сделка живёт на одной канбан-доске в настраиваемых этапах застройщика
(pipeline-step.md), имеет способ оплаты (влияет на этапы), тип договора ДДУ/ДКП,
банк/эскроу (US-INV-07/19), суммы, текущее бронирование (booking.md), документы
(deal-document.md) и таймлайн событий (deal-timeline-event.md).
Чат сделки живёт в messenger (ADR-0055); на сделке — только проекция ChatStatus.
Классификация
| Поле | Значение |
|---|---|
| Контекст | crm |
| Kind | AggregateRoot (дочерняя DealMemberEntity — Entity внутри агрегата) |
| Source of truth | crm |
| Таблица | crm.deals (+ crm.deal_members) |
| Tenant isolation | TenantId = DeveloperCompanyId (ADR-0052: «застройщик для сделки»); историческая атрибуция сторон — иммутабельный Origin-снимок OriginAgencyCompanyId/OriginAgentUserId; текущий исполнитель и инвесторы — через состав участников DealMemberEntity (ADR-0071); видимость — по членству (record-level) |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор сделки. |
TenantId | Guid | Да | index ix_deals_tenant_id | из объекта (владелец объекта недвижимости) | DeveloperCompanyId застройщика объекта. |
LeadId | Guid | Да | FK fk_deals_lead_id → crm.leads(id), index | app | Лид-основание (сделка — только из лида). |
ClientId | Guid | Да | FK fk_deals_client_id → crm.clients(id), index | из лида | Карточка клиента агентства. |
FixationRequestId | Guid? | Нет | FK fk_deals_fixation_request_id → crm.fixation_requests(id), index where not null (НЕ unique) | app | Одобренная фиксация-основание вовлечения застройщика для пары «инвестор × застройщик». Одна фиксация авторизует много сделок по объектам этого застройщика (связь фиксация→сделки 1:N), поэтому значение не уникально среди сделок. |
IntentId | Guid? | Нет | FK fk_deals_intent_id → crm.lead_booking_intents(id), index where not null | app | Намерение инвестора по объекту (lead.md), из которого выросла сделка. Заполнено, если сделка стартовала из подтверждённого/предложенного намерения; связывает Draft-сделку с намерением, чьё подтверждение инвестором — часть гейта Draft → Application. |
IntentConfirmedAt | DateTimeOffset? | Нет | — | app (из намерения) | Момент подтверждения инвестором связанного намерения (snapshot LeadBookingIntentEntity.ConfirmedAt); null, пока намерение не подтверждено (Flow B). Одно из условий гейта Draft → Application. |
Title | string | Да | varchar(256), не пустое | app (генерация из объекта недвижимости + правка) | Заголовок карточки на канбане. |
Status | DealStatus | Да | enum как text, varchar(32), index ix_deals_status | app | Статус процесса (US-INV-03: заявка/бронь/продажа/отказ + внутренний Completed, см. enum). |
StepId | Guid | Да | FK fk_deals_step_id → crm.pipeline_steps(id), index ix_deals_step_id | app (drag-n-drop / авто-переходы) | Текущий настраиваемый этап канбана. |
StepGroup | PipelineStepGroup | Да | enum как text, varchar(32), index | денормализация из PipelineStepEntity.Group | Системная группа текущего этапа (быстрые фильтры/статистика без join; поддерживается методом MoveToStep). |
PrimaryInvestorUserId | Guid | Да | index ix_deals_primary_investor_user_id | производное из активного участника InvestorPrimary | Основной инвестор сделки (мутабельная денормализация; допускается несколько инвесторов-участников — InvestorSecondary). Поддерживается при смене основного инвестора. |
OriginAgentUserId | Guid | Да | index ix_deals_origin_agent_user_id | из лида (снимок на момент создания) | Агент-инициатор — «кто привёл клиента и создал сделку» (US-AGT-01). Историческая атрибуция (воронка агентства, основание комиссии rewards, антифрод), иммутабелен: не пересчитывается при смене агентом агентства/передаче ведения (ADR-0061 п.4, ADR-0071, INV-DEAL-10). |
OriginAgencyCompanyId | Guid | Да | index ix_deals_origin_agency_company_id | из лида (снимок на момент создания) | Агентство агента-инициатора на момент создания сделки. Историческая атрибуция, иммутабелен (ADR-0061 п.4, ADR-0071, INV-DEAL-10). Текущее агентство-владелец = агентство активного участника Executor (производное, не хранится как снимок). |
ExecutorAgentUserId | Guid | Да | index ix_deals_executor_agent_user_id | производное из активного участника Executor | Текущий исполнитель сделки — единственный источник истины исполнителя (получатель комиссии). Мутабельная денормализация состава участников; меняется передачей ведения (reassign). |
ResponsibleAgentUserId | Guid? | Нет | index where not null | производное из активного участника Responsible | Ответственный/куратор ведения, если назначен (может ≠ Executor). Мутабельная денормализация состава участников. |
PropertyObjectId | Guid | Да | index ix_deals_property_object_id; логическая ссылка на chessboard | app (выбор объекта) | Объект сделки. Заменяем: «изменить объект сделки если клиент передумал» — метод ChangePropertyObject, старое значение — в таймлайн (PropertyObjectChanged). |
ProjectId | Guid | Да | index | реплика из chessboard по PropertyObjectId | Проект объекта (фильтры застройщика US-DEV-01, статистика ЛК по проекту). |
BuildingId | Guid? | Нет | index where not null | реплика из chessboard | Здание/литер (фильтр статистики ЛК). |
ContractType | DealContractType? | Нет (обязателен с группы Contract, инвариант INV-DEAL-6) | enum как text, varchar(16) | app (менеджер застройщика/агент) | Тип договора: ДДУ / ДКП (US-INV-08; влияет на этапы). |
PaymentMethod | PaymentMethod? | Нет (обязателен при конвертации лида в сделку) | enum как text, varchar(32) | app | Способ оплаты (enum — purchase-program.md); влияет на набор этапов. |
InstallmentPresetId | Guid? | Нет | FK fk_deals_installment_preset_id → crm.installment_presets(id); обязателен при PaymentMethod = Installment (check ck_deals_installment_preset) | app | Выбранный пресет рассрочки застройщика. |
Price | Money (owned) | Да | см. VO ниже | из chessboard на момент создания/смены объекта недвижимости | Полная стоимость объекта недвижимости (US-INV-05: цена за м² × площадь — источник цифр chessboard, здесь snapshot). |
BookingId | Guid? | Нет | FK fk_deals_booking_id → crm.bookings(id), unique where not null | app | Текущее (последнее активное) бронирование сделки (booking.md). |
BankId | Guid? | Нет | логическая ссылка на справочник банков chessboard | app (менеджер застройщика) | Банк проведения сделки/эскроу (US-INV-07: «Не указан», если null). |
EscrowAccountNumber | string? | Нет | varchar(64); хранится в открытом виде, наружу — маска (см. Безопасность) | app (инвестор, US-INV-19) | Номер эскроу-счёта. |
EscrowStatus | EscrowStatus | Да | enum как text, varchar(32), default None | app | None / Provided («Эскроу забронирован», US-INV-19). |
ContractsPreparedAt | DateTimeOffset? | Нет | — | app (US-DEV-04) | Момент отметки «Договоры подготовлены» (US-INV-20). |
RosreestrRegisteredAt | DateTimeOffset? | Нет | — | app (застройщик) | Подтверждение регистрации в Росреестре. |
CompletedAt | DateTimeOffset? | Нет | — | app (застройщик) | Подтверждение завершения сделки. |
CancelledAt | DateTimeOffset? | Нет | — | app | Момент отказа. |
CancelReason | string? | Нет | varchar(1000); обязателен при Status = Cancelled (check ck_deals_cancel_reason) | app | Причина отказа. |
ChatStatus | DealChatStatus | Да | enum как text, varchar(32), default Pending | consumer pin.messenger.deal-chat-status-changed.v1 | Проекция готовности чата: Pending/Provisioning/Ready/Failed (ADR-0055). |
TermsAcknowledgedAt | DateTimeOffset? | Нет | — | app (инвестор, US-INV-18) | Отметка «Ознакомлен» с условиями сделки. |
CreatedAt | DateTimeOffset | Да | index ix_deals_created_at desc | app | Дата создания. |
CreatedByUserId | Guid? | Нет | — | app | Агент/Менеджер ПИН, создавший сделку (из лида по объекту). |
UpdatedAt | DateTimeOffset | Да | index ix_deals_updated_at desc | app | Дата последнего обновления (US-INV-04). |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | — | app | Soft-delete (административно). |
DeletedByUserId | Guid? | Нет | — | app | Кто скрыл. |
Enum DealStatus
Отображаемые статусы US-INV-03 — «заявка, бронь, продажа, отказ»; Completed — внутреннее терминальное
состояние после подтверждения завершения застройщиком, в UI показывается как «продажа (завершена)».
Draft — предварительное состояние «сделка агента с инвестором до вовлечения застройщика»: видно агенту/
инвестору/Менеджеру ПИН, но невидимо застройщику (developer-facing проекции и канбан застройщика
фильтруют Status != Draft). Расхождение зафиксировано в README (принятые решения).
| Значение | Отображение | Описание |
|---|---|---|
Draft | Черновик | Сделка агента с инвестором по объекту, застройщик ещё не вовлечён: состав Executor + InvestorPrimary + PinManager без SellManager; TenantId уже задан (застройщик объекта), но сделка скрыта из канбана/статистики застройщика (фильтр Status != Draft). Создаётся, когда агент завёл сделку по объекту, для которого ещё нет подтверждённого намерения инвестора и/или одобренной фиксации. Из Draft — только в Application (гейт вовлечения застройщика) или Cancelled. |
Application | Заявка | Сделка создана, объект выбран, бронирования ещё нет. Застройщик вовлечён (SellManager в составе). |
Booking | Бронь | Активен процесс бронирования (Резерв/Бронь, booking.md). |
Sale | Продажа | Оплата брони подтверждена/бронь завершена, идёт оформление ДДУ/ДКП → оплата → Росреестр. |
Completed | Продажа (завершена) | Застройщик подтвердил завершение; событие в investor-owner/rewards. |
Cancelled | Отказ | Сделка прекращена (любой стороной), CancelReason обязателен. |
Enum DealContractType
| Значение | Описание |
|---|---|
Ddu | Договор долевого участия (эскроу обязателен — INV-DEAL-7). |
Dkp | Договор купли-продажи. |
Enum EscrowStatus
| Значение | Описание |
|---|---|
None | Номер счёта не указан («Не указан»). |
Provided | Инвестор указал номер («Эскроу забронирован», US-INV-19). |
Enum DealChatStatus
| Значение | Описание |
|---|---|
Pending | Чат ещё не запрошен/не создан. |
Provisioning | messenger создаёт чат по событию pin.crm.deal-created.v1. |
Ready | Чат создан, участники синхронизированы. |
Failed | Создание чата упало; ретрай консюмером messenger. |
Value objects
Money (owned)
| Поле | Тип | Обязательность | Ограничения | Колонка |
|---|---|---|---|---|
Amount | decimal | Да | numeric(18,2), >= 0 | price_amount |
CurrencyId | short | Да | типизированная ссылка на справочник валют handbook (без FK); default — валюта RUB (запись с IsoCode = 'RUB') | price_currency_id |
:::info Мультивалютность не поддерживается
Типизированная ссылка ≠ мультивалютность: денежные операции остаются RUB-only (ADR-0048). Отдельного
строкового кода валюты контракт не несёт — display-код (RUB) потребители берут из CurrencySnapshot
в исходящих событиях либо из lookup справочника
(snapshots.md); валидация CurrencyId на write-path — internal
resolve handbook (resolve-references.md).
:::
Модели JSONB (IDocument, versioned)
JSONB-модели не используются (типизированные payload — в deal-timeline-event.md).
Дочерняя сущность DealMemberEntity
Таблица crm.deal_members. Единая модель участников ParticipantRole (ADR-0071; тот же enum у лида и
фиксации): исполнитель/ответственный/со-агентские роли + инвесторы + стороны застройщика (SellManager) и
ПИН (PinManager добавляется автоматически, US-MGR-01, глоссарий «Участник сделки»). Активный участник —
RemovedAt is null; денормализованные ExecutorAgentUserId/ResponsibleAgentUserId/PrimaryInvestorUserId
на сделке поддерживаются атомарно со сменой состава.
| Поле | Тип | Обязательность | Ограничения | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор участия. |
DealId | Guid | Да | FK fk_deal_members_deal_id → crm.deals(id) cascade; unique ux_deal_members_deal_user (DealId,UserId) | app | Сделка. |
TenantId | Guid | Да | index | из сделки | Дублируется для tenant-фильтра. |
UserId | Guid | Да | index ix_deal_members_user_id; логическая ссылка на identity.users(id) | app | Пользователь-участник. |
Role | ParticipantRole | Да | enum как text, varchar(32) | app | Роль участника (см. enum). Меняется операцией смены роли/передачи ведения. Неизвестное потребителю значение — строка + warning (additive). |
AddedByUserId | Guid? | Нет | — | app | Кто добавил (null — система: авто-состав при создании). |
CreatedAt | DateTimeOffset | Да | — | app | Момент добавления. |
RemovedAt | DateTimeOffset? | Нет | — | app | Soft-удаление участия (история для чата/аудита; активные участники — RemovedAt is null). |
RemovedByUserId | Guid? | Нет | — | app | Кто удалил. |
Enum ParticipantRole
Единый additive-enum состава участников (ADR-0071); общий для лида/сделки/фиксации. На сделке доступны все
значения; SellManager/PinManager — только на сделке. Соответствие прежних значений: Investor →
InvestorPrimary, Agent → Executor.
| Значение | Область | Описание |
|---|---|---|
Executor | лид/сделка | Текущий исполнитель — ровно один активный; единственный источник истины исполнителя и получатель комиссии. |
Responsible | лид/сделка | Ответственный/куратор ведения (опционален, может ≠ Executor). |
CoAgent | лид/сделка | Со-агент (нефинансовый в v1, ADR-0069); допускается несколько. |
Assistant | лид/сделка | Ассистент (нефинансовый); допускается несколько. |
Lawyer | лид/сделка | Юрист сопровождения (нефинансовый); допускается несколько. |
InvestorPrimary | лид/сделка | Основной инвестор-покупатель — ровно один активный. |
InvestorSecondary | лид/сделка | Созаёмщики/соинвесторы — произвольное число. |
SellManager | только сделка | Менеджер продаж застройщика. |
PinManager | только сделка | Менеджер ПИН — авто-участник всех сделок (US-MGR-01..06), не более одного активного. |
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
LeadEntity | reference (основание) | LeadId | загрузка при создании | restrict |
ClientEntity | reference | ClientId | загрузка при создании | restrict |
FixationRequestEntity | reference (авторизация, 1 фиксация → N сделок) | FixationRequestId | загрузка при гейте Draft → Application (проверка одобренной фиксации пары «инвестор × застройщик») | restrict |
LeadBookingIntentEntity | reference (намерение по объекту) | IntentId | загрузка при создании / на гейте Draft → Application (проверка ConfirmedByInvestor) | restrict |
PipelineStepEntity | reference | StepId | загрузка при MoveToStep | restrict |
DealMemberEntity | owned-child | deal_members.deal_id | загрузка при карточке; список участников сделки | cascade |
BookingEntity | reference (1 активная) | BookingId / bookings.deal_id | выборка по DealId (активные статусы) | restrict |
DealDocumentEntity | reference (children) | deal_documents.deal_id | выборка (вкладка «Документы») | restrict |
DealTimelineEventEntity | reference (children) | deal_timeline_events.deal_id | выборка с пагинацией (вкладка «Ход сделки») | restrict |
Объект недвижимости/проект/банк (chessboard) | внешняя логическая | PropertyObjectId/ProjectId/BankId | display-данные API chessboard/catalog | — |
Инварианты и переходы состояний
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-DEAL-1 | Сделка создаётся только из лида Status = Active; PropertyObjectId обязателен всегда; TenantId = застройщик выбранного PropertyObjectId (set-once, immutable — конфликта с INV-DEAL-10 нет). Ветка по гейту вовлечения застройщика: при создании сразу в Application PaymentMethod обязателен; при создании в Draft (гейт не выполнен) PaymentMethod необязателен — его требование откладывается до перехода Draft → Application (вовлечение застройщика отложено) | при создании сделки | CRM_LEAD_NOT_ACTIVE / CRM_DEAL_PAYMENT_METHOD_REQUIRED |
| INV-DEAL-2 | Начальный состав участников зависит от статуса создания: Draft — Executor + InvestorPrimary + авто-PinManager без SellManager (застройщик не вовлечён); Application — те же + SellManager (застройщик вовлечён). SellManager появляется в составе только с переходом в Application (при создании или на гейте Draft → Application) | при создании сделки | — |
| INV-DEAL-3 | Среди активных участников (RemovedAt is null): ровно один Executor, ровно один InvestorPrimary, не более одного PinManager; UserId уникален на сделку; произвольное число CoAgent/Assistant/Lawyer/InvestorSecondary; Responsible опционален (≤1); сделку нельзя оставить без активного Executor — снятие/замена исполнителя только атомарной передачей ведения (новый Executor назначается в той же операции) | при добавлении/удалении/смене роли/передаче ведения участника (unique ux_deal_members_deal_user) | CRM_DEAL_MEMBER_ROLE_CONFLICT / CRM_DEAL_MEMBER_DUPLICATE / CRM_LAST_EXECUTOR_REQUIRED |
| INV-DEAL-4 | ChangePropertyObject разрешён в статусах Application/Booking (при Booking — только с одновременной отменой активной брони); новый объект недвижимости того же TenantId; Price перечитывается; в таймлайн — PropertyObjectChanged | при смене объекта сделки | CRM_DEAL_PROPERTY_OBJECT_CHANGE_FORBIDDEN |
| INV-DEAL-5 | MoveToStep: целевой этап принадлежит TenantId и не архивирован; StepGroup денормализуется; переход в этап группы Cancelled требует CancelReason | при смене этапа | CRM_DEAL_STEP_FORBIDDEN |
| INV-DEAL-6 | С момента перехода Sale (подготовка договора) обязательны ContractType и PaymentMethod; для Installment — InstallmentPresetId | при старте продажи | CRM_DEAL_CONTRACT_TYPE_REQUIRED |
| INV-DEAL-7 | ContractsPreparedAt/RosreestrRegisteredAt/CompletedAt проставляются только менеджером TenantId строго в порядке: договоры → (эскроу Provided, если Ddu) → Росреестр → завершение | при отметке этапов оформления | CRM_DEAL_STAGE_ORDER_VIOLATION |
| INV-DEAL-8 | Cancelled — из любого нетерминального статуса; активная бронь отменяется в одном согласованном изменении (объект недвижимости освобождается событием) | при отмене сделки | CRM_DEAL_ALREADY_TERMINAL |
| INV-DEAL-9 | Терминальные Completed/Cancelled не редактируются (кроме чтения) | при любой операции над сделкой | CRM_DEAL_ALREADY_TERMINAL |
| INV-DEAL-10 | Иммутабельны только Origin-поля OriginAgencyCompanyId/OriginAgentUserId: фиксируются из лида при создании (set-once), не пересчитываются и не back-fill-ятся при смене агентом агентства/передаче ведения/увольнении (ADR-0061 п.4, ADR-0071). Переписывание исказило бы историю сделки, воронку агентства и основание комиссии rewards. ExecutorAgentUserId/ResponsibleAgentUserId/PrimaryInvestorUserId — мутабельные производные состава участников (reassign/set-primary), set-once на них не действует | при попытке правки Origin-полей | CRM_DEAL_SNAPSHOT_IMMUTABLE |
| INV-DEAL-11 | Гейт Draft → Application (вовлечение застройщика). Переход разрешён, только когда одновременно: (а) связанное намерение подтверждено инвестором (LeadBookingIntentEntity.ConfirmedByInvestor = true для IntentId) — иначе CRM_DEAL_INTENT_NOT_CONFIRMED; (б) для пары «инвестор × застройщик» (InvestorPrimary × TenantId) есть одобренная фиксация — иначе CRM_DEAL_FIXATION_REQUIRED; (в) задан PaymentMethod (требование INV-DEAL-1 для Application) — иначе CRM_DEAL_PAYMENT_METHOD_REQUIRED. При выполнении гейта в состав добавляется SellManager (INV-DEAL-2), проставляется IntentConfirmedAt, публикуются developer-facing события. Обратного перехода Application → Draft нет | при переходе Draft → Application (вовлечение застройщика) | CRM_DEAL_INTENT_NOT_CONFIRMED / CRM_DEAL_FIXATION_REQUIRED / CRM_DEAL_PAYMENT_METHOD_REQUIRED |
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
DealCreated / DealMemberAdded / DealPropertyObjectChanged / DealStatusChanged | доменное | соответствующие переходы агрегата | внутренние подписчики (таймлайн пишется в том же согласованном изменении) |
DealCreatedEvent | интеграционное, topic pin.crm.deal-created.v1, ContractVersion = 1 | Создание сделки | DealId, TenantId, OriginAgencyCompanyId, OriginAgentUserId, ExecutorAgentUserId, PrimaryInvestorUserId, PropertyObjectId, ProjectId, Title, Status (может быть Draft), PaymentMethod (может отсутствовать при Draft), активные участники [{UserId, Role}] (Role: ParticipantRole, при Draft — без SellManager); messenger/notifications реагируют и на Draft (чат/уведомления агент+инвестор), developer-facing консюмеры — только на Application (фильтр Status != Draft); контракт — ../events/deal-created.md |
DealStageChangedEvent | интеграционное, topic pin.crm.deal-stage-changed.v1, ContractVersion = 1 | MoveToStep и смена Status (в т.ч. Draft → Application — вовлечение застройщика) | DealId, TenantId, FromStepId, ToStepId, FromStatus, ToStatus, ActorUserId; консюмеры: messenger (карточка в чат), notifications; переход Draft → Application = момент, с которого сделка становится видимой застройщику; контракт — ../events/deal-stage-changed.md |
DealMembersChangedEvent | интеграционное, topic pin.crm.deal-members-changed.v1, ContractVersion = 1 | Add/Remove/ChangeRole/передача ведения участника | DealId, TenantId, полный активный состав [{UserId, Role}] (Role: ParticipantRole) + дельта; консюмеры: messenger (синк участников чата), notifications |
DealCompletedEvent | интеграционное, topic pin.crm.deal-completed.v1, ContractVersion = 1 | Sale → Completed | DealId, TenantId, PrimaryInvestorUserId, CommissionRecipientUserId (= ExecutorAgentUserId на момент завершения — получатель комиссии, ADR-0071), OriginAgentUserId, OriginAgencyCompanyId (иммутабельная атрибуция — основание начислений), PropertyObjectId, ProjectId, атрибуты объекта (PropertyType, LayoutId?, LayoutGroupId?, Rooms?, AreaTotalM2), BasePrice/FinalPrice {Amount, CurrencyId} + Currency (CurrencySnapshot, |
snapshots.md), ContractType, CompletedAt; консюмеры: investor-owner (объект в портфель), rewards (комиссионное + реферальное начисления: получатель — CommissionRecipientUserId, основание — Origin), chessboard (объект недвижимости «Продан»), notifications; контракт — ../events/deal-completed.md | |||
DealCancelledEvent | интеграционное, topic pin.crm.deal-cancelled.v1, ContractVersion = 1 | * → Cancelled | DealId, TenantId, CancelReason, CancelledAt, PropertyObjectId (освобождение объекта недвижимости консюмером chessboard, если бронь была активна); консюмеры: chessboard, notifications, rewards |
DocumentRequestedEvent | интеграционное, topic pin.crm.document-requested.v1, ContractVersion = 1 | Запрос документов (бронь/ДДУ/ДКП) | см. deal-document.md; консюмеры: messenger (карточка «загрузите документы», US-INV-16), notifications |
:::info Атомарность публикации Интеграционные события публикуются атомарно с мутацией агрегата и записью таймлайна. :::
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK; ix_deals_tenant_id; (tenant_id,step_id) — канбан; (tenant_id,status,created_at desc) — списки/статистика ЛК застройщика; (origin_agency_company_id,origin_agent_user_id,status) — историческая воронка/статистика агентства; ix_deals_executor_agent_user_id — «мои текущие сделки» исполнителя; ix_deals_responsible_agent_user_id where not null; ix_deals_primary_investor_user_id — «Мои сделки» инвестора; ix_deals_property_object_id; ix_deals_lead_id; ix_deals_updated_at desc; deal_members: PK, ux_deal_members_deal_user, ix_deal_members_user_id |
| Полнотекстовый поиск | Elasticsearch-проекция DealSearchDocument (../search/deals.md, ADR-0059) — Title + ФИО клиента (snapshot); PostgreSQL-FTS (tsvector/GIN) не используется |
| Партиционирование / retention | none (ADR-0056; сделки — низкообъёмная сущность) |
| Concurrency | оптимистическая блокировка: гонки drag-n-drop / решений сторон |
| Permissions / PII | crm.deals.view / crm.deals.manage / crm.deals.decide (методы застройщика). Видимость — по членству (ADR-0071), а не по скаляру-владельцу: доступ = актор ∈ активные участники сделки (любая роль ParticipantRole) ИЛИ администратор/руководитель компании-владельца (агентство активного Executor — по scope-иерархии ADR-0025; застройщик — свой TenantId) ИЛИ Менеджер ПИН (кросс-tenant platform-scope permission, US-MGR-01..06 + ручные статусы/этапы). Сделки в статусе Draft невидимы застройщику: застройщик ещё не вовлечён (в составе нет SellManager), поэтому канбан застройщика, его списки/статистика и developer-facing проекции применяют фильтр Status != Draft; сторона застройщика видит сделку только с момента Application. Инвестор видит сделку как участник (InvestorPrimary/InvestorSecondary), без данных о фиксации; передача ведения снимает доступ уходящего исполнителя вместе с его членством (Origin-атрибуция не трогается). Служба заботы — read-only через permissions. EscrowAccountNumber — Sensitive: наружу маска ****NNNN, полный — только SellManager/PinManager; в логах запрещён. |
| Audit | отметки создания/изменения/удаления (кто и когда), soft-delete; бизнес-история — таймлайн (deal-timeline-event.md, append-only) |
Кросс-сервисные контракты и реплики
Сделка опирается на реплики соседних контекстов; она не читает их таблицы синхронно в командах, а держит snapshot/проекции.
| Данные | Источник | Как попадает | Поведение при отсутствии/устаревании |
|---|---|---|---|
PropertyObjectId/ProjectId/BuildingId, цена объекта недвижимости | chessboard | реплика на момент создания/смены объекта недвижимости; Price — snapshot | при создании/смене объекта недвижимости требуется актуальная реплика объекта; если объект недвижимости не найден в реплике → CRM_DEAL_PROPERTY_OBJECT_UNKNOWN (создание/смена блокируется). Цена в сделке — snapshot и не пересчитывается задним числом при изменении прайса в шахматке (историческая корректность суммы) |
Справочник банков BankId | chessboard | display-реплика | неизвестный/устаревший BankId — показывается как «Не указан», операция не падает |
ChatStatus | messenger (pin.messenger.deal-chat-status-changed.v1) | проекция | до прихода события — Pending/Provisioning; UI показывает «чат создаётся»; сделка полностью функциональна и без готового чата |
| ФИО/телефон клиента (в поиске/карточке) | crm.clients (сам контекст) → identity | snapshot в ClientEntity + реплика identity | см. client.md; в самой сделке PII не дублируется сверх нужного |
Что сделка предоставляет наружу — интеграционные события ниже (ключ партиционирования = DealId,
ContractVersion = 1): pin.crm.deal-created, -stage-changed, -members-changed, -completed,
-cancelled, -document-requested. Consumers полагаются на полноту активного состава участников в
deal-created/members-changed и на snapshot суммы/объекта недвижимости в deal-completed.
Межсервисные последовательности и промежуточные состояния
:::info Гарантии доставки
Все цепочки — асинхронные, at-least-once, порядок событий одной сделки гарантирован ключом DealId.
:::
Создание сделки. В одном согласованном изменении: запись сделки (Draft или Application по гейту
INV-DEAL-1/INV-DEAL-11), состав участников (INV-DEAL-2: при Draft — без SellManager), строка таймлайна
DealCreated, событие в лид DealCreated, исходящее DealCreatedEvent. Далее асинхронно: messenger
создаёт чат по DealCreatedEvent (для Draft — чат агент+инвестор, без застройщика) и по готовности
публикует deal-chat-status-changed → проекция ChatStatus в сделке проходит Pending → Provisioning → Ready (или Failed с ретраем консюмера messenger). notifications уведомляет участников.
- Что видит пользователь. Карточка сделки, канбан агента/инвестора и таймлайн доступны сразу; сделка в
Draftне видна застройщику (фильтрStatus != Draft). Блок чата показывает «создаётся», покаChatStatus ≠ Ready. Отказ провижининга (Failed) не блокирует работу со сделкой — messenger ретраит, статус вернётся вReady.
Вовлечение застройщика (Draft → Application). Когда выполнен гейт INV-DEAL-11 (намерение подтверждено
инвестором + одобренная фиксация для пары «инвестор × застройщик» + задан PaymentMethod): в одном
согласованном изменении статус переходит в Application, в состав добавляется SellManager, проставляется
IntentConfirmedAt, пишется таймлайн, публикуются DealStageChangedEvent (Draft → Application) и
DealMembersChangedEvent (появился SellManager). С этого момента сделка видна застройщику и попадает на его
канбан; developer-facing консюмеры реагируют начиная с Application.
Смена этапа/статуса. MoveToStep/смена Status → DealStageChangedEvent (→ messenger карточка в
чат, notifications). StepGroup денормализуется в том же изменении (INV-DEAL-5), поэтому фильтры/
статистика согласованы немедленно, а карточка в чате — с задержкой.
Смена состава участников. Add/Remove/ChangeRole → DealMembersChangedEvent с полным активным
составом (не дельтой) → messenger синхронизирует участников чата (идемпотентный набор — устойчив к
переупорядочиванию доставки).
Завершение сделки. Sale → Completed (актор — менеджер застройщика, после Росреестра, INV-DEAL-7) →
DealCompletedEvent → investor-owner (объект в портфель инвестора), rewards (начисление комиссии:
получатель — CommissionRecipientUserId = активный Executor на момент завершения; основание/атрибуция —
иммутабельный OriginAgentUserId/OriginAgencyCompanyId + snapshot Price), chessboard (объект
недвижимости «Продан»), notifications. Атрибуция и основание считаются по иммутабельному Origin-снимку
(INV-DEAL-10), поэтому последующая смена агентом агентства/передача ведения их не искажает; получатель —
исполнитель, назначенный на момент завершения.
Отмена сделки. → Cancelled (INV-DEAL-8): активная бронь отменяется в том же согласованном изменении;
DealCancelledEvent → chessboard освобождает объект недвижимости (если бронь была активна),
notifications, rewards (аннулирование основания комиссии).
Отказы, идемпотентность и конкуренция
- Фиксация → сделки (1:N). Одобренная фиксация авторизует работу агента с клиентом по проектам
застройщика, но сама по себе сделку не создаёт: на одну фиксацию заводится сколько угодно сделок по
разным объектам того же застройщика, поэтому
FixationRequestIdне уникален среди сделок. Дедуп сделок по объекту обеспечивает не фиксация, а инвариант «одно активное намерение на (LeadId,PropertyObjectId)» (lead.md) — на один объект лида в моменте не более одной незакрытой сделки. - Конкурентный drag-n-drop. Одновременные
MoveToStepот разных участников — оптимистическая блокировка по версии: побеждает первый коммит, проигравший перечитывает актуальный этап (его карточка «отскакивает» на фактическую колонку). Целевой этап архивирован/чужой →CRM_DEAL_STEP_FORBIDDEN. - Порядок оформления.
ContractsPreparedAt/RosreestrRegisteredAt/CompletedAt— строго по порядку (INV-DEAL-7); попытка отметить шаг вне очереди →CRM_DEAL_STAGE_ORDER_VIOLATION, независимо от гонок. - Гонка «завершение ↔ отмена». Один терминальный переход побеждает; проигравший видит
CRM_DEAL_ALREADY_TERMINAL(INV-DEAL-9), терминальные статусы иммутабельны. - Частичный отказ консюмеров. Сделка — источник истины: недоступность
messenger/rewards/investor-ownerне откатывает переход; события переотправляются (at-least-once), консюмеры идемпотентны поMessageId. Провал провижининга чата отражается какChatStatus = Failedи лечится ретраем messenger. - Реплика объекта недвижимости устарела. Если к моменту
ChangePropertyObjectобъект недвижимости снят застройщиком с продажи в шахматке — смена отклоняется на источнике (chessboard-реплика), сделка остаётся на прежнем объекте недвижимости; сумма-snapshot не меняется без явной смены объекта недвижимости.
Acceptance-критерии
- Happy сразу в Application (гейт выполнен). Дано: лид
Active, объект застройщика; для пары «инвестор × застройщик» есть одобренная фиксация; объект — подтверждённое инвестором намерение; заданPaymentMethod. Когда агент создаёт сделку. Тогда сделкаApplicationс составомExecutor+InvestorPrimary+SellManager+PinManager(OriginAgentUserId/OriginAgencyCompanyId= агент/агентство создателя,ExecutorAgentUserId= создатель),IntentId/IntentConfirmedAtзаполнены, таймлайнDealCreated, опубликованDealCreatedEvent, у лида — событиеDealCreated. - Создание в Draft (гейт не выполнен). Дано: лид
Active, объект застройщика, но нет подтверждённого намерения и/или одобренной фиксации. Когда агент создаёт сделку. Тогда сделкаDraftс составомExecutor+InvestorPrimary+PinManager(безSellManager),PaymentMethodможет отсутствовать, сделка невидима застройщику (Status != Draft), опубликованDealCreatedEvent(Status = Draft). - Вовлечение застройщика (
Draft → Application). Дано: сделкаDraft, связанное намерение подтверждено инвестором, есть одобренная фиксация пары «инвестор × застройщик», заданPaymentMethod. Когда агент вовлекает застройщика. Тогда сделка →Application, в состав добавленSellManager, проставленIntentConfirmedAt, опубликованыDealStageChangedEvent(Draft → Application) иDealMembersChangedEvent; сделка становится видимой застройщику. - Гейт: намерение не подтверждено. Дано: сделка
Draft, намерениеConfirmedByInvestor = false. Когда агент пытается вовлечь застройщика. Тогда отказCRM_DEAL_INTENT_NOT_CONFIRMED, статус остаётсяDraft. - Гейт: нет одобренной фиксации. Дано: сделка
Draft, намерение подтверждено, но для пары «инвестор × застройщик» нет одобренной фиксации. Когда агент пытается вовлечь застройщика. Тогда отказCRM_DEAL_FIXATION_REQUIRED, статус остаётсяDraft. - Лид не активен. Дано: лид
Expired/Archived. Когда создаётся сделка. Тогда отказCRM_LEAD_NOT_ACTIVE. - Нет способа оплаты при создании сразу в Application. Дано: гейт выполнен, но
PaymentMethodне задан. Тогда отказCRM_DEAL_PAYMENT_METHOD_REQUIRED(дляDraftPaymentMethodпри создании не обязателен). - Дубль участника/роли. Дано: в сделке уже есть активный
Executor. Когда добавляют второгоExecutor(или второгоInvestorPrimary). Тогда отказCRM_DEAL_MEMBER_ROLE_CONFLICT; тот жеUserIdповторно →CRM_DEAL_MEMBER_DUPLICATE. - Нельзя оставить без исполнителя. Дано: сделка с единственным активным
Executor. Когда его удаляют/ снимают роль без назначения преемника в той же операции. Тогда отказCRM_LAST_EXECUTOR_REQUIRED; передача ведения (reassign) с новымExecutorв одной операции — проходит. - Старт продажи без договора. Дано: переход в
SaleбезContractType. Тогда отказCRM_DEAL_CONTRACT_TYPE_REQUIRED; дляInstallmentбезInstallmentPresetId— тоже блок. - Нарушение порядка оформления. Дано:
ContractsPreparedAtне проставлен. Когда менеджер отмечает Росреестр. Тогда отказCRM_DEAL_STAGE_ORDER_VIOLATION. - Завершение → портфель и комиссия. Дано: сделка
Sale, Росреестр подтверждён. Когда менеджер завершает. Тогда сделкаCompleted,DealCompletedEventполучаютinvestor-owner,rewards,chessboard. - Иммутабельность Origin-атрибуции. Дано: агент сменил агентство (или передано ведение). Когда
пересчитывается атрибуция. Тогда
OriginAgencyCompanyId/OriginAgentUserIdсделки не меняются (INV-DEAL-10); при завершении получатель комиссии — активныйExecutor(CommissionRecipientUserId), основание — Origin. - Отмена освобождает объект недвижимости. Дано: сделка
Bookingс активной бронью. Когда сделку отменяют с причиной. Тогда броньCancelledтем же изменением,DealCancelledEvent→chessboardосвобождает объект недвижимости.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (13): Entity / Aggregate: BookingEntity, Entity / Aggregate: BookingProgramEntity, Entity / Aggregate: ChatEntity (+ParticipantEntity как owned-child), Entity / Aggregate: ChatEventEntity, Entity / Aggregate: ClientEntity, Entity / Aggregate: DealDocumentEntity, Entity / Aggregate: DealTimelineEventEntity (TPH), Entity / Aggregate: FixationRequestEntity, Entity / Aggregate: LeadEntity, Entity / Aggregate: MessageEntity (TPH: Text/Attachment/EventCard), Entity / Aggregate: PipelineStepEntity + SEED enum PipelineStepGroup, Entity / Aggregate: PurchaseProgramEntity (+ InstallmentPresetEntity) (+1)
- API (70): DELETE /api/agent/crm/deals/{dealId}/members/{memberId} — Удалить участника сделки, GET /api/agent/crm/clients/{clientId} — Карточка клиента, GET /api/agent/crm/deals — Сделки агента (список/поиск), GET /api/agent/crm/deals/{dealId} — Карточка сделки (агент), GET /api/agent/crm/fixations/{fixationId} — Карточка фиксации (агент), GET /api/agent/crm/leads/{leadId} — Карточка лида, GET /api/agent/crm/statistics — Статистика агентства/агента, GET /api/developer/crm/booking-programs — Программы бронирования, GET /api/developer/crm/deals — Сделки застройщика (список/поиск), GET /api/developer/crm/deals/{dealId} — Карточка сделки (застройщик), GET /api/developer/crm/pipeline-steps — Этапы воронки застройщика, GET /api/developer/crm/purchase-program — Программа покупки застройщика (+58)
- Use Cases (25): UC-BKG-002. Бронирование Резерв → Бронь, UC-BKG-003. Истечение резерва/брони и продление, UC-BKG-004. Оплата платного бронирования (чек + подтверждение), UC-CAL-007. Дедлайны брони CRM в календаре агента, UC-CAL-011. Переназначение открытых дедлайнов и встреч при смене исполнителя, UC-CRM-002. Агент создаёт сделку из лида (выбор объекта, гейт вовлечения застройщика), UC-CRM-003. Движение сделки по канбан-доске, UC-CRM-004. Документы сделки: запрос → загрузка → проверка → статусы, UC-CRM-005. Оформление сделки: договоры → эскроу → Росреестр → завершение, UC-CRM-006. Вмешательство Менеджера ПИН, UC-CRM-007. Добавление участника в сделку, UC-CRM-008. Снятие участника со сделки (+13)
- События (15): Event Contract: pin.crm.booking-cancelled (BookingCancelledEvent), Event Contract: pin.crm.booking-confirmed (BookingConfirmedEvent), Event Contract: pin.crm.booking-created (BookingCreatedEvent), Event Contract: pin.crm.booking-expired (BookingExpiredEvent), Event Contract: pin.crm.booking-paid (BookingPaidEvent), Event Contract: pin.crm.deal-cancelled (DealCancelledEvent), Event Contract: pin.crm.deal-completed (DealCompletedEvent), Event Contract: pin.crm.deal-created (DealCreatedEvent), Event Contract: pin.crm.deal-members-changed (DealMembersChangedEvent), Event Contract: pin.crm.deal-stage-changed (DealStageChangedEvent), Event Contract: pin.crm.document-requested (DocumentRequestedEvent), Event Contract: pin.crm.document-status-changed (DocumentStatusChangedEvent) (+3)
- Консюмеры (6): Consumer: Calendar CRM Deal Sync (calendar-crm-deal-events), Consumer: CRM Agent Offboarding (CrmAgentOffboardingConsumer), Consumer: CRM Deal Cancelled, Consumer: CRM Deal Completed, Consumer: CRM PropertyObject Status (pin-chessboard-crm-property-object-status), Consumer: Deal Chat Status (pin-crm-deal-chat-status)
- Фоновые задачи (2): Background Job: BookingExpirationJob, Background Job: TimelineOverdueJob (worker timeline-overdue)
- Саги (2): Saga «Booking» (Резерв → Бронь → передача в продажу), Saga «Deal Completion» (fan-out завершения сделки)
- Поиск (1): DealSearchDocument
- adr (4): ADR-0061: Независимый агент, отвязка от агентства и приглашения по номеру телефона, ADR-0070: Мульти-агентство и контекст активного агентства, ADR-0071: Владение процессами CRM компанией и единая модель участников, ADR-0072: Увольнение/исключение агента и каскад доступа
- scenarios (1): UC-OVR-008. Агент выходит из агентства, сохраняя метрики
- index.md (1): CRM Service
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- pipeline-step.md, booking.md, purchase-program.md, deal-document.md, deal-timeline-event.md.
- Поиск списков сделок — Elasticsearch-проекция deals.md (ADR-0059).
- Карта API операций сделки —
../api/README.md(будущий путь по конвенции). - ADR-0052 (tenancy), ADR-0055 (чат сделки по событиям), решение 9 ТЗ (эквайринг отложен).