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 area | public |
| Feature group | persons |
| Статус | draft |
| Документ | docs/06-services/profile/api/public/update-person.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Bearer JWT), UserType = Investor |
| Auth policy / Permissions | authenticated; permission profile.personal.manage-self |
| Scope (RBAC) | self |
| Record-level | PersonEntity.OwnerUserId = UserId; чужой personId → 404 |
| Tenant isolation | не применяется (ADR-0052) |
| Audit | update: фиксируется автор изменения, растёт версия персоны; доменное событие PersonDataChangedEvent (имена полей, без значений) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/v1/profile/persons/{personId} |
| Success status | 200 |
| Idempotency header | не применяется (PUT идемпотентен по значению) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса (body UpdatePersonForm)
PUT-семантика «полное состояние редактируемых полей»: отсутствие optional-поля = очистка значения.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
personId | route | Guid | Да | NotEmpty; персона владельца | чужая → 404 |
firstName | body | string | Да | 1–128 символов | PII → шифруется |
lastName | body | string | Да | 1–128 символов | PII → шифруется |
middleName | body | string? | Нет | 1–128 символов | PII → шифруется |
birthDate | body | DateOnly? | Нет | ≤ сегодня; ≥ 1900-01-01; null запрещён, если у персоны активная заявка верификации ссылается на неё (guard INV-VRF — правки в Draft допустимы) | PII → шифруется |
citizenshipCountryId | body | short? | Нет | ссылка на справочник стран handbook; резолв через internal resolve, неизвестная/архивная → PROFILE_COUNTRY_UNKNOWN | открытое поле |
gender | body | PersonGender? | Нет | enum Male/Female | открытое поле |
version | body | long | Да | = текущей PersonEntity.Version (optimistic-предикат UI) | — |
Модель ответа PersonModel
Дом модели — list-persons.md; возвращается обновлённая персона (version — новая).
Алгоритм
- Контекст и доступ. Операцию выполняет авторизованный инвестор (владелец персоны,
UserIdиз токена); вне tenant; требуется permissionprofile.personal.manage-self. - Проверка входа. Проверить формат/длины/даты полей (таблица выше, plaintext до шифрования) —
иначе
ValidationError. Если заданcitizenshipCountryId— резолв страны через internal resolve справочникаhandbook(resolve-references.md): страна не найдена или архивна →PROFILE_COUNTRY_UNKNOWN(422); справочник недоступен → fail-closedPROFILE_HANDBOOK_UNAVAILABLE(503). - Загрузка и предусловия. Найти профиль владельца; при его отсутствии —
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(решение ревьюер принимает по снапшоту). - Бизнес-правила и маппинг. Обновить ФИО, дату рождения, гражданство, пол; вся запись заново
шифруется текущим ключом (INV-PRS-8 — все cipher-поля одним ключом). Маппинг поле-в-поле:
firstName/lastName/middleName→ шифрованное ФИО;birthDate→BirthDateCipher;citizenshipCountryId→CitizenshipCountryId;gender→Gender. Растёт версия персоны, фиксируется автор правки; доменное событиеPersonDataChangedEvent(только имена изменённых полей, без значений). - Что меняется. Значения персоны обновляются в одном согласованном изменении. Если версия из запроса
не совпадает с текущей версией персоны —
PROFILE_CONCURRENCY_CONFLICT(правка не должна затирать чужую). - События. Публикуется
PersonUpsertedKafkaEvent(ContractVersion = 1, topicpin.profile.persons.v1, key = id персоны;Id,OwnerUserId,Relation,Status,DocumentsCount,Version— новое значение; без PII). - Результат. Обновлённая карточка персоны (значения из 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 |
ValidationError | 400 | Формат/длины/даты | RestApiValidationErrorResponse | no retry |
PROFILE_NOT_FOUND | 404 | Профиль не создан | «Профиль ещё создаётся» | retry с backoff |
PROFILE_USER_BLOCKED | 400 | Status != Active | «Учётная запись заблокирована» | no retry |
PROFILE_MANAGED_EXTERNALLY | 403 | Контакты управляются внешней системой (INV-PRS-3) | «Данные управляются внешней системой» | no retry |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | не Investor | «Доступно только инвесторам» | no retry |
PROFILE_PERSON_NOT_FOUND | 404 | Нет персоны / чужая | «Персона не найдена» | no retry |
PROFILE_PERSON_ARCHIVED | 400 | Status = Archived | «Персона в архиве — сначала восстановите» | no retry |
PROFILE_CONCURRENCY_CONFLICT | 409 | Гонка версий (version) | «Данные изменены, повторите» | retry с новыми данными |
PROFILE_COUNTRY_UNKNOWN | 422 | citizenshipCountryId не найден в справочнике стран или архивен | «Страна не найдена в справочнике» | no retry (исправить страну) |
PROFILE_HANDBOOK_UNAVAILABLE | 503 | Справочник недоступен при валидации citizenshipCountryId (fail-closed) | «Сервис временно недоступен» | retry |
PROFILE_CRYPTO_UNAVAILABLE | 503 | Ключи недоступны | «Сервис временно недоступен» | 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), HTTP200. - Дано формат/длины/даты нарушены (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).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): GET /api/v1/profile/persons — Список персон аккаунта (listPersons), Profile API — реестр операций областей public / internal / admin, PUT /api/v1/profile/verification/{requestId} — Правка черновика заявки KYC (updateVerificationDraft)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: person.md (правка ФИО / даты рождения / гражданства,
INV-PRS-3/8,
PersonDataChangedEvent). - Операции: create-person.md, archive-person.md.