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

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 areapublic
Feature groupdocuments
Статусdraft
Документdocs/06-services/profile/api/public/update-person-document.md

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

ПроверкаЗначение
Actor typeUser (Bearer JWT), UserType = Investor
Auth policy / Permissionsauthenticated; permission profile.documents.manage-self
Scope (RBAC)self
Record-levelOwnerUserId = UserId; чужие personId/documentId → 404
Tenant isolationне применяется (ADR-0052)
Auditupdate: фиксируется автор изменения, растёт версия документа и персоны; доменное событие PersonDocumentUpdatedEvent (имена изменённых полей, без значений)

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
personIdrouteGuidДаNotEmptyчужая → 404
documentIdrouteGuidДаNotEmpty; активный документ этой персонычужой → 404
countryIdbodyshortДассылка на справочник стран handbook; правила — как в add-person-document.md: резолв через internal resolve (неизвестная/архивная → PROFILE_COUNTRY_UNKNOWN, INV-DOC-9), сверка IsoCode2 с типом документа (INV-DOC-2)
titlebodystring?Условно1–256, только для Custom (INV-DOC-3); для прочих регенерируется
payloadbodytyped по текущему Type документаДапроверяется по модели типа документа (дом person-document.md) до шифрования (INV-DOC-1); type в body отсутствует (INV-DOC-7)PII → шифруется
versionbodylongДа= текущей PersonDocumentEntity.Version

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

Дом модели — list-person-documents.md; обновлённый документ.

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный инвестор (владелец персоны); вне tenant (ADR-0052); требуется permission profile.documents.manage-self.
  2. Проверка входа. personId/documentId не пусты; сам payload проверяется по модели типа документа на шаге 4 (тип берётся у загруженного документа). При нарушении формата — ValidationError.
  3. Загрузка и предусловия. Найти профиль владельца; при его отсутствии — 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 (правка не должна затирать чужую).
  4. Бизнес-правила и маппинг. Проверить payload по модели типа документа (INV-DOC-1). Резолвить countryId через internal resolve справочника handbook: страна не найдена или архивна → PROFILE_COUNTRY_UNKNOWN (422, INV-DOC-9); справочник недоступен → PROFILE_HANDBOOK_UNAVAILABLE (503, fail-closed). Сверить IsoCode2 резолвленной страны с типом документа и CountryId payload (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.
  5. Что меняется. Обновляются значения документа; растёт версия документа и персоны; фиксируется автор правки. Всё применяется в одном согласованном изменении; уникальный индекс дедупликации страхует гонку.
  6. События. Публикуется PersonUpsertedKafkaEvent (topic pin.profile.persons.v1, key = id персоны; DocumentsCount без изменений, Version — новый; без PII); доменное PersonDocumentUpdatedEvent (имена изменённых полей, без значений).
  7. Результат. Обновлённая карточка документа (эхо 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
ValidationError400Payload не соответствует типу / regexRestApiValidationErrorResponseno retry
PROFILE_NOT_FOUND404Профиль не создан«Профиль ещё создаётся»retry
PROFILE_USER_BLOCKED400Status != Active«Учётная запись заблокирована»no retry
PROFILE_MANAGED_EXTERNALLY403TalentSync«Данные управляются внешней системой»no retry
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403не Investor«Доступно только инвесторам»no retry
PROFILE_PERSON_NOT_FOUND404Нет персоны / чужая«Персона не найдена»no retry
PROFILE_DOCUMENT_NOT_FOUND404Нет документа у персоны«Документ не найден»no retry
PROFILE_DOCUMENT_ARCHIVED400Документ в архиве«Документ в архиве»no retry
PROFILE_DOCUMENT_COUNTRY_MISMATCH400IsoCode2 резолвленной страны не соответствует типу / payload (INV-DOC-2)«Страна не соответствует типу документа»no retry
PROFILE_COUNTRY_UNKNOWN422countryId не найден в справочнике стран или архивен (INV-DOC-9)«Страна не найдена в справочнике»no retry (исправить страну)
PROFILE_DOCUMENT_ALREADY_EXISTS409Дубль NumberHash (INV-DOC-4)«Такой документ уже добавлен»no retry
PROFILE_CONCURRENCY_CONFLICT409Гонка версий«Данные изменены, повторите»retry
PROFILE_HANDBOOK_UNAVAILABLE503Справочник недоступен при резолве countryId (fail-closed)«Сервис временно недоступен»retry
PROFILE_CRYPTO_UNAVAILABLE503Ключи недоступны«Сервис временно недоступен»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 без изменений), HTTP 200.
  • Дано 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).

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

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

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