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 |
| Kind | AggregateRoot |
| Source of truth | crm (карточка клиента); ФИО/телефон зарегистрированного пользователя — identity (реплика display-полей) |
| Таблица | crm.clients |
| Tenant isolation | TenantId = AgencyCompanyId (ADR-0052: лид/клиентская база — процесс агентства) |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор клиента (clientId в маршрутах /crm/clients/[clientId]). |
TenantId | Guid | Да | index ix_clients_tenant_id; часть unique-индексов ниже | identity (actor) | Компания агентства (AgencyCompanyId). |
OriginAgentUserId | Guid | Да | index ix_clients_origin_agent_user_id; ссылка на identity.users(id) (логическая, без FK между контекстами); иммутабельно | app | Агент — создатель карточки (историческая атрибуция клиентской базы). Не единоличный владелец: доступ к клиенту определяется членством в агентстве-владельце (TenantId) и составом участников его процессов — см. Безопасность. Не переписывается при смене агентом агентства (ADR-0061 п.4). |
InvestorUserId | Guid? | Нет | 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 — клиент добавлен вручную до регистрации. |
Status | ClientStatus | Да | enum как text, varchar(32), index ix_clients_status | app | См. enum ниже. |
Name | FullName (owned) | Да | см. VO ниже | app (форма агента / lazy-регистрация) | ФИО клиента. Для зарегистрированного клиента синхронизируется консюмером pin.identity.users.v1 (display-реплика). |
PhoneNumber | string | Да | varchar(32), нормализация E.164; unique ux_clients_tenant_phone on (TenantId,PhoneNumber) where deleted_at is null | app | Телефон клиента — ключ сопоставления с identity при lazy-регистрации и с дубль-проверкой фиксаций. |
Email | string? | Нет | varchar(256), формат email | app | Контактный email. |
Comment | string? | Нет | varchar(4000) | app | Свободная заметка агента о клиенте. |
Source | ClientSource | Да | enum как text, varchar(32) | app / consumer | Откуда появился клиент (см. enum ниже). Иммутабельно. |
ActiveLeadId | Guid? | Нет | FK fk_clients_active_lead_id → crm.leads(id) (on delete set null) | app | Денормализованная ссылка на текущий активный лид этого клиента (быстрый переход из списка; поддерживается инвариантом INV-CLI-2). |
LastActivityAt | DateTimeOffset? | Нет | index ix_clients_last_activity_at | consumer / app | Момент последнего события клиента (просмотр, заявка, действие в сделке) — сортировка списка /crm/clients. |
CreatedAt | DateTimeOffset | Да | index ix_clients_created_at | app | Создание карточки. |
CreatedByUserId | Guid? | Нет | — | app | Агент/консюмер, создавший карточку (null — системный консюмер). |
UpdatedAt | DateTimeOffset | Да | — | app | Дата последнего изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | — | app | Soft-delete (архив карточки). |
DeletedByUserId | Guid? | Нет | — | 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.
| Поле | Тип | Обязательность | Ограничения | Колонка |
|---|---|---|---|---|
FirstName | string | Да | varchar(128), не пустое | name_first_name |
LastName | string | Да | varchar(128), допускается пустая строка при lazy-создании | name_last_name |
MiddleName | string? | Нет | varchar(128) | name_middle_name |
Модели JSONB (versioned)
JSONB-модели не используются.
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
LeadEntity (лиды клиента) | reference (child по FK) | leads.client_id → clients.id | выборка по ClientId (вкладка «история») | restrict (клиент не удаляется при живых лидах; только soft-delete) |
FixationRequestEntity | reference | fixation_requests.client_id → clients.id | выборка по ClientId | restrict |
DealEntity | reference | deals.client_id → clients.id | выборка по ClientId | restrict |
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-2 | ActiveLeadId указывает только на лид этого клиента со статусом 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 |
Историю клиента держат его процессы — лиды, фиксации, сделки — у каждого свой иммутабельный
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 → Archived | Id, 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) не используется |
| Партиционирование / retention | none (≤ 20k агентов × сотни клиентов — обычная таблица, ADR-0056) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую) |
| Permissions / PII | crm.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) |
InvestorUserId | identity (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.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (5): Entity / Aggregate: ArbitrageCaseEntity (+ ArbitrageNoteEntity), Entity / Aggregate: DealEntity, Entity / Aggregate: FixationRequestEntity, Entity / Aggregate: LeadEntity, Entity / Aggregate: TicketEntity
- API (25): GET /api/agent/crm/clients — Клиентская база агента (список + поиск), GET /api/agent/crm/clients/{clientId} — Карточка клиента, GET /api/agent/crm/deals — Сделки агента (список/поиск), GET /api/agent/crm/deals/{dealId} — Карточка сделки (агент), GET /api/agent/crm/fixations — Список фиксаций агентства (+ поиск), GET /api/agent/crm/fixations/{fixationId} — Карточка фиксации (агент), GET /api/agent/crm/leads — Список лидов агента, GET /api/agent/crm/leads/{leadId} — Карточка лида, GET /api/agent/crm/leads/{leadId}/events — Лента событий лида, GET /api/developer/crm/deals/{dealId} — Карточка сделки (застройщик), GET /api/developer/crm/fixations — Список фиксаций застройщика (+ FTS), GET /api/pin-manager/crm/deals — Сквозной список всех сделок платформы (+13)
- Use Cases (4): UC-ACT-004. Агент/менеджер видит активность инвестора (для карточки лида), UC-BKG-001. Инвестор инициирует бронирование с витрины, UC-CRM-001. Создание лида (заявка / реферал / вручную), UC-FIX-001. Агент отправляет заявку на фиксацию
- События (1): Event Contract: pin.crm.lead-created (LeadCreatedEvent)
- Консюмеры (4): Consumer: CRM Calendar Appointments Sync (crm-calendar-appointments), Consumer: CRM Identity User Sync (CrmUserUpsertedConsumer), Consumer: ReferralRegisteredConsumer (pin.referral.lead-attachments.v1), Consumer: User-Activity Actions (crm-user-activity-actions)
- Поиск (1): ClientSearchDocument
- adr (1): ADR-0071: Владение процессами CRM компанией и единая модель участников
- index.md (1): CRM Service
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- lead.md, fixation-request.md, deal.md — дочерние процессы клиента.
- Поиск клиентской базы — Elasticsearch-проекция ../search/clients.md (ADR-0059).
- README контекста; tenancy — ADR-0052.