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 type | pin.crm.lead-expired |
| Topic | pin.crm.lead-expired.v1 |
| Partitions / key | 3 (1–10k активных лидов, ADR-0056) / key = AggregateId = LeadId (порядок per-lead) |
| ContractVersion | 1 |
| Producer | crm: фоновая проверка истечения лидов lead-expiration (будущий док ../background/lead-expiration.md) |
| Статус | draft |
Событие публикуется атомарно со сменой состояния агрегата (actor = система/worker): пока перевод в
Expired не зафиксирован, событие не отправляется; после фиксации — доставляется гарантированно
(at-least-once), ключ = LeadId.
Payload LeadExpiredEvent
Дом полей — entity: LeadEntity; здесь перечислено только то, что реально в событии.
Payload сознательно узкий: только идентификаторы связи и момент истечения — без хотелок, ленты событий и
PII (ФИО/телефон в событие не входят, см. «Безопасность»).
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
LeadId | Guid | Да | LeadEntity.Id | Immutable | Ключ агрегата (= Kafka key). Истёкший лид. |
TenantId | Guid | Да | LeadEntity.TenantId | Immutable | Tenant boundary = AgencyCompanyId (ADR-0052); для лидов «на ПИН» — компания ПИН. |
ExecutorAgentUserId | Guid | Да | LeadEntity.ExecutorAgentUserId | Additive only | Текущий участник-исполнитель (активный Executor) — адресат уведомления «продлите лид». |
OriginAgentUserId | Guid | Да | LeadEntity.OriginAgentUserId | Immutable | Агент, изначально создавший лид: иммутабельная атрибуция (воронка агентства / антифрод). |
PrimaryInvestorUserId | Guid | Да | LeadEntity.PrimaryInvestorUserId | Immutable | Основной инвестор-участник пары (контекст лида для уведомления). |
ExpiredAt | DateTimeOffset | Да | LeadEntity.ExpiredAt | Immutable | Фактический момент перевода в Expired (= now цикла проверки). Non-nullable: событие рождается ровно в переходе. |
Вложенных моделей нет: все поля скалярные. 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, OriginAgentUserId | Internal | include |
ExecutorAgentUserId | Internal | include (адресат уведомления) |
ExpiredAt | Internal | include |
Телефоны и ФИО агента/инвестора в событие не включаются (правило 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 выполняются, Thenlead-expiredне порождается (отдельные потоки). - Given повторная доставка события, When
notificationsобрабатывает, Then уведомление уходит один раз (дедуп поMessageId). - Given breaking-изменение payload, When новый контракт, Then
pin.crm.lead-expired.v2+ dual-publish + sunset.v1.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (7): GET /api/agent/crm/leads — Список лидов агента, GET /api/investor/crm/leads — Мои лиды (инвестор), GET /api/investor/crm/leads/{leadId} — Карточка лида (инвестор), GET /api/pin-manager/crm/leads — Сквозной список всех лидов, GET /api/pin-manager/crm/leads/{leadId} — Карточка любого лида (Менеджер ПИН), POST /api/agent/crm/leads/{leadId}/archive — Архивировать лид, POST /api/agent/crm/leads/{leadId}/extend — Продлить лид
- События (3): Event Contract: pin.crm.lead-booking-intent-changed (LeadBookingIntentChangedEvent), Event Contract: pin.crm.lead-created (LeadCreatedEvent), Event Contract: pin.crm.lead-participants-changed (LeadParticipantsChangedEvent)
- Фоновые задачи (1): Background Job: LeadExpirationJob
- registries (1): Реестр событий PIN
Связанные документы
- Источник (операция): фоновая проверка
lead-expiration(будущий док../background/lead-expiration.md, INV-LEAD-4); сущность и переходActive → Expired— entity:LeadEntity. - Соседний контракт агрегата: lead-created.md (создание лида); карта событий — CRM Service README.
- ADR-0056 (polling-expiration), ADR-0052 (tenancy).