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

UC-PRF-003. Подача заявки на верификацию инвестора и её одобрение (KYC)

Назначение

Полный цикл простой верификации инвестора (KYC/AML): создание черновика заявки, заполнение (ФИО, дата рождения, паспортные данные через персону Self, сканы), отправка с фиксацией иммутабельного шифрованного снапшота, ревью сотрудником ПИН и одобрение с обновлением Profile.VerificationStatus = Verified. Связан с контекстом profile.

Metadata

ПолеЗначение
IDUC-PRF-003
Контекстprofile (см. docs/06-services/profile/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольИнвестор (подача, отзыв) — только UserType = Investor (INV-VRF-5); Сотрудник ПИН — ревьюер (очередь, решение)
Permissions / scopeИнвестор: profile.verification.submit (self-scope). Ревьюер: profile.verification.review (платформенный scope, admin-area API)
Tenantне tenanted (ADR-0052); заявки инвестора — record-level по OwnerUserId; ревьюер — кросс-record платформенный permission
Record-levelинвестор — только свои заявки; ревьюер — вся очередь Submitted + взятые им InReview

Предусловие

  • Выполнены UC-PRF-001/002: у инвестора есть активная персона Relation = Self с заполненными ФИО и датой рождения и активным паспортным документом (тип из {RfPassport, RbPassport, KzPassport, CisPassport, ForeignPassport}).
  • Profile.VerificationStatus != Verified (INV-VRF-9) и нет заявки в Draft/Submitted/InReview (INV-VRF-1).
  • Feature flag profile.verification.enabled включён.

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементЛК → «Верификация инвестора» → «Пройти верификацию»; интерфейс ревьюера ПИН → «Очередь верификаций»

Основной Поток

  1. Инвестор нажимает «Пройти верификацию». Фронт вызывает createVerificationRequest (POST /profile/verification-requests) с CreateVerificationRequestForm { requisiteTemplateId? (Guid — предзаполнение из шаблона) }.
  2. Создание черновика.
    • Проверить доступ по профилю (INV-PRF-2/3) и тип пользователя — инвестор (INV-VRF-5, иначе PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE).
    • Проверить, что инвестор ещё не верифицирован: VerificationStatus != Verified (INV-VRF-9, иначе PROFILE_ALREADY_VERIFIED).
    • Проверить отсутствие активной заявки: нет заявки в статусе Draft/Submitted/InReview (INV-VRF-1, иначе PROFILE_VERIFICATION_ALREADY_IN_PROGRESS; финальный арбитр гонки — уникальный индекс ux_verification_requests_owner_active).
    • Найти активную персону Relation = Self (Status = Active); отсутствует → PROFILE_VERIFICATION_PERSON_NOT_SELF (INV-VRF-3).
    • При переданном requisiteTemplateId — шаблон принадлежит актору, Status = Active, template.PersonId указывает на персону Self.
    • Создать VerificationRequestEntity (OwnerUserId = UserId, PersonId = selfPerson.Id, RequisiteTemplateId): Status = Draft; SnapshotCipher = null; ScanList = { ContractVersion: 1, Items: [] }; CreatedByUserId = UserId. Публикуется доменное VerificationRequestCreatedEvent(Id, OwnerUserId, PersonId) (Kafka — только на Submit). Результат — VerificationRequestResult { id, status: Draft, personId }.
  3. Инвестор проверяет/дополняет данные персоны Self (форма заявки редактирует персону — UC-PRF-001 шаги 5–6) и прикладывает дополнительные сканы к заявке: updateVerificationDraft (PUT /profile/verification-requests/{id}) с UpdateVerificationDraftForm { scans: List<DocumentScanForm { fileId, fileName, contentType, sizeBytes }> ≤ 10 }. Разрешено только в Status = Draft (INV-VRF-7, иначе PROFILE_VERIFICATION_NOT_EDITABLE), для владельца; валидируется DocumentScanList; сканы заявки заменяются целиком.
  4. Инвестор нажимает «Отправить на проверку». Фронт вызывает submitVerification (POST /profile/verification-requests/{id}/submit, без тела).
  5. Отправка на проверку.
    • Заявка принадлежит актору, Status = Draft; доступ по профилю (INV-PRF-2/3, INV-VRF-5).
    • Собрать данные персоны Self с документами и расшифровать ФИО и BirthDate.
    • Проверить полноту (INV-VRF-2): ФИО и BirthDate заполнены; есть активный паспортный документ; суммарно ≥ 1 скана (сканы документов снапшота + request.ScanList) — иначе PROFILE_VERIFICATION_INCOMPLETE.
    • Проверить возраст ≥ 18 лет на момент Submit (INV-VRF-4, иначе PROFILE_VERIFICATION_UNDERAGE).
    • Собрать VerificationSnapshotModel v1: PersonId, PersonVersion = person.Version, ФИО, BirthDate, CitizenshipCountryId, IdentityDocument = DocumentSnapshot { DocumentId, Type, CountryId, CountryIsoCode2 (юридический след), PayloadContractVersion, Payload (расшифрованный), Scans } (паспортный документ; при нескольких — выбранный в форме, по умолчанию самый свежий по IssueDate), AdditionalDocuments — остальные активные документы (≤ 10).
    • Зашифровать снапшот целиком: SnapshotCipher, SnapshotContractVersion = 1, KeyVersion = текущий.
    • Переход Draft → Submitted; SubmittedAt = now; снапшот далее иммутабелен (INV-VRF-7).
    • В том же согласованном изменении Profile.VerificationStatus = Pending (INV-PRF-6). Публикуются доменное VerificationRequestSubmittedEvent(Id, OwnerUserId, SubmittedAt) и интеграционное VerificationRequestSubmittedKafkaEvent (ContractVersion = 1, топик pin.profile.verification-requests.v1, key = OwnerUserId; payload без PII: Id, OwnerUserId, SubmittedAt) — потребители notifications (очередь заботе) и support (тикет ревью). Результат — VerificationRequestResult { id, status: Submitted, submittedAt }.
  6. Ревьюер ПИН открывает очередь: listVerificationQueue (GET /profile/admin/verification-requests ?status=Submitted) — заявки Status = Submitted, порядок по SubmittedAt (FIFO). Берёт заявку: takeVerificationInReview (POST .../{id}/take): только из Submitted; ReviewerUserId = actor, ReviewStartedAt = now, Status = InReview. Гонка двух ревьюеров разрешается оптимистической блокировкой по версии записи: проигравший получает PROFILE_VERIFICATION_ALREADY_TAKEN (INV-VRF-6).
  7. Ревьюер смотрит данные: viewVerificationSnapshot (GET .../{id}/snapshot) — доступ только у ReviewerUserId (или супервизора заботы с profile.verification.review); снапшот расшифровывается, и каждый доступ аудируется доменным событием VerificationSnapshotAccessedEvent(Id, ReviewerUserId, AccessedAt); сканы — presigned-ссылки storage по FileId.
  8. Ревьюер одобряет: approveVerification (POST .../{id}/approve, тело ApproveVerificationForm { decisionComment? (≤1024) }).
    • Заявка Status = InReview; решение принимает тот же ревьюер actor = ReviewerUserId (INV-VRF-8, иначе PROFILE_VERIFICATION_NOT_REVIEWER).
    • Переход Status = Approved, CompletedAt = now, DecisionComment ← форма. В том же согласованном изменении Profile.VerificationStatus = Verified, VerifiedAt = now, VerifiedRequestId = request.Id (INV-PRF-6). Публикуются доменное VerificationRequestDecidedEvent(Id, OwnerUserId, Approved) и интеграционное VerificationCompletedKafkaEvent (ContractVersion = 1, тот же топик, key = OwnerUserId; payload: Id, OwnerUserId, Status = Approved, RejectionCode = null, CompletedAt) — потребители notifications (уведомление инвестору), crm/catalog (бейдж «верифицирован»). Интерфейс ревьюера убирает заявку из очереди.
  9. Инвестор видит в ЛК статус «Верифицирован» (getMyVerification/getMyProfile); notifications доставляет уведомление по событию.

Альтернативные Потоки

A1. Отклонение заявки (Reject)

  1. Вместо шага 8 ревьюер вызывает rejectVerification (POST .../{id}/reject) с RejectVerificationForm { rejectionCode (IllegibleScans|DataMismatch|DocumentExpired| SuspectedFraud|Other), rejectionReason (1..1024 — обязателен; без цитирования значений документов), decisionComment? (≤1024, внутренний) }.
  2. Reject требует code + reason (при Other — развёрнутый reason); переход Status = Rejected, CompletedAt = now; в том же согласованном изменении Profile.VerificationStatus = Rejected; публикуется VerificationCompletedKafkaEvent { Status = Rejected, RejectionCode } (INV-VRF-8).
  3. Инвестор видит причину (RejectionReason; DecisionComment не показывается) и может создать новую заявку (шаг 1 основного потока; терминальная Rejected не блокирует INV-VRF-1).

A2. Отзыв заявки инвестором (Revoke)

  1. Пока заявка в Submitted (ещё не взята ревьюером), инвестор вызывает revokeVerification (POST .../{id}/revoke): Submitted → Draft, SnapshotCipher/SnapshotContractVersion/ KeyVersion/SubmittedAt обнуляются (INV-VRF-7 — доработка только через Draft); Profile.VerificationStatus возвращается в NotVerified (или Rejected, если предыдущая была отклонена — по машине состояний ../domain/profile.md).
  2. Из InReview отзыв запрещён → PROFILE_VERIFICATION_NOT_EDITABLE (409). Гонка «Revoke vs TakeInReview» разрешается оптимистической блокировкой по версии записи. Возврат к шагу 3 основного потока.

A3. Неполные данные при Submit

  1. PROFILE_VERIFICATION_INCOMPLETE (INV-VRF-2): нет ФИО/даты рождения, нет активного паспортного документа или ни одного скана. Фронт подсвечивает недостающее и ведёт в форму персоны (UC-PRF-001/002). Заявка остаётся в Draft. Возврат к шагу 3.
  2. PROFILE_VERIFICATION_UNDERAGE (INV-VRF-4): возраст < 18 — заявка не отправляется, сценарий завершается.

A4. Гонка двух ревьюеров

  1. Оба вызывают takeVerificationInReview; первый успешно сохраняет, второй получает конфликт версии записи → PROFILE_VERIFICATION_ALREADY_TAKEN (409, INV-VRF-6).
  2. Интерфейс ревьюера обновляет очередь. Возврат к шагу 6.

A5. Повторная заявка уже верифицированного

  1. createVerificationRequestPROFILE_ALREADY_VERIFIED (409, INV-VRF-9). Фронт показывает бейдж «Верифицирован». Сценарий завершается.

A6. Ключ шифрования недоступен при сборке/расшифровке снапшота (fail-closed)

  1. На submitVerification снапшот не собирается: заявка остаётся Draft, Profile.VerificationStatus не меняется, событие не публикуется (INTERNAL, 500; alert «ошибки шифрования ПДн»). Частичного перехода Draft → Submitted нет; инвестор повторяет отправку после восстановления ключей.
  2. На viewVerificationSnapshot расшифровка SnapshotCipher недоступна — ревьюер видит ошибку и не принимает решение; заявка остаётся InReview (решение по нерасшифрованным данным невозможно). Возврат к шагу 7 после восстановления.

A7. Заявка «зависла» в InReview (ревьюер не завершил рассмотрение)

  1. Из InReview нет автоматического возврата в очередь: заявка остаётся закреплённой за ReviewerUserId до решения. Инвестор в этот период видит статус «На проверке» (Pending); отзыв из InReview запрещён (A2, PROFILE_VERIFICATION_NOT_EDITABLE).
  2. Завершить рассмотрение вправе тот же ревьюер или супервизор заботы с profile.verification.review (INV-VRF-8 допускает решение супервизором; фиксируется в UpdatedByUserId). Возврат к шагу 8/A1.

Постусловие

  • Успех: VerificationRequestEntity.Status = Approved (терминальный, не удаляется — KYC-аудит); Profile.VerificationStatus = Verified, VerifiedAt/VerifiedRequestId заполнены; VerificationCompletedKafkaEvent опубликован; ЛК и потребители (crm/catalog) видят бейдж.
  • Снапшот заявки иммутабелен и зашифрован; решение навсегда привязано к данным на момент Submit (PersonVersion — сверка «данные не менялись»).
  • Все доступы ревьюера к расшифрованному снапшоту зафиксированы VerificationSnapshotAccessedEvent.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
ValidationError400Сканы/форма reject (обязательность code+reason)Подсветить поля
PROFILE_ALREADY_VERIFIED409INV-VRF-9Показать бейдж, скрыть кнопку
PROFILE_VERIFICATION_ALREADY_IN_PROGRESS409INV-VRF-1Открыть существующую заявку
PROFILE_VERIFICATION_PERSON_NOT_SELF422Нет активной персоны Self (INV-VRF-3)Вести в UC-PRF-001
PROFILE_VERIFICATION_INCOMPLETE422INV-VRF-2Чек-лист недостающего
PROFILE_VERIFICATION_UNDERAGE422INV-VRF-4Пояснение «18+»
PROFILE_VERIFICATION_NOT_EDITABLE409Правка/отзыв вне допустимого статуса (INV-VRF-7)Перечитать статус
PROFILE_VERIFICATION_ALREADY_TAKEN409Гонка ревьюеров (INV-VRF-6)Обновить очередь
PROFILE_VERIFICATION_NOT_REVIEWER403Решение не тем ревьюером (INV-VRF-8)Показать владельца ревью
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403Не инвестор (INV-VRF-5)Скрыть раздел
PROFILE_MANAGED_EXTERNALLY403TalentSync (INV-PRF-2)UC-PRF-005
404 NOT_FOUND404Чужая/несуществующая заявкаОбновить список

Доменные События

СобытиеКогда поднимаетсяSubscribers
VerificationRequestCreatedEventСоздание черновикаin-proc аудит
VerificationRequestSubmittedEventDraft → Submittedin-proc обработчик → Profile.VerificationStatus = Pending (INV-PRF-6)
VerificationRequestDecidedEventApproved/Rejectedin-proc обработчик → Profile.VerificationStatus = Verified/Rejected (одно согласованное изменение)
VerificationSnapshotAccessedEventКаждая расшифровка снапшота ревьюером (шаг 7)in-proc аудит (обязательная запись)

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
VerificationRequestSubmittedKafkaEvent.v1pin.profile.verification-requests.v1Draft → Submitted (Id, OwnerUserId, SubmittedAt — без PII)notifications, support (очередь ревью)
VerificationCompletedKafkaEvent.v1pin.profile.verification-requests.v1Approved/Rejected (Id, OwnerUserId, Status, RejectionCode?, CompletedAt)notifications (инвестору), crm, catalog (бейдж «верифицирован»)

Acceptance Criteria

#КритерийПроверка
AC1Submit фиксирует зашифрованный снапшот; последующие правки персоны НЕ меняют данные, которые видит ревьюертест: правка персоны после Submit
AC2На аккаунт — не более одной заявки в Draft/Submitted/InReview, в т.ч. при конкурентном созданииконкурентный тест unique-индекса
AC3Approve в одном согласованном изменении обновляет заявку, Profile.VerificationStatus = Verified и публикацию событияинтеграционный тест с падением между шагами
AC4Reject требует code + reason; инвестор видит reason, но не DecisionCommentтест A1
AC5Гонка ревьюеров: ровно один берёт заявку (оптимистическая блокировка по версии записи)конкурентный тест INV-VRF-6
AC6Каждый просмотр снапшота ревьюером порождает аудит-событиетест шага 7
AC7Kafka-payload обоих событий не содержит ФИО/дат/номеров документовконтракт-тест
AC8Заявки не удаляются: терминальные Approved/Rejected остаются в историипроверка отсутствия delete-API и soft-delete
AC9Submit при < 18 лет / без паспортного документа / без сканов отклоняется до смены статусатесты INV-VRF-2/4

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

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

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

  • API методы (будущие): ../api/public/create-verification-request.md, ../api/public/submit-verification.md, ../api/public/revoke-verification.md, ../api/admin/list-verification-queue.md, ../api/admin/take-verification-in-review.md, ../api/admin/approve-verification.md, ../api/admin/reject-verification.md, ../api/admin/view-verification-snapshot.md.
  • Domain entity: ../domain/verification-request.md (машина состояний, INV-VRF-1..9, VerificationSnapshotModel), ../domain/profile.md (денормализация VerificationStatus, INV-PRF-6), ../domain/person.md, ../domain/person-document.md.
  • Контракты событий: ../events/verification-request-submitted.md, ../events/verification-completed.md.
  • Потребители (будущие ТЗ): docs/06-services/notifications/, docs/06-services/support/, docs/06-services/crm/, docs/06-services/catalog/.
  • State machine: диаграмма в ../domain/verification-request.md.
  • Связанные UC: UC-PRF-001/002 (данные персоны и документы — предусловия), UC-PRF-004 (шаблон реквизитов как источник предзаполнения).