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

PUT /api/agent/crm/clients/{clientId} — Редактировать клиента

Правка карточки клиентской базы агента: ФИО (только у незарегистрированного клиента), email и комментарий; опциональная смена телефона с перепроверкой уникальности в агентстве (INV-CLI-1). У зарегистрированного клиента (InvestorUserId != null) display-поля (ФИО, телефон) — дом identity, их синхронизирует консюмер identity-user-upserted.md, поэтому этот метод их не перезаписывает. Дом полей сущности — domain/client.md.

Назначение

Дать агенту, ведущему клиента (участнику его процессов), и руководителю агентства редактировать карточку клиента на странице /crm/clients/[clientId]: заметку, контактный email, ФИО и телефон вручную заведённого (ещё не зарегистрированного) клиента. Оптимистическая блокировка по RowVersion защищает от гонки параллельного редактирования и фонового identity-синка.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areaagent (JWT агента/руководителя агентства)
Feature groupclients
Статусdraft
Документdocs/06-services/crm/api/agent/update-client.md

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

ПроверкаЗначение
Actor typeUser (Агент или Руководитель агентства)
Auth policy / Permissionscrm.clients.manage
Scope (RBAC)организация (агентство TenantId = AgencyCompanyId)
Record-levelдоступ по членству агентства: актор — активный участник процесса клиента или руководитель/администратор агентства того же TenantId (или Менеджер ПИН, platform-scope); иначе CRM_CLIENT_ACCESS_DENIED
Tenant isolationTenantId в каждом запросе
Auditupdate (UpdatedByUserId, UpdatedAt, Version++)
Доступ

Record-level: актор — активный участник процесса клиента или руководитель/администратор агентства того же TenantId (или Менеджер ПИН, platform-scope); иначе CRM_CLIENT_ACCESS_DENIED.

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/agent/crm/clients/{clientId}
Success status200 (карточка обновлена)
Idempotency headerне применяется (оптимистическая блокировка через RowVersion в теле)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequiredTenant isolation
ActorUserIdidentity claimsGuidДаRequiredrecord-level (участник процесса/руководитель)
ClientIdrouteGuidДа!= Guid.Empty; существующая живая карточка
FirstNamebodystringДа1..128, не пустое после trim; применяется только у незарегистрированногоPII
LastNamebodystringДа1..128, не пустое после trim; только у незарегистрированногоPII
MiddleNamebodystring?Нет0..128PII
PhoneNumberbodystringДанормализация и формат E.164 (^\+[1-9]\d{9,14}$), max 32; смена только у незарегистрированногоPII; маска +7***NNNN в логах
Emailbodystring?Нетформат email, 0..256PII
Commentbodystring?Нет0..4000
RowVersionbodyuintДаожидаемая версия строки, полученная при чтении карточкиоптимистическая блокировка

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

Проекция карточки клиента; дом полей — domain/client.md, ниже только проекция.

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаClientEntity.IdИдентификатор карточки.
FirstName / LastName / MiddleNamestring / string / string?Да/Да/НетClientEntity.Name (FullName)ФИО (актуально после правки/синка).
PhoneNumberstringДаClientEntity.PhoneNumberактору с доступом — полностьюТелефон клиента.
Emailstring?НетClientEntity.EmailКонтактный email.
Commentstring?НетClientEntity.CommentЗаметка агента.
StatusClientStatus (enum строкой)ДаClientEntity.StatusActive / Archived.
InvestorUserIdGuid?НетClientEntity.InvestorUserIdПользователь-инвестор; null — не зарегистрирован (ФИО/телефон редактируемы).
ActiveLeadIdGuid?НетClientEntity.ActiveLeadIdТекущий активный лид (INV-CLI-2).
LastActivityAtDateTimeOffset?НетClientEntity.LastActivityAtПоследняя активность.
RowVersionuintДаClientEntity.RowVersionНовая версия строки для следующего оптимистического апдейта.

Алгоритм

  1. Контекст и доступ. Операцию выполняет агент/руководитель агентства (permission crm.clients.manage) в рамках TenantId.
  2. Проверка входа и предусловий.
    • формат: ClientId != Guid.Empty; FirstName/LastName непустые ≤ 128; MiddleName ≤ 128; PhoneNumber — E.164, ≤ 32; Email — формат email ≤ 256 (nullable); Comment ≤ 4000 (nullable); RowVersion задан — иначе VALIDATION_FAILED;
    • карточка существует в TenantId — иначе CRM_CLIENT_NOT_FOUND;
    • состояние: карточка живая (DeletedAt is null); редактирование архивной (Archived) допускается (правка заметки), soft-deleted — нет → CRM_CLIENT_NOT_FOUND;
    • record-level (членство агентства): актор — активный участник процесса клиента или руководитель/администратор агентства того же TenantId (или Менеджер ПИН) — иначе CRM_CLIENT_ACCESS_DENIED.
  3. Что читаем. Карточку клиента (единственный агрегат правки). Уникальность телефона проверяется при фактической смене (шаг 4).
  4. Бизнес-правила и переходы.
    • Display-guard (registered): если клиент зарегистрирован (InvestorUserId != null) и запрошенное ФИО ≠ текущему или нормализованный телефон ≠ текущему → CRM_CLIENT_DISPLAY_READONLY: ФИО/телефон зарегистрированного клиента — дом identity, меняются через identity и приходят консюмером identity-user-upserted.md. Email/Comment редактируемы всегда.
    • Смена телефона (незарегистрированный): если телефон меняется — перепроверить INV-CLI-1: если другая живая карточка агентства уже занимает этот телефон → CRM_CLIENT_PHONE_DUPLICATE.
    • Маппинг: Name ← ФИО (только незарегистрированный); PhoneNumber ← нормализованный (только незарегистрированный); Email ← Email; Comment ← Comment; UpdatedByUserId = actor, UpdatedAt = now, Version++. Переходов статуса нет; доменные события не поднимаются (плоская правка — не бизнес-переход).
  5. Что меняется. Карточка клиента обновляется; поисковая проекция клиента (ClientSearchDocument: ФИО+телефон+email) обновляется проектором → поиск свежий. Оптимистическая блокировка по RowVersion: если строку изменили параллельно (агентом или фоновым identity-синком) — CRM_CONCURRENCY_CONFLICT (409, клиент перечитывает и повторяет).
  6. Результат. Обновлённая карточка с новым RowVersion → HTTP 200.

Диаграмма

Предусловия и постусловия

Предусловия. Актор имеет доступ к карточке по членству агентства (активный участник процесса клиента либо руководитель/администратор агентства того же TenantId), с crm.clients.manage. Карточка существует, живая (DeletedAt is null); допустимы статусы Active и Archived. Клиент располагает актуальным RowVersion, полученным при чтении карточки.

Важные условия

Контракт конкурентности (version-guard). Правка защищена оптимистической блокировкой по RowVersion. Победитель — первый закоммитивший (агент или фоновый identity-синк по identity-user-upserted.md); проигравший получает CRM_CONCURRENCY_CONFLICT, перечитывает свежую карточку и повторяет. Для зарегистрированного клиента ФИО/ телефон — дом identity: метод их не перезаписывает (display-guard), они меняются только через identity и приходят упомянутым консюмером.

Постусловие (успех). Для незарегистрированного клиента (InvestorUserId == null) обновлены Name и (при фактической смене) PhoneNumber; для любого клиента — Email и Comment; UpdatedByUserId = actor, UpdatedAt = now, Version инкрементирован, выдан новый RowVersion. Поисковая проекция ClientSearchDocument (ФИО + телефон + email) обновляется проектором (eventual, ADR-0059). Доменные и интеграционные события не публикуются (плоская правка — не бизнес-переход).

Постусловие (отказ). Карточка не изменена; Version/RowVersion прежние; проекция не трогается.

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

ТипДетали
Интеграционные событияnone (клиент не публикует Kafka-событий, client.md)
Внешние вызовыnone (identity — источник display-полей — синхронизирует консюмер, не эта команда)
Проекции / поискпоисковая проекция клиента (ClientSearchDocument) обновляется проектором (ADR-0059)
Уведомленияnone

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400формат ФИО/телефона/email/comment или отсутствует RowVersion«Проверьте корректность полей»no retry
CRM_CLIENT_NOT_FOUND404карточка не найдена в TenantId или soft-deleted«Клиент не найден»no retry
CRM_CLIENT_ACCESS_DENIED403actor не участник процессов клиента и не руководитель агентства«Нет доступа к карточке клиента»no retry
CRM_CLIENT_DISPLAY_READONLY422попытка сменить ФИО/телефон зарегистрированного клиента«ФИО и телефон зарегистрированного клиента меняются в профиле»no retry
CRM_CLIENT_PHONE_DUPLICATE409INV-CLI-1: новый телефон занят другой живой карточкой агентства«Клиент с таким телефоном уже есть»no retry (сменить телефон)
CRM_CONCURRENCY_CONFLICT409RowVersion не совпал (параллельная правка / identity-синк)«Карточка изменена, обновите и повторите»retry (перечитать → повторить)

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

  • Новые optional поля тела — additive (без breaking change); удаление/переименование/смена типа — breaking (migration + rollout). Enum ClientStatus в ответе — строкой; неизвестное значение клиент игнорирует (forward-compat).
  • Logs: client-updated (ClientId, набор изменённых полей, Version; телефон/email — маской), client-update-conflict, client-update-denied; metrics: crm_client_update_total{result}, latency p95; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy path. Given незарегистрированный клиент (InvestorUserId == null), корректные поля и актуальный RowVersion; When PUT; Then обновлены ФИО/телефон/email/comment, Version++, 200 с новым RowVersion; поисковая проекция обновляется eventual.
  • Display-guard. Given зарегистрированный клиент (InvestorUserId != null) и запрошенное ФИО либо телефон ≠ текущим; When PUT; Then 422 CRM_CLIENT_DISPLAY_READONLY.
  • Email/Comment у зарегистрированного. Given зарегистрированный клиент и изменены только Email/Comment; When PUT; Then 200 — email/комментарий редактируемы всегда.
  • Дубль телефона. Given незарегистрированный клиент, смена телефона на номер, занятый другой живой карточкой агентства; When PUT; Then 409 CRM_CLIENT_PHONE_DUPLICATE (INV-CLI-1).
  • Конкурентность. Given строку изменили параллельно (другой агент или фоновый identity-синк); When PUT с устаревшим RowVersion; Then 409 CRM_CONCURRENCY_CONFLICT; клиент перечитывает и повторяет.
  • Доступ. Given актор не участник процессов клиента и не руководитель агентства того же TenantId; When PUT; Then 403 CRM_CLIENT_ACCESS_DENIED.
  • Не найдена. Given карточка не найдена в TenantId или soft-deleted; When PUT; Then 404 CRM_CLIENT_NOT_FOUND.
  • Валидация. Given пустое ФИО, телефон не по E.164 или отсутствует RowVersion; When PUT; Then 400 VALIDATION_FAILED.
  • Архивная карточка. Given Archived, правка только заметки; When PUT; Then 200 (правка разрешена).

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

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

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