CRM Service
Центральный «денежный» bounded context PIN и самый крупный контекст платформы: сквозная воронка продажи недвижимости от клиентской базы агента до завершённой сделки, kanban-воронка застройщика, бронирования, программы покупки, документы и таймлайны сделок.
Назначение
crm владеет всей цепочкой продажи недвижимости PIN: клиентская база агента → лид → заявка на фиксацию →
бронирование → сделка — плюс настраиваемая kanban-воронка застройщика, программы бронирования и программы
покупки, документы сделки и таймлайны. Это центральный контекст платформы: агент ведёт клиентов и лидов,
застройщик решает по заявкам на фиксацию и управляет своей воронкой, инвестор видит свои сделки, документы и
статусы, менеджер ПИН — арбитр по всем сделкам. Самый большой bounded context — центр «денежной» воронки
продаж, где сходятся фиксации, бронирования, оплаты и завершение сделок.
Контекст в платформе
CRM стоит в центре сквозной воронки: слева — контексты, которые питают его событиями и данными (лента лида, записи на показы, данные объекта, роли и реквизиты), справа — контексты, которые реагируют на его события (чаты, комиссии, портфель, статусы объектов). Направление стрелок — направление потока данных/событий.
Metadata
| Поле | Значение |
|---|---|
| Статус | draft |
| Owning team | PIN Platform (CRM squad) |
| Domain | Сделки/продажи недвижимости |
| Data classification | Mixed (PII клиентов, Sensitive: эскроу/документы) |
| Runtime criticality | mission-critical (центр воронки денег) |
Ownership
| Artifact | Owner/Rule |
|---|---|
| PostgreSQL schema | crm |
| API areas | agent / developer / investor / pin-manager / admin / public (лид-заявки, purchase-config) |
| Owned entities | Client, Lead(+LeadEvent), FixationRequest, Deal(+DealMember), PipelineStep(+SEED группы), Booking, BookingProgram, PurchaseProgram(+InstallmentPreset), DealDocument, DealTimelineEvent |
| Produced events | pin.crm.lead-created/lead-expired/fixation-approved/fixation-rejected/deal-created/deal-stage-changed/deal-members-changed/deal-completed/deal-cancelled/booking-created/booking-paid/booking-confirmed/booking-expired/booking-cancelled/document-requested/document-status-changed.v1 |
| Consumed events | pin.referral.lead-attachments.v1, pin.user-activity.property-object-view-ended.v1, pin.user-activity.action-tracked.v1, pin.selections.selection-shared.v1, pin.messenger.deal-chat-status-changed.v1, pin.identity.users.v1, pin.calendar.appointment-created/appointment-confirmed/appointment-cancelled/appointment-completed.v1 |
| Search indexes | Elasticsearch-проекции (ADR-0059): crm-clients, crm-fixations, crm-deals (search/); PostgreSQL-FTS (tsvector) не используется |
| Analytics facts | нет (статистика ЛК — SQL-query по своим таблицам; view-статистика застройщика — контекст dev-stats, ADR-0057) |
Ответственность и границы
Делает: клиентская база агента (/crm/clients); лиды с типизированными хотелками, лентой событий
клиента, сроком жизни +1 месяц с продлением; заявки на фиксацию с дубль-детектом (Elasticsearch: fuzzy
ФИО + точный телефон) и решением застройщика; сделки на единой kanban-доске (системные группы SEED +
настраиваемые этапы застройщика); бронирование Резерв → Бронь с таймерами программ и ручным подтверждением
оплаты; программы покупки (100%/рассрочка/ипотека) с серверным расчётом графика платежей; документы сделки со
статусами проверки; таймлайны; статистика ЛК застройщика/агентства (query-методы по своим таблицам).
- Хранение/статусы объектов — владелец
chessboard; CRM просит смену статуса событиями. - Чаты — владелец
messenger(ADR-0055); CRM публикует события, хранит проекциюChatStatus. - Расчёт/выплата комиссий —
rewards; CRM даёт события фиксаций/сделок. - Реквизиты и персональные документы —
profile; CRM хранит толькоRequisiteSetId. - Эквайринг/платежи — решение 9 контекста: структуры есть, интеграция отложена.
- Хранение бинарных файлов — storage/CDN, только
FileId. - Календарь агента, слоты самозаписи и записи на показы/консультации — владелец
calendar(ADR-0064); CRM потребляетpin.calendar.appointment-*в воронку (consumers/calendar-appointments.md).
- Source of truth: все owned-сущности выше.
- Реплики/проекции: display-поля клиента из
identity(консюмерuser-upserted);ProjectId/BuildingId/цена объекта недвижимости — snapshot изchessboardна момент операции;ChatStatus— проекция messenger.
Зависимости
| Зависимость | Тип | Зачем Нужна | Failure mode |
|---|---|---|---|
PostgreSQL (crm) | db | все агрегаты, outbox | недоступность = недоступность сервиса |
| Elasticsearch | search | проекции поиска клиентов/фиксаций/сделок + дубль-детект фиксаций (ADR-0059) | деградация: полнотекстовый поиск/дубль-детект недоступны; source-of-truth списки читаются из PostgreSQL |
| Kafka | queue | входящие/исходящие события (outbox-first) | деградация: события копятся в outbox, консюмеры отстают |
identity | service | actor/tenant/permissions (JWT), lookup пользователей (GET /investors/lookup) | недоступность = 401/503 на командах |
chessboard | service/events | данные объекта недвижимости (владелец, цена, проект) при создании/смене объекта; статусы объекта недвижимости — консюмер наших booking-событий | создание сделки/брони при недоступном chessboard — отказ операции (ретрай клиентом) |
profile | service | RequisiteSetId при брони/договоре | выбор реквизитов недоступен, остальное работает |
messenger | consumer наших событий | чаты сделок, карточки целевых действий (ADR-0055) | чат отстаёт; ChatStatus = Provisioning/Failed |
referral, user-activity, selections | producer событий для нас | лента событий лида, авто-создание лидов | лента лида отстаёт (eventual) |
calendar | producer событий для нас | записи на показы/консультации (слоты): get-or-create клиента/лида + лента лида по pin.calendar.appointment-*.v1 (ADR-0064) | лид/лента по записям отстают (eventual); календарь и записи работают автономно |
| storage/CDN | external | пресайн-ссылки файлов документов | загрузка/скачивание документов недоступны |
Основные сущности
| Entity | Назначение | Source Of Truth | Документ |
|---|---|---|---|
ClientEntity | Клиентская база агента (/crm/clients) | crm | domain/client.md |
LeadEntity (+LeadEventEntity) | Активная связь инвестор↔агент, хотелки, лента событий, срок жизни | crm | domain/lead.md |
FixationRequestEntity | Заявка на фиксацию клиента у застройщика, дубль-детект (Elasticsearch), решение | crm | domain/fixation-request.md |
DealEntity (+DealMemberEntity) | Сделка: участники, этап доски, объект недвижимости, способ оплаты | crm | domain/deal.md |
PipelineStepEntity + SEED PipelineStepGroup | Одна доска: системные группы + настраиваемые этапы застройщика | crm | domain/pipeline-step.md |
BookingEntity | Бронирование Резерв→Бронь с таймерами и оплатой (US-INV-21) | crm | domain/booking.md |
BookingProgramEntity | Программа бронирования застройщика (платность/сумма/длительности) | crm | domain/booking-program.md |
PurchaseProgramEntity (+InstallmentPresetEntity) | Программы покупки (100%/рассрочка/ипотека, пресеты рассрочки) | crm | domain/purchase-program.md |
DealDocumentEntity | Документы сделки: типы, статусы проверки | crm | domain/deal-document.md |
DealTimelineEventEntity | TPH-таймлайн сделки (выполнено/в процессе/просрочено) | crm | domain/deal-timeline-event.md |
ER-обзор
API
Карта областей (детальные документы — api/<method>.md, создаются следующим слоем ТЗ):
- Agent (
/api/agent/crm/...): клиентская база (list/get/create/update/archive + поиск Elasticsearch), лиды (list/get/create/extend/archive/update-wishes + лента событий; состав участников — add/remove-participant, set-primary-investor), фиксации (list с фильтрами агент/застройщик + поиск Elasticsearch, get, create c override хотелок, cancel), сделки (list/search, get, create из лида по объекту — сразуApplicationпри выполненном гейте вовлечения застройщика либоDraft, вовлечение застройщикаengage-developerDraft → Application, change-property-object, move-step, отправка подборки, передача веденияreassign-executor/ назначение куратораreassign-responsible), бронирование (создать от имени клиента — решение 7), статистика агентства/агента (активные, за периоды, успех/провал, суммы, комиссии изrewards; владелец агентства — переключатель «свои/все»). - Developer (
/api/developer/crm/...): сделки (list/search, карточка), подтверждение документов брони, загрузка договора брони, подтверждение оплаты, запрос/подтверждение документов ДДУ/ДКП, Росреестр, завершение сделки, фиксации (list + поиск Elasticsearch, approve unique/non-unique, reject), этапы воронки (CRUD/цвет/порядок), программы бронирования (CRUD), программы покупки (CRUD пресетов), продление брони, статистика ЛК (фильтры проект/здание/периоды). - Investor (
/api/investor/crm/...): мои лиды/сделки (list, карточка, без фиксаций), таймлайн, документы (upload/download, US-INV-12/13), эскроу (US-INV-19), «Ознакомлен» (US-INV-18), оплата брони — прикрепление чека (US-INV-21), выбор реквизитов при брони. - Pin-manager: сквозные списки/карточки всех сделок/лидов/фиксаций, ручные статусы/этапы, ручные записи таймлайна (US-MGR-01..06; platform-scope permission).
- Public (гейт витрины): заявка на консультацию/презентацию (409
DUPLICATE_PHONEпри дубле телефона), заявка на бронирование (форма ФИО+телефон),getPurchaseConfig+ серверный график платежей. - Admin: операционные ручки — доступны администратору платформы (admin API, ADR-0053).
Events и consumers
| Type | Name | Topic | Назначение | Link |
|---|---|---|---|---|
| Produced | LeadCreatedEvent | pin.crm.lead-created.v1 | уведомление агенту, подтверждение реф-привязки | events/lead-created.md |
| Produced | LeadExpiredEvent | pin.crm.lead-expired.v1 | «продлите лид» агенту | events/lead-expired.md |
| Produced | FixationApprovedEvent | pin.crm.fixation-approved.v1 | право на комиссию (rewards), уведомления | events/fixation-approved.md |
| Produced | FixationRejectedEvent | pin.crm.fixation-rejected.v1 | уведомление агенту | events/fixation-rejected.md |
| Produced | DealCreatedEvent | pin.crm.deal-created.v1 | чат сделки (messenger), уведомления | events/deal-created.md |
| Produced | DealStageChangedEvent | pin.crm.deal-stage-changed.v1 | карточки чата, уведомления | events/deal-stage-changed.md |
| Produced | DealMembersChangedEvent | pin.crm.deal-members-changed.v1 | синк участников чата | events/deal-members-changed.md |
| Produced | DealCompletedEvent | pin.crm.deal-completed.v1 | портфель (investor-owner), комиссионное + реферальное начисления (rewards), объект недвижимости «Продан» (chessboard) | events/deal-completed.md |
| Produced | DealCancelledEvent | pin.crm.deal-cancelled.v1 | освобождение объекта недвижимости, уведомления | events/deal-cancelled.md |
| Produced | BookingCreatedEvent | pin.crm.booking-created.v1 | объект недвижимости → «Резерв» (chessboard), карточка чата | events/booking-created.md |
| Produced | BookingPaidEvent | pin.crm.booking-paid.v1 | карточка «договор оплачен», rewards | events/booking-paid.md |
| Produced | BookingConfirmedEvent | pin.crm.booking-confirmed.v1 | объект недвижимости → «Бронь» + таймер | events/booking-confirmed.md |
| Produced | BookingExpiredEvent | pin.crm.booking-expired.v1 | объект недвижимости → «Свободен», карточка «подтвердите/продлите» | events/booking-expired.md |
| Produced | BookingCancelledEvent | pin.crm.booking-cancelled.v1 | объект недвижимости → «Свободен» | events/booking-cancelled.md |
| Produced | DocumentRequestedEvent | pin.crm.document-requested.v1 | карточка «загрузите документы» (US-INV-16) | events/document-requested.md |
| Produced | DocumentStatusChangedEvent | pin.crm.document-status-changed.v1 | уведомления о статусе документа (US-INV-14) | events/document-status-changed.md |
| Consumed | pin.referral.lead-attachments.v1 | referral | авто-создание клиента/лида по правилам привязки | consumers/referral-registered.md |
| Consumed | pin.user-activity.property-object-view-ended.v1 | user-activity | событие лида PropertyObjectViewed | consumers/user-activity-actions.md |
| Consumed | pin.user-activity.action-tracked.v1 | user-activity | события лида FilterApplied/FavoriteAdded | consumers/user-activity-actions.md |
| Consumed | pin.selections.selection-shared.v1 | selections | события лида SelectionAttached/SelectionViewed | consumers/selection-shared.md |
| Consumed | pin.messenger.deal-chat-status-changed.v1 | messenger | проекция Deal.ChatStatus | consumers/deal-chat-status.md |
| Consumed | pin.identity.users.v1 | identity | display-реплика ФИО/телефона клиента | consumers/identity-user-upserted.md |
| Consumed | pin.calendar.appointment-created.v1 | calendar | get-or-create клиента/лида + лента: запись на показ/консультацию создана | consumers/calendar-appointments.md |
| Consumed | pin.calendar.appointment-confirmed.v1 | calendar | лента лида «встреча подтверждена» | consumers/calendar-appointments.md |
| Consumed | pin.calendar.appointment-cancelled.v1 | calendar | лента лида: запись отклонена/отменена/просрочена (по Reason) | consumers/calendar-appointments.md |
| Consumed | pin.calendar.appointment-completed.v1 | calendar | лента лида: исход встречи (Completed/NoShow) | consumers/calendar-appointments.md |
| Consumed | pin.identity.agent-membership-changed.v1 (Status = Left) | identity | offboarding-каскад: soft-remove участий агента в лидах/сделках агентства + пометка reassign, где он Executor | consumers/agent-offboarding.md |
Все produced-события доставляются через транзакционный outbox; все консюмеры идемпотентны по MessageId
(поле SourceMessageId у ленты лида). Версионирование топиков — суффиксом .vN; изменения контрактов —
additive (breaking-изменения = новая версия топика).
Гарантии доставки и согласованности (cross-service contract)
- Ключ партиционирования и порядок. Каждое
pin.crm.*событие партиционируется по своему агрегату: booking-события — поBookingId, deal-события — поDealId, lead/fixation-события — поLeadId/FixationRequestId. Это гарантирует порядок событий одного агрегата (booking-created→booking-confirmed→booking-expired/booking-cancelledне переставляются местами); межагрегатного порядка нет — консюмер не должен предполагать хронологию событий разных агрегатов. - Окно eventual-consistency. Статус объекта в
chessboard(Свободен/Резерв/Бронь/Продан) меняет консюмер booking/deal-событий асинхронно; типичное окно — доставка события + обработка консюмером (в пределах одного таймер-цикла worker-а). Пока событие не долетело, витрина шахматки показывает прежний статус объекта, а карточка сделки/брони в CRM — уже новый: source of truth процесса — CRM, статуса объекта —chessboard. Промежуточное состояние «бронь оформлена, объект в шахматке ещё Свободен» — ожидаемое, не ошибка. - Поведение при устаревании реплик. Реплика
chessboard(владелец/цена/проект) читается snapshot-ом на момент операции; при недоступности источника команды создания сделки/смены объекта завершаются отказом (CHESSBOARD_UNAVAILABLE, ретрай клиентом), а не работают по устаревшим данным. Display-репликаidentity(ФИО/телефон клиента) обновляется консюмеромuser-upsertedасинхронно; расхождение отображаемого имени — временное и на бизнес-решения (фиксация/сделка) не влияет (snapshot ФИО берётся из карточки клиента на момент операции). Проекции поиска (Elasticsearch, ADR-0059) — тоже eventual: при их отставании/недоступности списки и дубль-детект деградируют, source-of-truth выборки читаются из PostgreSQL. - Идемпотентность downstream. Все fan-out консюмеры саг (
deal-completed, booking-события) идемпотентны по бизнес-ключу (DealId/BookingId): повторная доставка при retry/DLQ не двоит эффект (портфельinvestor-owner, комиссияrewards, статус объектаchessboard).
Sagas
CRM оркеструет два cross-service процесса без отдельного оркестратора-сервиса (ADR-0056): агрегат/сделка
— это состояние саги, фоновые worker-ы — драйверы таймаутов, транзакционный outbox — доставка переходов;
downstream-контексты реагируют хореографией + компенсацией по событиям pin.crm.*. Дом переходов/полей —
в агрегатах (booking.md / deal.md); saga-документы описывают только
кросс-сервисную координацию (участники, state machine, компенсация, таймауты, reconciliation).
| Saga | Оркестратор | Процесс | Компенсация | Документ |
|---|---|---|---|---|
Booking | crm (агрегат BookingEntity = инстанс; worker booking-expiration = таймаут-драйвер) | Резерв → Бронь → передача в продажу | release объекта недвижимости (chessboard Release) при истечении/отмене; страховка — chessboard/property-object-hold-expiration; денежного отката нет (эквайринг отложен) | sagas/booking-saga.md |
Deal Completion | crm (Deal.Complete, событие deal-completed) | Fan-out завершения: chessboard MarkSold + investor-owner портфель + rewards комиссионное и реферальное начисления (продолжение — commission-settlement-saga) | отката нет — завершение терминально (INV-DEAL-9); downstream-сбои → retry/DLQ per-consumer (идемпотентно по DealId) | sagas/deal-completion-saga.md |
Обе саги — фазы сквозных воронок UC-OVR-004 и UC-OVR-006; шаблон — saga-template.
Runtime и эксплуатация
| Область | Требование |
|---|---|
| Health checks | /health/live, /health/ready (PostgreSQL + Kafka), /health/startup (миграции применены) |
| SLO/SLA | команды p95 < 500 мс; списки p95 < 1 с; таймеры брони — точность worker-цикла ≤ 1 мин |
| Background jobs | booking-expiration (цикл 1 мин), lead-expiration (цикл 1 ч), timeline-overdue (цикл 5 мин) — все идемпотентны (ADR-0056) |
| Dashboard | Grafana pin-crm (создаётся при реализации) |
| Alerts | лаг консюмеров > 5 мин; outbox backlog > 1000; ошибки worker-ов; ChatStatus = Failed > 10/ч |
| Runbooks | docs/06-services/crm/runbooks/ (будущий путь по конвенции) |
| Feature flags | нет критических; эквайринг — выключенный модуль (решение 9) |
Безопасность
- Actor types: Инвестор, Агент, Руководитель агентства, Менеджер застройщика, Менеджер ПИН, Служба заботы (read-only + арбитраж), администратор платформы (ADR-0053), система (worker/consumer).
- Permission model: Scoped-RBAC + record-level (
identity/domain/permission.md); permissionscrm.clients.*,crm.leads.*,crm.fixations.*(+.decide),crm.deals.*(+.decide),crm.bookings.*(+.decide),crm.booking-programs.*,crm.purchase-programs.*,crm.deal-documents.*,crm.pipeline.*. - Tenant isolation (ADR-0052): лид/клиент —
TenantId = AgencyCompanyId; фиксация/сделка/бронь/этапы/ программы —TenantId = DeveloperCompanyId+ иммутабельная атрибуцияOriginAgencyCompanyId/OriginAgentUserId, текущий исполнительExecutorAgentUserIdи инвесторы-участники (PrimaryInvestorUserId/InvestorSecondary; у фиксации — скалярInvestorUserId) для record-level/ membership-выборок второй стороны (ADR-0071); кросс-tenant — только platform-scope (Менеджер ПИН, забота). - Sensitive/PII: ФИО/телефоны клиентов (PII), эскроу-счёт (Sensitive, маска
****NNNN), документы (метаданные здесь, бинарь в storage). Инвестор не видит фиксаций; застройщик не видит лидов/клиентской базы. - Audit: аудит-поля (создатель/изменивший, время создания/изменения) + версия на всех агрегатах; бизнес-история — append-only таймлайны.
Известные ограничения и assumptions
- Эквайринг не реализуется (решение 9): оплата брони подтверждается менеджером по чеку.
- Simplicity-first (ADR-0056): PostgreSQL — основная СУБД (таймеры — polling worker-ы без Redis/TTL); целевая нагрузка 1–10k активных. Полнотекстовый поиск (клиенты/фиксации/сделки) и дубль-детект фиксаций вынесены в Elasticsearch-проекции — ADR-0059 отменяет исходный пункт «PostgreSQL-FTS без внешнего поиска» для CRM.
- Комиссии считает
rewards; в статистике агентства CRM показывает данныеrewardsпо API/событиям. - Расхождение статусов: US-INV-03 называет 4 статуса (заявка/бронь/продажа/отказ) — в модели добавлен
терминальный
Completed(см. решение 4 ниже).
Принятые решения контекста
| # | Вопрос | Решение |
|---|---|---|
| 1 | Две точки входа брони (решение 7) | Единый агрегат BookingEntity c InitiatedBy; таймеры — параметры BookingProgramEntity (вкл. ReserveDurationMinutes, default 60) |
| 2 | Хотелки при фиксации | «Дом» — Lead.Wishes; фиксация несёт nullable snapshot WishesOverride (лид не мутируется) |
| 3 | Tenancy двухсторонних сущностей | По ADR-0052: владелец процесса — tenant; вторая сторона — явные колонки + record-level |
| 4 | Статусы сделки | Application/Booking/Sale/Completed/Cancelled; Completed отображается «продажа (завершена)» — расхождение с 4 статусами US зафиксировано |
| 5 | Статусы объекта в шахматке | Меняет chessboard как консюмер booking/deal-событий (границы контекстов); CRM чужие таблицы не пишет |
| 6 | Чат сделки | ADR-0055: события CRM → messenger; ChatStatus — проекция |
| 7 | Дубль-детект фиксаций | Elasticsearch-проекция (fuzzy ФИО, ADR-0059) + точное совпадение телефона (keyword); решение «уникальный/не уникальный» — за менеджером |
| 8 | График платежей | Серверный расчёт графика по формуле рассрочки; дом формулы — domain/purchase-program.md |
Связанные документы
- Домены: domain/ (10 документов выше); события:
events/и консюмеры:consumers/, API:api/, поиск (Elasticsearch-проекции): search/, use-cases:use-cases/, background:background/— следующий слой ТЗ контекста (будущие пути по конвенции). - Обзор: technical-assignment.md, service-boundaries.md, glossary.md.
- ADR: 0052 tenancy, 0055 мини-мессенджер, 0056 simplicity-first, 0059 Elasticsearch-проекции.