POST /api/v1/profile/verification — Создание черновика заявки KYC (createVerificationRequest)
Назначение
Создать черновик (Draft) заявки на простую верификацию инвестора (KYC/AML) по персоне
Relation = Self владельца аккаунта и, опционально, предзаполнить его из шаблона реквизитов. Черновик —
рабочая заготовка: инвестор дозаполняет данные персоны/сканы и затем отправляет заявку операцией
submitVerification (submit-verification.md) — только там строится иммутабельный
зашифрованный снапшот и публикуется событие в шину. На создании снапшот не формируется, шифрование
данных не выполняется, PII в ответ не попадает. Верифицируется всегда учётная запись (OwnerUserId);
предмет проверки — персона Self (INV-VRF-3). Операцию вызывает инвестор из ЛК.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | profile |
| API area | public |
| Feature group | verification |
| Статус | draft |
| Документ | docs/06-services/profile/api/public/create-verification-request.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Bearer JWT), UserType = Investor (INV-VRF-5) |
| Auth policy / Permissions | authenticated; permission profile.verification.submit |
| Scope (RBAC) | self |
| Record-level | OwnerUserId = UserId из sub (из body не принимается) |
| Tenant isolation | не применяется (ADR-0052: инвестор вне tenant) |
| Audit | create: CreatedByUserId = UserId, Version = 1, событие VerificationRequestCreatedEvent |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/profile/verification |
| Success status | 201 + Location: /api/v1/profile/verification/{id} |
| Idempotency header | Idempotency-Key (опционально; двойной сабмит формы дополнительно гасит INV-VRF-1) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса (body CreateVerificationRequestForm)
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
personId | body | Guid | Да | NotEmpty; должна ссылаться на активную персону владельца с Relation = Self (INV-VRF-3) | чужая/не Self → см. ошибки |
requisiteTemplateId | body | Guid? | Нет | если задан — NotEmpty; активный шаблон владельца (для предзаполнения UI); информационная ссылка — фактический состав фиксируется снапшотом на Submit, не здесь | чужой/архивный → PROFILE_TEMPLATE_NOT_FOUND |
Модель ответа VerificationRequestModel
VerificationRequestModel — результат команды (проекция свежесозданного черновика: без снапшота и PII);
её дом — этот документ. Полную read-проекцию заявки (MyVerificationModel: статус, решение ревьюера,
сканы) отдаёт get-my-verification.md.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
id | Guid | Да | VerificationRequestEntity.Id | — | Идентификатор заявки. |
status | VerificationRequestStatus (string) | Да | entity.Status | — | На создании всегда Draft (enum — verification-request.md). |
personId | Guid | Да | entity.PersonId | — | Персона-предмет проверки (Self). |
requisiteTemplateId | Guid? | Нет | entity.RequisiteTemplateId | — | Шаблон предзаполнения (информационно) либо null. |
version | long | Да | entity.Version | — | Оптимистичная версия агрегата (1 при создании). |
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор (актор
User,UserIdиз токена, permissionprofile.verification.submit); tenant не применяется (ADR-0052). Повторный запрос с тем жеIdempotency-Keyвозвращает ранее сохранённый результат без повторного создания. - Проверка входа.
personIdзадан;requisiteTemplateId— если задан, не пустой. Нарушение →ValidationError. Семантические проверки персоны/статуса верификации — на шаге 3. - Проверка предусловий (сначала владелец, затем предмет проверки):
- Профиль владельца существует; иначе
PROFILE_NOT_FOUND(профиль ещё не создан консюмером identity). Профиль активен (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-VRF-5/INV-PRF-4). - Аккаунт ещё не верифицирован:
VerificationStatus != Verified; иначеPROFILE_ALREADY_VERIFIED(INV-VRF-9): новая заявка не нужна. - Нет незавершённой заявки владельца в статусах
Draft/Submitted/InReview; иначеPROFILE_VERIFICATION_ALREADY_IN_PROGRESS(INV-VRF-1). - Персона
personIdсуществует и принадлежит владельцу; иначеPROFILE_PERSON_NOT_FOUND. Персона активна и еёRelation = Self; иначеPROFILE_VERIFICATION_PERSON_NOT_SELF(INV-VRF-3). - Только если
requisiteTemplateIdзадан: активный шаблон владельца; иначеPROFILE_TEMPLATE_NOT_FOUND(чужой/архивный/несуществующий). Проверяется только владение и активность; согласованность состава с персоной валидируется приSubmit.
- Профиль владельца существует; иначе
- Бизнес-правила и маппинг. Снапшот не строится:
SnapshotCipher = null,SnapshotContractVersion = null,KeyVersion = null,ScanList = DocumentScanList.Empty(INV-VRF-7 — снапшот появляется только наSubmit). Создаётся черновик:Status = Draft,CreatedByUserId = UserId,Version = 1; переход[*] → Draft(INV-VRF-1/3/5/9 уже проверены); фиксируется фактVerificationRequestCreatedEvent.Profile.VerificationStatusне меняется (переход вPending— только наSubmit, INV-PRF-6). - Согласованность. В одном согласованном изменении сохраняются заявка и запись идемпотентности; гонку
двойного
Draftстрахует уникальность активной заявки владельца (конфликт →PROFILE_VERIFICATION_ALREADY_IN_PROGRESS). - Результат. Модель
VerificationRequestModel(id,status = Draft,personId,requisiteTemplateId?,version = 1). HTTP201+Location: /api/v1/profile/verification/{id}. Интеграционных событий нет — событие в шину (VerificationRequestSubmittedKafkaEvent,pin.profile.verification-requests.v1) публикуется лишь наSubmit(verification-request-submitted.md).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | Нет/просрочен JWT | без тела | re-login |
ValidationError | 400 | personId/requisiteTemplateId пусты/некорректны | перечень невалидных полей | no retry |
PROFILE_NOT_FOUND | 404 | Профиль не создан консюмером | «Профиль ещё создаётся» | retry с backoff |
PROFILE_USER_BLOCKED | 400 | Status != Active (INV-PRF-3) | «Учётная запись заблокирована» | no retry |
PROFILE_MANAGED_EXTERNALLY | 403 | ContactsManagedBy = TalentSync | «Данные управляются внешней системой» | no retry |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | UserType != Investor (INV-VRF-5) | «Доступно только инвесторам» | no retry |
PROFILE_PERSON_NOT_FOUND | 404 | Персона не найдена / чужая | «Персона не найдена» | no retry |
PROFILE_VERIFICATION_PERSON_NOT_SELF | 409 | Relation != Self или персона в архиве (INV-VRF-3) | «Верификация доступна только по основной персоне» | no retry |
PROFILE_TEMPLATE_NOT_FOUND | 404 | requisiteTemplateId задан, но чужой/архивный/несуществующий | «Шаблон реквизитов не найден» | no retry |
PROFILE_VERIFICATION_ALREADY_IN_PROGRESS | 409 | Есть заявка Draft/Submitted/InReview (INV-VRF-1) | «Заявка на верификацию уже в работе» | no retry |
PROFILE_ALREADY_VERIFIED | 409 | VerificationStatus = Verified (INV-VRF-9) | «Аккаунт уже верифицирован» | no retry |
Совместимость и наблюдаемость
- Форма расширяется только optional-полями (additive); удаление/переименование
personId— breaking (migration + rollout). Снапшот и PII в контракте создания отсутствуют по построению. - Logs:
UserId,RequestId,PersonId,RequisiteTemplateId?(без значений PII — их на создании нет); metrics:profile_api_operations_total{operation="createVerificationRequest",result},profile_verification_drafts_created_total; traces — OTel; dashboardprofile-overview.
Acceptance Criteria
- Given инвестор с активной персоной
Self, без незавершённой заявки и с аккаунтом неVerified, WhenPOSTpersonId = Self, Then201+Location; заявкаDraft, снапшотnull(SnapshotCipher/SnapshotContractVersion/KeyVersion=null,ScanList = Empty),Version = 1;Profile.VerificationStatusне меняется; событие в шину не публикуется (публикуется только наSubmit). - Given задан свой активный
requisiteTemplateId, WhenPOST, Then201; шаблон ссылается информационно (фактический состав фиксируется снапшотом наSubmit, не здесь). - Given
requisiteTemplateIdчужой/архивный/несуществующий, WhenPOST, ThenPROFILE_TEMPLATE_NOT_FOUND(404). - Given
personIdнеSelfили персона в архиве, WhenPOST, ThenPROFILE_VERIFICATION_PERSON_NOT_SELF(409) — INV-VRF-3. - Given
personIdчужой, WhenPOST, ThenPROFILE_PERSON_NOT_FOUND(404). - Given есть заявка владельца в
Draft/Submitted/InReview, WhenPOST, ThenPROFILE_VERIFICATION_ALREADY_IN_PROGRESS(409) — INV-VRF-1. - Given аккаунт уже
Verified, WhenPOST, ThenPROFILE_ALREADY_VERIFIED(409) — INV-VRF-9. - Given
UserType != Investor/TalentSync/ профиль заблокирован / профиль не создан, WhenPOST, ThenPROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE(403) /PROFILE_MANAGED_EXTERNALLY(403) /PROFILE_USER_BLOCKED(400) /PROFILE_NOT_FOUND(404) соответственно. - Given повтор с тем же
Idempotency-Key, WhenPOST, Then возвращается ранее сохранённый результат без создания второго черновика. - Given два конкурентных создания черновика, When оба
POST, Then одно201, второе —PROFILE_VERIFICATION_ALREADY_IN_PROGRESS(409) (страховка уникальностью активной заявки владельца).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): GET /api/v1/profile/verification — Моя заявка на верификацию (getMyVerification), Profile API — реестр операций областей public / internal / admin, PUT /api/v1/profile/verification/{requestId} — Правка черновика заявки KYC (updateVerificationDraft)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: verification-request.md (INV-VRF-1/3/5/7/9, статусная
машина,
VerificationRequestStatus, снапшот наSubmit). - Guard-ы и денормализация: profile.md (INV-PRF-2..4/6,
VerificationStatus); предмет проверки — person.md (Relation = Self); предзаполнение — requisite-template.md. - Продолжение flow: submit-verification.md (сборка снапшота, событие в шину); событие — verification-request-submitted.md.