Event Contract: pin.calendar.appointment-completed (AppointmentCompletedEvent)
Версионируемый контракт интеграционного события «Исход встречи зафиксирован» — агент отметил, что встреча
состоялась (Completed) или клиент не пришёл (NoShow). Consumer-документы ссылаются сюда, не переописывая
payload. Дом полей агрегата — entity: AppointmentEntity.
Назначение
Фиксирует терминальный исход Confirmed-записи после наступления времени встречи (INV-APT-4). Публичное:
crm пополняет ленту лида результатом встречи — Completed двигает работу с клиентом дальше, NoShow —
сигнал о потерянном контакте (повторная связь, ре-активация лида). Каскадные эффекты (слот Archived,
календарное событие Completed) — внутри calendar и в payload не входят.
| Поле | Значение |
|---|
| Event type | pin.calendar.appointment-completed |
| Topic | pin.calendar.appointment-completed.v1 |
| Partitions / key | 3 (ADR-0056) / key = AppointmentId (порядок событий одной записи) |
| ContractVersion | 1 |
| Producer | calendar: фиксация исхода агентом (../api/agent/complete-appointment.md, permission calendar.appointments.decide, только из Confirmed и только при now ≥ StartAt) |
| Статус | draft |
Событие публикуется атомарно с переходом записи → Completed/→ NoShow и каскадом «слот Archived +
календарное событие Completed»: пока изменение не зафиксировано, событие не отправляется; после фиксации —
доставляется гарантированно (at-least-once, transactional outbox), ключ = AppointmentId.
Payload AppointmentCompletedEvent
Дом полей — entity: AppointmentEntity (раздел «Доменные и интеграционные
события»); здесь перечислено только то, что реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|
AppointmentId | Guid | Да | AppointmentEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | AppointmentEntity.TenantId | Immutable | Компания агента (ADR-0052). |
AgentUserId | Guid | Да | AppointmentEntity.AgentUserId | Immutable | Агент/консультант, зафиксировавший исход. |
InvestorUserId | Guid | Да | AppointmentEntity.InvestorUserId | Immutable | Инвестор — участник встречи. |
Kind | string (enum EventKind) | Да | AppointmentEntity.Kind | Additive only (новые значения) | Вид встречи: Showing / Consultation. |
StartAt | DateTimeOffset | Да | AppointmentEntity.StartAt | Immutable (снапшот слота) | Начало встречи, UTC. |
TimeZone | string | Да | AppointmentEntity.TimeZone | Additive only | IANA-таймзона слота. |
Outcome | string (enum AppointmentOutcome) | Да | производное от финального статуса записи | Additive only (новые значения) | Исход встречи, см. enum ниже. |
CompletedAt | DateTimeOffset | Да | AppointmentEntity.CompletedAt | Immutable | Момент фиксации исхода агентом. |
Enum AppointmentOutcome
| Значение | Финальный статус записи | Описание |
|---|
Completed | Completed | Встреча состоялась. |
NoShow | NoShow | Клиент не пришёл; календарное событие при этом также Completed — факт встречи в календаре закрыт, исход хранится на записи. |
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|
Агент зафиксировал «встреча состоялась» (now ≥ StartAt, INV-APT-4) | Confirmed → Completed (Outcome = Completed; слот Booked → Archived, событие → Completed) | атомарно с переходом и каскадом |
Агент зафиксировал «клиент не пришёл» (now ≥ StartAt) | Confirmed → NoShow (Outcome = NoShow; каскад тот же) | атомарно с переходом и каскадом |
Потребители (consumers)
| Consumer-контекст | Side effects | Идемпотентность | Задержка (SLA) |
|---|
crm | Запись в ленту лида с исходом встречи: Completed — «встреча состоялась» (продолжение воронки: подборка, фиксация, сделка); NoShow — «клиент не пришёл» (сигнал повторной связи). Консюмер — ../../crm/consumers/calendar-appointments.md | MessageId (unique-ключ записи ленты) + business key AppointmentId | < 1 мин |
notifications | Выделенный пользовательский NotificationKind для исхода в v1 не заведён: исход фиксирует сам агент, инвестору результат не рассылается. Консюмер ../../notifications/consumers/calendar-events.md принимает событие и завершает обработку без пользовательских уведомлений (no-op) — уведомления по исходу (например, запрос обратной связи у инвестора при Outcome = Completed) добавляются additive-но без смены контракта | MessageId | — |
Версионирование и совместимость
ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает
обработку. Топик кодирует версию по конвенции суффиксом .v1.
- Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump; новые
значения
Outcome — additive.
- Breaking (удаление/переименование/смена типа или смысла) —
ContractVersion = 2 + новый топик
pin.calendar.appointment-completed.v2 + dual-publish на период миграции + deadline миграции consumers +
sunset .v1.
- Unknown enum (
Outcome/Kind) — fallback: сохранить строку как есть, warning; лента CRM фиксирует
нейтральное «встреча завершена» (fail safe).
Безопасность
| Поле | Класс данных | Правило |
|---|
AppointmentId, TenantId, AgentUserId, InvestorUserId, Kind, StartAt, TimeZone, Outcome, CompletedAt | Internal | include (только идентификаторы, время и enum) |
- PII (ФИО/телефоны) в событии отсутствует по дизайну: display-данные консюмеры берут из своей реплики
pin.identity.users.v1; свободнотекстовых полей в контракте нет.
- Доступ к топику — m2m ACL Kafka (
crm, notifications).
Доставка, retry/DLQ и восстановление
- Гарантия. At-least-once после атомарной публикации из outbox; ключ =
AppointmentId — порядок событий
одной записи в топике сохранён. Дубликаты возможны — консюмеры идемпотентны по MessageId.
- Retry. Транзиентный сбой консюмера → повтор с backoff; партиция не продвигается дальше застрявшего
оффсета, порядок по ключу сохраняется.
- DLQ. Неразбираемое/несовместимое сообщение после конечного числа попыток — в DLQ с алертом; записи
других инвесторов не блокируются.
- Кросс-топик порядок. Исход — финальная картина записи (терминальные статусы иммутабельны, INV-APT-6):
устаревшие
appointment-confirmed, обработанные после исхода, консюмер игнорирует по бизнес-состоянию
(no-op, warning). Если appointment-completed обработан раньше appointment-created (редкий случай
отставания партиции другого топика) — get-or-create клиента/лида выполняется по данным этого события,
лента дополнится задним числом без дублей.
- Промежуточное состояние. Исход в календаре агента виден сразу; лента CRM — после обработки события
(обычно < 1 мин).
Acceptance-критерии (контракт потребления)
- Состоялась. Дано: агент зафиксировал
Outcome = Completed. Когда crm обрабатывает событие. Тогда в
ленте лида — «встреча состоялась» с видом и временем встречи.
- Неявка. Дано: агент зафиксировал
Outcome = NoShow. Когда crm обрабатывает событие. Тогда в ленте
лида — «клиент не пришёл»; инвестору уведомление не отправляется.
- Идемпотентность. Дано: сообщение доставлено дважды. Тогда запись ленты не дублируется (no-op).
- Неизвестный Outcome. Дано: сообщение с новым значением
Outcome. Когда консюмер обрабатывает. Тогда
warning + нейтральная запись ленты «встреча завершена»; сообщение не в DLQ.
- Несовместимый payload. Дано: сообщение без обязательного поля. Когда консюмер повторно не смог его
обработать. Тогда сообщение уходит в DLQ, остальные записи обрабатываются.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы