Entity / Aggregate: ProjectEntity
Дом полей агрегата «Проект» (ЖК / апарт-отель) контекста chessboard. Все проекции (лента и карточка
проекта в catalog, измерение Project в dev-stats, payload событий) ссылаются сюда и описывают только
дельту.
Назначение
ProjectEntity — единица застройки застройщика: жилой комплекс или апарт-отель. Инвестор видит ленту
published-проектов, ищет и фильтрует по типу/классу/региону/статусу строительства/цене, открывает карточку
с вкладками: Информация, Локация, Финансирование, Юридическая информация, Ход строительства, Видео, Карта.
Проект агрегирует здания (литеры/корпуса) и объекты недвижимости, несёт диапазон цен и окупаемость для
карточки, декларации и ход строительства.
Как и весь домен шахматки — гибрид: базовые данные реплицируются из внешней системы Talent Chessboard, PIN дополняет и корректирует локальные поля.
Классификация
| Поле | Значение |
|---|---|
| Контекст | chessboard |
| Kind | AggregateRoot (гибрид с репликой Talent) |
| Source of truth | chessboard (PIN-поля) / talent.microservices.chessboard (реплика-поля) |
| Таблица | chessboard.projects |
| Tenant isolation | TenantId = DeveloperId (= CompanyId застройщика, ADR-0052); публичное чтение Published — без tenant-фильтра, мутации — только своим tenant-ом |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app / Talent sync | Идентификатор проекта. |
TenantId | Guid | Да | index ix_projects_tenant_id; = DeveloperId | app | Tenant boundary (ADR-0052). |
DeveloperId | Guid | Да | FK fk_projects_developer → chessboard.developers.id, index | app / Talent sync | Застройщик-владелец. Дублирует TenantId семантически (единое значение, INV-PRJ-1). |
Status | ProjectStatus | Да | enum (текст), varchar(32), index ix_projects_status | app | Draft / Published / Hidden / Archived (см. stateDiagram). Лента показывает только Published. |
Name | string | Да | varchar(256), min 1, index ix_projects_name (lower) | Talent sync / app | Название проекта (ЖК/отеля). Полнотекстовый поиск ленты (name+address) — Elasticsearch-проекция search/projects.md (ADR-0059). |
Description | string? | Нет | varchar(8000) | app / Talent sync | Публичное описание (вкладка «Информация»). |
Type | ProjectType | Да | enum (текст), varchar(32), index ix_projects_type | Talent sync / app | Тип проекта: ResidentialComplex / ApartHotel / Hotel / Villa — reference-data.md. Фильтр ленты. |
Class | ProjectClass | Да | enum (текст), varchar(32), index ix_projects_class | Talent sync / app | Класс: Standard / Comfort / Business / Premium / Elite — reference-data.md. Фильтр ленты. |
ConstructionStatus | ConstructionStatus | Да | enum (текст), varchar(32), index ix_projects_construction_status | Talent sync / app | Статус строительства проекта в целом: Planned / UnderConstruction / Commissioned / Delivered — reference-data.md. Фильтр ленты. |
CountryId | short | Да | логическая ссылка на справочник стран (реплика handbook-replica.md, без FK); default — страна «Россия» (запись с IsoCode2 = 'RU') | app / Talent sync | Страна проекта. |
RegionId | long? | Нет | логическая ссылка на справочник регионов (реплика, без FK), index ix_projects_region_id (where not null) | app / Talent sync (name-match) | Регион проекта — ключ фильтра «регион» ленты. Обязателен (резолвлен) при публикации (INV-PRJ-3). |
RegionName | string? | Нет | varchar(128) | реплика при записи / строка Talent при нерезолвленном синке | Display-имя региона (денормализация для карточки/ленты): при RegionId != null снимается из реплики; при нерезолвленном name-match синка Talent — исходное имя из Talent (RegionId = null, talent-chessboard-sync); refresh при переименовании в справочнике — consumer handbook-sync (HR-6, handbook-replica.md). |
CityId | long? | Нет | логическая ссылка на справочник городов (реплика, без FK), index ix_projects_city_id (where not null) | app / Talent sync (name-match) | Город проекта. Обязателен (резолвлен) при публикации (INV-PRJ-3). |
CityName | string? | Нет | varchar(128) | реплика при записи / строка Talent при нерезолвленном синке | Display-имя города: при CityId != null снимается из реплики; при нерезолвленном синке Talent — исходное имя из Talent (CityId = null); refresh — consumer handbook-sync (HR-6). |
Stars | short? | Нет | check stars between 1 and 5; заполняется при Type in (ApartHotel, Hotel) | Talent sync / app | Звёздность отеля. |
HotelierId | Guid? | Нет | FK fk_projects_hotelier → chessboard.hoteliers.id, index where not null | app | Оператор апарт-отеля (hotelier.md); обязателен для Type in (ApartHotel, Hotel) при публикации (INV-PRJ-4). |
Location | ProjectLocationDocument (jsonb) | Да (дефолт {} v1) | jsonb, typed | app / Talent sync | Локация: адрес, гео, транспорт (вкладки «Локация», «Карта»). |
Infrastructure | ProjectInfrastructureDocument (jsonb) | Да (дефолт) | jsonb, typed | app | Инфраструктура проекта и окружения. |
Financing | ProjectFinancingDocument (jsonb) | Да (дефолт) | jsonb, typed | app | Финансирование: эскроу-банки, проектное финансирование (вкладка «Финансирование»). |
LegalInfo | ProjectLegalInfoDocument (jsonb) | Да (дефолт) | jsonb, typed | app | Юридические данные: застройщик-юрлицо, разрешения, проектные декларации (вкладка «Юридическая информация»). |
ConstructionProgress | ProjectConstructionProgressDocument (jsonb) | Да (дефолт) | jsonb, typed | app | Ход строительства: отчёты по периодам, фото/видео, вебкамеры (вкладки «Ход строительства», «Видео»). |
PresentationVideoUrl | string? | Нет | varchar(512), валидация URL | app | Главное презентационное видео карточки. |
CoverImageFileId | Guid? | Нет | без FK (storage/CDN) | app / Talent sync | Обложка карточки в ленте. |
Price | NullableRange<decimal> | Нет | колонки price_min / price_max, numeric(14,2), >= 0, price_max >= price_min, index ix_projects_price_min | вычисляется (background) | Диапазон цен свободных объектов недвижимости проекта (Price.Min … Price.Max, денормализация для ленты и фильтра «ценовой диапазон»; пересчёт — job project-aggregates-recalc по событиям объектов недвижимости). |
CurrencyId | short | Да | логическая ссылка на справочник валют (реплика handbook-replica.md, без FK); default — валюта «RUB» (запись с IsoCode = 'RUB') | app | Валюта цен проекта (одна на проект, INV-PRJ-6). Типизированная ссылка ≠ мультивалютность: денежные операции остаются RUB-only (ADR-0048); в исходящих событиях — CurrencyId + CurrencySnapshot (snapshots.md). |
PropertyObjectsTotalCount | int | Да | >= 0, default 0 | вычисляется (background) | Всего объектов недвижимости (карточка «кол-во объектов»). |
PropertyObjectsFreeCount | int | Да | >= 0, default 0 | вычисляется (background) | Свободных объектов недвижимости. |
PaybackYears | decimal? | Нет | numeric(5,2), > 0 | app / invest-analytics (ручной ввод либо расчёт) | Окупаемость проекта в годах (карточка ленты; детальный расчёт — контекст invest-analytics). |
ExternalTalentProjectId | Guid? | Нет | unique ux_projects_external_talent_project_id (where not null) | Talent sync | Id проекта в Talent Chessboard. |
SyncSource | ChessboardSyncSource | Да | enum (текст), varchar(32) | app / consumer | Pin / TalentChessboard (реплика-поля: Name, Type, Class, ConstructionStatus, гео-поля CountryId/RegionId/RegionName/CityId/CityName, Stars, CoverImageFileId, address-часть Location). |
ReplicaVersion | long? | Нет (обязателен при реплике) | >= 0 | Talent sync | Anti-out-of-order guard (INV-PRJ-5). |
LastSyncedAt | DateTimeOffset? | Нет | — | Talent sync | Время последнего применённого события синка. |
CreatedAt | DateTimeOffset | Да | index ix_projects_created_at (сортировка «по дате добавления») | app | Дата создания. |
CreatedByUserId | Guid? | Нет | — | app | Кто создал. |
UpdatedAt | DateTimeOffset | Да | — | app | Дата изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | partial-index filter | app | Soft-delete; заполняется вместе с Status = Archived. |
DeletedByUserId | Guid? | Нет | — | app | Кто архивировал. |
Version | long | Да | >= 1 | app | Версия агрегата. |
Enum ProjectStatus (хранится текстом)
| Значение | Описание |
|---|---|
Draft | Проект заведён (синком/менеджером), заполняется; в публичных проекциях отсутствует. |
Published | Опубликован: попадает в ленту/поиск/фильтры каталога. |
Hidden | Временно скрыт застройщиком/ПИН: не отдаётся публично, данные и объекты недвижимости сохранны. |
Archived | Архив (soft-delete, DeletedAt заполнен): продажи завершены/проект снят; исторические ссылки живы. |
Value objects
GeoPoint (внутри ProjectLocationDocument)
| Поле | Тип | Обязательность | Ограничения | Путь |
|---|---|---|---|---|
Latitude | double | Да | -90..90 | jsonb path location.geo.latitude |
Longitude | double | Да | -180..180 | jsonb path location.geo.longitude |
Price — NullableRange<decimal>
Диапазон цен свободных объектов недвижимости проекта, хранится двумя границами price_min / price_max
(обе nullable); инвариант price_max >= price_min при обоих заполненных значениях; значения вычисляются
фоновым процессом project-aggregates-recalc. Индекс ix_projects_price_min — по нижней границе (фильтр
«ценовой диапазон» ленты). Остальные плоские поля — обычные колонки; составные структуры проекта живут в
typed jsonb (ниже).
Модели JSONB (versioned)
Все пять документов — самостоятельные типизированные модели c собственным ContractVersion (добавление
поля — additive той же версией; изменение семантики — bump версии); бессхемные object/metadata
запрещены.
ProjectLocationDocument (jsonb, колонка location, ContractVersion = 1)
Страна/регион/город — не в документе, а в колонках-ссылках проекта (CountryId/RegionId/CityId +
display-имена, см. таблицу полей); документ несёт только адресные детали ниже уровня города и гео-данные.
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
ContractVersion | int | Да | = 1 | v1 |
District | string? | Нет | varchar(128) — район; свободная строка (сущности «район» в справочнике нет) | v1 |
AddressLine | string? | Нет | varchar(512) — улица, дом | v1 |
PostalCode | string? | Нет | varchar(16) | v1 |
Geo | GeoPoint? | Нет | см. VO выше | v1 |
SeaDistanceMeters | int? | Нет | >= 0 — расстояние до моря | v1 |
CenterDistanceMinutes | int? | Нет | >= 0 — минут до центра | v1 |
TransportNodes | IReadOnlyList<TransportNodeModel> | Да (может быть пустым) | ≤ 20 элементов | v1 |
Вложенная модель TransportNodeModel
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
Kind | TransportNodeKind (enum-string) | Да | Metro / Airport / Railway / Highway / BusStop | v1 |
Name | string | Да | varchar(128) | v1 |
DistanceMinutes | int | Да | 0..600 | v1 |
TravelMode | TravelMode (enum-string) | Да | Walk / Car / PublicTransport | v1 |
ProjectInfrastructureDocument (jsonb, колонка infrastructure, ContractVersion = 1)
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
ContractVersion | int | Да | = 1 | v1 |
OwnItems | IReadOnlyList<InfrastructureItemModel> | Да (может быть пустым) | ≤ 50 элементов — инфраструктура самого проекта (паркинг, СПА, бассейн, ресторан) | v1 |
NearbyItems | IReadOnlyList<InfrastructureItemModel> | Да (может быть пустым) | ≤ 50 элементов — окружение (школы, магазины, пляж) | v1 |
Вложенная модель InfrastructureItemModel
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
Kind | InfrastructureKind (enum-string) | Да | Parking / Pool / Spa / Fitness / Restaurant / Kindergarten / School / Shop / Beach / Park / Medical / Other | v1 |
Title | string | Да | varchar(256) | v1 |
DistanceMinutes | int? | Нет | 0..600; null для собственной инфраструктуры | v1 |
IconFileId | Guid? | Нет | storage/CDN | v1 |
ProjectFinancingDocument (jsonb, колонка financing, ContractVersion = 1)
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
ContractVersion | int | Да | = 1 | v1 |
SalesModel | SalesModel (enum-string) | Да | Ddu214 (ДДУ, эскроу) / Dkp (ДКП) / Mixed; дефолт Ddu214 | v1 |
EscrowBankIds | IReadOnlyList<Guid> | Да (может быть пустым) | каждый — существующий chessboard.banks.id (валидация при записи, INV-PRJ-7); ≤ 10 | v1 |
ProjectFinancingBankId | Guid? | Нет | существующий chessboard.banks.id | v1 |
MortgageAvailable | bool | Да | дефолт false | v1 |
MortgageBankIds | IReadOnlyList<Guid> | Да (может быть пустым) | существующие banks.id; ≤ 20 | v1 |
InstallmentAvailable | bool | Да | дефолт false — наличие рассрочки (параметры программ — контекст crm, booking) | v1 |
ProjectLegalInfoDocument (jsonb, колонка legal_info, ContractVersion = 1)
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
ContractVersion | int | Да | = 1 | v1 |
LegalEntityName | string? | Нет | varchar(512) — юрлицо-застройщик проекта (СПВ) | v1 |
Inn | string? | Нет | varchar(12), pattern ^\d{10}(\d{2})?$ | v1 |
Ogrn | string? | Нет | varchar(15), pattern ^\d{13}(\d{2})?$ | v1 |
BuildingPermitNumber | string? | Нет | varchar(128) — разрешение на строительство | v1 |
BuildingPermitIssuedAt | DateOnly? | Нет | — | v1 |
LandPlotCadastralNumber | string? | Нет | varchar(64) | v1 |
Declarations | IReadOnlyList<ProjectDeclarationModel> | Да (может быть пустым) | ≤ 50 элементов | v1 |
Вложенная модель ProjectDeclarationModel
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
Title | string | Да | varchar(256) — «Проектная декларация №N» | v1 |
DocumentFileId | Guid | Да | storage/CDN | v1 |
PublishedAt | DateOnly | Да | — | v1 |
ProjectConstructionProgressDocument (jsonb, колонка construction_progress, ContractVersion = 1)
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
ContractVersion | int | Да | = 1 | v1 |
WebcamUrls | IReadOnlyList<string> | Да (может быть пустым) | элемент varchar(512), URL; ≤ 10 | v1 |
Reports | IReadOnlyList<ConstructionReportModel> | Да (может быть пустым) | ≤ 200 элементов; сортировка по Period desc | v1 |
Вложенная модель ConstructionReportModel
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
Period | string | Да | char(7), формат YYYY-MM | v1 |
Title | string | Да | varchar(256) | v1 |
Description | string? | Нет | varchar(4000) | v1 |
PhotoFileIds | IReadOnlyList<Guid> | Да (может быть пустым) | ≤ 100 | v1 |
VideoUrls | IReadOnlyList<string> | Да (может быть пустым) | URL, ≤ 10 | v1 |
ReadinessPercent | decimal? | Нет | 0..100, precision (5,2) — готовность проекта на конец периода | v1 |
Связи
| Связь | Тип | Ключ | Delete behavior |
|---|---|---|---|
DeveloperEntity | reference (родитель) | projects.developer_id → developers.id | restrict |
BuildingEntity (здания) | reference (обратная) | buildings.project_id → projects.id | restrict (архив проекта каскадно скрывает public-проекции зданий/объектов недвижимости, физически не удаляет) |
PropertyObjectEntity (объекты недвижимости) | reference (обратная, через здание и напрямую) | property_objects.project_id → projects.id | restrict |
LayoutGroupEntity / LayoutEntity | reference (обратная) | layout_groups.project_id, layouts.project_id | restrict |
HotelierEntity | reference | projects.hotelier_id → hoteliers.id | restrict |
BankEntity (эскроу/ипотека) | логическая (id внутри Financing jsonb, без FK) | Financing.EscrowBankIds[] | — (архив банка не ломает проект; проекции фильтруют) |
| Talent project | replica | ExternalTalentProjectId | — |
Инварианты и переходы состояний
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-PRJ-1 TenantEqualsDeveloper | TenantId == DeveloperId всегда | при создании | CHESSBOARD_PROJECT_TENANT_MISMATCH |
| INV-PRJ-2 DeveloperExists | DeveloperId существует и не удалён | при создании | CHESSBOARD_DEVELOPER_NOT_FOUND |
| INV-PRJ-3 PublishRequiresData | Публикация только при заполненных Name, Description, CoverImageFileId, Location.Geo, резолвленных RegionId и CityId (ссылки справочника, не null) и Developer.Status = Published | при публикации | CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE |
| INV-PRJ-4 HotelRequiresHotelier | При Type in (ApartHotel, Hotel) публикация требует HotelierId != null | при публикации | CHESSBOARD_PROJECT_HOTELIER_REQUIRED |
| INV-PRJ-5 MonotonicReplica | Событие синка применяется только при event.Version > ReplicaVersion | при применении события синка | идемпотентный пропуск |
| INV-PRJ-6 SingleCurrency | Все объекты недвижимости проекта имеют CurrencyId = Project.CurrencyId | при изменении объекта недвижимости | CHESSBOARD_PROPERTY_OBJECT_CURRENCY_MISMATCH |
| INV-PRJ-7 BanksExist | Каждый id в Financing.EscrowBankIds / MortgageBankIds / ProjectFinancingBankId существует | при записи Financing | CHESSBOARD_PROJECT_BANK_NOT_FOUND |
| INV-PRJ-8 ArchiveConsistency | Status = Archived ⇔ DeletedAt != null; архив допустим только без объектов недвижимости в Reserved/Booked | при архивировании | CHESSBOARD_PROJECT_HAS_ACTIVE_HOLDS |
| INV-PRJ-9 SyncOwnedFields | Реплика-поля меняются только синком; PIN-поля — только локально | при изменении полей (раздельные пути) | CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED |
| INV-PRJ-10 HandbookRefsValid | CountryId/RegionId/CityId/CurrencyId существуют в справочнике и не архивны; гео-цепочка согласована (city.CountryId = CountryId; при заданном RegionId — city.RegionId = RegionId и region.CountryId = CountryId) — проверка по реплике (handbook-replica.md) | при создании/изменении значения справочной ссылки | CHESSBOARD_GEO_REF_UNKNOWN / CHESSBOARD_GEO_REF_ARCHIVED / CHESSBOARD_CITY_REGION_MISMATCH (см. таблицу ошибок) |
Переходы состояний (из / в / условие / инициатор / событие)
| Из | В | Условие (guard) | Инициатор | Порождает |
|---|---|---|---|---|
[*] | Draft | DeveloperId существует и не удалён (INV-PRJ-2); TenantId = DeveloperId (INV-PRJ-1) | синк Talent / застройщик (Developers API) | ProjectCreatedEvent + ProjectUpsertedKafkaEvent |
Draft | Published | заполнены Name, Description, CoverImageFileId, Location.Geo; резолвлены RegionId и CityId; Developer.Status = Published (INV-PRJ-3); при Type in (ApartHotel, Hotel) — HotelierId != null (INV-PRJ-4) | застройщик / ПИН | ProjectStatusChangedEvent + ProjectUpsertedKafkaEvent |
Published | Hidden | решение застройщика/ПИН (временное скрытие; данные и объекты сохранны) | застройщик / ПИН | ProjectStatusChangedEvent + ProjectUpsertedKafkaEvent |
Hidden | Published | Developer.Status = Published | застройщик / ПИН | ProjectStatusChangedEvent + ProjectUpsertedKafkaEvent |
Published | Draft | снятие с публикации для доработки | застройщик / ПИН | ProjectStatusChangedEvent + ProjectUpsertedKafkaEvent |
Draft / Hidden | Archived | Status = Archived ⇔ DeletedAt != null; нет объектов недвижимости в Reserved/Booked (INV-PRJ-8) | застройщик / ПИН | ProjectStatusChangedEvent + ProjectUpsertedKafkaEvent |
Терминальность: Archived — конечное состояние (soft-delete, DeletedAt заполнен); исторические ссылки
(сделки crm, статистика dev-stats) остаются валидны. Hidden профиля застройщика каскадно скрывает
public-проекции всех его проектов фильтром при чтении (INV-DEV-6), не меняя Project.Status. Реплика-поля
проекта (Name, Type, Class, ConstructionStatus, гео-поля, Stars, CoverImageFileId, address-часть
Location) меняет только синк, публичность и PIN-вкладки — только локально (INV-PRJ-9). Публикация каскадом из
синка не делается — синк заводит проект в Draft.
Ошибки справочных ссылок (INV-PRJ-10)
Валидация выполняется по локальной реплике справочников с fallback-резолвом в handbook при лаге реплики —
механика и порядок проверок: handbook-replica.md. Синк Talent резолвит адресные строки
внешней системы в ссылки справочника name-match-ем по реплике; нерезолвленная ссылка (RegionId/CityId = null, исходное имя Talent сохраняется в display-поле RegionName/CityName, warn-метрика) не блокирует
применение синка, но блокирует публикацию проекта (INV-PRJ-3) до ручного разрешения
(../consumers/talent-chessboard-sync.md).
| Код | HTTP | Условие |
|---|---|---|
CHESSBOARD_GEO_REF_UNKNOWN | 422 | справочная ссылка (CountryId/RegionId/CityId/CurrencyId) не найдена ни в реплике, ни fallback-резолвом |
CHESSBOARD_GEO_REF_ARCHIVED | 422 | при создании/изменении указана архивная запись справочника |
CHESSBOARD_CITY_REGION_MISMATCH | 422 | город/регион/страна рассогласованы (гео-цепочка не сходится) |
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
ProjectCreatedEvent | доменное | Успешное создание | Id, DeveloperId, SyncSource |
ProjectStatusChangedEvent | доменное | Любой переход Status | Id, From, To, actor |
ProjectAggregatesRecalcRequestedEvent | доменное | Изменение цен/статусов объектов недвижимости | ProjectId — триггер пересчёта Price (Min/Max), PropertyObjectsFreeCount |
ProjectUpsertedKafkaEvent | интеграционное (ContractVersion = 1, топик pin.chessboard.project-upserted.v1, key = Id) | Создание и каждое значимое обновление (Name/Status/Type/Class/ConstructionStatus/гео-полей/цены/jsonb-вкладки), а также refresh display-имён при переименовании в справочнике (HR-6, handbook-replica.md) | Snapshot: Id, TenantId, DeveloperId, Status, Name, Type, Class, ConstructionStatus, гео-ссылки CountryId/RegionId/CityId + снапшоты CountrySnapshot/RegionSnapshot/CitySnapshot (snapshots.md), Stars, HotelierId, Price (NullableRange<decimal>), CurrencyId + CurrencySnapshot, PropertyObjectsTotalCount, PropertyObjectsFreeCount, PaybackYears, CoverImageFileId, Version. Потребители: catalog (витрина), dev-stats (измерение Project), identity (scope «группа проектов/проект»), news (привязка новостей к проекту) |
Интеграционные события публикуются атомарно со сменой состояния агрегата.
Кросс-контекстный контракт (ProjectUpsertedKafkaEvent)
- Ключ / версия. key =
Id,ContractVersion = 1, монотонныйVersion; потребители — upsert-by-id, доставка at-least-once, дедуп поId+Version. - Что предоставляет. Snapshot карточки/ленты; топик
pin.chessboard.project-upserted.v1— источник проекта дляcatalog(витрина/лента/фильтры),dev-stats(измерение Project),identity(материализация scope «группа проектов/проект» для Scoped-RBAC),news(привязка новостей к проекту). - Ценовые/счётные поля — производные.
Price(Min/Max),PropertyObjectsTotalCount,PropertyObjectsFreeCountпересчитывает джобproject-aggregates-recalcпо событиям объектов недвижимости (debounce 30 с) и переопубликуетProjectUpsertedKafkaEventпри их изменении. Downstream получают их из этого же snapshot, отдельного «счётного» события нет. - Порядок между сущностями. События
Project/Building/Layout/LayoutGroup/PropertyObjectидут разными топиками, каждый keyed своимId→ кросс-сущностный порядок не гарантирован;catalogстроит витрину идемпотентно поId+Version, толерантно к порядку прихода родитель/дочерний. - Окно eventual-consistency. Публикация проекта → появление в ленте/поиске
catalog: асинхронно, лаг — доставка Kafka + применение (+ Elasticsearch-проекция для поиска, search/projects.md).
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK pk_projects; ux_projects_external_talent_project_id unique where not null; ix_projects_tenant_id; ix_projects_developer_id; ix_projects_status; ix_projects_type; ix_projects_class; ix_projects_construction_status; ix_projects_region_id (where not null); ix_projects_city_id (where not null); ix_projects_price_min; ix_projects_created_at. Все рабочие индексы — where deleted_at is null. Полнотекстовый поиск ленты (name+address) — Elasticsearch-проекция ProjectSearchDocument (search/projects.md, ADR-0059) |
| Партиционирование / retention | none — проектов тысячи; retention бессрочный (soft-delete) |
| Concurrency | оптимистическая блокировка на HTTP-мутациях; для событий синка — guard по ReplicaVersion (INV-PRJ-5); денормализованные счётчики обновляются точечно, без загрузки агрегата |
| Permissions / PII | Public-чтение Published — без permission, но неавторизованным скрываются точные цены и шахматка (проекция «ограниченная карточка»); CRUD своих проектов — chessboard.projects.manage (scope = организация/группа проектов/проект); admin — полный доступ (ADR-0053). PII нет; юрданные (LegalInfo) — публичные по 214-ФЗ |
| Audit | версия агрегата + отметки кто/когда; значимые изменения — события |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (11): Реплика справочников handbook в chessboard (chessboard.handbook_*), Entity / Aggregate: BankEntity, Entity / Aggregate: BuildingEntity, Entity / Aggregate: CatalogProjectCard, Entity / Aggregate: DeveloperEntity, Entity / Aggregate: HotelierEntity, Entity / Aggregate: LayoutEntity, Entity / Aggregate: LayoutGroupEntity, Entity / Aggregate: Post, Entity / Aggregate: PropertyObjectEntity, Enum-справочники контекста chessboard
- API (27): Admin CRUD банков — создание / список / карточка / правка / архив / lookup, Admin CRUD отельеров (создание / список / карточка / правка / архив / lookup) — область admin, Chessboard API — реестр операций областей Developers и Admin, GET /api/v1/developers/lookup/{banks,hoteliers} — Lookup справочников банков/отельеров, GET /api/v1/developers/projects — Поиск/список проектов застройщика, GET /api/v1/developers/projects/{projectId} — Карточка проекта застройщика, GET /api/v1/developers/projects/{projectId}/buildings — Список зданий проекта, GET /api/v1/developers/projects/{projectId}/layout-groups — Список групп планировок, GET /api/v1/developers/projects/{projectId}/layouts — Список планировок проекта, GET /api/v1/developers/projects/{projectId}/property-objects — Список объектов недвижимости проекта, GET /api/v1/projects/{id} — Карточка проекта: ограниченная / полная, GET /api/v1/public/projects — Лента published-проектов (поиск + фильтры + сортировки) (+15)
- Use Cases (6): UC-CHS-001. Создание и публикация проекта застройщиком, UC-CHS-002. Добавление здания (литера/корпуса) и объектов проекта застройщиком, UC-CHS-003. Изменение цены объекта застройщиком (события в аналитику), UC-CHS-004. Просмотр шахматки проекта: гость vs инвестор (разница полей), UC-CHS-005. Синхронизация реплики из Talent Chessboard (system), UC-CHS-007. Администратор ведёт справочники банков и отельеров
- События (2): Event Contract: pin.chessboard.layout-group-upserted (LayoutGroupUpsertedKafkaEvent), Event Contract: pin.chessboard.project-upserted (ProjectUpsertedKafkaEvent)
- Консюмеры (4): Consumer: Chessboard Project Deleted (pin-identity-chessboard-projects), Consumer: Chessboard Projection Sync (pin-catalog-chessboard-sync), Consumer: Handbook Sync (pin-chessboard-handbook-sync), Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- Фоновые задачи (2): Background Job: project-aggregates-recalc (пересчёт денормализованных агрегатов), Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)
- Поиск (1): Search Document: ProjectSearchDocument
- index.md (1): Chessboard Service (Pin.Chessboard)
- dimensions (1): dim_project (ClickHouse dimension)
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- developer.md — родитель; building.md, property-object.md, layout.md, layout-group.md — дочерние; bank.md, hotelier.md — справочники; reference-data.md — enum-справочники.
- handbook-replica.md — реплика справочников
handbook(валидацияCountryId/RegionId/CityId/CurrencyId, refreshRegionName/CityName); snapshot-модели событий — ../../handbook/domain/snapshots.md (ADR-0063). - ../consumers/talent-chessboard-sync.md — синк; ../consumers/handbook-sync.md — синк справочников; ADR-0052 (tenancy), ADR-0056 (simplicity-first: денормализация вместо OLAP).