PUT /api/v1/profile/verification/{requestId} — Правка черновика заявки KYC (updateVerificationDraft)
Назначение
Инвестор дорабатывает черновик заявки на верификацию до её отправки: прикрепляет к заявке сканы и, при
необходимости, переустанавливает ссылку на шаблон реквизитов. Правки допустимы только в статусе Draft
(INV-VRF-7, verification-request.md); отправленную, взятую в ревью
или завершённую заявку можно доработать только отозвав её обратно в Draft
(revoke-verification.md).
Сами ФИО / дату рождения / документы персоны этот метод не меняет — они правятся через операции персон
и документов (update-person.md, update-person-document.md);
заявка ссылается на живую активную персону Relation = Self (INV-VRF-3, единственная — INV-PRS-1), а
иммутабельный зашифрованный снапшот строится позже, в момент отправки
(submit-verification.md). Поэтому персона-предмет здесь отдельно не
переуказывается.
| Поле | Значение |
|---|
| Сервис/контекст | profile |
| API area | public |
| Feature group | verification |
| Статус | draft |
| Документ | docs/06-services/profile/api/public/update-verification-draft.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Bearer JWT), UserType = Investor |
| Auth policy / Permissions | authenticated; permission profile.verification.submit (INV-VRF-5) |
| Scope (RBAC) | self |
| Record-level | OwnerUserId = UserId (sub); чужая заявка → 404 (маскируется под «не найдено») |
| Tenant isolation | не применяется (ADR-0052 — инвестор вне tenant) |
| Audit | update: фиксируется автор изменения, растёт версия заявки; доменное событие VerificationRequestDraftUpdatedEvent (имена полей); сканы — untrusted input (whitelist + сверка со storage) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | PUT |
| Route | /api/v1/profile/verification/{requestId} |
| Success status | 200 |
| Idempotency header | не применяется (PUT идемпотентен по значению: ScanList заменяется целиком) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса (body UpdateVerificationDraftForm)
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
requestId | route | Guid | Да | NotEmpty; заявка владельца в Status = Draft | чужая → 404 |
scanList | body | DocumentScanList | Да (может быть пустым) | полная замена набора сканов заявки; Items ≤ 10, каждый: fileId (storage), fileName 1–256 без path-компонентов, contentType из whitelist (image/jpeg,image/png,image/webp,application/pdf), sizeBytes 1–20 971 520 (INV-DOC-5 аналог) | untrusted — сверка со storage |
requisiteTemplateId | body | Guid? | Нет | если задан — существующий Active-шаблон владельца, PersonId = PersonId заявки; null → ссылка очищается | — |
version | body | long | Да | = текущей VerificationRequestEntity.Version (optimistic) | — |
DocumentScanList — дом-модель person-document.md
(здесь без переописания полей). Файл предварительно загружается клиентом в сервис storage
(двухфазная схема, FileId; контракт storage — вне этого документа,
prototype-mapping.md).
Модель ответа VerificationRequestModel
Дом модели — get-my-verification.md (будущий по конвенции); ниже — проекция,
возвращаемая после правки (без PII — снапшота ещё нет).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
id | Guid | Да | VerificationRequestEntity.Id | — | Идентификатор заявки. |
status | VerificationRequestStatus (string) | Да | entity | — | Draft (после правки статус не меняется). |
personId | Guid | Да | entity | — | Персона-предмет (Self); здесь не меняется. |
requisiteTemplateId | Guid? | Нет | entity | — | Информационная ссылка на шаблон (состав фиксируется снапшотом на Submit). |
scans | List<DocumentScan> | Да | ScanList.Items | метаданные | Приложенные к заявке сканы (скачивание — presigned-ссылка storage). |
isReadyToSubmit | bool | Да | computed | — | Предпросмотр INV-VRF-2: у Self заполнены ФИО+дата рождения, есть активный паспорт, суммарно ≥ 1 скана. |
createdAt / updatedAt | DateTimeOffset | Да | entity | — | Аудит-таймстемпы. |
version | long | Да | entity | — | Новая Version (для следующей optimistic-правки/Submit). |
Алгоритм
- Контекст и доступ. Операцию выполняет авторизованный инвестор (владелец заявки,
UserId из
токена); вне tenant (ADR-0052); требуется permission profile.verification.submit.
- Проверка входа.
requestId не пуст; scanList — ≤ 10 элементов, contentType из whitelist,
sizeBytes в допустимом диапазоне, fileName 1–256 без /,\ (INV-DOC-5 аналог);
requisiteTemplateId, если присутствует, — непустой Guid. При нарушении — ValidationError.
- Загрузка и предусловия. Найти профиль владельца; при отсутствии —
PROFILE_NOT_FOUND. Проверить
гварды профиля (INV-PRF-2..4, INV-VRF-5): неактивная учётка → PROFILE_USER_BLOCKED, внешнее
управление контактами → PROFILE_MANAGED_EXTERNALLY, не инвестор / нет права →
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE. Найти заявку; если она не принадлежит владельцу —
PROFILE_VERIFICATION_NOT_FOUND; если статус не Draft — PROFILE_VERIFICATION_NOT_EDITABLE
(INV-VRF-7); если версия из запроса не совпадает — PROFILE_CONCURRENCY_CONFLICT. Если задан
requisiteTemplateId: шаблон должен существовать, принадлежать владельцу и ссылаться на ту же персону,
что и заявка; иначе — PROFILE_VERIFICATION_TEMPLATE_NOT_FOUND. Каждый скан сверяется с сервисом
storage (таймаут 5 с, без повтора) — файл существует, принадлежит владельцу, размер и тип совпадают;
иначе — PROFILE_SCAN_FILE_NOT_FOUND; недоступность storage — PROFILE_STORAGE_UNAVAILABLE.
- Бизнес-правила и маппинг. Переход
Draft → Draft (INV-VRF-7 разрешает правки только в Draft).
Набор сканов заявки заменяется целиком (DocumentScanList, ContractVersion = 1); ссылка на шаблон
реквизитов устанавливается или очищается; персона-предмет не меняется (живая Self, INV-VRF-3);
снапшот остаётся пустым — он строится только при отправке (INV-VRF-7). Растёт версия заявки, фиксируется
автор правки; доменное событие VerificationRequestDraftUpdatedEvent (имена изменённых полей).
- Что меняется. Сканы, ссылка на шаблон и версия/аудит заявки обновляются в одном согласованном
изменении; интеграционных событий нет. Оптимистическая блокировка страхует гонку «правка черновика vs
отправка/отзыв».
- События. Только доменное
VerificationRequestDraftUpdatedEvent (в процессе при коммите).
Интеграционных Kafka-событий нет — черновик не покидает контекст до отправки (тогда публикуется
VerificationRequestSubmittedKafkaEvent, verification-request.md).
- Результат. Обновлённый черновик (
Draft, scans, requisiteTemplateId, isReadyToSubmit, новая
version), HTTP 200. Снапшота ещё нет (появится при отправке).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Интеграционные события | нет (Kafka не публикуется до отправки заявки) |
| Внешние вызовы | сверка каждого скана с сервисом storage — таймаут 5 с, без повтора; недоступность → PROFILE_STORAGE_UNAVAILABLE |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
| — (auth) | 401 | Нет/просрочен JWT | без тела | re-login |
ValidationError | 400 | scanList (лимит/whitelist/размер/имя) / requisiteTemplateId | RestApiValidationErrorResponse | no retry |
PROFILE_NOT_FOUND | 404 | Профиль не создан | «Профиль ещё создаётся» | retry |
PROFILE_USER_BLOCKED | 400 | Profile.Status != Active | «Учётная запись заблокирована» | no retry |
PROFILE_MANAGED_EXTERNALLY | 403 | ContactsManagedBy = TalentSync | «Данные управляются внешней системой» | no retry |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | не Investor / нет permission (INV-VRF-5) | «Доступно только инвесторам» | no retry |
PROFILE_VERIFICATION_NOT_FOUND | 404 | Нет заявки / чужая (record-level) | «Заявка не найдена» | no retry |
PROFILE_VERIFICATION_NOT_EDITABLE | 409 | Status != Draft (INV-VRF-7) | «Заявку нельзя редактировать в текущем статусе» | no retry |
PROFILE_VERIFICATION_TEMPLATE_NOT_FOUND | 404 | Шаблон не найден / чужой / персона не совпадает | «Шаблон реквизитов не найден» | no retry |
PROFILE_SCAN_FILE_NOT_FOUND | 400 | Файл не найден в storage / чужой / метаданные не совпали | «Файл не найден — загрузите заново» | повторная загрузка |
PROFILE_STORAGE_UNAVAILABLE | 502 | storage недоступен | «Хранилище временно недоступно» | retry |
PROFILE_CONCURRENCY_CONFLICT | 409 | Гонка версий (version) | «Данные изменены, повторите» | retry |
Совместимость и наблюдаемость
scanList — полная замена набора (PUT); модель DocumentScanList versioned (ContractVersion, дом
person-document.md); whitelist contentType расширяется только additive.
- Правки черновика Kafka не публикуют — потребители видят заявку только с
Submit (совместимость событий —
verification-request-submitted.md).
- Logs:
UserId, RequestId, число сканов, changedFields (без значений PII и имён файлов); metrics:
profile_api_operations_total{operation="updateVerificationDraft",result},
profile_storage_calls_total{operation,result}; traces — OTel; dashboard profile-overview.
Acceptance criteria
- Дано заявка владельца в
Draft с верной version, все сканы существуют в storage и принадлежат владельцу; когда PUT; тогда ScanList заменяется целиком, ссылка на шаблон устанавливается/очищается, персона-предмет не меняется, снапшот остаётся пустым, растёт версия, поднято VerificationRequestDraftUpdatedEvent; Kafka не публикуется; HTTP 200 с рассчитанным isReadyToSubmit.
- Дано
scanList превышает лимит / нарушает whitelist contentType / размер / имя файла; когда PUT; тогда ValidationError (400).
- Дано заявка не в
Draft (INV-VRF-7); когда PUT; тогда PROFILE_VERIFICATION_NOT_EDITABLE (409) — доработка возможна только после revoke в Draft.
- Дано
requisiteTemplateId чужой / архивный / ссылается на другую персону, чем заявка; когда PUT; тогда PROFILE_VERIFICATION_TEMPLATE_NOT_FOUND (404).
- Дано скан не найден в storage / чужой / метаданные не совпали; когда PUT; тогда
PROFILE_SCAN_FILE_NOT_FOUND (400) — загрузить заново.
- Дано сервис storage недоступен; когда PUT; тогда
PROFILE_STORAGE_UNAVAILABLE (502, retry).
- Дано
version из запроса не совпадает (гонка «правка vs submit/revoke»); когда PUT; тогда PROFILE_CONCURRENCY_CONFLICT (409).
- Дано черновик изменён; когда проверяется поток событий; тогда интеграционных Kafka-событий нет — потребители увидят заявку только после
Submit.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы