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

POST /api/v1/profile/verification/{requestId}/revoke — Отзыв заявки KYC на доработку (revokeVerification)

Назначение

Инвестор отзывает отправленную, но ещё не взятую ревьюером заявку на верификацию (KYC/AML) обратно в черновик для доработки: переход Submitted → Draft. Иммутабельный снапшот, зафиксированный при отправке, обнуляется (доработка правит данные персоны, а снапшот собирается заново при повторном Submit — INV-VRF-7, stateDiagram verification-request.md). Из InReview отзыв запрещён — заявку уже взял в работу сотрудник ПИН (INV-VRF-6). В той же транзакции денормализованный Profile.VerificationStatus возвращается Pending → NotVerified (profile.md, INV-PRF-6). Отзыв — внутреннее состояние ЛК, поэтому в шину не публикуется. Вызывает инвестор из ЛК.

Metadata

ПолеЗначение
Сервис/контекстprofile
API areapublic
Feature groupverification
Статусdraft
Документdocs/06-services/profile/api/public/revoke-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: Status Submitted → Draft, снапшот обнулён, SubmittedAt = null, UpdatedByUserId = UserId, Version++; события VerificationRequestRevokedEvent + ProfileVerificationStatusChangedEvent; значения снапшота/PII в логи и события не попадают

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/profile/verification/{requestId}/revoke
Success status200
Idempotency headerне применяется (повтор для заявки уже в Draft — идемпотентный no-op 200)
Correlationсквозной trace через заголовок traceparent

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

Тело запроса отсутствует — операция полностью описывается маршрутом и токеном.

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

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

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаVerificationRequestEntity.IdИдентификатор заявки.
PersonIdGuidДаentity.PersonIdПерсона-предмет проверки (Relation = Self).
RequisiteTemplateIdGuid?Нетentity.RequisiteTemplateIdШаблон-источник предзаполнения (информационная ссылка).
StatusVerificationRequestStatus (enum строкой)Даentity.StatusПосле операции — Draft.
HasSnapshotboolДаcomputed (entity.SnapshotCipher != null)После отзыва — false (снапшот обнулён).
SnapshotContractVersionint?Нетentity.SnapshotContractVersionПосле отзыва — null.
ScanCountintДаentity.ScanList.Items.CountЧисло сканов заявки; при отзыве сохраняется (нужны для доработки).
SubmittedAtDateTimeOffset?Да (после отзыва — null)entity.SubmittedAtМомент последней постановки в очередь; обнуляется.
UpdatedAtDateTimeOffsetДаentity.UpdatedAtМомент отзыва.
VersionlongДаentity.VersionВерсия агрегата (optimistic concurrency).

Значения снапшота (SnapshotCipher/SnapshotContractVersion/KeyVersion) наружу не отдаются — шифротекст не выходит за периметр (README.md).

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (UserId из токена, permission profile.verification.submit); tenant не применяется (ADR-0052). Идемпотентность естественная (шаг 3).
  2. Проверка входа. requestId задан. Нарушение → ValidationError. Существование/состояние/ принадлежность проверяются на шаге 3.
  3. Проверка предусловий (сначала владелец для guard-ов и денормализации, затем заявка):
    • Профиль владельца существует; иначе PROFILE_NOT_FOUND (профиль ещё не создан консюмером pin.identity.users.v1, identity-users-sync.md). Профиль активен (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-PRF-4/INV-VRF-5).
    • Заявка requestId существует и принадлежит владельцу; иначе PROFILE_VERIFICATION_NOT_FOUND (record-level: чужая заявка неотличима от отсутствующей).
    • Допустимость состояния: Submitted — переход разрешён (шаг 4); Draft — идемпотентный no-op успех (снапшота нет, менять нечего → шаг 6); InReview/Approved/RejectedPROFILE_VERIFICATION_NOT_EDITABLE (INV-VRF-7: InReview уже взята ревьюером — INV-VRF-6; терминальные неизменяемы).
  4. Бизнес-правила и переходы. Заявка переходит Submitted → Draft; снапшот обнуляетсяSnapshotCipher = null, SnapshotContractVersion = null, KeyVersion = null, SubmittedAt = null; ScanList сохраняется (сканы заявки нужны для доработки); UpdatedByUserId = UserId, Version++; фиксируется факт VerificationRequestRevokedEvent. Денормализация профиля (INV-PRF-6): переход Profile.VerificationStatus Pending → NotVerified; фиксируется факт ProfileVerificationStatusChangedEvent(From: Pending, To: NotVerified).
  5. Согласованность. В одном согласованном изменении сохраняются заявка (Submitted → Draft, снапшот обнулён) и профиль (VerificationStatus = NotVerified); интеграционных событий нет. Гонка «Submit ↔ Revoke» и параллельный TakeInReview ревьюером разрешаются конфликтом версий: если ревьюер успел перевести заявку в InReview между проверкой и коммитом — PROFILE_CONCURRENCY_CONFLICT.
  6. Результат. Модель VerificationRequestModel — заявка Status = Draft, HasSnapshot = false, SubmittedAt = null, сохранённый ScanCount. HTTP 200. Далее инвестор дорабатывает данные (update-verification-draft.md) и повторно отправляет (submit-verification.md). Ранее отправленное событие VerificationRequestSubmittedKafkaEvent (pin.profile.verification-requests.v1) не отзывается (события иммутабельны), а из очереди ревью заявка исчезает автоматически (listVerificationQueue фильтрует Status = Submitted).

Диаграмма

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

ТипДетали
Проекцииденормализация Profile.VerificationStatus (Pending → NotVerified) в той же транзакции

Интеграционные события в шину не публикуются (отзыв внутренний; VerificationRequestSubmittedKafkaEvent, отправленный при Submit, не отзывается).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400Пустой requestIdперечень невалидных полейno retry
PROFILE_NOT_FOUND404Профиль ещё не создан консюмером identity (лаг реплики)«Профиль ещё создаётся»retry с backoff
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-PRF-4/INV-VRF-5)«Доступно только инвесторам»no retry
PROFILE_VERIFICATION_NOT_FOUND404Нет заявки / чужая (record-level)«Заявка не найдена»no retry
PROFILE_VERIFICATION_NOT_EDITABLE409Status не Submitted (InReview/Approved/Rejected, INV-VRF-6/7)«Заявку нельзя отозвать в текущем статусе»no retry
PROFILE_CONCURRENCY_CONFLICT409Гонка Submit ↔ Revoke / TakeInReview (конфликт версий)«Данные изменены, повторите»retry (перечитать статус)

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

  • Семантика отзыва стабильна и обратима: после доработки заявка снова отправляется (Draft → Submitted). Новые optional-поля VerificationRequestModel — additive; переименование/смена типа/удаление — breaking (migration + rollout). Значения PII не попадают в ответ, логи, метрики и шину.
  • Logs: UserId, RequestId, переход Submitted → Draft (без значений снапшота/PII); metrics: profile_api_operations_total{operation="revokeVerification",result}, profile_verification_revoked_total, latency p95; traces — OTel; dashboard profile-overview (очередь верификаций); runbook — docs/06-services/profile/runbooks/ (будущий путь по конвенции).

Acceptance criteria

  • Дано заявка владельца в Submitted со снапшотом; когда владелец вызывает revoke; тогда статус → Draft, SnapshotCipher/SnapshotContractVersion/KeyVersion/SubmittedAt обнулены, ScanList сохранён, Profile.VerificationStatusNotVerified, HTTP 200 с HasSnapshot = false; интеграционных событий нет.
  • Дано заявка уже в Draft; когда revoke повторяется; тогда идемпотентный no-op 200 (снапшота нет, менять нечего), версия не растёт.
  • Дано заявка в InReview (взята ревьюером, INV-VRF-6); когда владелец вызывает revoke; тогда отказ PROFILE_VERIFICATION_NOT_EDITABLE (409), статус не меняется.
  • Дано заявка в терминальном Approved/Rejected; когда revoke; тогда PROFILE_VERIFICATION_NOT_EDITABLE (409).
  • Дано requestId принадлежит другому пользователю; когда revoke; тогда PROFILE_VERIFICATION_NOT_FOUND (404) — чужая заявка неотличима от отсутствующей.
  • Дано ревьюер переводит заявку в InReview между проверкой и коммитом revoke; когда revoke коммитится; тогда PROFILE_CONCURRENCY_CONFLICT (409): выигрывает TakeInReview, владелец перечитывает статус.
  • Дано профиль заблокирован / управляется извне / не инвестор; когда revoke; тогда соответствующий guard-отказ (PROFILE_USER_BLOCKED / PROFILE_MANAGED_EXTERNALLY / PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE).
  • Дано заявка отозвана; когда очередь ревью перечитывается; тогда заявка исчезает из неё сразу (фильтр Status = Submitted), а ранее отправленное VerificationRequestSubmittedKafkaEvent не отзывается (события иммутабельны) — потребители очереди (support/notifications) согласуются по факту повторного Submit.

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

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

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