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

Entity / Aggregate: PostCategory

Дом полей SEED-справочника категорий постов новостной ленты (контекст news). Категории различаются по типам публикующих пользователей: у разных типов пользователей доступны разные категории для публикации. Проекции (модель ответа api/public/categories, форма публикации в post.md) ссылаются сюда и перечисляют только дельту.

Назначение

PostCategory — платформенный справочник (seed-словарь) категорий, в которые пользователи публикуют посты общей ленты. Ключевое свойство — AllowedAuthorTypes: список типов авторов, которым разрешено публиковать в категорию. Например «ход строительства» доступен только менеджеру застройщика, «истории инвесторов» — только инвестору, «новости платформы» — всем. Категория — стабильный публичный ключ ленты: читается публично (кэш на фронте), правится редко и только администратором платформы (ADR-0053).

Справочник наполняется миграцией (seed) и далее ведётся через admin API. Записей — десятки; изменения редки, поэтому событий и денормализаций справочник не порождает.

Классификация

ПолеЗначение
Контекстnews
KindEntity (reference / seeded) — самостоятельного агрегата с событиями не образует
Source of truthэтот контекст (news)
Таблицаnews.post_categories
Tenant isolationне tenanted — платформенный справочник; доступность категорий конкретной компании решается на уровне workspace-настройки, а не строкой словаря (ADR-0052 п.3)
Concurrencyне критична: единственный писатель — администратор платформы, редкие правки
Удалениене поддерживается — вместо него Archived (INV-CAT-003)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор категории.
SlugstringДаvarchar(64), unique ux_post_categories_slug, pattern ^[a-z0-9]+(-[a-z0-9]+)*$, 1..64seed/adminСтабильный kebab-ключ (construction-progress, promotions, market-analytics, agent-blog, investor-story). Не меняется — публичная ссылка/фильтр ленты (INV-CAT-001).
TitlestringДаvarchar(128), min 1 после trimadminОтображаемое название категории.
Descriptionstring?Нетvarchar(512)adminПояснение назначения категории (маркетинговый текст).
AllowedAuthorTypesCategoryAuthorTypes (owned, jsonb)Даколонка allowed_author_types (jsonb); 1..5 уникальных элементовseed/adminТипы авторов, которым разрешена публикация в категорию. Домен — VO ниже.
SortOrderintДаdefault 0adminПорядок вывода в списке категорий (по возрастанию).
StatusCategoryStatusДаenum (хранится как text), varchar(16), index ix_post_categories_status (status, sort_order)appActive — доступна к публикации/показу; Archived — снята.
CreatedAtDateTimeOffsetДаappДата создания.
UpdatedAtDateTimeOffsetДаappПоследняя мутация.

Enum CategoryStatus

ЗначениеОписание
ActiveКатегория доступна: показывается в списке и допускает публикацию (при прохождении eligibility).
ArchivedСнята с использования: новые публикации запрещены, существующие посты сохраняют CategoryId; из публичного списка для формы публикации исключается. Обратимо (Archived → Active).

Enum PostAuthorType (дом — контекст news)

Типы публикующих пользователей. Дом концепта — здесь; post.md (Post.AuthorType) и eligibility-фильтр публичного API ссылаются сюда.

ЗначениеРоль (см. permission.md)Описание
DeveloperManagerdeveloperМенеджер застройщика (ход строительства, акции проекта).
AgencyManageragency-ownerРуководитель/менеджер агентства.
AgentagentАгент (блог, личные посты).
InvestorinvestorИнвестор (истории, личные посты).
PinEmployeepin-managerМенеджер ПИН (новости платформы, аналитика).

Value objects

CategoryAuthorTypes (owned; колонка jsonb allowed_author_types)

Сериализуется в jsonb-массив enum-строк.

ПолеТипОбязательностьОграниченияКолонка
TypesIReadOnlyList<PostAuthorType>Да1..5, элементы уникальны (≥1); сериализация — enum-строкиallowed_author_types

Инвариант VO: список непуст и без дубликатов; при нарушении — ошибка NEWS_CATEGORY_AUTHOR_TYPES_INVALID. Порядок элементов незначим (семантика — множество).

Модели JSONB

Versioned-документ не используется: allowed_author_types — типизированный VO (закрытый enum-массив), а не эволюционирующий документ, поэтому ContractVersion не заводится. Сериализованная форма (типизированный массив enum-строк):

["DeveloperManager", "AgencyManager", "Agent"]

Связи

СвязьТипКлючПравило загрузкиDelete behavior
PostCategoryPostreferenceposts.category_id → post_categories.id (FK, restrict)загрузка по CategoryId при публикации (eligibility, INV-CAT-002); список категорийrestrict — категорию с постами удалить нельзя, только Archived (INV-CAT-003)

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

ИнвариантУсловиеГде проверяетсяОшибка
INV-CAT-001 SlugUniqueSlug уникален по таблице; неизменяем после созданияunique index ux_post_categories_slug + проверка перед вставкойNEWS_CATEGORY_SLUG_DUPLICATE
INV-CAT-002 EligibilityGateПубликация допустима только если actor.UserType ∈ AllowedAuthorTypes.Types (питает INV-NWS-002 в post.md)при публикации поста: проверка типа актора против категорииNEWS_CATEGORY_AUTHOR_NOT_ELIGIBLE
INV-CAT-003 NoDeleteInUseПри наличии постов с CategoryId физическое удаление запрещено — только Active → Archivedпри удалении/архивации: наличие постов с CategoryId (FK restrict страхует на уровне БД)NEWS_CATEGORY_IN_USE
INV-CAT-004 AdminOnlyCRUD категорий доступен только m2m admin-актору (news.categories.manage)авторизация запроса (ADR-0053)FORBIDDEN

Доменные и интеграционные события

Событий нет. PostCategory — reference-справочник: изменения редки, вносятся администратором платформы и читаются публично (кэшируются на фронте). Ни доменных, ни интеграционных событий не поднимается — потребители читают категории синхронно через публичное API каталога категорий.

Согласованность и eligibility

  • Кэш публичного списка. GET /news/categories кэшируется на фронте; после Archive/Reactivate форма публикации и фильтр ленты догоняют новое состояние в пределах TTL кэша. Это допустимо: справочник меняется редко, авторитет — БД. Само правило eligibility при публикации проверяется по актуальной строке категории (не по кэшу), поэтому «протухший» список у клиента не даёт опубликовать в недопустимую категорию.
  • Eligibility-гейт — источник для поста. INV-CAT-002 (actor.UserType ∈ AllowedAuthorTypes.Types) питает INV-NWS-002 в post.md: категория — вход правила, финальное решение о публикации принимает агрегат Post. Изменение AllowedAuthorTypes не трогает уже опубликованные посты (они сохраняют CategoryId), но меняет допустимость новых публикаций.
  • Archived не рвёт существующие посты. Перевод в Archived исключает категорию из формы публикации, но посты с этим CategoryId остаются валидными и видимыми; при Reactivate категория снова доступна для публикаций.

Acceptance Criteria

  • Уникальный slug. Дано категория со Slug = X; когда админ создаёт другую с тем же Slug; тогда NEWS_CATEGORY_SLUG_DUPLICATE (INV-CAT-001); Slug после создания не меняется.
  • Eligibility разрешает. Дано категория с AllowedAuthorTypes = {DeveloperManager}; когда менеджер застройщика публикует в неё; тогда eligibility пройден (питает INV-NWS-002).
  • Eligibility запрещает. Дано та же категория; когда инвестор публикует в неё; тогда NEWS_CATEGORY_AUTHOR_NOT_ELIGIBLE (INV-CAT-002).
  • Архивация с постами. Дано категория, на которую ссылаются посты; когда админ пытается её удалить; тогда физическое удаление запрещено (FK restrict, INV-CAT-003) — доступен только Active → Archived.
  • Удаление пустой. Дано категория без единого поста; когда админ удаляет; тогда удаление допустимо (INV-CAT-003).
  • Реактивация. Дано Archived категория; когда админ реактивирует; тогда Active, снова доступна для публикации и в форме.
  • Валидность типов авторов. Дано пустой или дублирующий список AllowedAuthorTypes; когда сохранение; тогда NEWS_CATEGORY_AUTHOR_TYPES_INVALID (VO: 1..5 уникальных).
  • Только админ. Дано актор без news.categories.manage; когда попытка CRUD категории; тогда FORBIDDEN (INV-CAT-004, ADR-0053).

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK pk_post_categories (id); ux_post_categories_slug unique btree (slug); ix_post_categories_status btree (status, sort_order) — выборка активных категорий в порядке показа
Партиционирование / retentionnone — десятки строк; хранится бессрочно (архивация вместо удаления)
Concurrencyне критична: единственный писатель — администратор платформы, редкие правки
Permissions / PIIчтение — public (GET /news/categories; для формы публикации — eligibility-фильтр AllowedAuthorTypes под тип актора; для чтения ленты — полный список Active). Запись — m2m admin (news.categories.manage, ADR-0053). PII нет
Auditизменения журналируются admin-API (ADR-0053); отдельной audit-сущности и soft-delete нет — история статуса выражена Archived

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

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

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