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

Event Contract: pin.crm.document-status-changed (DocumentStatusChangedEvent)

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

Назначение

Фиксирует смену статуса проверки документа сделки: файл загружен, взят на проверку, принят или отклонён (DealDocumentStatus: Uploaded → UnderReview → Accepted/Rejected, US-INV-14 — «инвестор видит: загружено, на проверке, принято, отклонено; если отклонён — указана причина»). Producer — crm (загрузка/проверка документа, инвариант INV-DOC-6). Публичное: даёт notifications snapshot перехода без синхронного запроса в crm, чтобы уведомить инвестора о статусе его документа. Первичный запрос документа ([*] → Requested) публикуется отдельным событием document-requested, а не этим.

Metadata

ПолеЗначение
Event typepin.crm.document-status-changed
Topicpin.crm.document-status-changed.v1
Partitions / key3 (crm-нагрузка 1–10k активных, ADR-0056) / key = AggregateId = DocumentId (порядок статусов одного документа)
ContractVersion1
Producercrm: загрузка документа, взятие в проверку, решение принять/отклонить — deal-document.md
Статусdraft

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

Payload DocumentStatusChangedEvent

Дом полей — entity: DealDocumentEntity (раздел «Доменные и интеграционные события»); здесь перечислено только то, что реально в событии.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
DocumentIdGuidДаDealDocumentEntity.IdImmutableКлюч агрегата (= Kafka key).
DealIdGuidДаDealDocumentEntity.DealIdImmutableСделка (deal.md); recipient-routing у consumer-а.
TenantIdGuidДаDealDocumentEntity.TenantIdImmutableTenant boundary (= DeveloperCompanyId сделки, ADR-0052).
DocumentTypestring (enum DealDocumentType)ДаDealDocumentEntity.TypeAdditive only (новые значения)ClientPassport / BookingContract / SignedBookingContract / BookingPaymentProof / ContractDraftDocument / SaleContract / SignedSaleContract / PaymentProof / RosreestrDocument / Other.
FromStatusstring (enum DealDocumentStatus)Дастатус до переходаAdditive onlyПредыдущий статус (Requested/Uploaded/UnderReview).
ToStatusstring (enum DealDocumentStatus)ДаDealDocumentEntity.StatusAdditive onlyНовый статус (Uploaded/UnderReview/Accepted/Rejected).
RejectReasonstring?Нет (обязателен при ToStatus = Rejected)DealDocumentEntity.RejectReasonAdditive onlyПричина отклонения (US-INV-14; varchar(1000)).
ChangedByUserIdGuidДаUploadedByUserId (переход в Uploaded) / ReviewedByUserIdAccepted/Rejected) / actor взятия в проверкуImmutable semanticsКто выполнил переход.
ChangedAtDateTimeOffsetДаUploadedAt / ReviewedAt / UpdatedAt соответствующего переходаImmutableМомент перехода.

Значения enum DealDocumentStatus/DealDocumentType — источник истины deal-document.md. FileId, FileName, ContentType, SizeBytes в payload не входят: бинарь и метаданные файла остаются в crm/storage, consumer-у для уведомления не нужны (скачивание — по пресайн-ссылке storage с проверкой участия).

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

УсловиеПереход состоянияПубликация
Сторона-владелец типа загрузила файлRequested → Uploaded или [*] → Uploadedатомарно со сменой состояния агрегата
Получатель взял на проверку (типы с проверкой)Uploaded → UnderReviewта же
Решение принятьUnderReview → Accepted (терминальный)та же
Решение отклонитьUnderReview → Rejected (терминальный; RejectReason задан)та же

Переход [*] → Requested не публикуется этим событием (см. document-requested). Повторная загрузка после Rejectedновая запись DealDocumentEntity (INV-DOC-4), поэтому даёт новое событие с FromStatus = Requested/Uploaded и своим DocumentId.

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

Consumer-контекстSide effectsИдемпотентностьЗадержка (SLA)
notificationsУведомление инвестору о статусе его документа (US-INV-14.3); recipient (Investor сделки) резолвится из DealId по собственной deal-проекции consumer-а (питается deal-created/deal-members-changed); текст включает DocumentType, ToStatus, при RejectedRejectReasonMessageId + монотонный guard по (DocumentId, ChangedAt)< 5 мин

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

  • Гарантия публикации. Событие уходит только после фиксации перехода статуса DealDocumentEntity (INV-DOC-6); при откате не публикуется. Доставка — at-least-once, ключ DocumentId.
  • Порядок и guard. Статусы одного документа упорядочены ключом DocumentId; потребитель держит монотонный guard по (DocumentId, ChangedAt) — устаревшая доставка пропускается.
  • Идемпотентность потребителя. notifications — дедуп по MessageId + guard по ChangedAt; повторная доставка не шлёт второе уведомление.
  • Retry. Транзиентный сбой потребителя → повтор обработки с backoff; событие доступно до успешной обработки.
  • DLQ. Неустранимый сбой после исчерпания попыток → dead-letter c алертом; проигрывание безопасно (идемпотентно). Неизвестный статус — нейтральное уведомление (fail-safe), в DLQ не уходит.

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

  • ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку.
  • Additive-only для minor: новые optional-поля с безопасным default — без bump топика.
  • Breaking (удаление/переименование/смена типа/смысла) — ContractVersion = 2 + новый топик pin.crm.document-status-changed.v2 + dual-publish на период миграции + deadline миграции consumer-ов + sunset .v1.
  • Порядок статусов одного документа гарантирован key-ом (DocumentId); consumer держит guard по ChangedAt.
Fail-safe при неизвестном enum

Unknown enum (DocumentType/FromStatus/ToStatus) — fallback: сохранить строку как есть, warning; для уведомления — нейтральный текст «статус документа обновлён» (fail safe, без падения консюмера).

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

ПолеКласс данныхПравило
DocumentId, DealId, TenantId, DocumentType, FromStatus, ToStatus, ChangedByUserIdInternalinclude
ChangedAtInternalinclude
RejectReasonInternal (свободный текст)include; в логах не печатать целиком (может содержать ФИО/детали) — Rejected(reason=***)
  • Actor type: система (producer — crm; consumer — notifications).
  • Permission/policy: publish — внутренняя мутация под crm.deal-documents.manage/.review; consume — m2m ACL топика (только notifications); trusted/untrusted boundary — событие только из доверенного crm.
  • Tenant/scope: TenantId в payload; consumer уведомляет только участника-инвестора сделки (record-level).
Masking/PII

Событие НЕ несёт ФИО, телефонов, файла и платёжных реквизитов — только идентификаторы, enum-статусы и причина отклонения. ФИО/телефон получателя consumer берёт из своей реплики (маска в логах +7***1234, ФИО — инициалы). Секретов, токенов, raw credentials, unbounded вложений событие не содержит.

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

  • Логи (без PII): факт публикации document-status-changed с DocumentId/DealId/DocumentType/ FromStatusToStatus/ChangedByUserId/ChangedAt; RejectReason целиком не печатать — Rejected(reason=***) (может содержать ФИО/детали).
  • Метрики: число переходов по топику в разрезе ToStatus; доля Rejected; сквозная задержка до notifications (< 5 мин); глубина DLQ (порог алерта > 0); число срабатываний guard по ChangedAt.
  • Доступ: топик — m2m ACL Kafka (только notifications); FileId/бинарь/метаданные файла в событие не входят; ФИО/телефон получателя consumer резолвит из своей реплики и маскирует в логах (+7***1234).

Acceptance Criteria

  • Given документ в статусе Requested, When сторона-владелец типа загружает файл, Then ровно одно document-status-changed с Requested → Uploaded.
  • Given документ в UnderReview, When проверяющий отклоняет, Then событие UnderReview → Rejected с непустым RejectReason (US-INV-14).
  • Given документ в UnderReview, When проверяющий принимает, Then событие UnderReview → Accepted (терминальный).
  • Given повторная загрузка после Rejected, When создаётся новая запись документа (INV-DOC-4), Then событие несёт новый DocumentId (старый терминален и не переиспользуется).
  • Given устаревшая/повторная доставка, When notifications обрабатывает, Then уведомление не задваивается (guard по ChangedAt + MessageId).
  • Given переход [*] → Requested, When документ запрошен, Then это событие не публикуется — фиксирует document-requested.
  • Given неизвестное значение статуса/типа, When потребитель обрабатывает, Then нейтральный текст «статус документа обновлён» + warning (без падения).
  • Given breaking-изменение payload, When новый контракт, Then pin.crm.document-status-changed.v2 + dual-publish + sunset .v1.

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

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

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