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

POST /api/v1/admin/profile/verification/{requestId}/approve — Одобрить заявку KYC (approveVerification)

Назначение

Ревьюер ПИН одобряет взятую в работу заявку на верификацию/квалификацию инвестора (KYC/AML): терминальный переход InReview → Approved (INV-VRF-8 DecisionByReviewer, verification-request.md). В том же согласованном изменении аккаунт становится верифицированным — денормализованный Profile.VerificationStatus: Pending → Verified (INV-PRF-6, profile.md), а факт решения транслируется наружу событием VerificationCompletedKafkaEvent (топик pin.profile.verification-requests.v1): notifications уведомляет инвестора, crm/catalog показывают бейдж «верифицирован». Расшифрованный снапшот на этой операции не читается — решение принято ранее на аудируемом просмотре viewVerificationSnapshot. Операция за feature flag profile.verification.enabled.

Metadata

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

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

ПроверкаЗначение
Actor typeOperator(Admin) — reviewer (сотрудник ПИН, Bearer JWT)
Auth policy / Permissionsauthenticated; permission profile.verification.review (нет permission → 403)
Scope (RBAC)платформенный (админ, вне tenant)
Record-levelтолько actor == VerificationRequestEntity.ReviewerUserId или супервизор заботы с тем же permission (INV-VRF-8); иначе PROFILE_VERIFICATION_NOT_REVIEWER
Tenant isolationне применяется (ADR-0052: данные инвестора вне tenant; заявка не tenanted)
Auditupdate: решение Approved, CompletedAt, DecisionComment; actor-stamping UpdatedByUserId = actor, Version++; доменное событие VerificationRequestDecidedEvent. KYC-записи не удаляются (аудит-история решений)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/admin/profile/verification/{requestId}/approve
Success status200
Idempotency headerне применяется (терминальный переход; повтор на уже Approved → 409 PROFILE_VERIFICATION_INVALID_STATE)
CorrelationOpenTelemetry trace (traceparent)

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ReviewerUserIdidentity claims (sub)GuidДавалидный JWT; permission profile.verification.review401 без токена; 403 без permission
requestIdrouteGuidДаNotEmpty; существующая заявка в InReviewчужая по ReviewerUserId → 403
DecisionCommentbodystring?Нет≤ 1024; внутренний комментарий ревьюера, инвестору не показывается; не цитировать значения документовInternal: наружу и в Kafka не публикуется

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

Admin-проекция заявки (общая для операций review-очереди: take/approve/reject). Дом полей — verification-request.md; здесь только дельта без чувствительных значений снапшота (расшифровка — отдельная аудируемая операция viewVerificationSnapshot).

ПолеТипОбязательностьИсточникMaskingОписание
idGuidДаVerificationRequestEntity.IdИдентификатор заявки.
ownerUserIdGuidДаVerificationRequestEntity.OwnerUserIdВерифицированная учётная запись (инвестор).
personIdGuidДаVerificationRequestEntity.PersonIdПерсона-предмет проверки (Relation = Self).
requisiteTemplateIdGuid?НетVerificationRequestEntity.RequisiteTemplateIdШаблон предзаполнения; информационно.
statusVerificationRequestStatusДаVerificationRequestEntity.StatusПосле операции — Approved (терминальный).
submittedAtDateTimeOffset?НетVerificationRequestEntity.SubmittedAtМомент отправки в очередь.
reviewStartedAtDateTimeOffset?НетVerificationRequestEntity.ReviewStartedAtМомент взятия в ревью.
reviewerUserIdGuid?НетVerificationRequestEntity.ReviewerUserIdРевьюер, принявший решение.
completedAtDateTimeOffset?НетVerificationRequestEntity.CompletedAtМомент решения (= now).
decisionCommentstring?НетVerificationRequestEntity.DecisionCommentadmin-onlyВнутренний комментарий; отдаётся только в admin-контур, инвестору — нет.
scanCountintДаVerificationRequestEntity.ScanList.Items.CountКол-во сканов заявки (метаданные).
createdAtDateTimeOffsetДаVerificationRequestEntity.CreatedAtСоздание черновика.
versionlongДаVerificationRequestEntity.VersionВерсия агрегата после мутации.

Алгоритм

  1. Контекст и доступ. Операцию выполняет ревьюер ПИН (ReviewerUserId = sub) с permission profile.verification.review; без permission — 403. Если feature flag profile.verification.enabled выключен → PROFILE_VERIFICATION_DISABLED (404). Tenant не применяется (ADR-0052).
  2. Проверка входа и состояния. requestId не пустой, DecisionComment ≤ 1024 — иначе ValidationError. Заявка должна существовать — иначе PROFILE_VERIFICATION_NOT_FOUND (404). Заявка должна быть в статусе InReview — иначе PROFILE_VERIFICATION_INVALID_STATE (409: Submitted ещё не взята, Approved/Rejected уже терминальны). Решение принимает только назначенный ревьюер (ReviewerUserId заявки) или супервизор заботы с тем же permission — иначе PROFILE_VERIFICATION_NOT_REVIEWER (403, INV-VRF-8).
  3. Что читаем. Заявку — как предмет решения; профиль-владелец (OwnerUserId) — для денормализации статуса (владелец существует всегда: профиль создаётся раньше заявки). Снапшот (SnapshotCipher) не расшифровывается — PII на этой операции не читается (решение принято ранее на viewVerificationSnapshot).
  4. Переход и денормализация. Заявка переходит InReview → Approved (guard INV-VRF-8: актор — назначенный ревьюер/супервизор): Status = Approved, CompletedAt = now, DecisionComment = вход, актор фиксируется в UpdatedByUserId. В том же изменении профиль-владелец помечается верифицированным (INV-PRF-6): VerificationStatus: Pending → Verified, VerifiedAt = now, VerifiedRequestId = id заявки.
  5. Согласованность. Заявка, профиль и публикация события фиксируются в одном согласованном изменении (атомарно). Конкурентная правка (гонка ревьюеров или консюмера identity) отклоняется как PROFILE_CONCURRENCY_CONFLICT (409).
  6. События. Наружу публикуется VerificationCompletedKafkaEvent (топик pin.profile.verification-requests.v1, key OwnerUserId, AggregateType = "VerificationRequest"), payload { Id, OwnerUserId, Status = Approved, RejectionCode = null, CompletedAt }без PII (контракт — ../events/verification-completed.md). Потребители: notifications (уведомление инвестору), crm/catalog (бейдж «верифицирован»).
  7. Результат. Admin-проекция заявки (status = Approved, completedAt, reviewerUserId, decisionComment; снапшот не включён). HTTP 200.

Диаграмма

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

ТипДетали
Integration eventsVerificationCompletedKafkaEventpin.profile.verification-requests.v1 (key OwnerUserId, Status = Approved)
External callsnone (снапшот не расшифровывается — PII не читается)
Cache / projections / searchProfile.VerificationStatus → Verified (+VerifiedAt, VerifiedRequestId) в той же транзакции (INV-PRF-6)
Notificationsопосредованно: notifications — консюмер verification-completed (уведомление инвестору «вы верифицированы»)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
— (authz) / PROFILE_VERIFICATION_NOT_REVIEWER403Нет permission profile.verification.review, либо actor не назначенный ревьюер/не супервизор (INV-VRF-8)«Недостаточно прав для решения по заявке»no retry
ValidationError400Пустой requestId / DecisionComment > 1024RestApiValidationErrorResponseno retry
PROFILE_VERIFICATION_DISABLED404Feature flag profile.verification.enabled выключен«Раздел верификации недоступен»no retry
PROFILE_VERIFICATION_NOT_FOUND404Заявка по requestId не найдена«Заявка не найдена»no retry
PROFILE_VERIFICATION_INVALID_STATE409Status != InReview (не взята / уже терминальна)«Заявка не в статусе рассмотрения»no retry (перечитать очередь)
PROFILE_CONCURRENCY_CONFLICT409Гонка версии агрегата (xmin) заявки/профиля«Данные изменены, повторите»retry (перечитка + повтор)

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

  • Расширения VerificationRequestModel — additive (optional-поля без breaking change); удаление/переименование/смена типа — breaking (migration + rollout). Контракт Kafka-события — по ../events/verification-completed.md (ContractVersion, unknown-enum правила).
  • Logs: requestId, ownerUserId, reviewerUserId, decision=Approved (без DecisionComment и значений PII); metrics: profile_api_operations_total{operation="approveVerification",result}, profile_verification_decisions_total{decision="approved"}, latency p99; traces — OTel. Dashboard profile-overview; runbook — очередь ревью KYC (docs/06-services/profile/runbooks/, будущий путь по конвенции).

Acceptance Criteria

  • Happy path. Given заявка InReview, актор — назначенный ревьюер с profile.verification.review; When approve; Then 200, InReview → Approved (CompletedAt, DecisionComment), в том же изменении Profile.VerificationStatus: Pending → Verified (VerifiedAt, VerifiedRequestId, INV-PRF-6), опубликован VerificationCompletedKafkaEvent(Status = Approved, RejectionCode = null) без PII.
  • Не взята в ревью. Given заявка Submitted (ещё не взята); When approve; Then 409 PROFILE_VERIFICATION_INVALID_STATE; ни заявка, ни профиль не меняются, событие не публикуется.
  • Не тот ревьюер. Given заявка InReview, но актор не назначенный ревьюер и не супервизор заботы; When approve; Then 403 PROFILE_VERIFICATION_NOT_REVIEWER (INV-VRF-8).
  • Повторное одобрение. Given заявка уже Approved; When повтор; Then 409 PROFILE_VERIFICATION_INVALID_STATE; событие остаётся опубликованным ровно один раз (при первом решении).
  • Гонка ревьюеров. Given двое проходят проверку статуса одновременно; When оба фиксируют решение; Then один — 200, второй — 409 PROFILE_CONCURRENCY_CONFLICT; событие ровно одно.
  • Флаг выключен. Given profile.verification.enabled = off; When approve; Then 404 PROFILE_VERIFICATION_DISABLED.
  • Кросс-сервисная согласованность. Given approve зафиксирован; When crm/catalog ещё не потребили событие; Then internal getVerificationStatus(ownerUserId) уже отдаёт Verified синхронно (INV-PRF-6), а бейдж «верифицирован» в crm/catalog появляется в течение секунд после потребления VerificationCompletedKafkaEvent (окно eventual-consistency).

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

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

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