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

GET /api/agent/crm/clients/{clientId} — Карточка клиента

Read-модель карточки клиента агентской воронки (/crm/clients/[clientId]): ФИО, контакты, комментарий, источник, активный лид и счётчики связей (лиды/фиксации/сделки). Дом полей сущности — client.md. Вкладки карточки events/collections/historyотдельные запросы (см. «Побочные эффекты»), в этот ответ не входят.

Назначение

Открыть карточку клиента из списка /crm/clients для агента, ведущего клиента (участника его процессов), или руководителя агентства: показать контактные данные, свободный комментарий, происхождение записи, текущий активный лид пары «агент ↔ инвестор» и агрегированные счётчики дочерних связей.

Metadata

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

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

Доступ
ПроверкаЗначение
Actor typeUser (Агент; либо Руководитель агентства)
Auth policy / Permissionscrm.clients.view (JWT)
Scope (RBAC)организация (агентство AgencyCompanyId)
Record-levelдоступ по членству агентства: актор — активный участник процесса клиента (лид/фиксация/сделка) или руководитель/администратор агентства-владельца (TenantId) или Менеджер ПИН (client.md)
Tenant isolationTenantId = AgencyCompanyId в каждом запросе
Auditread (не sensitive: клиент — данные своего агентства); PII в логи не пишется

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired, != Guid.EmptyTenant isolation (AgencyCompanyId)
ActorUserIdidentity claimsGuidДаRequiredrecord-level (участник процесса/руководитель)
IsAgencyManageridentity claims (роль/permission)boolДавычисляется из scope permissionдаёт доступ к любому клиенту своего TenantId (не только к процессам актора)
ClientIdrouteGuidДа!= Guid.Empty; существующий клиент своего TenantId

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаClientEntity.IdclientId в маршруте карточки.
NameFullNameModelДаClientEntity.Name (VO FullName)ФИО; структура — VO FullName, client.md.
PhoneNumberstringДаClientEntity.PhoneNumberнет (актор с доступом видит полностью; в логах маска +7***NNNN)Телефон клиента (PII).
Emailstring?НетClientEntity.EmailнетКонтактный email (PII).
Commentstring?НетClientEntity.CommentСвободная заметка агента.
SourceClientSource (enum строкой)ДаClientEntity.SourceПроисхождение записи (Manual/ConsultationRequest/BookingRequest/Referral), client.md.
StatusClientStatus (enum строкой)ДаClientEntity.StatusActive/Archived.
InvestorUserIdGuid?НетClientEntity.InvestorUserIdПользователь-инвестор (если зарегистрирован); ключ вкладки collections.
ActiveLeadClientActiveLeadModel?Нетлид по ClientEntity.ActiveLeadIdТекущий активный лид; null — активного лида нет.
CountersClientRelationCountersModelДаcomputedСчётчики дочерних связей.
LastActivityAtDateTimeOffset?НетClientEntity.LastActivityAtПоследнее событие клиента.
CreatedAtDateTimeOffsetДаClientEntity.CreatedAtСоздание карточки.

FullNameModel

Проекция VO FullName (дом — client.md): LastName: string, FirstName: string, MiddleName: string?. Дельты нет — поля переносятся 1:1.

ClientActiveLeadModel

ПолеТипОбязательностьИсточникОписание
LeadIdGuidДаLeadEntity.IdАктивный лид пары (переход в карточку лида).
StatusLeadStatus (enum строкой)ДаLeadEntity.StatusВсегда Active (инвариант ActiveLeadId, INV-CLI-2).
SourceLeadSource (enum строкой)ДаLeadEntity.SourceКак появился лид (lead.md).
ExpiresAtDateTimeOffsetДаLeadEntity.ExpiresAtСрок жизни лида.
LastEventAtDateTimeOffset?НетLeadEntity.LastEventAtПоследнее событие ленты лида.

ClientRelationCountersModel

ПолеТипОбязательностьИсточникОписание
LeadsCountintДачисло лидов клиента (ClientId)Всего лидов клиента (lead.md).
FixationRequestsCountintДачисло заявок на фиксацию (ClientId)Всего заявок (fixation-request.md).
DealsCountintДачисло сделок клиента (ClientId)Всего сделок (deal.md).

Алгоритм

  1. Контекст и доступ. Операцию выполняет агент (permission crm.clients.view) в tenant своего агентства (TenantId = AgencyCompanyId); признак IsAgencyManager даёт доступ к любому клиенту своего TenantId.
  2. Проверка входа и предусловий.
    • формат: ClientId != Guid.Empty, TenantId != Guid.Empty — иначе ошибка валидации;
    • клиент существует — иначе CRM_CLIENT_NOT_FOUND (404);
    • tenant isolation: клиент того же TenantId; чужой tenant трактуется как CRM_CLIENT_NOT_FOUND (без утечки существования);
    • record-level (членство агентства): актор — активный участник процесса клиента (лид/фиксация/сделка) или IsAgencyManager (руководитель/администратор TenantId) или Менеджер ПИН — иначе FORBIDDEN (403);
    • состояние: чтение разрешено для Active и Archived (архивная карточка открывается).
  3. Что читаем. Карточку клиента; активный лид (если ActiveLeadId != null) — проекция в ClientActiveLeadModel; скалярные счётчики лидов/фиксаций/сделок клиента в границах TenantId. Списки дочерних сущностей здесь не загружаются (они — отдельные вкладки).
  4. Маппинг. Поле-в-поле ClientEntityClientCardModel; лид → ClientActiveLeadModel; счётчики → ClientRelationCountersModel. Enum-поля сериализуются строкой.
  5. Результат. Карточка клиента → HTTP 200. Freshness — актуально на момент запроса (ФИО зарегистрированного клиента — eventual-реплика из identity). Ошибки → ProblemDetails (404/403/400).

Диаграмма

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

ТипДетали
Интеграционные событияnone (безопасный GET)
Внешние вызовыnone (ФИО зарегистрированного клиента — уже реплика от identity в crm.clients)
Проекции / поискnone (чтение)
Уведомленияnone
Отдельные запросы вкладокevents — лента через лиды клиента (lead.md); collections — API selections по InvestorUserId (selections README); history — списки лидов/фиксаций/сделок. В этот ответ не входят.

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400ClientId/TenantId пустой«Некорректный идентификатор клиента»no retry
CRM_CLIENT_NOT_FOUND404клиент не найден или TenantId не совпал«Клиент не найден»no retry
FORBIDDEN403актор не участник процессов клиента и не руководитель агентства/Менеджер ПИН«Нет доступа к карточке клиента»no retry

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

  • Новые optional поля ClientCardModel — additive (без breaking change); удаление/переименование/смена типа — breaking (migration + rollout). Неизвестные значения enum (ClientSource/ClientStatus/LeadSource) — клиент трактует как «прочее», не падает.
  • Logs: client-card viewed (ClientId, TenantId, ActorUserId; телефон/email — не пишутся); metrics: crm_client_card_requests_total{result}, latency p95 (< 1 с, SLO чтений README); traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy path. Given агент, ведущий клиента (участник его процессов), и существующий клиент своего TenantId; When GET; Then 200 с ClientCardModel (контакты, источник, активный лид, счётчики), телефон — полностью, в логах маска +7***NNNN.
  • Руководитель. Given руководитель агентства и клиент своего TenantId другого агента; When GET; Then 200 (доступ по членству открыт признаком IsAgencyManager — все клиенты TenantId).
  • Чужой клиент. Given рядовой агент и клиент того же TenantId, в процессах которого актор не участвует (создан другим агентом); When GET; Then 403 FORBIDDEN.
  • Другой tenant. Given клиент другого TenantId; When GET; Then 404 CRM_CLIENT_NOT_FOUND (без утечки существования).
  • Не найден. Given несуществующий ClientId; When GET; Then 404 CRM_CLIENT_NOT_FOUND.
  • Валидация. Given ClientId/TenantId = Guid.Empty; When GET; Then 400 VALIDATION_FAILED.
  • Без активного лида. Given ActiveLeadId == null; When GET; Then 200, ActiveLead = null, счётчики корректны.
  • Архивная карточка. Given Status = Archived; When GET; Then 200 (архивная карточка открывается).

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

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

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