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

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
KindAggregateRoot (для Talent-компаний — гибрид AggregateRoot + Replica)
Source of truthidentity (PIN-компании) / talent.microservices.identity (реплика-поля Talent-компаний)
Таблицаidentity.companies
Tenant isolationСама компания — корень tenant: Id компании и есть TenantId дочерних сущностей; таблица companies не tenanted (platform-level)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKapp / Talent (TenantId)Уникальный идентификатор компании. Для реплики равен TenantId Talent — это упрощает связку «Talent tenant → PIN company» без lookup-таблицы.
TypeCompanyTypeДаvarchar(32), index ix_companies_type (btree)app / consumer синкаТип компании: Developer (Застройщик), Agency (Агентство), Broker (Брокер), Partner (Партнёр), Pin (компания ПИН). Иммутабельно после создания.
StatusCompanyStatusДаvarchar(32), index ix_companies_statusappСтатус жизненного цикла: Pending / Active / Suspended / Archived (см. stateDiagram ниже).
NamestringДаvarchar(256), min 1, index ix_companies_name (btree, lower)app / Talent syncОтображаемое (брендовое) название компании. Для Talent-реплик управляется синком.
SlugstringДаvarchar(64), unique ux_companies_slug, pattern ^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$app (генерация из Name + разрешение коллизий суффиксом)Человекочитаемый адрес workspace компании (URL кабинета, публичная страница).
Descriptionstring?Нетvarchar(2000)appПубличное описание компании (карточка застройщика/агентства).
LogoFileIdGuid?Нетбез FK (внешний storage/CDN)app / Talent syncИдентификатор файла логотипа во внешнем storage-сервисе (пресайн-ссылки — граница storage/CDN).
Websitestring?Нетvarchar(256), валидация URLappСайт компании.
ContactEmailstring?Нетvarchar(256), формат emailappПубличный контактный email компании (не PII конкретного пользователя).
ContactPhonestring?Нетvarchar(32), нормализация E.164appПубличный контактный телефон компании.
RequisitesCompanyRequisites (owned)Нет (все поля nullable)см. VO нижеapp / Talent syncЮридические реквизиты.
WorkspaceSettingsCompanyWorkspaceSettingsDocument (jsonb)Да (дефолт при создании)jsonb, typed, ContractVersionappНастройки workspace компании (см. раздел JSONB).
ExternalTalentTenantIdGuid?Нетunique ux_companies_external_talent_tenant_id (where not null)Talent syncId tenant-а в talent.microservices.identity. null — компания создана в PIN и не синхронизируется.
SyncSourceCompanySyncSourceДаvarchar(32)app / consumerПроисхождение записи: Pin (создана в PIN) / TalentIdentity (реплика). Определяет, какие поля управляются синком (см. talent-replica.md).
ReplicaVersionlong?Нет (обязателен при SyncSource = TalentIdentity)>= 0Talent syncВерсия агрегата в событии Talent; защита от out-of-order (событие с меньшей версией отбрасывается).
LastSyncedAtDateTimeOffset?НетTalent syncВремя применения последнего события синка.
CreatedAtDateTimeOffsetДаindex ix_companies_created_atappШтамп создания.
CreatedByUserIdGuid?Нетбез FK (system-actor для синка)appКто создал (система/консьюмер синка/admin-API).
UpdatedAtDateTimeOffsetДаappШтамп изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?Нетpartial-index filter where deleted_at is null на все рабочие индексыappSoft-delete; заполняется только вместе с Status = Archived.
DeletedByUserIdGuid?НетappКто архивировал.
VersionlongДа>= 1appДоменная версия — инкремент при каждой мутации, идёт в 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)

ПолеТипОбязательностьОграниченияКолонка
LegalNamestring?Нетvarchar(512)requisites_legal_name
Innstring?Нетvarchar(12), pattern ^\d{10}(\d{2})?$ (10 — юрлицо, 12 — ИП)requisites_inn
Kppstring?Нетvarchar(9), pattern ^\d{9}$requisites_kpp
Ogrnstring?Нетvarchar(15), pattern ^\d{13}(\d{2})?$requisites_ogrn
LegalAddressstring?Нетvarchar(512)requisites_legal_address

Индекс: ix_companies_requisites_inn (btree, where not null) — поиск дублей компаний при заведении.

Модели JSONB (versioned)

CompanyWorkspaceSettingsDocument (jsonb, колонка workspace_settings, ContractVersion = 1)

ПолеТипОбязательностьОграниченияSince version
ContractVersionintДа= 1v1
DefaultRoleIdGuid?Нетссылка на RoleDefinition.Id этой компании; проверяется при записиv1
FixationVisibilityFixationVisibilityMode (enum-string)ДаOwnerOnly / OwnerAndManagers / WholeCompany; дефолт OwnerAndManagersv1
AllowAgentSelfRegistrationboolДадефолт false — агенты добавляются только приглашениемv1
NewsCategoriesEnabledIReadOnlyList<string>Даэлементы — коды категорий новостей (varchar(64) каждый, ≤ 32 элементов); дефолт по Type компанииv1
LocalestringДаvarchar(8), BCP-47, дефолт ru-RUv1
Правила версии JSONB

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

Связи

СвязьТипКлючПравило загрузкиDelete behavior
UserEntity (пользователи компании)reference (обратная)users.company_id → companies.idFilter по CompanyIdrestrict (компания не удаляется при живых пользователях; архивирование каскадно блокирует вход)
RoleDefinition (роли компании)reference (обратная)role_definitions.company_id → companies.idFilter по CompanyIdrestrict
RoleAssignment (scope = организация)reference (обратная)role_assignments.scope_organization_id → companies.id (логическая, без FK — см. role.md)Filtersoft (отзыв назначений при Archived)
Talent tenantreplicaExternalTalentTenantIdconsumer синка FindBy(ExternalTalentTenantId)

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

ИнвариантУсловиеГде проверяетсяОшибка
INV-1 UniqueSlugSlug уникален среди не удалённых компанийunique index + FindBy(Slug) перед созданиемIDENTITY_COMPANY_SLUG_TAKEN
INV-2 UniqueTalentTenantExternalTalentTenantId уникаленunique index; consumer делает FindByUpsertIDENTITY_COMPANY_TALENT_TENANT_DUPLICATE
INV-3 SinglePinCompanyСуществует ровно одна компания Type = Pin (SEED)миграция SEED + guard в CreateIDENTITY_COMPANY_PIN_SINGLETON
INV-4 ImmutableTypeType не меняется после созданиясеттер отсутствует; метод Update не принимает TypeIDENTITY_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 > ReplicaVersionconsumer, до мутациинет ошибки — событие идемпотентно пропускается
INV-7 ArchiveConsistencyStatus = ArchivedDeletedAt != nullметод Archive() устанавливает обаIDENTITY_COMPANY_INVALID_ARCHIVE_STATE
INV-8 SuspendGuardSuspend() допустим только из ActiveIEntityWithStatus guard в методеIDENTITY_COMPANY_INVALID_STATUS_TRANSITION

Жизненный цикл: кросс-контекстные условия и enforcement

Archive: предусловие и постусловие

Предусловие — статус ∈ {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 не отзывает сессии синхронно

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/поля
CompanyCreatedEventIDomainEventУспешный Create (любой источник)Id, Type, SyncSource
CompanyStatusChangedEventIDomainEventЛюбой переход StatusId, From, To, actor
CompanyUpsertedKafkaEventIKafkaEvent (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
CompanySuspendedKafkaEventIKafkaEvent (тот же topic)Active → SuspendedId, 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
Партиционирование / retentionnone — компаний тысячи, не миллионы; 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); история изменений — через события

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

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

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