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

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
KindAggregateRoot (дочерняя DealMemberEntity — Entity внутри агрегата)
Source of truthcrm
Таблицаcrm.deals (+ crm.deal_members)
Tenant isolationTenantId = DeveloperCompanyId (ADR-0052: «застройщик для сделки»); историческая атрибуция сторон — иммутабельный Origin-снимок OriginAgencyCompanyId/OriginAgentUserId; текущий исполнитель и инвесторы — через состав участников DealMemberEntity (ADR-0071); видимость — по членству (record-level)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор сделки.
TenantIdGuidДаindex ix_deals_tenant_idиз объекта (владелец объекта недвижимости)DeveloperCompanyId застройщика объекта.
LeadIdGuidДаFK fk_deals_lead_id → crm.leads(id), indexappЛид-основание (сделка — только из лида).
ClientIdGuidДаFK fk_deals_client_id → crm.clients(id), indexиз лидаКарточка клиента агентства.
FixationRequestIdGuid?НетFK fk_deals_fixation_request_id → crm.fixation_requests(id), index where not null (НЕ unique)appОдобренная фиксация-основание вовлечения застройщика для пары «инвестор × застройщик». Одна фиксация авторизует много сделок по объектам этого застройщика (связь фиксация→сделки 1:N), поэтому значение не уникально среди сделок.
IntentIdGuid?НетFK fk_deals_intent_id → crm.lead_booking_intents(id), index where not nullappНамерение инвестора по объекту (lead.md), из которого выросла сделка. Заполнено, если сделка стартовала из подтверждённого/предложенного намерения; связывает Draft-сделку с намерением, чьё подтверждение инвестором — часть гейта Draft → Application.
IntentConfirmedAtDateTimeOffset?Нетapp (из намерения)Момент подтверждения инвестором связанного намерения (snapshot LeadBookingIntentEntity.ConfirmedAt); null, пока намерение не подтверждено (Flow B). Одно из условий гейта Draft → Application.
TitlestringДаvarchar(256), не пустоеapp (генерация из объекта недвижимости + правка)Заголовок карточки на канбане.
StatusDealStatusДаenum как text, varchar(32), index ix_deals_statusappСтатус процесса (US-INV-03: заявка/бронь/продажа/отказ + внутренний Completed, см. enum).
StepIdGuidДаFK fk_deals_step_id → crm.pipeline_steps(id), index ix_deals_step_idapp (drag-n-drop / авто-переходы)Текущий настраиваемый этап канбана.
StepGroupPipelineStepGroupДаenum как text, varchar(32), indexденормализация из PipelineStepEntity.GroupСистемная группа текущего этапа (быстрые фильтры/статистика без join; поддерживается методом MoveToStep).
PrimaryInvestorUserIdGuidДаindex ix_deals_primary_investor_user_idпроизводное из активного участника InvestorPrimaryОсновной инвестор сделки (мутабельная денормализация; допускается несколько инвесторов-участников — InvestorSecondary). Поддерживается при смене основного инвестора.
OriginAgentUserIdGuidДаindex ix_deals_origin_agent_user_idиз лида (снимок на момент создания)Агент-инициатор — «кто привёл клиента и создал сделку» (US-AGT-01). Историческая атрибуция (воронка агентства, основание комиссии rewards, антифрод), иммутабелен: не пересчитывается при смене агентом агентства/передаче ведения (ADR-0061 п.4, ADR-0071, INV-DEAL-10).
OriginAgencyCompanyIdGuidДаindex ix_deals_origin_agency_company_idиз лида (снимок на момент создания)Агентство агента-инициатора на момент создания сделки. Историческая атрибуция, иммутабелен (ADR-0061 п.4, ADR-0071, INV-DEAL-10). Текущее агентство-владелец = агентство активного участника Executor (производное, не хранится как снимок).
ExecutorAgentUserIdGuidДаindex ix_deals_executor_agent_user_idпроизводное из активного участника ExecutorТекущий исполнитель сделки — единственный источник истины исполнителя (получатель комиссии). Мутабельная денормализация состава участников; меняется передачей ведения (reassign).
ResponsibleAgentUserIdGuid?Нетindex where not nullпроизводное из активного участника ResponsibleОтветственный/куратор ведения, если назначен (может ≠ Executor). Мутабельная денормализация состава участников.
PropertyObjectIdGuidДаindex ix_deals_property_object_id; логическая ссылка на chessboardapp (выбор объекта)Объект сделки. Заменяем: «изменить объект сделки если клиент передумал» — метод ChangePropertyObject, старое значение — в таймлайн (PropertyObjectChanged).
ProjectIdGuidДаindexреплика из chessboard по PropertyObjectIdПроект объекта (фильтры застройщика US-DEV-01, статистика ЛК по проекту).
BuildingIdGuid?Нетindex where not nullреплика из chessboardЗдание/литер (фильтр статистики ЛК).
ContractTypeDealContractType?Нет (обязателен с группы Contract, инвариант INV-DEAL-6)enum как text, varchar(16)app (менеджер застройщика/агент)Тип договора: ДДУ / ДКП (US-INV-08; влияет на этапы).
PaymentMethodPaymentMethod?Нет (обязателен при конвертации лида в сделку)enum как text, varchar(32)appСпособ оплаты (enum — purchase-program.md); влияет на набор этапов.
InstallmentPresetIdGuid?НетFK fk_deals_installment_preset_id → crm.installment_presets(id); обязателен при PaymentMethod = Installment (check ck_deals_installment_preset)appВыбранный пресет рассрочки застройщика.
PriceMoney (owned)Дасм. VO нижеиз chessboard на момент создания/смены объекта недвижимостиПолная стоимость объекта недвижимости (US-INV-05: цена за м² × площадь — источник цифр chessboard, здесь snapshot).
BookingIdGuid?НетFK fk_deals_booking_id → crm.bookings(id), unique where not nullappТекущее (последнее активное) бронирование сделки (booking.md).
BankIdGuid?Нетлогическая ссылка на справочник банков chessboardapp (менеджер застройщика)Банк проведения сделки/эскроу (US-INV-07: «Не указан», если null).
EscrowAccountNumberstring?Нетvarchar(64); хранится в открытом виде, наружу — маска (см. Безопасность)app (инвестор, US-INV-19)Номер эскроу-счёта.
EscrowStatusEscrowStatusДаenum как text, varchar(32), default NoneappNone / Provided («Эскроу забронирован», US-INV-19).
ContractsPreparedAtDateTimeOffset?Нетapp (US-DEV-04)Момент отметки «Договоры подготовлены» (US-INV-20).
RosreestrRegisteredAtDateTimeOffset?Нетapp (застройщик)Подтверждение регистрации в Росреестре.
CompletedAtDateTimeOffset?Нетapp (застройщик)Подтверждение завершения сделки.
CancelledAtDateTimeOffset?НетappМомент отказа.
CancelReasonstring?Нетvarchar(1000); обязателен при Status = Cancelled (check ck_deals_cancel_reason)appПричина отказа.
ChatStatusDealChatStatusДаenum как text, varchar(32), default Pendingconsumer pin.messenger.deal-chat-status-changed.v1Проекция готовности чата: Pending/Provisioning/Ready/Failed (ADR-0055).
TermsAcknowledgedAtDateTimeOffset?Нетapp (инвестор, US-INV-18)Отметка «Ознакомлен» с условиями сделки.
CreatedAtDateTimeOffsetДаindex ix_deals_created_at descappДата создания.
CreatedByUserIdGuid?НетappАгент/Менеджер ПИН, создавший сделку (из лида по объекту).
UpdatedAtDateTimeOffsetДаindex ix_deals_updated_at descappДата последнего обновления (US-INV-04).
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?НетappSoft-delete (административно).
DeletedByUserIdGuid?Нет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Чат ещё не запрошен/не создан.
Provisioningmessenger создаёт чат по событию pin.crm.deal-created.v1.
ReadyЧат создан, участники синхронизированы.
FailedСоздание чата упало; ретрай консюмером messenger.

Value objects

Money (owned)

ПолеТипОбязательностьОграниченияКолонка
AmountdecimalДаnumeric(18,2), >= 0price_amount
CurrencyIdshortДатипизированная ссылка на справочник валют 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 на сделке поддерживаются атомарно со сменой состава.

ПолеТипОбязательностьОграниченияИсточникОписание
IdGuidДаPKappИдентификатор участия.
DealIdGuidДаFK fk_deal_members_deal_id → crm.deals(id) cascade; unique ux_deal_members_deal_user (DealId,UserId)appСделка.
TenantIdGuidДаindexиз сделкиДублируется для tenant-фильтра.
UserIdGuidДаindex ix_deal_members_user_id; логическая ссылка на identity.users(id)appПользователь-участник.
RoleParticipantRoleДаenum как text, varchar(32)appРоль участника (см. enum). Меняется операцией смены роли/передачи ведения. Неизвестное потребителю значение — строка + warning (additive).
AddedByUserIdGuid?НетappКто добавил (null — система: авто-состав при создании).
CreatedAtDateTimeOffsetДаappМомент добавления.
RemovedAtDateTimeOffset?НетappSoft-удаление участия (история для чата/аудита; активные участники — RemovedAt is null).
RemovedByUserIdGuid?НетappКто удалил.

Enum ParticipantRole

Единый additive-enum состава участников (ADR-0071); общий для лида/сделки/фиксации. На сделке доступны все значения; SellManager/PinManager — только на сделке. Соответствие прежних значений: InvestorInvestorPrimary, AgentExecutor.

ЗначениеОбластьОписание
Executorлид/сделкаТекущий исполнитель — ровно один активный; единственный источник истины исполнителя и получатель комиссии.
Responsibleлид/сделкаОтветственный/куратор ведения (опционален, может ≠ Executor).
CoAgentлид/сделкаСо-агент (нефинансовый в v1, ADR-0069); допускается несколько.
Assistantлид/сделкаАссистент (нефинансовый); допускается несколько.
Lawyerлид/сделкаЮрист сопровождения (нефинансовый); допускается несколько.
InvestorPrimaryлид/сделкаОсновной инвестор-покупатель — ровно один активный.
InvestorSecondaryлид/сделкаСозаёмщики/соинвесторы — произвольное число.
SellManagerтолько сделкаМенеджер продаж застройщика.
PinManagerтолько сделкаМенеджер ПИН — авто-участник всех сделок (US-MGR-01..06), не более одного активного.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
LeadEntityreference (основание)LeadIdзагрузка при созданииrestrict
ClientEntityreferenceClientIdзагрузка при созданииrestrict
FixationRequestEntityreference (авторизация, 1 фиксация → N сделок)FixationRequestIdзагрузка при гейте Draft → Application (проверка одобренной фиксации пары «инвестор × застройщик»)restrict
LeadBookingIntentEntityreference (намерение по объекту)IntentIdзагрузка при создании / на гейте Draft → Application (проверка ConfirmedByInvestor)restrict
PipelineStepEntityreferenceStepIdзагрузка при MoveToSteprestrict
DealMemberEntityowned-childdeal_members.deal_idзагрузка при карточке; список участников сделкиcascade
BookingEntityreference (1 активная)BookingId / bookings.deal_idвыборка по DealId (активные статусы)restrict
DealDocumentEntityreference (children)deal_documents.deal_idвыборка (вкладка «Документы»)restrict
DealTimelineEventEntityreference (children)deal_timeline_events.deal_idвыборка с пагинацией (вкладка «Ход сделки»)restrict
Объект недвижимости/проект/банк (chessboard)внешняя логическаяPropertyObjectId/ProjectId/BankIddisplay-данные 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Начальный состав участников зависит от статуса создания: DraftExecutor + 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-4ChangePropertyObject разрешён в статусах Application/Booking (при Booking — только с одновременной отменой активной брони); новый объект недвижимости того же TenantId; Price перечитывается; в таймлайн — PropertyObjectChangedпри смене объекта сделкиCRM_DEAL_PROPERTY_OBJECT_CHANGE_FORBIDDEN
INV-DEAL-5MoveToStep: целевой этап принадлежит TenantId и не архивирован; StepGroup денормализуется; переход в этап группы Cancelled требует CancelReasonпри смене этапаCRM_DEAL_STEP_FORBIDDEN
INV-DEAL-6С момента перехода Sale (подготовка договора) обязательны ContractType и PaymentMethod; для InstallmentInstallmentPresetIdпри старте продажиCRM_DEAL_CONTRACT_TYPE_REQUIRED
INV-DEAL-7ContractsPreparedAt/RosreestrRegisteredAt/CompletedAt проставляются только менеджером TenantId строго в порядке: договоры → (эскроу Provided, если Ddu) → Росреестр → завершениепри отметке этапов оформленияCRM_DEAL_STAGE_ORDER_VIOLATION
INV-DEAL-8Cancelled — из любого нетерминального статуса; активная бронь отменяется в одном согласованном изменении (объект недвижимости освобождается событием)при отмене сделки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 = 1MoveToStep и смена 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 = 1Add/Remove/ChangeRole/передача ведения участникаDealId, TenantId, полный активный состав [{UserId, Role}] (Role: ParticipantRole) + дельта; консюмеры: messenger (синк участников чата), notifications
DealCompletedEventинтеграционное, topic pin.crm.deal-completed.v1, ContractVersion = 1Sale → CompletedDealId, 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* → CancelledDealId, 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) не используется
Партиционирование / retentionnone (ADR-0056; сделки — низкообъёмная сущность)
Concurrencyоптимистическая блокировка: гонки drag-n-drop / решений сторон
Permissions / PIIcrm.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 и не пересчитывается задним числом при изменении прайса в шахматке (историческая корректность суммы)
Справочник банков BankIdchessboarddisplay-репликанеизвестный/устаревший BankId — показывается как «Не указан», операция не падает
ChatStatusmessenger (pin.messenger.deal-chat-status-changed.v1)проекциядо прихода события — Pending/Provisioning; UI показывает «чат создаётся»; сделка полностью функциональна и без готового чата
ФИО/телефон клиента (в поиске/карточке)crm.clients (сам контекст) → identitysnapshot в 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/смена StatusDealStageChangedEvent (→ messenger карточка в чат, notifications). StepGroup денормализуется в том же изменении (INV-DEAL-5), поэтому фильтры/ статистика согласованы немедленно, а карточка в чате — с задержкой.

Смена состава участников. Add/Remove/ChangeRole → DealMembersChangedEvent с полным активным составом (не дельтой) → messenger синхронизирует участников чата (идемпотентный набор — устойчив к переупорядочиванию доставки).

Завершение сделки. Sale → Completed (актор — менеджер застройщика, после Росреестра, INV-DEAL-7) → DealCompletedEventinvestor-owner (объект в портфель инвестора), rewards (начисление комиссии: получатель — CommissionRecipientUserId = активный Executor на момент завершения; основание/атрибуция — иммутабельный OriginAgentUserId/OriginAgencyCompanyId + snapshot Price), chessboard (объект недвижимости «Продан»), notifications. Атрибуция и основание считаются по иммутабельному Origin-снимку (INV-DEAL-10), поэтому последующая смена агентом агентства/передача ведения их не искажает; получатель — исполнитель, назначенный на момент завершения.

Отмена сделки. → Cancelled (INV-DEAL-8): активная бронь отменяется в том же согласованном изменении; DealCancelledEventchessboard освобождает объект недвижимости (если бронь была активна), 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 (для Draft PaymentMethod при создании не обязателен).
  • Дубль участника/роли. Дано: в сделке уже есть активный 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 тем же изменением, DealCancelledEventchessboard освобождает объект недвижимости.

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

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

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