Entity / Aggregate: CompanyEntity
Дом полей агрегата «Компания» контекста identity. Все проекции (модели ответов API, payload событий,
реплики в других контекстах) ссылаются сюда и описывают только дельту.
Назначение
CompanyEntity — верхнеуровневая организация платформы PIN: Застройщик, Агентство недвижимости, Брокер,
Партнёр или сама компания ПИН. Компания является границей tenant (TenantId = CompanyId, ADR-0052) и
владельцем workspace: её пользователи, роли, назначения ролей и данные компании изолируются по её Id.
Двойственная природа:
- для компаний-застройщиков (и части агентств), пришедших из
talent.microservices.identity, сущность — реплика tenant-а Talent (правила синка — talent-replica.md); - для компаний, созданных в PIN (агентства, брокеры, партнёры, компания ПИН), — source of truth этот контекст.
Классификация
| Поле | Значение |
|---|---|
| Контекст | identity |
| Kind | AggregateRoot (для Talent-компаний — гибрид AggregateRoot + Replica) |
| Source of truth | identity (PIN-компании) / talent.microservices.identity (реплика-поля Talent-компаний) |
| Таблица | identity.companies |
| Tenant isolation | Сама компания — корень tenant: Id компании и есть TenantId дочерних сущностей; таблица companies не tenanted (platform-level) |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app / Talent (TenantId) | Уникальный идентификатор компании. Для реплики равен TenantId Talent — это упрощает связку «Talent tenant → PIN company» без lookup-таблицы. |
Type | CompanyType | Да | varchar(32), index ix_companies_type (btree) | app / consumer синка | Тип компании: Developer (Застройщик), Agency (Агентство), Broker (Брокер), Partner (Партнёр), Pin (компания ПИН). Иммутабельно после создания. |
Status | CompanyStatus | Да | varchar(32), index ix_companies_status | app | Статус жизненного цикла: Pending / Active / Suspended / Archived (см. stateDiagram ниже). |
Name | string | Да | varchar(256), min 1, index ix_companies_name (btree, lower) | app / Talent sync | Отображаемое (брендовое) название компании. Для Talent-реплик управляется синком. |
Slug | string | Да | varchar(64), unique ux_companies_slug, pattern ^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$ | app (генерация из Name + разрешение коллизий суффиксом) | Человекочитаемый адрес workspace компании (URL кабинета, публичная страница). |
Description | string? | Нет | varchar(2000) | app | Публичное описание компании (карточка застройщика/агентства). |
LogoFileId | Guid? | Нет | без FK (внешний storage/CDN) | app / Talent sync | Идентификатор файла логотипа во внешнем storage-сервисе (пресайн-ссылки — граница storage/CDN). |
Website | string? | Нет | varchar(256), валидация URL | app | Сайт компании. |
ContactEmail | string? | Нет | varchar(256), формат email | app | Публичный контактный email компании (не PII конкретного пользователя). |
ContactPhone | string? | Нет | varchar(32), нормализация E.164 | app | Публичный контактный телефон компании. |
Requisites | CompanyRequisites (owned) | Нет (все поля nullable) | см. VO ниже | app / Talent sync | Юридические реквизиты. |
WorkspaceSettings | CompanyWorkspaceSettingsDocument (jsonb) | Да (дефолт при создании) | jsonb, typed, ContractVersion | app | Настройки workspace компании (см. раздел JSONB). |
ExternalTalentTenantId | Guid? | Нет | unique ux_companies_external_talent_tenant_id (where not null) | Talent sync | Id tenant-а в talent.microservices.identity. null — компания создана в PIN и не синхронизируется. |
SyncSource | CompanySyncSource | Да | varchar(32) | app / consumer | Происхождение записи: Pin (создана в PIN) / TalentIdentity (реплика). Определяет, какие поля управляются синком (см. talent-replica.md). |
ReplicaVersion | long? | Нет (обязателен при SyncSource = TalentIdentity) | >= 0 | Talent sync | Версия агрегата в событии Talent; защита от out-of-order (событие с меньшей версией отбрасывается). |
LastSyncedAt | DateTimeOffset? | Нет | — | Talent sync | Время применения последнего события синка. |
CreatedAt | DateTimeOffset | Да | index ix_companies_created_at | app | Штамп создания. |
CreatedByUserId | Guid? | Нет | без FK (system-actor для синка) | app | Кто создал (система/консьюмер синка/admin-API). |
UpdatedAt | DateTimeOffset | Да | — | app | Штамп изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | partial-index filter where deleted_at is null на все рабочие индексы | app | Soft-delete; заполняется только вместе с Status = Archived. |
DeletedByUserId | Guid? | Нет | — | app | Кто архивировал. |
Version | long | Да | >= 1 | app | Доменная версия — инкремент при каждой мутации, идёт в payload событий. |
Enum CompanyType (хранится как text)
| Значение | Описание | Источник появления |
|---|---|---|
Developer | Застройщик — владелец проектов и объектов; управляет шахматкой, фиксациями, сделками со своей стороны. | Реплика из Talent Identity (основной путь) или admin-API. |
Agency | Агентство недвижимости — команда агентов, клиентская база, комиссионная политика. | admin-API PIN; сотрудники ПИН-агентства регистрируются здесь же. |
Broker | Брокер — самостоятельный брокер/малое агентство; функционально равен Agency, отдельный тип для группировки и отчётности. | admin-API PIN. |
Partner | Партнёр — компания-участник партнёрской программы (реферальные выплаты юрлицу). | admin-API PIN. |
Pin | Компания ПИН — площадка: Менеджеры ПИН, Служба заботы. Единственная компания этого типа (инвариант INV-3). | SEED. |
Enum CompanyStatus
| Значение | Описание |
|---|---|
Pending | Создана, но ещё не активирована (ожидает первичного заполнения/подтверждения синком). |
Active | Рабочее состояние: пользователи входят, workspace доступен. |
Suspended | Временно заблокирована (нарушения/долг/запрос Talent): вход пользователей компании запрещён, данные сохранны. |
Archived | Архив: soft-delete (DeletedAt заполнен), исторические ссылки живы, операции запрещены. |
Enum CompanySyncSource
| Значение | Описание |
|---|---|
Pin | Запись создана и полностью управляется в PIN. |
TalentIdentity | Реплика tenant-а Talent: реплика-поля (Name, LogoFileId, Requisites, статус-маппинг) перезаписываются синком; PIN-поля (Slug, Description, WorkspaceSettings) — локальные. |
Value objects
CompanyRequisites (owned)
| Поле | Тип | Обязательность | Ограничения | Колонка |
|---|---|---|---|---|
LegalName | string? | Нет | varchar(512) | requisites_legal_name |
Inn | string? | Нет | varchar(12), pattern ^\d{10}(\d{2})?$ (10 — юрлицо, 12 — ИП) | requisites_inn |
Kpp | string? | Нет | varchar(9), pattern ^\d{9}$ | requisites_kpp |
Ogrn | string? | Нет | varchar(15), pattern ^\d{13}(\d{2})?$ | requisites_ogrn |
LegalAddress | string? | Нет | varchar(512) | requisites_legal_address |
Индекс: ix_companies_requisites_inn (btree, where not null) — поиск дублей компаний при заведении.
Модели JSONB (versioned)
CompanyWorkspaceSettingsDocument (jsonb, колонка workspace_settings, ContractVersion = 1)
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
ContractVersion | int | Да | = 1 | v1 |
DefaultRoleId | Guid? | Нет | ссылка на RoleDefinition.Id этой компании; проверяется при записи | v1 |
FixationVisibility | FixationVisibilityMode (enum-string) | Да | OwnerOnly / OwnerAndManagers / WholeCompany; дефолт OwnerAndManagers | v1 |
AllowAgentSelfRegistration | bool | Да | дефолт false — агенты добавляются только приглашением | v1 |
NewsCategoriesEnabled | IReadOnlyList<string> | Да | элементы — коды категорий новостей (varchar(64) каждый, ≤ 32 элементов); дефолт по Type компании | v1 |
Locale | string | Да | varchar(8), BCP-47, дефолт ru-RU | v1 |
Добавление поля — additive (той же версией с nullable/default), изменение семантики —
ContractVersion + 1 + migration-reader. Запрещено хранить в jsonb секреты, PII, токены.
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
UserEntity (пользователи компании) | reference (обратная) | users.company_id → companies.id | Filter по CompanyId | restrict (компания не удаляется при живых пользователях; архивирование каскадно блокирует вход) |
RoleDefinition (роли компании) | reference (обратная) | role_definitions.company_id → companies.id | Filter по CompanyId | restrict |
RoleAssignment (scope = организация) | reference (обратная) | role_assignments.scope_organization_id → companies.id (логическая, без FK — см. role.md) | Filter | soft (отзыв назначений при Archived) |
| Talent tenant | replica | ExternalTalentTenantId | consumer синка FindBy(ExternalTalentTenantId) | — |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-1 UniqueSlug | Slug уникален среди не удалённых компаний | unique index + FindBy(Slug) перед созданием | IDENTITY_COMPANY_SLUG_TAKEN |
| INV-2 UniqueTalentTenant | ExternalTalentTenantId уникален | unique index; consumer делает FindBy → Upsert | IDENTITY_COMPANY_TALENT_TENANT_DUPLICATE |
| INV-3 SinglePinCompany | Существует ровно одна компания Type = Pin (SEED) | миграция SEED + guard в Create | IDENTITY_COMPANY_PIN_SINGLETON |
| INV-4 ImmutableType | Type не меняется после создания | сеттер отсутствует; метод Update не принимает Type | IDENTITY_COMPANY_TYPE_IMMUTABLE |
| INV-5 SyncOwnedFields | При SyncSource = TalentIdentity поля Name, LogoFileId, Requisites меняются только consumer-ом синка | метод агрегата ApplyTalentSnapshot vs UpdateLocal (разные наборы полей) | IDENTITY_COMPANY_FIELD_SYNC_MANAGED |
| INV-6 MonotonicReplica | Событие синка применяется только при event.Version > ReplicaVersion | consumer, до мутации | нет ошибки — событие идемпотентно пропускается |
| INV-7 ArchiveConsistency | Status = Archived ⇔ DeletedAt != null | метод Archive() устанавливает оба | IDENTITY_COMPANY_INVALID_ARCHIVE_STATE |
| INV-8 SuspendGuard | Suspend() допустим только из Active | IEntityWithStatus guard в методе | IDENTITY_COMPANY_INVALID_STATUS_TRANSITION |
Жизненный цикл: кросс-контекстные условия и enforcement
Предусловие — статус ∈ {Active, Suspended, Pending} и нет активных сделок компании: проверяется
синхронным запросом к crm на write-path. Есть активные сделки → архив отклонён (компания остаётся в
текущем статусе); crm недоступен → fail-closed (архив откладывается, а не проводится вслепую).
Постусловие — Status = Archived, DeletedAt заполнен (INV-7), активные RoleAssignment scope этой
организации отозваны (каскад, role.md), вход её пользователей закрыт (INV-9 user.md); ушли
CompanyStatusChangedEvent + CompanyUpsertedKafkaEvent со Status = Archived.
Suspend(TalentBlocked/PinDecision/Debt) не трогает активные RefreshSession: новый вход и обновление
сессии блокируются лениво (INV-9 user.md проверяет Company.Status = Active в confirm/refresh), а уже
выданный access-JWT доживает до своего exp (окно ≤ TTL access-токена). Для немедленного выката ограничений
потребителям публикуется CompanySuspendedKafkaEvent (Reason); downstream-контексты трактуют компанию как
неактивную по этому событию.
Pending → Active для Talent-реплики происходит по применению первого snapshot (talent-replica.md R3);
если первым пришёл TenantDeletedEvent — компания архивируется из Pending (переход выше), пользователи не
заводятся.
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
CompanyCreatedEvent | IDomainEvent | Успешный Create (любой источник) | Id, Type, SyncSource |
CompanyStatusChangedEvent | IDomainEvent | Любой переход Status | Id, From, To, actor |
CompanyUpsertedKafkaEvent | IKafkaEvent (ContractVersion = 1, TopicSettings: topic pin.identity.companies.v1, key = Id) | Создание и каждое значимое обновление (Name/Type/Status/Requisites/Logo) | Полный snapshot: Id, Type, Status, Name, Slug, LogoFileId, Requisites, Version; потребители: catalog, crm, rewards, news |
CompanySuspendedKafkaEvent | IKafkaEvent (тот же topic) | Active → Suspended | Id, SuspendedAt, причина (enum CompanySuspendReason: PinDecision / TalentBlocked / Debt) |
Интеграционные события публикуются атомарно со сменой состояния агрегата.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK pk_companies (Id); ux_companies_slug unique btree (slug) where deleted_at is null; ux_companies_external_talent_tenant_id unique btree where not null; ix_companies_type btree; ix_companies_status btree; ix_companies_name btree (lower(name)); ix_companies_requisites_inn btree where not null; ix_companies_created_at btree |
| Партиционирование / retention | none — компаний тысячи, не миллионы; retention бессрочный (soft-delete) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую); синк — ReplicaVersion-guard (INV-6) |
| Permissions / PII | Чтение своей компании — identity.companies.view; управление — identity.companies.manage (scope = организация); список всех компаний — только admin-API (ADR-0053). Реквизиты (Inn/Ogrn) — юрданные, не PII физлица; маскирование не требуется, но в публичные проекции каталога не отдаются |
| Audit | доменная Version + actor-stamping (CreatedBy/UpdatedBy/DeletedByUserId); история изменений — через события |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (8): Реплика Talent Identity: правила синка + Entity TalentSyncState, Entity (Replica): UserDisplayReplicaEntity, Entity / Aggregate: AuthorReplica + CompanyReplica, Entity / Aggregate: CatalogProjectCard, Entity / Aggregate: CompanyInvitationEntity, Entity / Aggregate: DeveloperEntity, Entity / Aggregate: RoleDefinition (+ RolePermission, RoleAssignment / UserRole, ResourceScope), Entity / Aggregate: UserEntity
- API (3): GET /api/v1/companies/my — Просмотр своей компании, Identity Company-User API — реестр методов и общие правила области, PUT /api/v1/companies/my — Обновление своей компании
- Use Cases (2): UC-IDN-006. Приглашение менеджера (агента) в компанию, UC-IDN-007. Синхронизация компании и её пользователей из Talent Identity (system)
- События (2): Event Contract: pin.identity.company-suspended (CompanySuspendedKafkaEvent), Event Contract: pin.identity.company-upserted (CompanyUpsertedKafkaEvent)
- Консюмеры (3): Consumer: Identity Companies (pin-chessboard-identity-companies), Consumer: identity.companies.v1 → CompanyReplica, Consumer: Talent Identity Sync (pin-identity-talent-sync)
- Фоновые задачи (1): Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Identity)
- index.md (1): Identity Service (Pin.Identity)
- registries (1): Реестр сущностей PIN
Связанные документы
- user.md — пользователи компании; role.md — роли/назначения в scope организации.
- talent-replica.md — правила синка Talent tenant → CompanyEntity.
- ADR-0052 (tenancy), ADR-0053 (админы вне identity).
- Шаблон: entity-template.md; правила: cross-cutting-document-requirements.md.