PUT /api/v1/profile/persons/{personId}/documents/{documentId} — Обновление документа персоны (updatePersonDocument)
Назначение
Инвестор обновляет значения полей ранее добавленного документа персоны — например, исправляет опечатку
или вносит новый номер паспорта того же типа. Тип документа (Type) изменить нельзя (INV-DOC-7: смена
типа означает новый документ через add-person-document.md и архив старого).
Обновлённые значения перепроверяются и заново шифруются целиком, производные NumberHash/NumberMask и
открытые даты пересчитываются. Сканы этим методом не меняются — для них отдельные операции
attach-document-scan.md / remove-document-scan.md. На
выходе — обновлённая карточка документа.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | profile |
| API area | public |
| Feature group | documents |
| Статус | draft |
| Документ | docs/06-services/profile/api/public/update-person-document.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Bearer JWT), UserType = Investor |
| Auth policy / Permissions | authenticated; permission profile.documents.manage-self |
| Scope (RBAC) | self |
| Record-level | OwnerUserId = UserId; чужие personId/documentId → 404 |
| Tenant isolation | не применяется (ADR-0052) |
| Audit | update: фиксируется автор изменения, растёт версия документа и персоны; доменное событие PersonDocumentUpdatedEvent (имена изменённых полей, без значений) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/v1/profile/persons/{personId}/documents/{documentId} |
| Success status | 200 |
| Idempotency header | не применяется (PUT идемпотентен по значению) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса (body UpdatePersonDocumentForm)
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
personId | route | Guid | Да | NotEmpty | чужая → 404 |
documentId | route | Guid | Да | NotEmpty; активный документ этой персоны | чужой → 404 |
countryId | body | short | Да | ссылка на справочник стран handbook; правила — как в add-person-document.md: резолв через internal resolve (неизвестная/архивная → PROFILE_COUNTRY_UNKNOWN, INV-DOC-9), сверка IsoCode2 с типом документа (INV-DOC-2) | — |
title | body | string? | Условно | 1–256, только для Custom (INV-DOC-3); для прочих регенерируется | — |
payload | body | typed по текущему Type документа | Да | проверяется по модели типа документа (дом person-document.md) до шифрования (INV-DOC-1); type в body отсутствует (INV-DOC-7) | PII → шифруется |
version | body | long | Да | = текущей PersonDocumentEntity.Version | — |
Модель ответа PersonDocumentModel
Дом модели — list-person-documents.md; обновлённый документ.
Алгоритм
- Контекст и доступ. Операцию выполняет авторизованный инвестор (владелец персоны); вне tenant
(ADR-0052); требуется permission
profile.documents.manage-self. - Проверка входа.
personId/documentIdне пусты; самpayloadпроверяется по модели типа документа на шаге 4 (тип берётся у загруженного документа). При нарушении формата —ValidationError. - Загрузка и предусловия. Найти профиль владельца; при его отсутствии —
PROFILE_NOT_FOUND. Проверить гварды профиля (INV-PRF-2..4): внешнее управление контактами →PROFILE_MANAGED_EXTERNALLY, неактивная учётка →PROFILE_USER_BLOCKED, не инвестор →PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE. Найти персону с её документами; если персона не принадлежит владельцу —PROFILE_PERSON_NOT_FOUND. Найти документ у персоны; если его нет —PROFILE_DOCUMENT_NOT_FOUND; если документ в архиве —PROFILE_DOCUMENT_ARCHIVED. Если версия из запроса не совпадает с текущей версией документа —PROFILE_CONCURRENCY_CONFLICT(правка не должна затирать чужую). - Бизнес-правила и маппинг. Проверить
payloadпо модели типа документа (INV-DOC-1). РезолвитьcountryIdчерез internal resolve справочникаhandbook: страна не найдена или архивна →PROFILE_COUNTRY_UNKNOWN(422, INV-DOC-9); справочник недоступен →PROFILE_HANDBOOK_UNAVAILABLE(503, fail-closed). СверитьIsoCode2резолвленной страны с типом документа иCountryIdpayload (INV-DOC-2) — иначеPROFILE_DOCUMENT_COUNTRY_MISMATCH. Пересчитать производные: HMAC-хэш номераNumberHash, маскуNumberMask, отображаемое имяTitle(генерируется для стандартных типов, пользовательское дляCustom), открытые датыIssueDate/ExpiresAtиз payload, денормализациюCountryIsoCode2=IsoCode2резолвленной страны; сам payload заново шифруется целиком текущим ключом,PayloadContractVersion= актуальная версия модели. Если среди других активных документов той же парыPersonId+Typeуже есть документ с такимNumberHash(INV-DOC-4) —PROFILE_DOCUMENT_ALREADY_EXISTS. - Что меняется. Обновляются значения документа; растёт версия документа и персоны; фиксируется автор правки. Всё применяется в одном согласованном изменении; уникальный индекс дедупликации страхует гонку.
- События. Публикуется
PersonUpsertedKafkaEvent(topicpin.profile.persons.v1, key = id персоны;DocumentsCountбез изменений,Version— новый; без PII); доменноеPersonDocumentUpdatedEvent(имена изменённых полей, без значений). - Результат. Обновлённая карточка документа (эхо plaintext), HTTP 200. Снапшоты, ранее выданные CRM или зафиксированные в верификации, не меняются (иммутабельные копии).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | PersonUpsertedKafkaEvent / pin.profile.persons.v1 / key = id персоны |
| Внешние вызовы | провайдер ключей шифрования PII; справочник handbook (internal resolve) — резолв countryId (INV-DOC-2/9) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | Нет/просрочен JWT | без тела | re-login |
ValidationError | 400 | Payload не соответствует типу / regex | RestApiValidationErrorResponse | no retry |
PROFILE_NOT_FOUND | 404 | Профиль не создан | «Профиль ещё создаётся» | retry |
PROFILE_USER_BLOCKED | 400 | Status != Active | «Учётная запись заблокирована» | no retry |
PROFILE_MANAGED_EXTERNALLY | 403 | TalentSync | «Данные управляются внешней системой» | no retry |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | не Investor | «Доступно только инвесторам» | no retry |
PROFILE_PERSON_NOT_FOUND | 404 | Нет персоны / чужая | «Персона не найдена» | no retry |
PROFILE_DOCUMENT_NOT_FOUND | 404 | Нет документа у персоны | «Документ не найден» | no retry |
PROFILE_DOCUMENT_ARCHIVED | 400 | Документ в архиве | «Документ в архиве» | no retry |
PROFILE_DOCUMENT_COUNTRY_MISMATCH | 400 | IsoCode2 резолвленной страны не соответствует типу / payload (INV-DOC-2) | «Страна не соответствует типу документа» | no retry |
PROFILE_COUNTRY_UNKNOWN | 422 | countryId не найден в справочнике стран или архивен (INV-DOC-9) | «Страна не найдена в справочнике» | no retry (исправить страну) |
PROFILE_DOCUMENT_ALREADY_EXISTS | 409 | Дубль NumberHash (INV-DOC-4) | «Такой документ уже добавлен» | no retry |
PROFILE_CONCURRENCY_CONFLICT | 409 | Гонка версий | «Данные изменены, повторите» | retry |
PROFILE_HANDBOOK_UNAVAILABLE | 503 | Справочник недоступен при резолве countryId (fail-closed) | «Сервис временно недоступен» | retry |
PROFILE_CRYPTO_UNAVAILABLE | 503 | Ключи недоступны | «Сервис временно недоступен» | retry |
Совместимость и наблюдаемость
typeв форму не добавляется (INV-DOC-7 — breaking); payload-модели — только additive (дом).- Logs:
UserId,PersonId,DocumentId,Type,changedFields(без значений); metrics:profile_api_operations_total{operation="updatePersonDocument",result}; traces — OTel.
Acceptance criteria
- Дано активный документ персоны владельца и корректный
payloadпо текущемуTypeс вернойversion; когда PUT; тогда payload перешифрован целиком текущим ключом, пересчитаныNumberHash/NumberMask/Title/IssueDate/ExpiresAt, растут версии документа и персоны, публикуетсяPersonUpsertedKafkaEvent(без PII,DocumentsCountбез изменений), HTTP200. - Дано
payloadне соответствует модели типа документа (INV-DOC-1); когда PUT; тогдаValidationError(400), документ не изменён. - Дано новый номер даёт
NumberHash, совпадающий с другим активным документом той же парыPersonId+Type(INV-DOC-4); когда PUT; тогдаPROFILE_DOCUMENT_ALREADY_EXISTS(409). - Дано
IsoCode2резолвленной страны не соответствует типу документа (INV-DOC-2); когда PUT; тогдаPROFILE_DOCUMENT_COUNTRY_MISMATCH(400). - Дано
countryId, которого нет в справочнике стран (или страна архивна); когда PUT; тогдаPROFILE_COUNTRY_UNKNOWN(422, INV-DOC-9); документ не изменён. - Дано документ в архиве; когда PUT; тогда
PROFILE_DOCUMENT_ARCHIVED(400). - Дано
versionиз запроса не совпадает с текущей; когда PUT; тогдаPROFILE_CONCURRENCY_CONFLICT(409) — правка не затирает чужую. - Дано документ ранее попал в иммутабельный снапшот верификации или снапшот реквизитов CRM; когда документ обновлён; тогда ранее выданные снапшоты не меняются (копии).
- Дано провайдер ключей шифрования недоступен; когда PUT; тогда
PROFILE_CRYPTO_UNAVAILABLE(503).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): GET /api/v1/profile/persons/{personId}/documents — Список документов персоны (listPersonDocuments), Profile API — реестр операций областей public / internal / admin, PUT /api/v1/profile/verification/{requestId} — Правка черновика заявки KYC (updateVerificationDraft)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: person-document.md (INV-DOC-1..8); root — person.md (обновление документа персоны).
- Операции: add-person-document.md, archive-person-document.md.