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

Event Contract: pin.crm.deal-created (DealCreatedEvent)

Версионируемый контракт интеграционного события «сделка создана». Consumer-документы ссылаются сюда, не переописывая payload.

Назначение

Фиксирует появление новой DealEntity (entity: DealEntity) — классической связи «инвестор + агент + застройщик + объект» с начальным составом участников. Публичное: даёт downstream- контекстам самодостаточный snapshot сделки для действий без синхронного запроса в crmmessenger создаёт чат сделки и наполняет его участниками (ADR-0055), notifications рассылает участникам уведомление о создании.

Metadata

ПолеЗначение
Event typepin.crm.deal-created
Topicpin.crm.deal-created.v1
Partitions / key3 (низкообъёмная сущность, 1–10k активных, ADR-0056) / key = DealId (= AggregateId)
ContractVersion1
Producercrm: «Создать сделку» агента/Менеджера ПИН из лида по объекту (в Application при выполненном гейте вовлечения застройщика либо в Draft при невыполненном)
Статусdraft
Атомарность публикации

Событие публикуется атомарно с созданием агрегата: пока сделка не зафиксирована, событие не отправляется; после фиксации — доставляется гарантированно (at-least-once), ключ = DealId.

Payload DealCreatedEvent

Дом полей — entity: DealEntity (раздел «Доменные и интеграционные события»); здесь перечислено только то, что реально в событии. Событие несёт Guid-ссылки, а не ФИО/телефоны — consumers обогащают отображаемые данные из своей реплики pin.identity.users.v1 (см. Безопасность).

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
DealIdGuidДаDealEntity.IdImmutableКлюч агрегата (= Kafka key).
TenantIdGuidДаDealEntity.TenantIdImmutableTenant boundary = DeveloperCompanyId застройщика объекта (ADR-0052).
AgencyCompanyIdGuidДаDealEntity.AgencyCompanyIdImmutableАгентство-сторона сделки (record-level вторая сторона).
OriginAgencyCompanyIdGuidДаDealEntity.OriginAgencyCompanyIdAdditive onlyИммутабельный снимок агентства на момент создания процесса (историческая атрибуция, воронка, основания комиссий, антифрод; ADR-0071 п.2, не переписывается при смене агентом агентства).
OriginAgentUserIdGuidДаDealEntity.OriginAgentUserIdAdditive onlyИммутабельный снимок агента-создателя (кто привёл клиента и создал сделку); историческая атрибуция, не back-fill-ится (ADR-0071 п.2).
ExecutorAgentUserIdGuidДаактивный DealMemberEntity с Role = ExecutorAdditive onlyТекущий исполнитель сделки — денормализованная проекция активного участника Executor (единственный источник истины исполнителя, ADR-0071 п.4). При создании совпадает с OriginAgentUserId.
InvestorUserIdGuidДаактивный DealMemberEntity с Role = InvestorPrimaryAdditive onlyОсновной инвестор сделки (проекция участника InvestorPrimary); допускаются дополнительные InvestorSecondary в Members.
DeveloperManagerUserIdGuid?Нетактивный DealMemberEntity с Role = SellManagerAdditive onlyМенеджер продаж застройщика; заполнен при Status = Application (застройщик вовлечён, SellManager в составе); null при Status = Draft (застройщик ещё не вовлечён).
PropertyObjectIdGuidДаDealEntity.PropertyObjectIdImmutableОбъект сделки (логическая ссылка на chessboard).
ProjectIdGuidДаDealEntity.ProjectIdImmutableПроект объекта (реплика из chessboard).
StepIdGuidДаDealEntity.StepIdAdditive onlyНачальный этап канбана застройщика группы Application (pipeline-step.md).
TitlestringДаDealEntity.TitleAdditive onlyЗаголовок сделки, сгенерированный из объекта недвижимости («Объект №…, ЖК …»); имя чата у messenger. PII не содержит.
Statusstring (enum DealStatus)ДаDealEntity.StatusAdditive only (новые значения)Статус в момент создания: Application (гейт вовлечения застройщика выполнен) либо Draft (не выполнен — сделка агента с инвестором, застройщик не вовлечён); см. DealStatus, deal.md.
PaymentMethodstring? (enum PaymentMethod)УсловноDealEntity.PaymentMethodAdditive onlyСпособ оплаты; обязателен при Status = Application (INV-DEAL-1), может отсутствовать при Status = Draft; enum — дом purchase-program.md.
MembersIReadOnlyList<DealMemberSnapshot>Даактивные DealMemberEntity (RemovedAt is null)Additive onlyНачальный состав участников (модель ниже).
CreatedAtDateTimeOffsetДаDealEntity.CreatedAtImmutableМомент создания сделки.

Вложенная модель DealMemberSnapshot

Коллекция Members — контейнер типизированных участников (без ФИО/контактов). Дом ролей — ParticipantRole (единый enum участников лида и сделки, ADR-0071 п.3) в deal.md.

ПолеТипОбязательностьОграниченияОписание
UserIdGuidДалогическая ссылка на identity.users(id); уникален среди активныхПользователь-участник.
Rolestring (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.

Статус Draft и видимость застройщику

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, CreatedAtInternalinclude
OriginAgentUserId, ExecutorAgentUserId, InvestorUserId, DeveloperManagerUserId, Members[].UserIdInternal (id, не PII)include; ФИО/телефон не передаются — consumers резолвят из реплики pin.identity.users.v1
TitleInternalinclude; генерируется из объекта недвижимости, ФИО/телефонов не содержит
PII-free по дизайну

Событие сознательно не несёт ФИО/телефоны участников (только 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 агент создаёт сделку, Then deal-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.

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

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

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