PUT /api/investors/me — Обновление профиля инвестора (updateInvestor)
Назначение
Обновить ФИО текущего пользователя (форма содержит только name: FullName; расширенный профиль/документы —
контекст profile). Вызывается аутентифицированным пользователем.
INV-4
Для пользователей с ContactsManagedBy = TalentSync (менеджеры застройщика) операция запрещена — источник
знаний о таких пользователях внешний.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | public |
| Feature group | profile |
| Статус | approved |
| Документ | docs/06-services/identity/api/investor/update-investor.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Bearer JWT) |
| Auth policy / Permissions | authenticated; permission identity.users.view-self (self-update) |
| Scope (RBAC) | self |
| Record-level | только собственная запись (UserId из sub) |
| Tenant isolation | не применяется (ADR-0052) |
| Audit | update: UpdatedByUserId = UserId |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/investors/me |
| Success status | 200 |
| Idempotency header | не применяется (PUT идемпотентен по значению) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса (body UpdateInvestorForm)
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
name.firstName | body | string | Да | min 1, max 128 | PII |
name.lastName | body | string | Да | max 128 (пустая строка допустима — контракт FullName) | PII |
name.middleName | body | string? | Нет | max 128 | PII |
Модель ответа UpdateInvestorCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
investorId | string (Guid) | Да | UserEntity.Id | — | Идентификатор обновлённого пользователя. |
Алгоритм
- Контекст и доступ. Операцию выполняет аутентифицированный пользователь над собственной записью
(
UserIdиз токена). - Проверка входа.
nameзадан;firstNameнепустой ≤ 128;lastName≤ 128;middleName≤ 128 — иначеValidationError. - Проверки состояния. Получить пользователя; он
Active— иначеIDENTITY_USER_BLOCKED; контакты управляются самим пользователем (ContactsManagedBy == SelfService, INV-4) — иначеIDENTITY_CONTACTS_MANAGED_EXTERNALLY(менеджеры застройщика). - Правила и маппинг. Переименование пользователя (guard INV-4):
body.name.* → user.Name.*,UpdatedByUserId = UserId. Изменение публичного поляNameвлечёт публикацию снапшот-события. - Что меняется. В одном согласованном изменении обновляется пользователь; конкурентное обновление
разрешается оптимистической блокировкой (правка не должна затирать чужую) →
IDENTITY_CONCURRENCY_CONFLICT. - События.
UserUpsertedKafkaEvent→pin.identity.users.v1(ключ =UserId; снапшот публичных полей:Id,Type,CompanyId,Status,Name(новое значение),PhoneNumber,RegisteredVia,ReferrerUrl,Version); потребители обновляют реплики ФИО (crm-лиды, messenger-контакты, profile). - Результат.
investorId; HTTP 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | UserUpsertedKafkaEvent / pin.identity.users.v1 / ключ = UserId (снапшот нового Name) |
| Внешние вызовы | none |
| Проекции / поиск | в identity инвестор (CompanyId = null) в проекцию UserSearchDocument не входит (поиск пользователей); поиск дублей ФИО для фиксаций — реплика CRM, обновляется событием user-upserted |
| Уведомления | none |
Кросс-сервисный контракт и окно согласованности
- Предоставляет:
UserUpsertedKafkaEventв топикpin.identity.users.v1,ContractVersion = 1, ключ =UserId, снапшот публичных полей с новымName(Id,Type,CompanyId,Status,Name,PhoneNumber,RegisteredVia,ReferrerUrl,Version);Versionмонотонно растёт (guard порядка у потребителей). Потребители обновляют реплики ФИО:crm(лиды/поиск дублей для фиксаций),messenger(контакты),profile. - Ожидает от других: ничего синхронно; идентификатор и запись — источник истины identity.
- Окно согласованности: новое ФИО видно в identity сразу после коммита (
GET /me— read-your-writes); в чужих контекстах (лиды CRM, контакты мессенджера, профиль) — асинхронно (лаг — секунды) по мере обработки события. Событие публикуется при измененииName; повторный PUT с тем же значением — идемпотентный200без downstream-события.
Предусловия и постусловия
- Пред: пользователь аутентифицирован и
Active; контактами управляет сам (ContactsManagedBy = SelfService, INV-4);name.firstNameнепустой ≤ 128,name.lastName≤ 128 (пустая строка допустима),name.middleName≤ 128. - Пост (успех):
User.Nameобновлён,UpdatedByUserId = UserId,Versionувеличена; при фактическом изменении опубликованUserUpsertedKafkaEvent.
Acceptance Criteria
- Given
Active-инвестор сSelfService-контактами, WhenPUT /me {name}с новым ФИО, Then200{investorId},Nameобновлён, опубликованUserUpsertedKafkaEvent(key =UserId); реплики CRM/messenger/profile обновятся асинхронно. - Given
name.lastName = ""(lazy-регистрация), WhenPUT /me, Then200— пустая фамилия допустима по контрактуFullName. - Given тот же
name, что уже сохранён, WhenPUT /me, Then идемпотентный200; downstream- событие не публикуется (нет изменения). - Given
ContactsManagedBy = TalentSync(менеджер застройщика, INV-4), WhenPUT /me, ThenIDENTITY_CONTACTS_MANAGED_EXTERNALLY(400). - Given пустой
firstNameили превышение длин, WhenPUT /me, ThenValidationError(400). - Given учётка
Blocked/Archived, WhenPUT /me, ThenIDENTITY_USER_BLOCKED(400). - Given параллельное изменение той же учётки, When проигрыш оптимистической гонки, Then
IDENTITY_CONCURRENCY_CONFLICT(400), retry с новыми данными. - Given запрос без валидного JWT, When
PUT /me, Then401, клиент уходит на re-login.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | Нет/просрочен JWT | клиент выполняет re-login | re-login |
ValidationError | 400 | Пустое имя / превышение длин | Список ошибок валидации | no retry |
IDENTITY_USER_BLOCKED | 400 | Status != Active | «Учётная запись заблокирована» | no retry |
IDENTITY_CONTACTS_MANAGED_EXTERNALLY | 400 | ContactsManagedBy = TalentSync (INV-4) | «Профиль управляется внешней системой» | no retry |
IDENTITY_CONCURRENCY_CONFLICT | 400 | Проигрыш оптимистической гонки | «Данные изменены, повторите» | retry с новыми данными |
Совместимость и наблюдаемость
- Форма содержит только
name— добавление полей профиля здесь запрещено (дом расширенного профиля — контекстprofile); новые optional поля результата — additive. - Logs:
UserId, факт смены имени (без значений ФИО); metrics:identity_profile_updates_total{result}; traces — OTel.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): GET /api/investors/me — Текущий инвестор (getCurrentInvestor), GET /api/v1/profile/me — Получение своего профиля ЛК (getMyProfile), Identity API — карта областей и реестр investor-области
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: User (INV-4, VO
FullName, событиеUserUpsertedKafkaEvent). - Чтение: GET /api/investors/me; расширенный профиль —
docs/06-services/profile/.