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

POST /api/v1/profile/verification/{requestId}/submit — Отправка заявки KYC на ревью (submitVerification)

Назначение

Зафиксировать иммутабельный зашифрованный снапшот данных персоны и отправить заявку на простую верификацию инвестора (KYC/AML) в очередь ревью ПИН: переход Draft → Submitted. Метод собирает VerificationSnapshotModel из персоны Relation = Self и её документов (расшифровка → сборка → сериализация → шифрование одним KeyVersion), чтобы решение ревьюера навсегда опиралось на данные на момент Submit, даже если персона потом изменится (INV-VRF-7). В той же транзакции денормализуется Profile.VerificationStatus → Pending (INV-PRF-6) и публикуется событие очереди ревью. Дом полей — verification-request.md. Вызывает инвестор из ЛК.

Metadata

ПолеЗначение
Сервис/контекстprofile
API areapublic
Feature groupverification
Статусdraft
Документdocs/06-services/profile/api/public/submit-verification.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Bearer JWT), UserType = Investor (INV-VRF-5)
Auth policy / Permissionsauthenticated; permission profile.verification.submit
Scope (RBAC)self
Record-levelVerificationRequestEntity.OwnerUserId = UserId из sub (из тела/маршрута не принимается); чужой requestId → 404
Tenant isolationне применяется (ADR-0052: инвестор вне tenant)
Auditupdate: UpdatedByUserId = UserId, Version++; событие VerificationRequestSubmittedEvent; снапшот — только шифротекст (в логи/события не попадает)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/profile/verification/{requestId}/submit
Success status200
Idempotency headerIdempotency-Key (опционально); повтор дополнительно гасится статусной проверкой (не DraftPROFILE_VERIFICATION_NOT_EDITABLE)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

Тело запроса отсутствует (данные заявки уже накоплены в Draft через createVerificationRequest / updateVerificationDraft). Снапшот собирается сервером из персоны и её документов.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
requestIdrouteGuidДаNotEmpty; заявка владельца в статусе Draftчужая → 404

Модель ответа VerificationRequestModel

Безопасная read-проекция агрегата (verification-request.md) — без PII (снапшот наружу не отдаётся; расшифровка снапшота — только ревьюером через admin API). Дом модели — get-my-verification.md; приведён здесь для самодостаточности.

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаVerificationRequestEntity.IdИдентификатор заявки.
PersonIdGuidДаentity.PersonIdПерсона-предмет проверки (Self).
RequisiteTemplateIdGuid?Нетentity.RequisiteTemplateIdШаблон-источник предзаполнения (если был).
StatusVerificationRequestStatus (string)Даentity.StatusПосле операции — Submitted.
HasSnapshotboolДаentity.SnapshotCipher != nullСнапшот зафиксирован (после Submit — true).
SnapshotContractVersionint?Нетentity.SnapshotContractVersionВерсия модели снапшота (1).
ScanCountintДаentity.ScanList.Items.CountЧисло сканов, приложенных к заявке.
SubmittedAtDateTimeOffset?Да (после Submit)entity.SubmittedAtМомент постановки в очередь ревью.
VersionlongДаentity.VersionВерсия агрегата (optimistic concurrency).

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (UserId из токена); tenant не применяется. Повторный запрос с тем же Idempotency-Key возвращает ранее сохранённый результат без повторной сборки снапшота.
  2. Проверка предусловий. Заявка requestId существует и принадлежит владельцу; иначе PROFILE_VERIFICATION_NOT_FOUND (record-level, чужая не раскрывается). Статус заявки — Draft; иначе PROFILE_VERIFICATION_NOT_EDITABLE (INV-VRF-7). Профиль владельца активен (Status = Active, иначе PROFILE_USER_BLOCKED, INV-PRF-3), в режиме самоуправления (ContactsManagedBy = SelfService, иначе PROFILE_MANAGED_EXTERNALLY, INV-PRF-2), тип пользователя — Investor (иначе PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE, INV-VRF-5).
  3. Что читаем. Персона Self (request.PersonId) с её документами — нужна для сборки снапшота; проверки полноты выполняются в памяти.
  4. Бизнес-правила и сборка снапшота.
    • расшифровка персоны: ФИО и BirthDate расшифровываются ключом версии записи персоны;
    • полнота (INV-VRF-2): ФИО и BirthDate заполнены; среди активных документов есть паспортный (Type ∈ {RfPassport, RbPassport, KzPassport, CisPassport, ForeignPassport}); суммарно ≥ 1 скана (сканы документов снапшота + сканы заявки) — иначе PROFILE_VERIFICATION_INCOMPLETE;
    • возраст (INV-VRF-4): полных лет по BirthDate на текущий момент ≥ 18 — иначе PROFILE_VERIFICATION_UNDERAGE;
    • сборка VerificationSnapshotModel (ContractVersion = 1): PersonId, PersonVersion (сверка «данные не менялись»), FirstName/LastName/MiddleName, BirthDate, CitizenshipCountryId; IdentityDocument = снапшот паспортного документа (DocumentId, Type, CountryId, CountryIsoCode2 — юридический след, PayloadContractVersion, расшифрованный Payload, сканы; при нескольких — самый свежий по IssueDate); AdditionalDocuments — прочие активные документы (≤ 10, каждый как снапшот);
    • шифрование: снапшот сериализуется и шифруется одним текущим ключом; SnapshotContractVersion = 1, KeyVersion фиксируется (INV-VRF-7, снапшот иммутабелен далее); недоступность ключей — fail-closed PROFILE_CRYPTO_UNAVAILABLE;
    • переход: Draft → Submitted; SubmittedAt = now; UpdatedByUserId = UserId; Version++; фиксируется факт VerificationRequestSubmittedEvent. Денормализация профиля (INV-PRF-6): Profile.VerificationStatus NotVerified|Rejected → Pending.
  5. Согласованность. В одном согласованном изменении сохраняются заявка (Submitted, снапшот), профиль (Pending), интеграционное событие и запись идемпотентности; конкурентный Submit/Revoke разрешается конфликтом версий → PROFILE_CONCURRENCY_CONFLICT.
  6. Событие. Публикуется VerificationRequestSubmittedKafkaEvent (ContractVersion = 1, topic pin.profile.verification-requests.v1, key = OwnerUserId; payload без PII: Id, OwnerUserId, SubmittedAt) — контракт verification-request-submitted.md.
  7. Результат. Модель VerificationRequestModel (проекция агрегата, без снапшота); HTTP 200.

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияVerificationRequestSubmittedKafkaEvent / pin.profile.verification-requests.v1 / key = OwnerUserId (без PII)
Внешние вызовыпровайдер ключей шифрования: расшифровка персоны/документов + шифрование снапшота; недоступность → PROFILE_CRYPTO_UNAVAILABLE (fail-closed)
Проекцииденормализация Profile.VerificationStatus = Pending (та же транзакция)
Уведомленияопосредованно через событие: notifications (подтверждение инвестору, сигнал очереди ревью), support (тикет ревью)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400requestId пустойперечень невалидных полейno retry
PROFILE_VERIFICATION_NOT_FOUND404Заявка не найдена / чужая (record-level)«Заявка не найдена»no retry
PROFILE_VERIFICATION_NOT_EDITABLE409Status != Draft (INV-VRF-7)«Заявка уже отправлена»перечитать статус
PROFILE_VERIFICATION_INCOMPLETE400Нет ФИО/даты рождения, активного паспортного документа или ни одного скана (INV-VRF-2)«Заполните данные и приложите сканы»no retry
PROFILE_VERIFICATION_UNDERAGE400Возраст по BirthDate < 18 на момент Submit (INV-VRF-4)«Верификация доступна с 18 лет»no retry
PROFILE_USER_BLOCKED400Profile.Status != Active (INV-PRF-3)«Учётная запись заблокирована»no retry
PROFILE_MANAGED_EXTERNALLY403ContactsManagedBy = TalentSync (INV-PRF-2)«Данные управляются внешней системой»no retry
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403UserType != Investor (INV-VRF-5)«Доступно только инвесторам»no retry
PROFILE_CONCURRENCY_CONFLICT409Гонка Submit/Revoke (конфликт версий)«Повторите попытку»retry (перечитка)
PROFILE_CRYPTO_UNAVAILABLE503Ключи шифрования недоступны«Сервис временно недоступен»retry

Совместимость и наблюдаемость

  • VerificationSnapshotModel/VerificationRequestSubmittedKafkaEvent — versioned (ContractVersion = 1); изменения снапшота — additive (новые nullable-поля с bump SnapshotContractVersion); breaking — новый топик .v2 (дом контракта — событие). Значения PII не попадают в ответ, логи, метрики и шину.
  • Logs: UserId, requestId, PersonId, переход Draft→Submitted (без ФИО/дат/номеров документов); metrics: profile_api_operations_total{operation="submitVerification",result}, profile_verification_submitted_total, profile_crypto_errors_total; traces — OTel; dashboard profile-overview; alert «заявки в Submitted старше 48 ч».

Acceptance criteria

  • Дано черновик владельца в Draft, у персоны Self заполнены ФИО и дата рождения (возраст ≥ 18), есть активный паспортный документ и суммарно ≥ 1 скана; когда submit; тогда собирается и шифруется одним KeyVersion иммутабельный снапшот (SnapshotContractVersion = 1), статус → Submitted, SubmittedAt = now, Profile.VerificationStatusPending, публикуется VerificationRequestSubmittedKafkaEvent (без PII, key = OwnerUserId), HTTP 200 с HasSnapshot = true.
  • Дано нет паспортного документа / нет ФИО или даты рождения / ни одного скана (INV-VRF-2); когда submit; тогда PROFILE_VERIFICATION_INCOMPLETE (400): статус остаётся Draft, снапшот не создан, событий нет.
  • Дано возраст персоны по BirthDate < 18 на момент submit (INV-VRF-4); когда submit; тогда PROFILE_VERIFICATION_UNDERAGE (400).
  • Дано заявка не в Draft (уже Submitted/InReview/терминальная, INV-VRF-7); когда submit; тогда PROFILE_VERIFICATION_NOT_EDITABLE (409).
  • Дано провайдер ключей шифрования недоступен; когда submit; тогда fail-closed PROFILE_CRYPTO_UNAVAILABLE (503): заявка остаётся Draft, событий нет (retry позже).
  • Дано тот же Idempotency-Key повторяется после успеха; когда submit; тогда возвращается ранее сохранённый результат без повторной сборки снапшота и без повторной публикации события.
  • Дано конкурентные submit/revoke на ту же заявку; когда оба коммитятся; тогда проигравший получает PROFILE_CONCURRENCY_CONFLICT (409).
  • Дано заявка отправлена; когда проходит окно eventual-consistency; тогда асинхронно по одному событию (строгий порядок per-user): notifications шлёт подтверждение инвестору и сигнал в очередь ревью, support заводит тикет ревью — до этого инвестор видит заявку в Submitted со снапшотом.

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

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

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