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

POST /api/v1/admin/profile/verification/{requestId}/take — Взять заявку в ревью (takeVerificationInReview)

Назначение

Ревьюер ПИН забирает заявку на верификацию/квалификацию инвестора (KYC/AML) из очереди (listVerificationQueue, FIFO по SubmittedAt) в персональную работу: перевод Submitted → InReview с закреплением ReviewerUserId, чтобы двое сотрудников не рассматривали одну заявку. Конкурентная гонка двух ревьюеров разрешается оптимистичной блокировкой: проигравший получает конфликт (INV-VRF-6, verification-request.md). После взятия инвестор больше не может отозвать заявку (Revoke — только из Submitted, INV-VRF-7). Расшифровка снапшота здесь не выполняется — это отдельная аудируемая операция viewVerificationSnapshot. Операция за feature flag profile.verification.enabled.

Metadata

ПолеЗначение
Сервис/контекстprofile
API areaadmin (сотрудники ПИН)
Feature groupverification
Статусdraft
Документdocs/06-services/profile/api/admin/take-verification-in-review.md

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

ПроверкаЗначение
Actor typeOperator(Admin) — ревьюер ПИН (Bearer JWT); не инвестор
Auth policy / Permissionsauthenticated; permission profile.verification.review (INV-VRF-6/8, permission.md)
Scope (RBAC)платформенный (вне tenant): доступ ко всем заявкам очереди ревью
Record-levelне применяется (review — платформенный доступ; OwnerUserId заявки не ограничивает ревьюера)
Tenant isolationне применяется (ADR-0052: данные инвестора вне tenant)
Auditupdate (переход Submitted → InReview): actor-stamping UpdatedByUserId = ReviewerUserId, Version++, доменное событие VerificationRequestTakenInReviewEvent. Снапшот не расшифровывается — события VerificationSnapshotAccessedEvent здесь нет

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/admin/profile/verification/{requestId}/take
Success status200
Idempotency headerне применяется (естественная защита статусным guard-ом: повтор при Status != Submitted → 409)
CorrelationOpenTelemetry trace (traceparent)

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ReviewerUserIdidentity claims (sub)GuidДавалидный JWT сотрудника; NotEmpty; проставляется handler-ом, из body не принимается401 без токена; 403 без profile.verification.review
requestIdrouteGuidДаNotEmpty; существующая заявка (шаг 3)

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

Admin-проекция заявки (метаданные + статус + вехи ревью, без расшифрованного снапшота). Дом полей — verification-request.md; здесь только дельта-проекция. Та же модель — элемент постраничного списка listVerificationQueue.

ПолеТипОбязательностьИсточникMaskingОписание
idGuidДаVerificationRequestEntity.IdИдентификатор заявки.
ownerUserIdGuidДаVerificationRequestEntity.OwnerUserIdВерифицируемый инвестор.
personIdGuidДаVerificationRequestEntity.PersonIdПерсона-предмет проверки (Relation = Self).
requisiteTemplateIdGuid?НетVerificationRequestEntity.RequisiteTemplateIdШаблон предзаполнения (информационно).
statusVerificationRequestStatusДаVerificationRequestEntity.StatusПосле операции = InReview (enum строкой).
submittedAtDateTimeOffset?НетVerificationRequestEntity.SubmittedAtМомент постановки в очередь.
reviewStartedAtDateTimeOffset?НетVerificationRequestEntity.ReviewStartedAtПроставлен этой операцией (= now).
reviewerUserIdGuid?НетVerificationRequestEntity.ReviewerUserId= ReviewerUserId актора после взятия.
completedAtDateTimeOffset?НетVerificationRequestEntity.CompletedAtnull до терминального решения.
rejectionCodeVerificationRejectionCode?НетVerificationRequestEntity.RejectionCodenull на InReview.
rejectionReasonstring?НетVerificationRequestEntity.RejectionReasonnull на InReview.
decisionCommentstring?НетVerificationRequestEntity.DecisionCommentвидно только ревьюеруВнутренний комментарий; инвестору не отдаётся.
scanCountintДаVerificationRequestEntity.ScanList.Items.CountЧисло сканов заявки (метаданные, без содержимого).
versionlongДаVerificationRequestEntity.VersionДля оптимистичного контроля на UI.

Алгоритм

  1. Контекст и доступ. Операцию выполняет ревьюер ПИН (ReviewerUserId = sub) с permission profile.verification.review (нет → 403). Если feature flag profile.verification.enabled выключен → PROFILE_VERIFICATION_DISABLED (404). Tenant не применяется (ADR-0052).
  2. Проверка входа. requestId и ReviewerUserId не пустые — иначе ValidationError.
  3. Что читаем и проверяем. Заявка должна существовать — иначе PROFILE_VERIFICATION_NOT_FOUND (404). Заявка должна быть в статусе Submitted — иначе PROFILE_VERIFICATION_ALREADY_TAKEN (409, INV-VRF-6: уже взята другим ревьюером либо отозвана/решена). Снапшот не расшифровывается — PII не читается.
  4. Переход. Заявка переходит Submitted → InReview: Status = InReview, ReviewStartedAt = now, ReviewerUserId = актор, UpdatedByUserId = актор, UpdatedAt = now. Следствие: инвестор больше не может Revoke (допустим только из Submitted, INV-VRF-7).
  5. Согласованность. Взятие фиксируется атомарно под оптимистичной блокировкой. Гонка ревьюеров: если двое прошли проверку статуса одновременно, проигравший при фиксации получает PROFILE_CONCURRENCY_CONFLICT (409); более частый случай «уже взято» перехватывается раньше на шаге 3 (PROFILE_VERIFICATION_ALREADY_TAKEN).
  6. События. Интеграционных (Kafka) событий нет: взятие в ревью — внутренняя операция очереди, наружу не публикуется (инвестор уведомляется только на терминальном решении — VerificationCompletedKafkaEvent, verification-completed.md).
  7. Результат. Admin-проекция заявки (status = InReview, reviewStartedAt, reviewerUserId; снапшот не расшифровывается). HTTP 200.

Диаграмма

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

ТипДетали
Integration eventsnone (взятие в ревью наружу не публикуется)
External callsnone (снапшот не расшифровывается — PII не читается)
Cache / projections / searchnone (очередь listVerificationQueue перечитывается по Status/SubmittedAt)
Notificationsnone напрямую (инвестору — только на терминальном решении)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез тела (ветка Other)re-login
ForbiddenError403Нет permission profile.verification.review«Недостаточно прав»no retry
ValidationError400Пустой requestId/ReviewerUserIdRestApiValidationErrorResponseno retry
PROFILE_VERIFICATION_DISABLED404Feature flag profile.verification.enabled выключен«Раздел верификации недоступен»no retry
PROFILE_VERIFICATION_NOT_FOUND404Заявка requestId не существует«Заявка не найдена»no retry
PROFILE_VERIFICATION_ALREADY_TAKEN409Status != Submitted (взята другим ревьюером / отозвана / решена) — INV-VRF-6«Заявка уже в работе»no retry (обновить очередь)
PROFILE_CONCURRENCY_CONFLICT409Гонка версии агрегата — оба ревьюера прошли проверку статуса одновременно«Повторите попытку»retry (перечитать очередь)

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

  • Новые optional поля VerificationRequestModel — additive, без breaking change; удаление/переименование/ смена типа — breaking (migration + rollout). Unknown значение status/rejectionCode у клиента — трактовать консервативно (кнопки решения скрыть).
  • Logs: ReviewerUserId, requestId, OwnerUserId, переход Submitted→InReview, результат (без значений PII/снапшота); metrics: profile_api_operations_total{operation="takeVerificationInReview",result}, profile_verification_take_conflicts_total{reason} (already_taken/row_version); traces — OTel. Dashboard profile-overview; runbook — разбор всплеска конфликтов взятия / застрявших в InReview заявок.

Acceptance Criteria

  • Happy path. Given заявка Submitted, ревьюер с profile.verification.review; When take; Then 200, Submitted → InReview (ReviewerUserId, ReviewStartedAt = now); интеграционных событий нет.
  • Отзыв недоступен после взятия. Given заявка стала InReview; When инвестор пытается Revoke; Then отказ — Revoke допустим только из Submitted (INV-VRF-7).
  • Уже взята/решена. Given заявка не Submitted (взята другим / отозвана / решена); When take; Then 409 PROFILE_VERIFICATION_ALREADY_TAKEN (INV-VRF-6).
  • Гонка ревьюеров. Given двое прошли проверку статуса одновременно; When оба берут; Then один — 200, второй — 409 PROFILE_CONCURRENCY_CONFLICT.
  • Нет прав. Given актор без profile.verification.review; When take; Then 403.
  • Флаг выключен. Given profile.verification.enabled = off; When take; Then 404 PROFILE_VERIFICATION_DISABLED.
  • Нет заявки. Given несуществующий requestId; When take; Then 404 PROFILE_VERIFICATION_NOT_FOUND.

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

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

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

  • Сущность: verification-request.md (машина состояний, INV-VRF-6/7, оптимистичная блокировка, ReviewerUserId/ReviewStartedAt).
  • Соседние admin-операции очереди ревью (будущие): list-verification-queue.md, view-verification-snapshot.md (аудируемая расшифровка), approve-verification.md, reject-verification.md.
  • Self-действия инвестора: get-my-verification.md (canRevoke гаснет после InReview), submit-verification.md, revoke-verification.md (будущие).
  • Сценарий: UC-PRF-003-submit-and-approve-verification.md; событие итога — ../events/verification-completed.md.
  • Permissions — permission.md (profile.verification.review).