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 area | admin (сотрудники ПИН) |
| Feature group | verification |
| Статус | draft |
| Документ | docs/06-services/profile/api/admin/take-verification-in-review.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | Operator(Admin) — ревьюер ПИН (Bearer JWT); не инвестор |
| Auth policy / Permissions | authenticated; permission profile.verification.review (INV-VRF-6/8, permission.md) |
| Scope (RBAC) | платформенный (вне tenant): доступ ко всем заявкам очереди ревью |
| Record-level | не применяется (review — платформенный доступ; OwnerUserId заявки не ограничивает ревьюера) |
| Tenant isolation | не применяется (ADR-0052: данные инвестора вне tenant) |
| Audit | update (переход Submitted → InReview): actor-stamping UpdatedByUserId = ReviewerUserId, Version++, доменное событие VerificationRequestTakenInReviewEvent. Снапшот не расшифровывается — события VerificationSnapshotAccessedEvent здесь нет |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/admin/profile/verification/{requestId}/take |
| Success status | 200 |
| Idempotency header | не применяется (естественная защита статусным guard-ом: повтор при Status != Submitted → 409) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ReviewerUserId | identity claims (sub) | Guid | Да | валидный JWT сотрудника; NotEmpty; проставляется handler-ом, из body не принимается | 401 без токена; 403 без profile.verification.review |
requestId | route | Guid | Да | NotEmpty; существующая заявка (шаг 3) | — |
Модель ответа VerificationRequestModel
Admin-проекция заявки (метаданные + статус + вехи ревью, без расшифрованного снапшота). Дом полей —
verification-request.md; здесь только дельта-проекция. Та же модель —
элемент постраничного списка listVerificationQueue.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
id | Guid | Да | VerificationRequestEntity.Id | — | Идентификатор заявки. |
ownerUserId | Guid | Да | VerificationRequestEntity.OwnerUserId | — | Верифицируемый инвестор. |
personId | Guid | Да | VerificationRequestEntity.PersonId | — | Персона-предмет проверки (Relation = Self). |
requisiteTemplateId | Guid? | Нет | VerificationRequestEntity.RequisiteTemplateId | — | Шаблон предзаполнения (информационно). |
status | VerificationRequestStatus | Да | VerificationRequestEntity.Status | — | После операции = InReview (enum строкой). |
submittedAt | DateTimeOffset? | Нет | VerificationRequestEntity.SubmittedAt | — | Момент постановки в очередь. |
reviewStartedAt | DateTimeOffset? | Нет | VerificationRequestEntity.ReviewStartedAt | — | Проставлен этой операцией (= now). |
reviewerUserId | Guid? | Нет | VerificationRequestEntity.ReviewerUserId | — | = ReviewerUserId актора после взятия. |
completedAt | DateTimeOffset? | Нет | VerificationRequestEntity.CompletedAt | — | null до терминального решения. |
rejectionCode | VerificationRejectionCode? | Нет | VerificationRequestEntity.RejectionCode | — | null на InReview. |
rejectionReason | string? | Нет | VerificationRequestEntity.RejectionReason | — | null на InReview. |
decisionComment | string? | Нет | VerificationRequestEntity.DecisionComment | видно только ревьюеру | Внутренний комментарий; инвестору не отдаётся. |
scanCount | int | Да | VerificationRequestEntity.ScanList.Items.Count | — | Число сканов заявки (метаданные, без содержимого). |
version | long | Да | VerificationRequestEntity.Version | — | Для оптимистичного контроля на UI. |
Алгоритм
- Контекст и доступ. Операцию выполняет ревьюер ПИН (
ReviewerUserId=sub) с permissionprofile.verification.review(нет → 403). Если feature flagprofile.verification.enabledвыключен →PROFILE_VERIFICATION_DISABLED(404). Tenant не применяется (ADR-0052). - Проверка входа.
requestIdиReviewerUserIdне пустые — иначеValidationError. - Что читаем и проверяем. Заявка должна существовать — иначе
PROFILE_VERIFICATION_NOT_FOUND(404). Заявка должна быть в статусеSubmitted— иначеPROFILE_VERIFICATION_ALREADY_TAKEN(409, INV-VRF-6: уже взята другим ревьюером либо отозвана/решена). Снапшот не расшифровывается — PII не читается. - Переход. Заявка переходит
Submitted → InReview:Status = InReview,ReviewStartedAt = now,ReviewerUserId= актор,UpdatedByUserId= актор,UpdatedAt = now. Следствие: инвестор больше не можетRevoke(допустим только изSubmitted, INV-VRF-7). - Согласованность. Взятие фиксируется атомарно под оптимистичной блокировкой. Гонка ревьюеров: если
двое прошли проверку статуса одновременно, проигравший при фиксации получает
PROFILE_CONCURRENCY_CONFLICT(409); более частый случай «уже взято» перехватывается раньше на шаге 3 (PROFILE_VERIFICATION_ALREADY_TAKEN). - События. Интеграционных (Kafka) событий нет: взятие в ревью — внутренняя операция очереди, наружу
не публикуется (инвестор уведомляется только на терминальном решении —
VerificationCompletedKafkaEvent, verification-completed.md). - Результат. Admin-проекция заявки (
status = InReview,reviewStartedAt,reviewerUserId; снапшот не расшифровывается). HTTP 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (взятие в ревью наружу не публикуется) |
| External calls | none (снапшот не расшифровывается — PII не читается) |
| Cache / projections / search | none (очередь listVerificationQueue перечитывается по Status/SubmittedAt) |
| Notifications | none напрямую (инвестору — только на терминальном решении) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | Нет/просрочен JWT | без тела (ветка Other) | re-login |
ForbiddenError | 403 | Нет permission profile.verification.review | «Недостаточно прав» | no retry |
ValidationError | 400 | Пустой requestId/ReviewerUserId | RestApiValidationErrorResponse | no retry |
PROFILE_VERIFICATION_DISABLED | 404 | Feature flag profile.verification.enabled выключен | «Раздел верификации недоступен» | no retry |
PROFILE_VERIFICATION_NOT_FOUND | 404 | Заявка requestId не существует | «Заявка не найдена» | no retry |
PROFILE_VERIFICATION_ALREADY_TAKEN | 409 | Status != Submitted (взята другим ревьюером / отозвана / решена) — INV-VRF-6 | «Заявка уже в работе» | no retry (обновить очередь) |
PROFILE_CONCURRENCY_CONFLICT | 409 | Гонка версии агрегата — оба ревьюера прошли проверку статуса одновременно | «Повторите попытку» | 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. Dashboardprofile-overview; runbook — разбор всплеска конфликтов взятия / застрявших вInReviewзаявок.
Acceptance Criteria
- Happy path. Given заявка
Submitted, ревьюер сprofile.verification.review; When take; Then200,Submitted → InReview(ReviewerUserId,ReviewStartedAt = now); интеграционных событий нет. - Отзыв недоступен после взятия. Given заявка стала
InReview; When инвестор пытаетсяRevoke; Then отказ — Revoke допустим только изSubmitted(INV-VRF-7). - Уже взята/решена. Given заявка не
Submitted(взята другим / отозвана / решена); When take; Then409 PROFILE_VERIFICATION_ALREADY_TAKEN(INV-VRF-6). - Гонка ревьюеров. Given двое прошли проверку статуса одновременно; When оба берут; Then один —
200, второй —409 PROFILE_CONCURRENCY_CONFLICT. - Нет прав. Given актор без
profile.verification.review; When take; Then403. - Флаг выключен. Given
profile.verification.enabled = off; When take; Then404 PROFILE_VERIFICATION_DISABLED. - Нет заявки. Given несуществующий
requestId; When take; Then404 PROFILE_VERIFICATION_NOT_FOUND.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): GET /api/v1/admin/profile/verification/{requestId}/snapshot — Аудируемый просмотр снапшота KYC (viewVerificationSnapshot), POST /api/v1/profile/verification/{requestId}/revoke — Отзыв заявки KYC на доработку (revokeVerification), Profile API — реестр операций областей public / internal / admin
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: 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).