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

PUT /api/v1/profile/persons/{personId} — Обновление персоны (updatePerson)

Назначение

Инвестор обновляет персональные данные своей персоны: ФИО, дату рождения, гражданство, пол. Значения заново шифруются текущим ключом (INV-PRS-8). Тип связи персоны с аккаунтом (Relation) после создания изменить нельзя (смена связи = новая персона); статус меняется отдельными операциями archive-person.md/restore-person.md. Менеджеру застройщика операция запрещена — его персональные данные ведёт внешняя система (INV-PRS-3/INV-IDN-001). На выходе — обновлённая карточка персоны.

Metadata

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

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

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

HTTP-контракт

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

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

PUT-семантика «полное состояние редактируемых полей»: отсутствие optional-поля = очистка значения.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
personIdrouteGuidДаNotEmpty; персона владельцачужая → 404
firstNamebodystringДа1–128 символовPII → шифруется
lastNamebodystringДа1–128 символовPII → шифруется
middleNamebodystring?Нет1–128 символовPII → шифруется
birthDatebodyDateOnly?Нет≤ сегодня; ≥ 1900-01-01; null запрещён, если у персоны активная заявка верификации ссылается на неё (guard INV-VRF — правки в Draft допустимы)PII → шифруется
citizenshipCountryIdbodyshort?Нетссылка на справочник стран handbook; резолв через internal resolve, неизвестная/архивная → PROFILE_COUNTRY_UNKNOWNоткрытое поле
genderbodyPersonGender?Нетenum Male/Femaleоткрытое поле
versionbodylongДа= текущей PersonEntity.Version (optimistic-предикат UI)

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

Дом модели — list-persons.md; возвращается обновлённая персона (version — новая).

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный инвестор (владелец персоны, UserId из токена); вне tenant; требуется permission profile.personal.manage-self.
  2. Проверка входа. Проверить формат/длины/даты полей (таблица выше, plaintext до шифрования) — иначе ValidationError. Если задан citizenshipCountryId — резолв страны через internal resolve справочника handbook (resolve-references.md): страна не найдена или архивна → PROFILE_COUNTRY_UNKNOWN (422); справочник недоступен → fail-closed PROFILE_HANDBOOK_UNAVAILABLE (503).
  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_PERSON_ARCHIVED (сначала восстановить). Если по персоне есть заявка верификации в статусах Submitted/InReview — правки допустимы (снапшот иммутабелен, INV-VRF-7), но в ответ добавляется предупреждающий заголовок X-Profile-Warning: verification-snapshot-differs (решение ревьюер принимает по снапшоту).
  4. Бизнес-правила и маппинг. Обновить ФИО, дату рождения, гражданство, пол; вся запись заново шифруется текущим ключом (INV-PRS-8 — все cipher-поля одним ключом). Маппинг поле-в-поле: firstName/ lastName/middleName → шифрованное ФИО; birthDateBirthDateCipher; citizenshipCountryIdCitizenshipCountryId; genderGender. Растёт версия персоны, фиксируется автор правки; доменное событие PersonDataChangedEvent (только имена изменённых полей, без значений).
  5. Что меняется. Значения персоны обновляются в одном согласованном изменении. Если версия из запроса не совпадает с текущей версией персоны — PROFILE_CONCURRENCY_CONFLICT (правка не должна затирать чужую).
  6. События. Публикуется PersonUpsertedKafkaEvent (ContractVersion = 1, topic pin.profile.persons.v1, key = id персоны; Id, OwnerUserId, Relation, Status, DocumentsCount, Version — новое значение; без PII).
  7. Результат. Обновлённая карточка персоны (значения из plaintext входа), HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияPersonUpsertedKafkaEvent / pin.profile.persons.v1 / key = id персоны
Внешние вызовыпровайдер ключей шифрования PII; справочник handbook (internal resolve) — валидация citizenshipCountryId, только если поле задано

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400Формат/длины/датыRestApiValidationErrorResponseno retry
PROFILE_NOT_FOUND404Профиль не создан«Профиль ещё создаётся»retry с backoff
PROFILE_USER_BLOCKED400Status != Active«Учётная запись заблокирована»no retry
PROFILE_MANAGED_EXTERNALLY403Контакты управляются внешней системой (INV-PRS-3)«Данные управляются внешней системой»no retry
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403не Investor«Доступно только инвесторам»no retry
PROFILE_PERSON_NOT_FOUND404Нет персоны / чужая«Персона не найдена»no retry
PROFILE_PERSON_ARCHIVED400Status = Archived«Персона в архиве — сначала восстановите»no retry
PROFILE_CONCURRENCY_CONFLICT409Гонка версий (version)«Данные изменены, повторите»retry с новыми данными
PROFILE_COUNTRY_UNKNOWN422citizenshipCountryId не найден в справочнике стран или архивен«Страна не найдена в справочнике»no retry (исправить страну)
PROFILE_HANDBOOK_UNAVAILABLE503Справочник недоступен при валидации citizenshipCountryId (fail-closed)«Сервис временно недоступен»retry
PROFILE_CRYPTO_UNAVAILABLE503Ключи недоступны«Сервис временно недоступен»retry

Совместимость и наблюдаемость

  • Additive-расширение формы; relation в body отсутствует осознанно (иммутабелен) — добавление станет breaking и требует ревизии INV-PRS-1.
  • Ответный заголовок X-Profile-Warning: verification-snapshot-differs — additive-сигнал (при правке персоны с активной заявкой Submitted/InReview); отсутствие заголовка потребители трактуют как норму.
  • Logs: UserId, PersonId, changedFields (имена, без значений); metrics: profile_api_operations_total{operation="updatePerson",result}; traces — OTel.

Acceptance criteria

  • Дано активная персона владельца и валидные поля с верной version; когда PUT; тогда ФИО/дата рождения/гражданство/пол обновлены, вся запись перешифрована единым ключом (INV-PRS-8), растёт версия, поднято PersonDataChangedEvent, опубликован PersonUpsertedKafkaEvent (без PII), HTTP 200.
  • Дано формат/длины/даты нарушены (plaintext до шифрования); когда PUT; тогда ValidationError (400).
  • Дано citizenshipCountryId, которого нет в справочнике стран (или страна архивна); когда PUT; тогда PROFILE_COUNTRY_UNKNOWN (422); персона не изменена.
  • Дано персона в архиве; когда PUT; тогда PROFILE_PERSON_ARCHIVED (400) — сначала восстановить.
  • Дано по персоне есть заявка верификации в Submitted/InReview; когда PUT; тогда правка проходит (снапшот иммутабелен, INV-VRF-7), в ответ добавляется заголовок X-Profile-Warning: verification-snapshot-differs.
  • Дано version из запроса не совпадает с текущей; когда PUT; тогда PROFILE_CONCURRENCY_CONFLICT (409).
  • Дано personId чужой; когда PUT; тогда PROFILE_PERSON_NOT_FOUND (404).
  • Дано пользователь — менеджер застройщика (TalentSync) или не инвестор; когда PUT; тогда PROFILE_MANAGED_EXTERNALLY (403) / PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE (403).
  • Дано провайдер ключей шифрования недоступен; когда PUT; тогда PROFILE_CRYPTO_UNAVAILABLE (503).

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

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

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