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

Event Contract: pin.crm.lead-expired (LeadExpiredEvent)

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

Назначение

Фиксирует автоматический перевод лида в терминально-историческое состояние: у активного лида (LeadEntity) наступил ExpiresAt, и фоновая проверка lead-expiration перевела Status: Active → Expired (INV-LEAD-4). Событие публичное: даёт notifications сигнал уведомить текущего исполнителя «продлите лид», не запрашивая синхронно crm. Лид можно реактивировать продлением (Extend), поэтому событие — не удаление, а смена статуса связи «инвестор ↔ агент». Дом полей и переход — entity: LeadEntity (разделы «Инварианты и переходы состояний», «Доменные и интеграционные события»).

Metadata

ПолеЗначение
Event typepin.crm.lead-expired
Topicpin.crm.lead-expired.v1
Partitions / key3 (1–10k активных лидов, ADR-0056) / key = AggregateId = LeadId (порядок per-lead)
ContractVersion1
Producercrm: фоновая проверка истечения лидов lead-expiration (будущий док ../background/lead-expiration.md)
Статусdraft

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

Payload LeadExpiredEvent

Дом полей — entity: LeadEntity; здесь перечислено только то, что реально в событии. Payload сознательно узкий: только идентификаторы связи и момент истечения — без хотелок, ленты событий и PII (ФИО/телефон в событие не входят, см. «Безопасность»).

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
LeadIdGuidДаLeadEntity.IdImmutableКлюч агрегата (= Kafka key). Истёкший лид.
TenantIdGuidДаLeadEntity.TenantIdImmutableTenant boundary = AgencyCompanyId (ADR-0052); для лидов «на ПИН» — компания ПИН.
ExecutorAgentUserIdGuidДаLeadEntity.ExecutorAgentUserIdAdditive onlyТекущий участник-исполнитель (активный Executor) — адресат уведомления «продлите лид».
OriginAgentUserIdGuidДаLeadEntity.OriginAgentUserIdImmutableАгент, изначально создавший лид: иммутабельная атрибуция (воронка агентства / антифрод).
PrimaryInvestorUserIdGuidДаLeadEntity.PrimaryInvestorUserIdImmutableОсновной инвестор-участник пары (контекст лида для уведомления).
ExpiredAtDateTimeOffsetДаLeadEntity.ExpiredAtImmutableФактический момент перевода в Expired (= now цикла проверки). Non-nullable: событие рождается ровно в переходе.
Что не входит в payload

Вложенных моделей нет: все поля скалярные. ExpiresAt, ExtensionsCount, Wishes, лента lead_events в payload не входят — они не нужны потребителю уведомления и остаются в доме crm.

Когда публикуется

УсловиеПереход состоянияПубликация
Цикл lead-expiration (ADR-0056, ~1 ч): среди лидов с Status = Active и ExpiresAt < now агрегат переводится в Expired, проставляется ExpiredAt = now (INV-LEAD-4)Active → Expiredатомарно со сменой состояния агрегата; идемпотентно — повторный цикл не находит уже переведённые
Область действия события

Событие публикуется только для перехода Active → Expired фоновой проверкой. Ручное продление (Extend, реактивация Expired → Active) и архивирование (Archive) — отдельные потоки и своих lead-expired событий не порождают. Создание лида — соседний контракт lead-created.md (топик pin.crm.lead-created.v1).

Потребители (consumers)

Consumer-контекстSide effectsИдемпотентностьЗадержка (SLA)
notificationsУведомление исполнителю (ExecutorAgentUserId) «лид истёк — продлите»: контакт получателя резолвится из собственной реплики пользователя notifications (топик pin.identity.users.v1), а не из этого событияMessageId (dedup потребителя)< 5 мин
Обратная связь

Потребитель не пишет обратно в crm; продление лида — отдельная команда агента (ExtendLead, lead.md INV-LEAD-3), инициируемая из уведомления.

Доставка, порядок и обработка сбоев

  • Гарантия публикации. Событие уходит только после фиксации перехода Active → Expired фоновой проверкой; при откате не публикуется. Доставка — at-least-once, ключ LeadId.
  • Порядок. События одного лида упорядочены ключом LeadId.
  • Идемпотентность. На стороне продюсера истечение идемпотентно (повторный цикл не находит уже переведённые лиды — ADR-0056); на стороне потребителя notifications — дедуп по MessageId.
  • Retry. Транзиентный сбой потребителя → повтор обработки с backoff; событие доступно до успешной обработки, уведомление «продлите лид» не теряется.
  • DLQ. Неустранимый сбой после исчерпания попыток → dead-letter c алертом; проигрывание безопасно (дедуп по MessageId).

Версионирование и совместимость

  • ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Версия топика — по конвенции суффиксом .v1.
  • Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump.
  • Breaking (удаление/переименование/смена типа/смысла) — новый ContractVersion = 2 + новый топик pin.crm.lead-expired.v2 + dual-publish на период миграции + deadline миграции consumers + sunset .v1.
  • Enum в payload нет (только идентификаторы и timestamp) — unknown-enum-fallback не применяется.

Безопасность

ПолеКласс данныхПравило
LeadId, TenantId, PrimaryInvestorUserId, OriginAgentUserIdInternalinclude
ExecutorAgentUserIdInternalinclude (адресат уведомления)
ExpiredAtInternalinclude
PII в payload отсутствует по конструкции

Телефоны и ФИО агента/инвестора в событие не включаются (правило lead.md: «PII в payload событий отсутствует — только id и агрегированные величины»). Потребитель notifications подтягивает контакт по ExecutorAgentUserId из своей реплики identity — событие не переносит контактные данные между контекстами.

  • Маскирование в логах: если consumer/producer логирует полученный/отправленный контакт при формировании уведомления, телефон маскируется +7***1234, ФИО — не логируется целиком.
  • Событие НЕ содержит secrets, tokens, raw credentials, полные PII/платёжные реквизиты, unbounded вложения.

Наблюдаемость

  • Логи (без PII): факт публикации lead-expired с LeadId/TenantId/ExecutorAgentUserId/ OriginAgentUserId/ExpiredAt (идентификаторы, не PII); ФИО/телефон агента/инвестора в payload и логах отсутствуют.
  • Метрики: число истёкших лидов за цикл проверки; сквозная задержка до notifications (< 5 мин); доля лидов, продлённых после уведомления (эффективность напоминания); глубина DLQ (порог алерта > 0); доля повторных доставок.
  • Доступ: топик — m2m ACL Kafka (единственный consumer notifications); контакт получателя consumer резолвит из своей реплики identity и маскирует в логах (+7***1234).

Acceptance Criteria

  • Given активный лид с ExpiresAt < now, When отрабатывает цикл lead-expiration, Then переход Active → Expired, ExpiredAt = now, ровно одно lead-expired публикуется атомарно с переходом.
  • Given уже истёкший лид, When повторный цикл проверки, Then истечение не применяется и событие не публикуется (идемпотентность).
  • Given агент выполнил Extend до срабатывания проверки, When цикл доходит до лида, Then он активен (ExpiresAt в будущем) — lead-expired не публикуется.
  • Given реактивация Expired → Active или архивирование (Archive), When выполняются, Then lead-expired не порождается (отдельные потоки).
  • Given повторная доставка события, When notifications обрабатывает, Then уведомление уходит один раз (дедуп по MessageId).
  • Given breaking-изменение payload, When новый контракт, Then pin.crm.lead-expired.v2 + dual-publish + sunset .v1.

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

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

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

  • Источник (операция): фоновая проверка lead-expiration (будущий док ../background/lead-expiration.md, INV-LEAD-4); сущность и переход Active → Expiredentity: LeadEntity.
  • Соседний контракт агрегата: lead-created.md (создание лида); карта событий — CRM Service README.
  • ADR-0056 (polling-expiration), ADR-0052 (tenancy).