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). Отзыв — внутреннее состояние ЛК, поэтому в шину не публикуется. Вызывает инвестор из ЛК.
| Поле | Значение |
|---|
| Сервис/контекст | profile |
| API area | public |
| Feature group | verification |
| Статус | draft |
| Документ | docs/06-services/profile/api/public/revoke-verification.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Bearer JWT), UserType = Investor (INV-VRF-5) |
| Auth policy / Permissions | authenticated; permission profile.verification.submit |
| Scope (RBAC) | self |
| Record-level | VerificationRequestEntity.OwnerUserId = UserId из sub; из тела/маршрута владелец не принимается; чужой requestId → 404 |
| Tenant isolation | не применяется (ADR-0052: инвестор вне tenant) |
| Audit | update: Status Submitted → Draft, снапшот обнулён, SubmittedAt = null, UpdatedByUserId = UserId, Version++; события VerificationRequestRevokedEvent + ProfileVerificationStatusChangedEvent; значения снапшота/PII в логи и события не попадают |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/profile/verification/{requestId}/revoke |
| Success status | 200 |
| Idempotency header | не применяется (повтор для заявки уже в Draft — идемпотентный no-op 200) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Тело запроса отсутствует — операция полностью описывается маршрутом и токеном.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
requestId | route | Guid | Да | NotEmpty; заявка владельца в статусе Submitted | чужая → 404 |
Модель ответа VerificationRequestModel
Безопасная read-проекция агрегата (verification-request.md) — без
PII (шифротекст снапшота наружу не отдаётся, только флаг HasSnapshot; расшифровка — лишь ревьюером через
admin API). Дом модели — get-my-verification.md; приведён здесь для
самодостаточности. После отзыва возвращается заявка со Status = Draft.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | VerificationRequestEntity.Id | — | Идентификатор заявки. |
PersonId | Guid | Да | entity.PersonId | — | Персона-предмет проверки (Relation = Self). |
RequisiteTemplateId | Guid? | Нет | entity.RequisiteTemplateId | — | Шаблон-источник предзаполнения (информационная ссылка). |
Status | VerificationRequestStatus (enum строкой) | Да | entity.Status | — | После операции — Draft. |
HasSnapshot | bool | Да | computed (entity.SnapshotCipher != null) | — | После отзыва — false (снапшот обнулён). |
SnapshotContractVersion | int? | Нет | entity.SnapshotContractVersion | — | После отзыва — null. |
ScanCount | int | Да | entity.ScanList.Items.Count | — | Число сканов заявки; при отзыве сохраняется (нужны для доработки). |
SubmittedAt | DateTimeOffset? | Да (после отзыва — null) | entity.SubmittedAt | — | Момент последней постановки в очередь; обнуляется. |
UpdatedAt | DateTimeOffset | Да | entity.UpdatedAt | — | Момент отзыва. |
Version | long | Да | entity.Version | — | Версия агрегата (optimistic concurrency). |
Значения снапшота (SnapshotCipher/SnapshotContractVersion/KeyVersion) наружу не отдаются — шифротекст
не выходит за периметр (README.md).
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор (
UserId из токена, permission
profile.verification.submit); tenant не применяется (ADR-0052). Идемпотентность естественная (шаг 3).
- Проверка входа.
requestId задан. Нарушение → ValidationError. Существование/состояние/
принадлежность проверяются на шаге 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/Rejected →
PROFILE_VERIFICATION_NOT_EDITABLE (INV-VRF-7: InReview уже взята ревьюером — INV-VRF-6;
терминальные неизменяемы).
- Бизнес-правила и переходы. Заявка переходит
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).
- Согласованность. В одном согласованном изменении сохраняются заявка (
Submitted → Draft, снапшот
обнулён) и профиль (VerificationStatus = NotVerified); интеграционных событий нет. Гонка «Submit ↔
Revoke» и параллельный TakeInReview ревьюером разрешаются конфликтом версий: если ревьюер успел
перевести заявку в InReview между проверкой и коммитом — PROFILE_CONCURRENCY_CONFLICT.
- Результат. Модель
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 |
ValidationError | 400 | Пустой requestId | перечень невалидных полей | no retry |
PROFILE_NOT_FOUND | 404 | Профиль ещё не создан консюмером identity (лаг реплики) | «Профиль ещё создаётся» | retry с backoff |
PROFILE_USER_BLOCKED | 400 | Profile.Status != Active (INV-PRF-3) | «Учётная запись заблокирована» | no retry |
PROFILE_MANAGED_EXTERNALLY | 403 | ContactsManagedBy = TalentSync (INV-PRF-2) | «Данные управляются внешней системой» | no retry |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | UserType != Investor (INV-PRF-4/INV-VRF-5) | «Доступно только инвесторам» | no retry |
PROFILE_VERIFICATION_NOT_FOUND | 404 | Нет заявки / чужая (record-level) | «Заявка не найдена» | no retry |
PROFILE_VERIFICATION_NOT_EDITABLE | 409 | Status не Submitted (InReview/Approved/Rejected, INV-VRF-6/7) | «Заявку нельзя отозвать в текущем статусе» | no retry |
PROFILE_CONCURRENCY_CONFLICT | 409 | Гонка 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.VerificationStatus → NotVerified, 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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы