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

Entity / Aggregate: ClientEntity

Дом полей сущности «Клиент агента» контекста crm — клиентская база агента (списки клиентов /crm/clients и карточка клиента /crm/clients/[clientId]/{events,collections,history}). Все проекции (списки клиентской базы, карточка клиента, payload событий) ссылаются сюда и описывают только дельту.

Назначение

ClientEntity — запись клиентской базы агентства: карточка человека (потенциального или действующего инвестора), которого ведёт конкретный агент. Клиент — «адресная книга + агрегатор» связей: на одного клиента ссылаются его лиды (lead.md), заявки на фиксацию (fixation-request.md) и сделки (deal.md). Появляется автоматически при создании первого лида пары «агент ↔ инвестор» либо вручную (агент добавил клиента в базу до какой-либо активности). О клиентской базе агентства не знает застройщик — это внутренние данные агентства («о лиде не знает застройщик»).

Клиент ≠ пользователь identity: InvestorUserId может быть null, пока человек не зарегистрирован (lazy-регистрация происходит при заявке — см. identity/domain/user.md, RegistrationSource = LazyRegistration).

Классификация

ПолеЗначение
Контекстcrm
KindAggregateRoot
Source of truthcrm (карточка клиента); ФИО/телефон зарегистрированного пользователя — identity (реплика display-полей)
Таблицаcrm.clients
Tenant isolationTenantId = AgencyCompanyId (ADR-0052: лид/клиентская база — процесс агентства)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор клиента (clientId в маршрутах /crm/clients/[clientId]).
TenantIdGuidДаindex ix_clients_tenant_id; часть unique-индексов нижеidentity (actor)Компания агентства (AgencyCompanyId).
OriginAgentUserIdGuidДаindex ix_clients_origin_agent_user_id; ссылка на identity.users(id) (логическая, без FK между контекстами); иммутабельноappАгент — создатель карточки (историческая атрибуция клиентской базы). Не единоличный владелец: доступ к клиенту определяется членством в агентстве-владельце (TenantId) и составом участников его процессов — см. Безопасность. Не переписывается при смене агентом агентства (ADR-0061 п.4).
InvestorUserIdGuid?Нетunique ux_clients_tenant_investor on (TenantId,InvestorUserId) where investor_user_id is not null and deleted_at is null; логическая ссылка на identity.users(id)app / consumer лид-заявкиПользователь-инвестор, если зарегистрирован. null — клиент добавлен вручную до регистрации.
StatusClientStatusДаenum как text, varchar(32), index ix_clients_statusappСм. enum ниже.
NameFullName (owned)Дасм. VO нижеapp (форма агента / lazy-регистрация)ФИО клиента. Для зарегистрированного клиента синхронизируется консюмером pin.identity.users.v1 (display-реплика).
PhoneNumberstringДаvarchar(32), нормализация E.164; unique ux_clients_tenant_phone on (TenantId,PhoneNumber) where deleted_at is nullappТелефон клиента — ключ сопоставления с identity при lazy-регистрации и с дубль-проверкой фиксаций.
Emailstring?Нетvarchar(256), формат emailappКонтактный email.
Commentstring?Нетvarchar(4000)appСвободная заметка агента о клиенте.
SourceClientSourceДаenum как text, varchar(32)app / consumerОткуда появился клиент (см. enum ниже). Иммутабельно.
ActiveLeadIdGuid?НетFK fk_clients_active_lead_id → crm.leads(id) (on delete set null)appДенормализованная ссылка на текущий активный лид этого клиента (быстрый переход из списка; поддерживается инвариантом INV-CLI-2).
LastActivityAtDateTimeOffset?Нетindex ix_clients_last_activity_atconsumer / appМомент последнего события клиента (просмотр, заявка, действие в сделке) — сортировка списка /crm/clients.
CreatedAtDateTimeOffsetДаindex ix_clients_created_atappСоздание карточки.
CreatedByUserIdGuid?НетappАгент/консюмер, создавший карточку (null — системный консюмер).
UpdatedAtDateTimeOffsetДаappДата последнего изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?НетappSoft-delete (архив карточки).
DeletedByUserIdGuid?НетappКто архивировал.

Enum ClientStatus

ЗначениеОписание
ActiveКлиент в работе (есть или ожидается активность).
ArchivedКарточка убрана из рабочего списка (без удаления истории).

Enum ClientSource

ЗначениеОписание
ManualАгент добавил клиента вручную в /crm/clients.
ConsultationRequestСоздан консюмером лид-заявки «консультация/презентация» (UC-1/UC-2).
BookingRequestСоздан консюмером заявки на бронирование (UC-3).
ReferralСоздан по событию реферальной привязки pin.referral.lead-attachments.v1.

Value objects

FullName (owned)

Единый VO SDK, тот же контракт, что в identity/domain/user.md.

ПолеТипОбязательностьОграниченияКолонка
FirstNamestringДаvarchar(128), не пустоеname_first_name
LastNamestringДаvarchar(128), допускается пустая строка при lazy-созданииname_last_name
MiddleNamestring?Нетvarchar(128)name_middle_name

Модели JSONB (versioned)

JSONB-модели не используются.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
LeadEntity (лиды клиента)reference (child по FK)leads.client_id → clients.idвыборка по ClientId (вкладка «история»)restrict (клиент не удаляется при живых лидах; только soft-delete)
FixationRequestEntityreferencefixation_requests.client_id → clients.idвыборка по ClientIdrestrict
DealEntityreferencedeals.client_id → clients.idвыборка по ClientIdrestrict
UserEntity (identity)replica display-полейInvestorUserId (логическая)по событию pin.identity.users.v1
События клиента (вкладка events)проекцияLeadEventEntity.LeadId ∈ leads(ClientId)выборка через лиды клиента (lead.md)
Подборки клиента (вкладка collections)внешняя ссылкаselections API по InvestorUserIdзапрос в selections (документ — docs/06-services/selections/README.md)

Инварианты и переходы состояний

ИнвариантУсловиеКогда проверяетсяОшибка
INV-CLI-1Телефон уникален в рамках агентства: не более одной живой (DeletedAt is null) карточки с одним PhoneNumber на TenantIdпри создании карточки (unique ux_clients_tenant_phone)CRM_CLIENT_PHONE_DUPLICATE
INV-CLI-2ActiveLeadId указывает только на лид этого клиента со статусом Active; при закрытии лида поле обнуляетсяпри привязке/отвязке активного лидаCRM_CLIENT_ACTIVE_LEAD_MISMATCH
INV-CLI-3Зарегистрированный пользователь привязан к агентству не более одного раза (TenantId,InvestorUserId)в консюмерах (unique ux_clients_tenant_investor)CRM_CLIENT_INVESTOR_DUPLICATE
INV-CLI-4Архивировать (Archived/soft-delete) можно только клиента без активных лидов, ожидающих фиксаций и незавершённых сделокпри архивировании карточкиCRM_CLIENT_HAS_ACTIVE_RELATIONS
Историческая привязка (ADR-0061 п.4)

Историю клиента держат его процессы — лиды, фиксации, сделки — у каждого свой иммутабельный record-level snapshot AgencyCompanyId/AgentUserId (INV-LEAD-6, INV-FIX-7, INV-DEAL-10), который не пересчитывается и не back-fill-ится при смене агентом агентства. Карточка клиента остаётся в клиентской базе агентства-владельца (TenantId); при уходе агента из агентства закрытые процессы не переносятся в новую компанию (иначе исказятся воронка агентства и основания комиссий rewards). Личная статистика самого агента — user-centric, дом — rewards (agent-performance.md).

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
ClientCreatedдоменноеСоздание карточки (любой Source)Id, TenantId, OriginAgentUserId, InvestorUserId?, Source
ClientArchivedдоменноеActive → ArchivedId, TenantId, DeletedByUserId

Интеграционных Kafka-событий клиент не публикует: клиентская база — внутренние данные агентства; внешние контексты реагируют на события лидов/сделок.

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK btree (id); ix_clients_tenant_id btree; ix_clients_origin_agent_user_id btree; ux_clients_tenant_phone unique partial; ux_clients_tenant_investor unique partial; ix_clients_status btree; ix_clients_last_activity_at btree desc; ix_clients_created_at btree
Полнотекстовый поискElasticsearch-проекция ClientSearchDocument (../search/clients.md, ADR-0059) — ФИО + нормализованный телефон + email; PostgreSQL-FTS (tsvector/GIN) не используется
Партиционирование / retentionnone (≤ 20k агентов × сотни клиентов — обычная таблица, ADR-0056)
Concurrencyоптимистическая блокировка (правка не должна затирать чужую)
Permissions / PIIcrm.clients.view / crm.clients.manage (каталог — identity/domain/permission.md). Доступ к карточке клиента — по членству агентства, а не по «OriginAgentUserId = actor»: актор видит клиента, если он входит в активный состав участников хотя бы одного процесса клиента (лид/фиксация/сделка), ИЛИ он руководитель/администратор агентства-владельца (TenantId = AgencyCompanyId) — видит всех клиентов своего tenant с фильтром по агенту, ИЛИ Менеджер ПИН — сквозной доступ permission уровня «платформа» (ADR-0052). Создатель карточки (OriginAgentUserId) обычно входит в состав участников своих процессов, но потеря членства/переуступка не отзывает доступ у нового активного исполнителя. Застройщик и инвестор доступа не имеют. PII: PhoneNumber/Email/Name — класс PII; в логи — только Id; в списках других агентов не показываются.
Auditотметки создания/изменения/удаления (кто и когда), soft-delete

Кросс-сервисные контракты и реплики

ДанныеИсточникКак попадаетПоведение при отсутствии/устаревании
Name (ФИО), display-поля зарегистрированного клиентаidentityконсюмер pin.identity.users.v1 по InvestorUserIdдо прихода события ФИО — то, что ввёл агент/lazy-регистрация; расхождение с identity лечится следующим upsert-событием (identity — источник истины для display)
InvestorUserIdidentity (lazy-регистрация)консюмер лид/booking-заявки при регистрациипока null — клиент ведётся как незарегистрированный; сопоставление с identity — по нормализованному PhoneNumber
Подборки клиента (вкладка collections)selectionsсинхронный запрос по InvestorUserIdесли сервис недоступен/данных нет — блок подборок пуст, карточка функциональна

Собственных интеграционных Kafka-событий клиент не публикует — клиентская база внутренняя; внешние контексты реагируют на события лидов/сделок/фиксаций, где клиент присутствует как id.

Межсервисные последовательности и промежуточные состояния

Создание клиента консюмером. При первой заявке пары «агент ↔ инвестор» (консультация/бронирование) или реферальной привязке консюмер создаёт карточку клиента (если её нет) и лид в одном согласованном изменении на стороне crm. Ключ сопоставления с существующей карточкой — нормализованный PhoneNumber в рамках TenantId (INV-CLI-1). Порядок: клиент → лид → событие ленты.

Регистрация ранее ручного клиента. Агент завёл клиента вручную (InvestorUserId = null) → позже человек регистрируется (lazy) → консюмер проставляет InvestorUserId по совпадению телефона; далее ФИО синхронизируется из identity. Двойной привязки одного пользователя к агентству не возникает (INV-CLI-3).

  • Что видит агент. Ручная карточка сразу в списке /crm/clients; после регистрации в ней появляется связанный пользователь и его подборки/история — по мере доставки событий.

Отказы, идемпотентность и конкуренция

  • Дедуп по телефону. INV-CLI-1 (unique partial ux_clients_tenant_phone): гонка двух источников с одним телефоном → ровно одна живая карточка; конкурент переиспользует её либо получает CRM_CLIENT_PHONE_DUPLICATE.
  • Дедуп по пользователю. INV-CLI-3 (unique partial ux_clients_tenant_investor): повторная привязка того же InvestorUserId к агентству отклоняется (CRM_CLIENT_INVESTOR_DUPLICATE).
  • Согласованность ActiveLeadId. Поле указывает только на Active-лид этого клиента; при закрытии лида обнуляется (INV-CLI-2); рассинхрон → CRM_CLIENT_ACTIVE_LEAD_MISMATCH.
  • Архивирование при живых связях. Архивировать/скрыть можно только клиента без активных лидов, ожидающих фиксаций и незавершённых сделок (INV-CLI-4) → иначе CRM_CLIENT_HAS_ACTIVE_RELATIONS.
  • Реплика identity устарела. Display-ФИО может кратко отличаться от identity; это не блокирует работу — выравнивается следующим user-upserted; источник истины для процессов — сам crm (телефон/связи).
  • Иммутабельная историческая привязка. Процессы клиента (лиды/фиксации/сделки) держат собственные record-level snapshot агентства/агента (ADR-0061 п.4); при уходе агента из агентства они не переносятся, карточка остаётся в базе агентства-владельца.

Acceptance-критерии

  • Happy (авто-создание). Дано: заявка пары, карточки клиента ещё нет. Когда консюмер обрабатывает заявку. Тогда есть карточка Active с телефоном, привязанный лид, событие ClientCreated.
  • Дубль телефона. Дано: живая карточка с телефоном X. Когда создаётся вторая с тем же X. Тогда отказ CRM_CLIENT_PHONE_DUPLICATE, второй карточки нет.
  • Lazy-привязка. Дано: ручная карточка InvestorUserId = null. Когда человек регистрируется с тем же телефоном. Тогда InvestorUserId проставлен, ФИО синхронизируется из identity.
  • Архив с активной сделкой. Дано: у клиента незавершённая сделка. Когда агент архивирует карточку. Тогда отказ CRM_CLIENT_HAS_ACTIVE_RELATIONS.
  • Видимость по членству. Дано: агент A и клиент, созданный агентом B в том же агентстве, в чьих процессах A не участвует. Когда A запрашивает список. Тогда клиент не виден A (виден B как участнику и руководителю/администратору агентства с фильтром по агенту). Когда A становится активным участником процесса этого клиента (переуступка/со-агент). Тогда клиент становится виден A.

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

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

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