Event Contract: pin.profile.verification-completed (VerificationCompletedKafkaEvent)
Версионируемый контракт интеграционного события «верификация инвестора завершена». Consumer-документы
ссылаются сюда, не переописывая payload. Дом полей —
entity: VerificationRequestEntity.
Один контракт покрывает оба терминальных исхода KYC — Approved и Rejected (поле Status):
отдельные события «approved»/«rejected» не заводятся, чтобы потребители обрабатывали итог единообразно и
порядок per-user в общем топике сохранялся (решение зафиксировано в
verification-request.md, раздел «Доменные и интеграционные события»).
Назначение
Фиксирует решение ревьюера ПИН по заявке KYC/AML: переход InReview → Approved либо InReview → Rejected
(методы Approve/Reject агрегата, инвариант INV-VRF-8 DecisionByReviewer). В той же транзакции
обновляется денормализованный Profile.VerificationStatus (Pending → Verified / Pending → Rejected,
INV-PRF-6 в profile.md).
Публичное: downstream-контексты узнают итог без синхронного запроса в profile — notifications
уведомляет инвестора, crm и catalog включают признак «верифицированный инвестор» (бейдж на карточке
клиента, доступ к функциям для верифицированных).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.profile.verification-completed |
| C# type | VerificationCompletedKafkaEvent |
| Topic | pin.profile.verification-requests.v1 (общий с VerificationRequestSubmittedKafkaEvent; общий key = порядок per-user) |
| Partitions / key | 3 партиции (ADR-0056) / key = OwnerUserId |
| ContractVersion | static int ContractVersion = 1 |
| Producer | profile: admin-handlers approveVerification / rejectVerification (permission profile.verification.review) |
| Owning team | PIN Platform Core |
| Статус | draft |
Метаданные события — идентификатор сообщения, время события
CreatedAt, версия (=ContractVersion), payload,AggregateId = Id(заявка),AggregateType = "VerificationRequest". Trace — на outbox-записи. Событие публикуется через транзакционный outbox атомарно с решением по заявке и обновлениемProfileEntity(одно согласованное изменение: заявка +ProfileEntity+ событие).
TenantId отсутствует: инвестор вне tenant (ADR-0052); изоляция record-level по OwnerUserId.
Payload VerificationCompletedKafkaEvent
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | VerificationRequestEntity.Id | Immutable | Идентификатор заявки (= AggregateId); связь с ранее полученным verification-request-submitted. |
OwnerUserId | Guid | Да | VerificationRequestEntity.OwnerUserId | Immutable | Верифицированная/отклонённая учётная запись (= Kafka key). |
Status | string (enum VerificationRequestStatus, только терминальные) | Да | VerificationRequestEntity.Status | Additive only (новые значения) | Approved — аккаунт верифицирован; Rejected — отказ. Другие значения enum в этом событии не встречаются. |
RejectionCode | string? (enum VerificationRejectionCode) | Нет (обязателен при Status = Rejected, null при Approved) | VerificationRequestEntity.RejectionCode | Additive only (новые значения) | Машиночитаемая причина отказа: IllegibleScans / DataMismatch / DocumentExpired / SuspectedFraud / Other (семантика — verification-request.md). |
CompletedAt | DateTimeOffset | Да | VerificationRequestEntity.CompletedAt | Immutable | Момент решения (Approved/Rejected). |
Вложенных моделей нет. RejectionReason (человекочитаемое пояснение), DecisionComment (внутренний
комментарий ревьюера), ReviewerUserId и снапшот данных в payload не входят: текст причины инвестор
читает в ЛК (getMyVerification), комментарий и личность ревьюера — внутренние данные profile
(аудит), PII не публикуется.
Когда публикуется
| Условие | Переход состояния | Транзакция |
|---|---|---|
approveVerification (ревьюер, INV-VRF-8) | заявка InReview → Approved; Profile.VerificationStatus: Pending → Verified (+VerifiedAt, VerifiedRequestId) | заявка + ProfileEntity + событие в одном согласованном изменении |
rejectVerification (ревьюер, RejectionCode + RejectionReason обязательны, INV-VRF-8) | заявка InReview → Rejected; Profile.VerificationStatus: Pending → Rejected | та же |
Оба статуса терминальны: по одному Id событие публикуется не более одного раза (после Rejected инвестор
создаёт новую заявку с новым Id — INV-VRF-9 снят, цикл начинается с
verification-request-submitted).
Потребители (consumers)
| Consumer-контекст | Consumer | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
notifications | NotificationsVerificationCompletedConsumer | Уведомление инвестору: при Approved — «вы верифицированы»; при Rejected — «заявка отклонена, подробности в ЛК» (RejectionCode маппится на шаблон текста; RejectionReason в уведомление не попадает — только в ЛК) | MessageId + business key Id (терминальность) | < 5 мин |
crm | CrmVerificationCompletedConsumer | Флаг «верифицированный инвестор» на реплике клиента (OwnerUserId): бейдж в карточке клиента/сделки для менеджеров и агентов | MessageId + Id; при Rejected флаг не ставится | < 5 мин |
catalog | CatalogVerificationCompletedConsumer | Признак верифицированности пользователя — доступ к функциям каталога для верифицированных инвесторов | тот же | < 5 мин |
Consumer-документы — будущие пути docs/06-services/notifications/consumers/profile-verification-completed.md,
docs/06-services/crm/consumers/profile-verification-completed.md,
docs/06-services/catalog/consumers/profile-verification-completed.md (по будущим ТЗ; конвенция ссылок).
Синхронная альтернатива для m2m — internal API getVerificationStatus(userId)
(README, область Internal).
Доставка и восстановление
- At-least-once через транзакционный outbox. Событие фиксируется в одном согласованном изменении с
решением по заявке и обновлением
ProfileEntity; при недоступности брокера решение ревьюера не теряется (сохранено в БД), публикация досылается ретраем. Потребители обязаны быть идемпотентными (возможны дубли). - Порядок per-user по key =
OwnerUserId:completedприходит послеsubmittedтого же пользователя (общий топик, один ключ). Между пользователями порядок не гарантирован. - Терминальность и идемпотентность. По одному
Idсобытие публикуется не более одного раза (оба статуса терминальны); дедуп потребителя — поMessageId+ business keyId. Повторная доставка не должна повторно слать уведомление/повторно менять флаг верифицированности. - Poison-message. Необрабатываемое после ретраев событие (сбой БД потребителя) → DLQ контекста-потребителя
- alert; статус в
profileостаётся источником истины, восстановление — ре-плей из DLQ. НеизвестныйStatusв DLQ не отправляется — fail-closed (признак верифицированности не включается, см. ниже).
- alert; статус в
Версионирование и совместимость (реальность SDK)
ContractVersion = 1; приenvelope.Version != ContractVersion— warn-and-proceed (upcasting в SDK нет).- Additive-only: новые optional-поля с default — без bump.
- Breaking —
ContractVersion = 2+ топикpin.profile.verification-requests.v2+ dual-publish обоих контрактов топика + deadline миграции + sunset.v1. - Unknown enum:
Status— fail closed, вести себя какRejected(признак верифицированности не включать), warning;RejectionCode— fallbackOther, warning. - Порядок per-user гарантирован key =
OwnerUserId(completed приходит после submitted того же пользователя).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
Id, OwnerUserId, Status, CompletedAt | Internal | include |
RejectionCode | Internal | include; инвестору дословно не показывается — потребители маппят на локализованный шаблон, детали — в ЛК |
Событие НЕ содержит PII (снапшот заявки — только шифротекст в БД profile), RejectionReason,
DecisionComment, ReviewerUserId, secrets, tokens. Доступ к топику — m2m ACL Kafka
(notifications/crm/catalog).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: NotificationEntity, Entity / Aggregate: VerificationRequestEntity
- API (5): GET /api/v1/internal/profile/verification-status/{userId} — Статус верификации для m2m (getVerificationStatus), GET /api/v1/profile/verification — Моя заявка на верификацию (getMyVerification), POST /api/v1/admin/profile/verification/{requestId}/approve — Одобрить заявку KYC (approveVerification), POST /api/v1/admin/profile/verification/{requestId}/reject — Отклонить заявку KYC (rejectVerification), POST /api/v1/admin/profile/verification/{requestId}/take — Взять заявку в ревью (takeVerificationInReview)
- Use Cases (1): UC-PRF-003. Подача заявки на верификацию инвестора и её одобрение (KYC)
- События (1): Event Contract: pin.profile.verification-request-submitted (VerificationRequestSubmittedKafkaEvent)
- Консюмеры (1): Consumer: Profile Events → Notifications (notifications-profile-events)
- registries (1): Реестр событий PIN
- index.md (1): Profile Service (Pin.Profile)
Связанные документы
- Сущность (машина состояний, инварианты INV-VRF-6..9): verification-request.md;
денормализация: profile.md (
ProfileVerificationStatus, INV-PRF-6). - Соседний контракт того же топика: verification-request-submitted.md.
- Источники (операции): admin-review API
approveVerification/rejectVerification— карта в README; permissionprofile.verification.review— permission.md. - Решения: ADR-0052 (инвестор вне tenant), ADR-0056 (simplicity-first).