Entity / Aggregate: DeveloperEntity
Дом полей агрегата «Застройщик» контекста chessboard. Все проекции (модели ответов API, payload событий,
реплики измерений в dev-stats, витрина catalog) ссылаются сюда и описывают только дельту.
Назначение
DeveloperEntity — профиль застройщика в PIN: публичная карточка девелопера (название, лого, описание,
опыт, показатели) и корень иерархии шахматки Developer → Project → Building → PropertyObject. Сущность
двойственная:
- реплика застройщика из внешней системы
talent.microservices.chessboard(базовые данные приходят синком — ../consumers/talent-chessboard-sync.md); - плюс дополнительные PIN-данные (описание, показатели надёжности, публичность), которыми владеет этот контекст и которые синком не перезаписываются.
Связь с identity: застройщик как организация — это CompanyEntity c Type = Developer
(../../identity/domain/company.md); DeveloperEntity.Id равен
CompanyEntity.Id, что делает его одновременно значением TenantId для всех дочерних сущностей шахматки
(ADR-0052: TenantId = CompanyId).
Классификация
| Поле | Значение |
|---|---|
| Контекст | chessboard |
| Kind | AggregateRoot (гибрид с репликой Talent: реплика-поля + локальные PIN-поля) |
| Source of truth | chessboard (PIN-поля) / talent.microservices.chessboard (реплика-поля) / identity (существование компании, её статус) |
| Таблица | chessboard.developers |
| Tenant isolation | Сама сущность — корень tenant шахматки: Id = CompanyId; таблица не tenanted (platform-level чтение каталога), запись — только своим tenant-ом/синком |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK; логическая ссылка на identity.companies.id (без физического FK — другой контекст) | consumer pin.identity.companies.v1 / синк Talent | Идентификатор застройщика = CompanyId в identity. Единый id снимает lookup «компания ↔ застройщик» и задаёт TenantId дочерних сущностей. |
Status | DeveloperStatus | Да | enum (текст), varchar(32), index ix_developers_status | app | Публичность профиля: Draft / Published / Hidden (см. stateDiagram ниже). |
Name | string | Да | varchar(256), min 1, index ix_developers_name (lower) | Talent sync / consumer identity | Публичное название застройщика. Реплика-поле: при SyncSource = TalentChessboard управляется синком (INV-DEV-3). |
LegalName | string? | Нет | varchar(512) | Talent sync / app | Юридическое наименование (для карточки и деклараций). |
Description | string? | Нет | varchar(4000) | app (PIN-поле) | Публичное описание застройщика в PIN; синком не перезаписывается. |
LogoFileId | Guid? | Нет | без FK (внешний storage/CDN) | Talent sync / app | Файл логотипа во внешнем storage. |
Website | string? | Нет | varchar(256), валидация URL | app | Сайт застройщика. |
ContactPhone | string? | Нет | varchar(32), нормализация E.164 | app | Публичный телефон отдела продаж (не PII конкретного пользователя). |
ContactEmail | string? | Нет | varchar(256), формат email | app | Публичный email. |
FoundedYear | short? | Нет | check founded_year between 1900 and 2100 | app / Talent sync | Год основания (карточка «опыт застройщика»). |
Highlights | DeveloperHighlightsDocument (jsonb) | Да (дефолт при создании) | jsonb, typed, ContractVersion | app (PIN-поле) | Показатели карточки: сдано объектов, строится, надёжность (см. раздел JSONB). |
ProjectsPublishedCount | int | Да | >= 0, default 0 | вычисляется (background) | Денормализованный счётчик опубликованных проектов; пересчитывается по доменным событиям проекта. |
ExternalTalentDeveloperId | Guid? | Нет | unique ux_developers_external_talent_developer_id (where not null) | Talent sync | Id застройщика в Talent Chessboard. null — профиль создан в PIN и не синхронизируется. |
SyncSource | ChessboardSyncSource | Да | enum (текст), varchar(32) | app / consumer | Pin (создан в PIN) / TalentChessboard (реплика). Определяет набор реплика-полей (INV-DEV-3). Enum — reference-data.md. |
ReplicaVersion | long? | Нет (обязателен при SyncSource = TalentChessboard) | >= 0 | Talent sync | Версия агрегата в событии Talent; защита от out-of-order (INV-DEV-4). |
LastSyncedAt | DateTimeOffset? | Нет | — | Talent sync | Время применения последнего события синка. |
CreatedAt | DateTimeOffset | Да | index ix_developers_created_at | app | Дата создания. |
CreatedByUserId | Guid? | Нет | без FK (system-actor для синка) | app | Кто создал. |
UpdatedAt | DateTimeOffset | Да | — | app | Дата изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | partial-index filter where deleted_at is null на рабочие индексы | app | Soft-delete (застройщик не удаляется физически — на него ссылаются проекты, сделки, статистика). |
DeletedByUserId | Guid? | Нет | — | app | Кто удалил (архивировал). |
Version | long | Да | >= 1 | app | Версия агрегата — идёт в payload событий. |
Enum DeveloperStatus (хранится текстом)
| Значение | Описание |
|---|---|
Draft | Профиль создан (синком или менеджером), не показывается в публичном каталоге; данные заполняются. |
Published | Профиль публичен: карточка застройщика видна в каталоге, проекты могут публиковаться. |
Hidden | Профиль скрыт решением ПИН/застройщика: карточка и все проекты застройщика не отдаются в public-проекции; данные сохранны. |
Модели JSONB (versioned)
DeveloperHighlightsDocument (jsonb, колонка highlights, ContractVersion = 1)
Показатели публичной карточки застройщика (надёжность, счётчики опыта).
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
ContractVersion | int | Да | = 1 | v1 |
ObjectsDeliveredCount | int? | Нет | >= 0 | v1 |
ObjectsInProgressCount | int? | Нет | >= 0 | v1 |
YearsOnMarket | short? | Нет | 0..200; если null — вычисляется фронтом из FoundedYear | v1 |
ReliabilityValue | decimal? | Нет | 0..ReliabilityMax, precision (4,1) | v1 |
ReliabilityMax | decimal | Да | default 10, precision (4,1) | v1 |
AwardTitles | IReadOnlyList<string> | Да (может быть пустым) | элемент varchar(256), ≤ 20 элементов | v1 |
Добавление поля — additive (той же версией, nullable/default); изменение семантики —
ContractVersion + 1. Секреты/PII в jsonb запрещены.
Связи
| Связь | Тип | Ключ | Delete behavior |
|---|---|---|---|
ProjectEntity (проекты застройщика) | reference (обратная) | projects.developer_id → developers.id | restrict (застройщик не удаляется при живых проектах; Hidden каскадно скрывает public-проекции) |
CompanyEntity (identity) | replica (логическая) | developers.id = identity.companies.id | — (suspend компании ⇒ Hidden, см. события) |
| Talent developer | replica | ExternalTalentDeveloperId | — |
Инварианты и переходы состояний
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-DEV-1 IdEqualsCompanyId | Id существует как CompanyEntity.Id c Type = Developer в identity | при создании профиля (через реплику компаний / admin-операцию) | CHESSBOARD_DEVELOPER_COMPANY_NOT_FOUND |
| INV-DEV-2 UniqueTalentDeveloper | ExternalTalentDeveloperId уникален | при применении события синка (upsert по внешнему id) | CHESSBOARD_DEVELOPER_TALENT_DUPLICATE |
| INV-DEV-3 SyncOwnedFields | При SyncSource = TalentChessboard поля Name, LegalName, LogoFileId, FoundedYear меняются только синком; PIN-поля (Description, Highlights, Website, контакты, Status) — только API | при изменении полей (раздельные пути) | CHESSBOARD_DEVELOPER_FIELD_SYNC_MANAGED |
| INV-DEV-4 MonotonicReplica | Событие синка применяется только при event.Version > ReplicaVersion | при применении события синка | идемпотентный пропуск |
| INV-DEV-5 PublishRequiresData | Публикация допустима только при заполненных Name, LogoFileId, Description | при публикации | CHESSBOARD_DEVELOPER_PUBLISH_INCOMPLETE |
| INV-DEV-6 HiddenCascades | Status = Hidden ⇒ public-проекции всех проектов застройщика не отдаются (фильтр при чтении, не мутация проектов) | при чтении public-проекций + событие для catalog | — |
Переходы состояний (из / в / условие / инициатор / событие)
| Из | В | Условие (guard) | Инициатор | Порождает |
|---|---|---|---|---|
[*] | Draft | Id существует как CompanyEntity.Id c Type = Developer (INV-DEV-1) | consumer pin.identity.companies.v1 / синк Talent / admin | DeveloperCreatedEvent + DeveloperUpsertedKafkaEvent |
Draft | Published | заполнены Name, LogoFileId, Description (INV-DEV-5) | застройщик (менеджер) / ПИН (Developers/Admin API) | DeveloperStatusChangedEvent + DeveloperUpsertedKafkaEvent |
Published | Hidden | решение застройщика/ПИН или Company.Status → Suspended/Archived в identity (каскад консюмера, INV-DEV-6) | застройщик / ПИН / consumer identity | DeveloperStatusChangedEvent + DeveloperUpsertedKafkaEvent |
Hidden | Published | компания Active в identity (снятие suspend → Unhide) | застройщик / ПИН / consumer identity | DeveloperStatusChangedEvent + DeveloperUpsertedKafkaEvent |
Published | Draft | снятие с публикации для доработки | застройщик / ПИН | DeveloperStatusChangedEvent + DeveloperUpsertedKafkaEvent |
Публичность имеет два независимых драйвера: (1) решение застройщика/ПИН (Developers/Admin API) и (2) каскад
из identity — блокировка компании (Suspended/Archived) принудительно уводит профиль в Hidden
(consumer identity-companies), а её реактивация (Active) возвращает
Hidden → Published. Переход Draft → Published каскадом из identity не делается — публикация всегда
остаётся явным решением (INV-DEV-5). Hidden профиля скрывает public-проекции всех его проектов фильтром
при чтении, не мутируя сами проекты (INV-DEV-6).
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
DeveloperCreatedEvent | доменное | Успешное создание | Id, SyncSource |
DeveloperStatusChangedEvent | доменное | Любой переход Status | Id, From, To, actor |
DeveloperUpsertedKafkaEvent | интеграционное (ContractVersion = 1, топик pin.chessboard.developer-upserted.v1, key = Id) | Создание и каждое значимое обновление (Name/Status/LogoFileId/Highlights) | Snapshot: Id, Status, Name, LegalName, LogoFileId, FoundedYear, Highlights, Version. Потребители: catalog (карточка), dev-stats (измерение Developer), crm (отображение), rewards |
Интеграционные события публикуются атомарно со сменой состояния агрегата.
Кросс-контекстный контракт (DeveloperUpsertedKafkaEvent)
- Ключ / версия. key =
Id,ContractVersion = 1, монотонныйVersion; потребители — upsert-by-id, доставка at-least-once, дедуп поId+Version. - Что предоставляет. Полный snapshot карточки застройщика; топик
pin.chessboard.developer-upserted.v1— единственный источник профиля застройщика для downstream:catalog(карточка),dev-stats(измерение Developer),crm(отображение),rewards. Downstream не подписываются наpin.identity.companies.v1напрямую — реплику застройщика материализует consumer identity внутриchessboard. - Что ожидается от источников. От identity —
CompanyUpsertedKafkaEvent/CompanySuspendedKafkaEvent(Type = Developer→ создание/статус); от Talent —TalentDeveloperUpsertedEvent(реплика-поля). Владение полями разделено INV-DEV-3:Name/LegalName/LogoFileId/FoundedYear— синк Talent; публичность и PIN-поля — API; identity перезаписывает толькоName/LegalNameи то лишь вне Talent-владения. - Окно eventual-consistency. Suspend компании в identity →
Hiddenпрофиля → скрытие карточки и всех проектов вcatalog: последовательность асинхронная, лаг — доставка Kafka + применение консюмером (SLA < 1 мин); в промежутке гость ещё может увидеть карточку.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK pk_developers (Id); ux_developers_external_talent_developer_id unique where not null; ix_developers_status; ix_developers_name (lower(name)) where deleted_at is null; ix_developers_created_at |
| Партиционирование / retention | none — застройщиков сотни; retention бессрочный (soft-delete) |
| Concurrency | оптимистическая блокировка на HTTP-мутациях; для событий синка — guard по ReplicaVersion (INV-DEV-4) |
| Permissions / PII | Public-чтение Published — без permission (гость видит карточку); управление своим профилем — chessboard.developers.manage (scope = организация, актор — менеджер застройщика, TenantId актора = Id); полный список/статусы — admin API (ADR-0053). PII отсутствует (контакты — организационные) |
| Audit | версия агрегата + отметки кто/когда; история изменений — через события |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: ProjectEntity, Enum-справочники контекста chessboard
- API (6): Chessboard API — реестр операций областей Developers и Admin, GET /api/v1/public/projects — Лента published-проектов (поиск + фильтры + сортировки), GET /api/v1/public/projects/{projectId} — Карточка проекта (ограниченная/полная), GET /api/v1/public/projects/{projectId}/buildings/{buildingId} — Карточка здания (литера/корпуса), GET /api/v1/public/property-objects/{propertyObjectId} — Карточка объекта/объекта недвижимости (ограниченная/полная), POST /api/v1/developers/projects/{projectId}/publish — Публикация проекта
- Use Cases (3): UC-CHS-001. Создание и публикация проекта застройщиком, UC-CHS-004. Просмотр шахматки проекта: гость vs инвестор (разница полей), UC-CHS-005. Синхронизация реплики из Talent Chessboard (system)
- События (1): Event Contract: pin.chessboard.developer-upserted (DeveloperUpsertedKafkaEvent)
- Консюмеры (3): Consumer: Chessboard Projection Sync (pin-catalog-chessboard-sync), Consumer: Identity Companies (pin-chessboard-identity-companies), Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- Фоновые задачи (1): Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)
- index.md (1): Chessboard Service (Pin.Chessboard)
- dimensions (1): dim_developer (ClickHouse dimension)
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- project.md — проекты застройщика; reference-data.md — enum
ChessboardSyncSource. - ../consumers/talent-chessboard-sync.md — правила синка Talent.
- ../../identity/domain/company.md — компания-первоисточник; ADR-0052 (tenancy).