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

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
KindAggregateRoot (гибрид с репликой Talent: реплика-поля + локальные PIN-поля)
Source of truthchessboard (PIN-поля) / talent.microservices.chessboard (реплика-поля) / identity (существование компании, её статус)
Таблицаchessboard.developers
Tenant isolationСама сущность — корень tenant шахматки: Id = CompanyId; таблица не tenanted (platform-level чтение каталога), запись — только своим tenant-ом/синком

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPK; логическая ссылка на identity.companies.id (без физического FK — другой контекст)consumer pin.identity.companies.v1 / синк TalentИдентификатор застройщика = CompanyId в identity. Единый id снимает lookup «компания ↔ застройщик» и задаёт TenantId дочерних сущностей.
StatusDeveloperStatusДаenum (текст), varchar(32), index ix_developers_statusappПубличность профиля: Draft / Published / Hidden (см. stateDiagram ниже).
NamestringДаvarchar(256), min 1, index ix_developers_name (lower)Talent sync / consumer identityПубличное название застройщика. Реплика-поле: при SyncSource = TalentChessboard управляется синком (INV-DEV-3).
LegalNamestring?Нетvarchar(512)Talent sync / appЮридическое наименование (для карточки и деклараций).
Descriptionstring?Нетvarchar(4000)app (PIN-поле)Публичное описание застройщика в PIN; синком не перезаписывается.
LogoFileIdGuid?Нетбез FK (внешний storage/CDN)Talent sync / appФайл логотипа во внешнем storage.
Websitestring?Нетvarchar(256), валидация URLappСайт застройщика.
ContactPhonestring?Нетvarchar(32), нормализация E.164appПубличный телефон отдела продаж (не PII конкретного пользователя).
ContactEmailstring?Нетvarchar(256), формат emailappПубличный email.
FoundedYearshort?Нетcheck founded_year between 1900 and 2100app / Talent syncГод основания (карточка «опыт застройщика»).
HighlightsDeveloperHighlightsDocument (jsonb)Да (дефолт при создании)jsonb, typed, ContractVersionapp (PIN-поле)Показатели карточки: сдано объектов, строится, надёжность (см. раздел JSONB).
ProjectsPublishedCountintДа>= 0, default 0вычисляется (background)Денормализованный счётчик опубликованных проектов; пересчитывается по доменным событиям проекта.
ExternalTalentDeveloperIdGuid?Нетunique ux_developers_external_talent_developer_id (where not null)Talent syncId застройщика в Talent Chessboard. null — профиль создан в PIN и не синхронизируется.
SyncSourceChessboardSyncSourceДаenum (текст), varchar(32)app / consumerPin (создан в PIN) / TalentChessboard (реплика). Определяет набор реплика-полей (INV-DEV-3). Enum — reference-data.md.
ReplicaVersionlong?Нет (обязателен при SyncSource = TalentChessboard)>= 0Talent syncВерсия агрегата в событии Talent; защита от out-of-order (INV-DEV-4).
LastSyncedAtDateTimeOffset?НетTalent syncВремя применения последнего события синка.
CreatedAtDateTimeOffsetДаindex ix_developers_created_atappДата создания.
CreatedByUserIdGuid?Нетбез FK (system-actor для синка)appКто создал.
UpdatedAtDateTimeOffsetДаappДата изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?Нетpartial-index filter where deleted_at is null на рабочие индексыappSoft-delete (застройщик не удаляется физически — на него ссылаются проекты, сделки, статистика).
DeletedByUserIdGuid?НетappКто удалил (архивировал).
VersionlongДа>= 1appВерсия агрегата — идёт в payload событий.

Enum DeveloperStatus (хранится текстом)

ЗначениеОписание
DraftПрофиль создан (синком или менеджером), не показывается в публичном каталоге; данные заполняются.
PublishedПрофиль публичен: карточка застройщика видна в каталоге, проекты могут публиковаться.
HiddenПрофиль скрыт решением ПИН/застройщика: карточка и все проекты застройщика не отдаются в public-проекции; данные сохранны.

Модели JSONB (versioned)

DeveloperHighlightsDocument (jsonb, колонка highlights, ContractVersion = 1)

Показатели публичной карточки застройщика (надёжность, счётчики опыта).

ПолеТипОбязательностьОграниченияSince version
ContractVersionintДа= 1v1
ObjectsDeliveredCountint?Нет>= 0v1
ObjectsInProgressCountint?Нет>= 0v1
YearsOnMarketshort?Нет0..200; если null — вычисляется фронтом из FoundedYearv1
ReliabilityValuedecimal?Нет0..ReliabilityMax, precision (4,1)v1
ReliabilityMaxdecimalДаdefault 10, precision (4,1)v1
AwardTitlesIReadOnlyList<string>Да (может быть пустым)элемент varchar(256), ≤ 20 элементовv1
Правила версионирования jsonb

Добавление поля — additive (той же версией, nullable/default); изменение семантики — ContractVersion + 1. Секреты/PII в jsonb запрещены.

Связи

СвязьТипКлючDelete behavior
ProjectEntity (проекты застройщика)reference (обратная)projects.developer_id → developers.idrestrict (застройщик не удаляется при живых проектах; Hidden каскадно скрывает public-проекции)
CompanyEntity (identity)replica (логическая)developers.id = identity.companies.id— (suspend компании ⇒ Hidden, см. события)
Talent developerreplicaExternalTalentDeveloperId

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

ИнвариантУсловиеКогда проверяетсяОшибка
INV-DEV-1 IdEqualsCompanyIdId существует как CompanyEntity.Id c Type = Developer в identityпри создании профиля (через реплику компаний / admin-операцию)CHESSBOARD_DEVELOPER_COMPANY_NOT_FOUND
INV-DEV-2 UniqueTalentDeveloperExternalTalentDeveloperId уникаленпри применении события синка (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 HiddenCascadesStatus = Hidden ⇒ public-проекции всех проектов застройщика не отдаются (фильтр при чтении, не мутация проектов)при чтении public-проекций + событие для catalog

Переходы состояний (из / в / условие / инициатор / событие)

ИзВУсловие (guard)ИнициаторПорождает
[*]DraftId существует как CompanyEntity.Id c Type = Developer (INV-DEV-1)consumer pin.identity.companies.v1 / синк Talent / adminDeveloperCreatedEvent + DeveloperUpsertedKafkaEvent
DraftPublishedзаполнены Name, LogoFileId, Description (INV-DEV-5)застройщик (менеджер) / ПИН (Developers/Admin API)DeveloperStatusChangedEvent + DeveloperUpsertedKafkaEvent
PublishedHiddenрешение застройщика/ПИН или Company.Status → Suspended/Archived в identity (каскад консюмера, INV-DEV-6)застройщик / ПИН / consumer identityDeveloperStatusChangedEvent + DeveloperUpsertedKafkaEvent
HiddenPublishedкомпания Active в identity (снятие suspend → Unhide)застройщик / ПИН / consumer identityDeveloperStatusChangedEvent + DeveloperUpsertedKafkaEvent
PublishedDraftснятие с публикации для доработкизастройщик / ПИН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доменноеЛюбой переход StatusId, 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
Партиционирование / retentionnone — застройщиков сотни; retention бессрочный (soft-delete)
Concurrencyоптимистическая блокировка на HTTP-мутациях; для событий синка — guard по ReplicaVersion (INV-DEV-4)
Permissions / PIIPublic-чтение Published — без permission (гость видит карточку); управление своим профилем — chessboard.developers.manage (scope = организация, актор — менеджер застройщика, TenantId актора = Id); полный список/статусы — admin API (ADR-0053). PII отсутствует (контакты — организационные)
Auditверсия агрегата + отметки кто/когда; история изменений — через события

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

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

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