Event Contract: pin.crm.deal-created (DealCreatedEvent)
Версионируемый контракт интеграционного события «сделка создана». Consumer-документы ссылаются сюда, не переописывая payload.
Назначение
Фиксирует появление новой DealEntity (entity: DealEntity) — классической связи
«инвестор + агент + застройщик + объект» с начальным составом участников. Публичное: даёт downstream-
контекстам самодостаточный snapshot сделки для действий без синхронного запроса в crm — messenger
создаёт чат сделки и наполняет его участниками (ADR-0055), notifications рассылает участникам уведомление
о создании.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.crm.deal-created |
| Topic | pin.crm.deal-created.v1 |
| Partitions / key | 3 (низкообъёмная сущность, 1–10k активных, ADR-0056) / key = DealId (= AggregateId) |
| ContractVersion | 1 |
| Producer | crm: «Создать сделку» агента/Менеджера ПИН из лида по объекту (в Application при выполненном гейте вовлечения застройщика либо в Draft при невыполненном) |
| Статус | draft |
Событие публикуется атомарно с созданием агрегата: пока сделка не зафиксирована, событие не
отправляется; после фиксации — доставляется гарантированно (at-least-once), ключ = DealId.
Payload DealCreatedEvent
Дом полей — entity: DealEntity (раздел «Доменные и интеграционные события»);
здесь перечислено только то, что реально в событии. Событие несёт Guid-ссылки, а не ФИО/телефоны —
consumers обогащают отображаемые данные из своей реплики pin.identity.users.v1 (см. Безопасность).
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
DealId | Guid | Да | DealEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | DealEntity.TenantId | Immutable | Tenant boundary = DeveloperCompanyId застройщика объекта (ADR-0052). |
AgencyCompanyId | Guid | Да | DealEntity.AgencyCompanyId | Immutable | Агентство-сторона сделки (record-level вторая сторона). |
OriginAgencyCompanyId | Guid | Да | DealEntity.OriginAgencyCompanyId | Additive only | Иммутабельный снимок агентства на момент создания процесса (историческая атрибуция, воронка, основания комиссий, антифрод; ADR-0071 п.2, не переписывается при смене агентом агентства). |
OriginAgentUserId | Guid | Да | DealEntity.OriginAgentUserId | Additive only | Иммутабельный снимок агента-создателя (кто привёл клиента и создал сделку); историческая атрибуция, не back-fill-ится (ADR-0071 п.2). |
ExecutorAgentUserId | Guid | Да | активный DealMemberEntity с Role = Executor | Additive only | Текущий исполнитель сделки — денормализованная проекция активного участника Executor (единственный источник истины исполнителя, ADR-0071 п.4). При создании совпадает с OriginAgentUserId. |
InvestorUserId | Guid | Да | активный DealMemberEntity с Role = InvestorPrimary | Additive only | Основной инвестор сделки (проекция участника InvestorPrimary); допускаются дополнительные InvestorSecondary в Members. |
DeveloperManagerUserId | Guid? | Нет | активный DealMemberEntity с Role = SellManager | Additive only | Менеджер продаж застройщика; заполнен при Status = Application (застройщик вовлечён, SellManager в составе); null при Status = Draft (застройщик ещё не вовлечён). |
PropertyObjectId | Guid | Да | DealEntity.PropertyObjectId | Immutable | Объект сделки (логическая ссылка на chessboard). |
ProjectId | Guid | Да | DealEntity.ProjectId | Immutable | Проект объекта (реплика из chessboard). |
StepId | Guid | Да | DealEntity.StepId | Additive only | Начальный этап канбана застройщика группы Application (pipeline-step.md). |
Title | string | Да | DealEntity.Title | Additive only | Заголовок сделки, сгенерированный из объекта недвижимости («Объект №…, ЖК …»); имя чата у messenger. PII не содержит. |
Status | string (enum DealStatus) | Да | DealEntity.Status | Additive only (новые значения) | Статус в момент создания: Application (гейт вовлечения застройщика выполнен) либо Draft (не выполнен — сделка агента с инвестором, застройщик не вовлечён); см. DealStatus, deal.md. |
PaymentMethod | string? (enum PaymentMethod) | Условно | DealEntity.PaymentMethod | Additive only | Способ оплаты; обязателен при Status = Application (INV-DEAL-1), может отсутствовать при Status = Draft; enum — дом purchase-program.md. |
Members | IReadOnlyList<DealMemberSnapshot> | Да | активные DealMemberEntity (RemovedAt is null) | Additive only | Начальный состав участников (модель ниже). |
CreatedAt | DateTimeOffset | Да | DealEntity.CreatedAt | Immutable | Момент создания сделки. |
Вложенная модель DealMemberSnapshot
Коллекция Members — контейнер типизированных участников (без ФИО/контактов). Дом ролей —
ParticipantRole (единый enum участников лида и сделки, ADR-0071 п.3) в deal.md.
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
UserId | Guid | Да | логическая ссылка на identity.users(id); уникален среди активных | Пользователь-участник. |
Role | string (enum ParticipantRole) | Да | Executor / Responsible / CoAgent / Assistant / Lawyer / InvestorPrimary / InvestorSecondary / SellManager / PinManager | Роль в сделке; при создании — InvestorPrimary + Executor + авто-PinManager, плюс SellManager при Status = Application (при Status = Draft SellManager отсутствует — застройщик не вовлечён; INV-DEAL-2). Ровно один активный Executor и один InvestorPrimary (ADR-0071 п.3). |
Price, ContractType, EscrowAccountNumber, BankId в payload не входят: на момент создания их ещё
нет либо они не нужны consumer-ам (портфель/комиссия — событие deal-completed.md).
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|---|---|
«Создать сделку» из лида Status = Active, гейт вовлечения застройщика выполнен (подтверждённое намерение + одобренная фиксация + PaymentMethod) | [*] → Application (stateDiagram deal.md); состав с SellManager | атомарно со сменой состояния агрегата |
«Создать сделку» из лида Status = Active, гейт не выполнен | [*] → Draft; состав без SellManager, PaymentMethod может отсутствовать | та же |
Последующее вовлечение застройщика (Draft → Application, engage-developer.md)
несёт не это событие, а deal-stage-changed.md + deal-members-changed.md.
Единичное событие на сделку (first-of-aggregate). Последующие изменения — отдельные контракты: deal-stage-changed.md, deal-members-changed.md, deal-completed.md, deal-cancelled.md.
Потребители (consumers)
| Consumer-контекст | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
messenger | Создать чат сделки, добавить участников по Members, вернуть готовность событием pin.messenger.deal-chat-status-changed.v1 → проекция Deal.ChatStatus (ADR-0055) | first-seen по DealId + MessageId (повторная доставка не плодит чат) | < 5 мин (ChatStatus: Provisioning → Ready) |
notifications | Уведомление участникам сделки («Сделка создана»); welcome-карточка в чат | MessageId | < 5 мин |
Все консюмеры идемпотентны по MessageId (конвенция README контекста) — повторная доставка Kafka
(at-least-once) не даёт двойных side effects.
messenger/notifications реагируют и на Status = Draft (создают чат агент+инвестор и уведомляют
участников; SellManager в составе ещё нет). Любые developer-facing консюмеры/проекции (канбан
застройщика, его списки/статистика) обрабатывают событие только при Status = Application и игнорируют
Draft (фильтр Status != Draft): пока сделка в Draft, застройщик её не видит. Событие вовлечения
застройщика (Draft → Application) приходит позднее как
deal-stage-changed.md +
deal-members-changed.md.
Доставка, порядок и обработка сбоев
- Гарантия публикации. Событие уходит только после фиксации создания агрегата; при откате не
публикуется. Доставка — at-least-once, ключ
DealId; событие единичное на сделку (first-of-aggregate). - Порядок. События одной сделки упорядочены ключом
DealId.deal-createdпредшествует всем последующим контрактам сделки (deal-stage-changed/deal-members-changed/deal-completed/deal-cancelled) на своём ключе. - Идемпотентность потребителя.
messenger— first-seen поDealId(+MessageId): повторная доставка не создаёт второй чат;notifications— поMessageId. - Событийная цепочка и eventual consistency.
crmпубликуетdeal-created(async);messengerсоздаёт чат и по готовности отвечаетpin.messenger.deal-chat-status-changed.v1, которыйcrmпроецирует вDeal.ChatStatus(Provisioning → Ready, ADR-0055). Окно рассинхрона — до готовности чата (SLA < 5 мин): в промежутке пользователь видитChatStatus = Provisioning(чат готовится), сделка при этом уже активна. - Retry. Транзиентный сбой потребителя → повтор обработки с backoff; событие доступно до успешной обработки.
- DLQ. Неустранимый сбой после исчерпания попыток → dead-letter c алертом; проигрывание безопасно
(first-seen). Пока
messengerне обработал событие (в т.ч. из DLQ),Deal.ChatStatusостаётсяProvisioning.
Версионирование и совместимость
ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Топик кодирует версию по конвенции — суффикс.v1.- Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump.
- Breaking (удаление/переименование/смена типа/смысла) — новый
ContractVersion = 2+ новый топикpin.crm.deal-created.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Unknown enum (
Status/PaymentMethod/DealMemberSnapshot.Role) — fallback: сохранить строку как есть, warning; consumer не падает на неизвестном значении.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
DealId, TenantId, AgencyCompanyId, OriginAgencyCompanyId, StepId, ProjectId, PropertyObjectId, Status, PaymentMethod, CreatedAt | Internal | include |
OriginAgentUserId, ExecutorAgentUserId, InvestorUserId, DeveloperManagerUserId, Members[].UserId | Internal (id, не PII) | include; ФИО/телефон не передаются — consumers резолвят из реплики pin.identity.users.v1 |
Title | Internal | include; генерируется из объекта недвижимости, ФИО/телефонов не содержит |
Событие сознательно не несёт ФИО/телефоны участников (только Guid-ссылки): маскирование раскрытых PII
здесь неприменимо, т.к. raw PII в payload отсутствует. В логах producer/consumer идентификаторы пользователей
внутренние; ФИО/телефон в лог не пишутся. Событие НЕ содержит secrets, tokens, credentials, платёжных
реквизитов, номера эскроу-счёта, unbounded вложений.
Наблюдаемость
- Логи (без PII): факт публикации
deal-createdсDealId/TenantId/ExecutorAgentUserId/OriginAgentUserId/InvestorUserId/PropertyObjectId/Status; ФИО/телефоны участников в логах не фигурируют (в payload их нет). Уmessenger— факт создания чата и переходChatStatus. - Метрики: число созданных сделок по топику; сквозная задержка до
messenger/notifications(< 5 мин); задержка доChatStatus = Ready(US-цель готовности чата); глубина DLQ (порог алерта > 0); доля повторных доставок. - Доступ: топик — m2m ACL Kafka (
messenger/notifications); PII в событие не входит.
Acceptance Criteria
- Given лид
Status = Active, гейт вовлечения застройщика выполнен (подтверждённое намерение + одобренная фиксация +PaymentMethod), When агент/Менеджер ПИН создаёт сделку, Then создаётсяDealEntityвApplication, ровно одноdeal-createdпубликуется сStatus = Application,Members= {InvestorPrimary,Executor,SellManager, авто-PinManager} (INV-DEAL-2),DeveloperManagerUserIdзаполнен, ровно один активныйExecutorи одинInvestorPrimary, аExecutorAgentUserId=OriginAgentUserId(создатель — первый исполнитель). - Given лид
Status = Active, гейт не выполнен, When агент создаёт сделку, Thendeal-createdпубликуется сStatus = Draft,Members= {InvestorPrimary,Executor, авто-PinManager} безSellManager,DeveloperManagerUserId = null,PaymentMethodможет отсутствовать; developer-facing консюмеры событие игнорируют (Status != Draft),messenger/notifications— обрабатывают. - Given повторная доставка события в
messenger, When обрабатывается, Then создаётся ровно один чат сделки (first-seen поDealId). - Given чат ещё создаётся, When пользователь открывает сделку, Then он видит
ChatStatus = Provisioningдо приходаdeal-chat-status-changed(→ Ready); сделка при этом активна. - Given неизвестное значение enum (
Status/PaymentMethod/DealMemberSnapshot.Role), When потребитель обрабатывает, Then строка сохраняется как есть + warning (без падения). - Given breaking-изменение payload, When новый контракт, Then
pin.crm.deal-created.v2+ dual-publish + sunset.v1.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Entity / Aggregate: ChatEntity (+ParticipantEntity как owned-child), Entity / Aggregate: ChatEventEntity, Entity / Aggregate: NotificationEntity
- API (8): GET /api/developer/crm/deals — Сделки застройщика (список/поиск), GET /api/developer/crm/statistics — Статистика ЛК застройщика, GET /api/investor/crm/deals — Мои сделки (инвестор), GET /api/investor/crm/deals/{dealId}/timeline — Таймлайн сделки (инвестор), POST /api/agent/crm/deals — Создать сделку из лида по объекту, POST /api/agent/crm/deals/{dealId}/selection — Отправить подборку в сделку, POST /api/investor/crm/deals/{dealId}/acknowledge-terms — Отметить «Ознакомлен», POST /api/pin-manager/crm/deals/{dealId}/timeline-note — Ручная запись в таймлайн
- Use Cases (5): UC-CRM-002. Агент создаёт сделку из лида (выбор объекта, гейт вовлечения застройщика), UC-CRM-006. Вмешательство Менеджера ПИН, UC-CRM-017. Flow B: агент предлагает объект → Draft-сделка → подтверждение инвестором → вовлечение застройщика, UC-CRM-018. Несколько сделок из одной одобренной фиксации (по разным объектам застройщика), UC-MSG-002. Авто-провизия чата сделки по событию CRM
- События (7): Event Contract: pin.crm.deal-cancelled (DealCancelledEvent), Event Contract: pin.crm.deal-completed (DealCompletedEvent), Event Contract: pin.crm.deal-members-changed (DealMembersChangedEvent), Event Contract: pin.crm.deal-stage-changed (DealStageChangedEvent), Event Contract: pin.crm.fixation-approved (FixationApprovedEvent), Event Contract: pin.crm.lead-booking-intent-changed (LeadBookingIntentChangedEvent), Event Contract: pin.messenger.deal-chat-status-changed (DealChatStatusChangedEvent)
- Консюмеры (5): Consumer: Calendar CRM Deal Sync (calendar-crm-deal-events), Consumer: CRM Events → Notifications (notifications-crm-events), Consumer: Deal Chat Provisioning (MessengerDealCreatedConsumer), Consumer: Deal Chat Status (pin-crm-deal-chat-status), Consumer: DevStatsFunnelDealCreatedConsumer
- Фоновые задачи (1): Background Job: ChatProvisionRetryJob
- scenarios (2): UC-OVR-004. От реферальной ссылки до объекта у собственника, UC-OVR-005. От лида до сделки (4 фазы)
- index.md (2): Calendar Service, Messenger Service
- registries (1): Реестр событий PIN
Связанные документы
- Сущность (дом полей,
DealStatus.Draft, гейт INV-DEAL-11): deal.md; связанные дома — pipeline-step.md (StepId/группаApplication), purchase-program.md (enumPaymentMethod), lead.md (намерение по объекту), fixation-request.md (авторизация работы с клиентом у застройщика). - Создание/вовлечение: create-deal.md (
Application|Draft), engage-developer.md (Draft → Application). - Соседние контракты сделки: deal-stage-changed.md, deal-members-changed.md, deal-completed.md, deal-cancelled.md.
- ADR: 0055 мини-мессенджер (чат сделки по событиям), 0052 tenancy, 0056 simplicity-first.
- README контекста: ../README.md.