UC-NWS-007. Категории по типам авторов (eligibility публикации)
Назначение
Автор своего типа (инвестор, агент, менеджер застройщика/агентства, сотрудник PIN) видит и публикует
пост только в разрешённые его типу категории ленты новостей — доступные категории отличаются по типам
публикующих. Eligibility проверяется дважды: на выдаче справочника (CanPublish) для фильтра формы и
повторно на бэке при create/publish (INV-NWS-002). Дом полей —
../domain/post-category.md (AllowedAuthorTypes, PostAuthorType,
INV-CAT-002) и ../domain/post.md (AuthorType, AuthorCompanyId, ProjectRefs,
INV-NWS-002/003); операции — в api-документах. Связан с контекстом news.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-NWS-007 |
| Контекст | news (см. docs/06-services/news/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Авторы разных типов (PostAuthorType): Investor, Agent, AgencyManager, DeveloperManager, PinEmployee; гость — только чтение справочника (CanPublish = false) |
| Permissions / scope | Публикация: news.posts.create-personal (self — investor/agent/agency-owner/developer), news.posts.create-company / news.posts.create-project (застройщик/агентство в scope компании); правка своих — news.posts.manage-own. Каталог — ../../identity/domain/permission.md. Eligibility публикации = actor.UserType ∈ Category.AllowedAuthorTypes ∧ соответствующий news.posts.create-* (INV-NWS-002) |
| Tenant | TenantId = AuthorCompanyId для DeveloperManager/AgencyManager; null для Investor/Agent/PinEmployee (ADR-0052 п.4). Справочник категорий — не tenanted |
| Record-level | Мутация — только AuthorUserId, либо менеджер компании в scope AuthorCompanyId, либо news.posts.moderate (INV-NWS-007) |
Предусловие
- Справочник
PostCategoryнаполнен SEED-миграцией (категории по типам авторов); нужные категории — в статусеActive(INV-CAT-003). - Автор аутентифицирован;
AuthorType= snapshotUserTypeиз контекста актора на момент создания. - Для
DeveloperManager/AgencyManagerAuthorCompanyIdберётся из scope-компании актора (не из тела).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI (форма публикации) → API call (два endpoint: выдача справочника, затем create/publish) |
| Триггерящий элемент | Форма «Создать пост» → GET /api/v1/categories?onlyEligible=true; далее POST /api/v1/posts (черновик) и POST /api/v1/posts/{id}/publish |
Основной Поток
Eligibility — один и тот же предикат AuthorType ∈ AllowedAuthorTypes на выдаче и на записи.
Фаза 1. Выдача доступных категорий — ../api/public/categories/list-categories.md
- Автор открывает форму публикации; фронт вызывает
GET /api/v1/categories?onlyEligible=true. - Загружаются
Active-категории; для каждой вычисляетсяCanPublish = ActorUserType ∈ AllowedAuthorTypes(тот же предикат, что INV-CAT-002); приOnlyEligible = trueостаются толькоCanPublish = true. Гость (Anonymous) →CanPublish = false→ пустой список. - Фронт показывает форме только разрешённые типу автора категории; полный
Active-список (безonlyEligible) остаётся для фильтров ленты.
Фаза 2. Create/publish с повторной проверкой eligibility
- Автор выбирает категорию и сохраняет черновик —
POST /api/v1/posts(../api/authoring/create-post-draft.md). Проверить: категория существует ∧Active, затем eligibility INV-NWS-002 / INV-CAT-002:AuthorType ∈ category.AllowedAuthorTypes.Types— иначеNEWS_CATEGORY_NOT_ALLOWED(поток A1). - Attribution по типу автора (INV-NWS-003):
- застройщик (
DeveloperManager) — публикует вconstruction-progress/promotionsс привязкой к компании (AuthorCompanyId = TenantId) и/или проектам (ProjectRefs,<= 20); непустойProjectRefsдопустим только дляDeveloperManager; - агент (
Agent) — публикует вagent-blog/promotionsк себе (AuthorCompanyId = null,ProjectRefsпуст); руководитель агентства (AgencyManager) — сAuthorCompanyId, но безProjectRefs; - инвестор (
Investor) — публикует вinvestor-storyот себя (AuthorCompanyId = null); - нарушение (напр.
ProjectRefsне-застройщиком,AuthorCompanyIdу инвестора) →NEWS_ATTRIBUTION_NOT_ALLOWED(поток A2).
- застройщик (
- Автор публикует —
POST /api/v1/posts/{id}/publish(../api/authoring/publish-post.md). Повторно проверяется INV-NWS-002 (категория могла быть архивирована/переназначена между черновиком и публикацией), контент (INV-NWS-004), пишется иммутабельныйPostVersion(INV-NWS-005) и публикуетсяpin.news.post-published.v1. Переход, версия и факт публикации события фиксируются в одном согласованном изменении. - Результат —
{ Status = Published, VersionNumber }; пост попадает в ленту (partialix_posts_feedwhere status='Published') и в Elasticsearch-проекцию поискаnews-postsв категории, разрешённой типу автора.
Что фиксировано, что перепроверяется.
AuthorType— snapshotUserTypeна момент создания черновика; повторная проверка на publish берёт сохранённыйPost.AuthorType, а не текущийUserTypeактора — смена роли пользователя между черновиком и публикацией eligibility не переоткрывает и не ужесточает. Перепроверяется же текущее состояние категории: между черновиком и publish её могли архивировать (Status = Archived→NEWS_CATEGORY_NOT_FOUND) или переназначитьAllowedAuthorTypes(тип выпал из набора →NEWS_CATEGORY_NOT_ALLOWED). На выдачу справочника (CanPublish, фаза 1) влияет текущийUserType, поэтому список категорий и уже сохранённый черновик могут разойтись — publish разрешает конфликт по сохранённомуAuthorTypeи актуальному статусу/набору категории.
SEED категорий (AC5; дом полей — ../domain/post-category.md)
AllowedAuthorTypes — enum-строки PostAuthorType (1..5, уникальны).
Slug | Title | AllowedAuthorTypes |
|---|---|---|
platform-news | Новости платформы | Investor, Agent, AgencyManager, DeveloperManager, PinEmployee |
construction-progress | Ход строительства | DeveloperManager |
promotions | Акции | DeveloperManager, AgencyManager, Agent |
agent-blog | Блог агента | Agent, AgencyManager |
investor-story | Истории инвесторов | Investor |
market-analytics | Аналитика рынка | PinEmployee |
Диаграмма
Альтернативные Потоки
A1. Попытка публикации в недоступную типу автора категорию
- Автор указывает
CategoryId, тип которого не входит вAllowedAuthorTypes(обход UI-фильтра фазы 1 или гонка с архивацией/правкой категории администратором между выдачей и записью). - Проверка INV-NWS-002 на create-draft и publish (
AuthorType ∉ category.AllowedAuthorTypes.Types) →NEWS_CATEGORY_NOT_ALLOWED(403). Пост не создаётся/не публикуется; событий нет.
Завершается ошибкой; клиент перечитывает GET /api/v1/categories?onlyEligible=true и выбирает доступную.
A2. Привязка ProjectRefs/AuthorCompanyId несоответствующим типом автора
- Не-застройщик (
Agent/Investor/AgencyManager/PinEmployee) передаёт непустойProjectRefs, либо инвестор/агент/сотрудник передаётAuthorCompanyId(INV-NWS-003:ProjectRefs— толькоDeveloperManager;AuthorCompanyId— толькоDeveloperManager/AgencyManagerи должен совпадать со scope актора). - Проверка attribution →
NEWS_ATTRIBUTION_NOT_ALLOWED(403). Пост не создаётся.
Завершается ошибкой; клиент публикует без недопустимой привязки (агент/инвестор — «к себе»).
Постусловие
- Пост создан/опубликован в категории, разрешённой типу автора (
AuthorType ∈ AllowedAuthorTypes), attribution соответствует типу (застройщик — компания/проекты; агент/инвестор — к себе). - При публикации:
Status = Published, зафиксированPostVersion(INV-NWS-005), опубликованpin.news.post-published.v1; пост в ленте и в Elasticsearch-проекции поиска. - Справочник
PostCategoryне изменён (read-only для авторов; запись — только admin API, INV-CAT-004). - При A1/A2 система не изменена: ни
Post, ни события.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
VALIDATION_FAILED | 400 | Пустой CategoryId; ProjectRefs > 20; нераспознанный литерал onlyEligible | Исправить форму |
UNAUTHORIZED | 401 | Нет/просрочен токен (аноним справочник читает без токена) | Повторная авторизация |
NEWS_CATEGORY_NOT_FOUND | 404 | Категория не найдена или Status = Archived | Перечитать справочник категорий |
NEWS_CATEGORY_NOT_ALLOWED | 403 | AuthorType ∉ Category.AllowedAuthorTypes (INV-NWS-002 / INV-CAT-002), A1 | Выбрать доступную категорию |
NEWS_ATTRIBUTION_NOT_ALLOWED | 403 | ProjectRefs/AuthorCompanyId не соответствуют типу автора или scope (INV-NWS-003), A2 | Убрать недопустимую привязку |
403 (record-level) | 403 | Не автор / не менеджер scope / нет news.posts.moderate (INV-NWS-007) | Нет прав на пост |
Доменные События
| Событие | Когда поднимается | Эффект |
|---|---|---|
PostContentChangedEvent | Создание черновика / правка Title/Subtitle/Name/блоков | пересчёт SearchText в той же транзакции (INV-NWS-008); для Published-поста — upsert Elasticsearch-проекции поиска |
PostVersionCapturedEvent | Publish/Republish (→ Published) | публикация PostPublishedKafkaEvent |
Справочник PostCategory доменных событий не порождает (reference-словарь, ../domain/post-category.md).
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
PostPublishedKafkaEvent | pin.news.post-published.v1 | Публикация поста в разрешённой категории (ContractVersion = 1, key = PostId) | notifications (подписчики автора), user-activity (таймлайн автора, опц.) |
Контракт/payload — ../events/post-published.md; публикация — атомарно с
переходом и записью PostVersion. Выдача категорий и create-draft интеграционных событий не порождают.
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | GET /api/v1/categories?onlyEligible=true возвращает только категории с CanPublish = true для ActorUserType; гость → пустой список | Integration-тест выдачи под разными UserType + анонимом |
| AC2 | CanPublish = ActorUserType ∈ AllowedAuthorTypes (тот же предикат, что гейт публикации INV-CAT-002); полный список без onlyEligible не фильтруется | Тест соответствия CanPublish ↔ eligibility publish |
| AC3 | Публикация в недоступную типу категорию (A1) → NEWS_CATEGORY_NOT_ALLOWED (403) и на create-draft, и на publish (повторная INV-NWS-002) | Негативный тест обоих endpoint |
| AC4 | Attribution по типу: застройщик — ProjectRefs/AuthorCompanyId разрешены; ProjectRefs не-застройщиком / AuthorCompanyId инвестором → NEWS_ATTRIBUTION_NOT_ALLOWED (A2) | Позитивный/негативный тесты INV-NWS-003 по каждому типу |
| AC5 | SEED категорий: construction-progress→DeveloperManager; agent-blog→Agent/AgencyManager; investor-story→Investor; promotions→застройщик+агент; platform-news→все | Тест SEED-миграции |
| AC6 | Успешная публикация оставляет пост в разрешённой категории (Status = Published, PostVersion, опубликован pin.news.post-published.v1) | Тест happy path + assert публикации события/версии |
| AC7 | Publish перепроверяет eligibility по сохранённому Post.AuthorType и текущему состоянию категории: смена роли актора между черновиком и publish на исход не влияет; архивация/переназначение AllowedAuthorTypes даёт NEWS_CATEGORY_NOT_FOUND/NEWS_CATEGORY_NOT_ALLOWED | Тесты: смена UserType перед publish (не влияет); архивация/правка категории между draft и publish |
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: выдача —
../api/public/categories/list-categories.md; создание —../api/authoring/create-post-draft.md; публикация —../api/authoring/publish-post.md; SEED/CRUD категорий —../api/admin/manage-categories.md. - Domain entities:
../domain/post-category.md(AllowedAuthorTypes,CategoryAuthorTypes, enumPostAuthorType/CategoryStatus, INV-CAT-002/003/004),../domain/post.md(AuthorType,AuthorCompanyId,ProjectRefs, INV-NWS-002/003/005/007,PostPublishedKafkaEvent). - Kafka событие:
../events/post-published.md. - Permission-каталог:
../../identity/domain/permission.md(news.posts.create-personal/create-company/create-project,news.posts.manage-own,news.categories.manage); карта области — README контекста. - ADR: 0052 tenancy, 0053 admin/m2m, 0056 simplicity-first.