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

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). Поэтому персона-предмет здесь отдельно не переуказывается.

Metadata

ПолеЗначение
Сервис/контекстprofile
API areapublic
Feature groupverification
Статусdraft
Документdocs/06-services/profile/api/public/update-verification-draft.md

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

ПроверкаЗначение
Actor typeUser (Bearer JWT), UserType = Investor
Auth policy / Permissionsauthenticated; permission profile.verification.submit (INV-VRF-5)
Scope (RBAC)self
Record-levelOwnerUserId = UserId (sub); чужая заявка → 404 (маскируется под «не найдено»)
Tenant isolationне применяется (ADR-0052 — инвестор вне tenant)
Auditupdate: фиксируется автор изменения, растёт версия заявки; доменное событие VerificationRequestDraftUpdatedEvent (имена полей); сканы — untrusted input (whitelist + сверка со storage)

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/v1/profile/verification/{requestId}
Success status200
Idempotency headerне применяется (PUT идемпотентен по значению: ScanList заменяется целиком)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса (body UpdateVerificationDraftForm)

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
requestIdrouteGuidДаNotEmpty; заявка владельца в Status = Draftчужая → 404
scanListbodyDocumentScanListДа (может быть пустым)полная замена набора сканов заявки; 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
requisiteTemplateIdbodyGuid?Нетесли задан — существующий Active-шаблон владельца, PersonId = PersonId заявки; null → ссылка очищается
versionbodylongДа= текущей VerificationRequestEntity.Version (optimistic)

DocumentScanList — дом-модель person-document.md (здесь без переописания полей). Файл предварительно загружается клиентом в сервис storage (двухфазная схема, FileId; контракт storage — вне этого документа, prototype-mapping.md).

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

Дом модели — get-my-verification.md (будущий по конвенции); ниже — проекция, возвращаемая после правки (без PII — снапшота ещё нет).

ПолеТипОбязательностьИсточникMaskingОписание
idGuidДаVerificationRequestEntity.IdИдентификатор заявки.
statusVerificationRequestStatus (string)ДаentityDraft (после правки статус не меняется).
personIdGuidДаentityПерсона-предмет (Self); здесь не меняется.
requisiteTemplateIdGuid?НетentityИнформационная ссылка на шаблон (состав фиксируется снапшотом на Submit).
scansList<DocumentScan>ДаScanList.ItemsметаданныеПриложенные к заявке сканы (скачивание — presigned-ссылка storage).
isReadyToSubmitboolДаcomputedПредпросмотр INV-VRF-2: у Self заполнены ФИО+дата рождения, есть активный паспорт, суммарно ≥ 1 скана.
createdAt / updatedAtDateTimeOffsetДаentityАудит-таймстемпы.
versionlongДаentityНовая Version (для следующей optimistic-правки/Submit).

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный инвестор (владелец заявки, UserId из токена); вне tenant (ADR-0052); требуется permission profile.verification.submit.
  2. Проверка входа. requestId не пуст; scanList — ≤ 10 элементов, contentType из whitelist, sizeBytes в допустимом диапазоне, fileName 1–256 без /,\ (INV-DOC-5 аналог); requisiteTemplateId, если присутствует, — непустой Guid. При нарушении — ValidationError.
  3. Загрузка и предусловия. Найти профиль владельца; при отсутствии — 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; если статус не DraftPROFILE_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.
  4. Бизнес-правила и маппинг. Переход Draft → Draft (INV-VRF-7 разрешает правки только в Draft). Набор сканов заявки заменяется целиком (DocumentScanList, ContractVersion = 1); ссылка на шаблон реквизитов устанавливается или очищается; персона-предмет не меняется (живая Self, INV-VRF-3); снапшот остаётся пустым — он строится только при отправке (INV-VRF-7). Растёт версия заявки, фиксируется автор правки; доменное событие VerificationRequestDraftUpdatedEvent (имена изменённых полей).
  5. Что меняется. Сканы, ссылка на шаблон и версия/аудит заявки обновляются в одном согласованном изменении; интеграционных событий нет. Оптимистическая блокировка страхует гонку «правка черновика vs отправка/отзыв».
  6. События. Только доменное VerificationRequestDraftUpdatedEvent (в процессе при коммите). Интеграционных Kafka-событий нет — черновик не покидает контекст до отправки (тогда публикуется VerificationRequestSubmittedKafkaEvent, verification-request.md).
  7. Результат. Обновлённый черновик (Draft, scans, requisiteTemplateId, isReadyToSubmit, новая version), HTTP 200. Снапшота ещё нет (появится при отправке).

Диаграмма

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

ТипДетали
Интеграционные событиянет (Kafka не публикуется до отправки заявки)
Внешние вызовысверка каждого скана с сервисом storage — таймаут 5 с, без повтора; недоступность → PROFILE_STORAGE_UNAVAILABLE

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400scanList (лимит/whitelist/размер/имя) / requisiteTemplateIdRestApiValidationErrorResponseno retry
PROFILE_NOT_FOUND404Профиль не создан«Профиль ещё создаётся»retry
PROFILE_USER_BLOCKED400Profile.Status != Active«Учётная запись заблокирована»no retry
PROFILE_MANAGED_EXTERNALLY403ContactsManagedBy = TalentSync«Данные управляются внешней системой»no retry
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403не Investor / нет permission (INV-VRF-5)«Доступно только инвесторам»no retry
PROFILE_VERIFICATION_NOT_FOUND404Нет заявки / чужая (record-level)«Заявка не найдена»no retry
PROFILE_VERIFICATION_NOT_EDITABLE409Status != Draft (INV-VRF-7)«Заявку нельзя редактировать в текущем статусе»no retry
PROFILE_VERIFICATION_TEMPLATE_NOT_FOUND404Шаблон не найден / чужой / персона не совпадает«Шаблон реквизитов не найден»no retry
PROFILE_SCAN_FILE_NOT_FOUND400Файл не найден в storage / чужой / метаданные не совпали«Файл не найден — загрузите заново»повторная загрузка
PROFILE_STORAGE_UNAVAILABLE502storage недоступен«Хранилище временно недоступно»retry
PROFILE_CONCURRENCY_CONFLICT409Гонка версий (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.

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

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

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