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

PUT /api/investors/me — Обновление профиля инвестора (updateInvestor)

Назначение

Обновить ФИО текущего пользователя (форма содержит только name: FullName; расширенный профиль/документы — контекст profile). Вызывается аутентифицированным пользователем.

INV-4

Для пользователей с ContactsManagedBy = TalentSync (менеджеры застройщика) операция запрещена — источник знаний о таких пользователях внешний.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areapublic
Feature groupprofile
Статусapproved
Документdocs/06-services/identity/api/investor/update-investor.md

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

ПроверкаЗначение
Actor typeUser (Bearer JWT)
Auth policy / Permissionsauthenticated; permission identity.users.view-self (self-update)
Scope (RBAC)self
Record-levelтолько собственная запись (UserId из sub)
Tenant isolationне применяется (ADR-0052)
Auditupdate: UpdatedByUserId = UserId

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/investors/me
Success status200
Idempotency headerне применяется (PUT идемпотентен по значению)
Correlationсквозной trace через traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
name.firstNamebodystringДаmin 1, max 128PII
name.lastNamebodystringДаmax 128 (пустая строка допустима — контракт FullName)PII
name.middleNamebodystring?Нетmax 128PII

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

ПолеТипОбязательностьИсточникMaskingОписание
investorIdstring (Guid)ДаUserEntity.IdИдентификатор обновлённого пользователя.

Алгоритм

  1. Контекст и доступ. Операцию выполняет аутентифицированный пользователь над собственной записью (UserId из токена).
  2. Проверка входа. name задан; firstName непустой ≤ 128; lastName ≤ 128; middleName ≤ 128 — иначе ValidationError.
  3. Проверки состояния. Получить пользователя; он Active — иначе IDENTITY_USER_BLOCKED; контакты управляются самим пользователем (ContactsManagedBy == SelfService, INV-4) — иначе IDENTITY_CONTACTS_MANAGED_EXTERNALLY (менеджеры застройщика).
  4. Правила и маппинг. Переименование пользователя (guard INV-4): body.name.* → user.Name.*, UpdatedByUserId = UserId. Изменение публичного поля Name влечёт публикацию снапшот-события.
  5. Что меняется. В одном согласованном изменении обновляется пользователь; конкурентное обновление разрешается оптимистической блокировкой (правка не должна затирать чужую) → IDENTITY_CONCURRENCY_CONFLICT.
  6. События. UserUpsertedKafkaEventpin.identity.users.v1 (ключ = UserId; снапшот публичных полей: Id, Type, CompanyId, Status, Name (новое значение), PhoneNumber, RegisteredVia, ReferrerUrl, Version); потребители обновляют реплики ФИО (crm-лиды, messenger-контакты, profile).
  7. Результат. 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-контактами, When PUT /me {name} с новым ФИО, Then 200 {investorId}, Name обновлён, опубликован UserUpsertedKafkaEvent (key = UserId); реплики CRM/messenger/profile обновятся асинхронно.
  • Given name.lastName = "" (lazy-регистрация), When PUT /me, Then 200 — пустая фамилия допустима по контракту FullName.
  • Given тот же name, что уже сохранён, When PUT /me, Then идемпотентный 200; downstream- событие не публикуется (нет изменения).
  • Given ContactsManagedBy = TalentSync (менеджер застройщика, INV-4), When PUT /me, Then IDENTITY_CONTACTS_MANAGED_EXTERNALLY (400).
  • Given пустой firstName или превышение длин, When PUT /me, Then ValidationError (400).
  • Given учётка Blocked/Archived, When PUT /me, Then IDENTITY_USER_BLOCKED (400).
  • Given параллельное изменение той же учётки, When проигрыш оптимистической гонки, Then IDENTITY_CONCURRENCY_CONFLICT (400), retry с новыми данными.
  • Given запрос без валидного JWT, When PUT /me, Then 401, клиент уходит на re-login.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTклиент выполняет re-loginre-login
ValidationError400Пустое имя / превышение длинСписок ошибок валидацииno retry
IDENTITY_USER_BLOCKED400Status != Active«Учётная запись заблокирована»no retry
IDENTITY_CONTACTS_MANAGED_EXTERNALLY400ContactsManagedBy = TalentSync (INV-4)«Профиль управляется внешней системой»no retry
IDENTITY_CONCURRENCY_CONFLICT400Проигрыш оптимистической гонки«Данные изменены, повторите»retry с новыми данными

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

  • Форма содержит только name — добавление полей профиля здесь запрещено (дом расширенного профиля — контекст profile); новые optional поля результата — additive.
  • Logs: UserId, факт смены имени (без значений ФИО); metrics: identity_profile_updates_total{result}; traces — OTel.

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

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

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

  • Сущность: User (INV-4, VO FullName, событие UserUpsertedKafkaEvent).
  • Чтение: GET /api/investors/me; расширенный профиль — docs/06-services/profile/.