POST /api/v1/posts — Создать черновик поста
Точка входа авторинга: пользователь создаёт черновик поста ленты новостей (Status = Draft) с
метаданными, но без контента-блоков. Публикация, версионирование и интеграционное событие
post-published выполняются отдельной операцией POST /api/v1/posts/{id}/publish — черновик в ленту и
поиск не попадает.
Назначение
Завести авторский черновик: зафиксировать автора (snapshot из identity), категорию, суффикс-slug и
заголовки, создать Post в статусе Draft с пустым контентом.
| Поле | Значение |
|---|
| Сервис/контекст | news |
| API area | authoring |
| Feature group | authoring |
| Статус | draft |
| Документ | docs/06-services/news/api/authoring/create-post-draft.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (инвестор, агент, менеджер застройщика/агентства, сотрудник PIN) |
| Auth policy / Permissions | news.posts.publish (канонический каталог разбивает по scope: news.posts.create-personal/create-company/create-project, permission.md) |
| Scope (RBAC) | self для инвестора/агента/сотрудника; scope компании для DeveloperManager/AgencyManager (AuthorCompanyId из claims) |
| Record-level | AuthorUserId = actor (менеджер — в scope AuthorCompanyId), INV-NWS-007 |
| Tenant isolation | TenantId = AuthorCompanyId (nullable); null для Investor/Agent/PinEmployee (ADR-0052 п.4) |
| Audit | create — actor-stamping CreatedByUserId; PII нет (авторские ФИО — реплика identity-replicas.md) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/posts |
| Success status | 201 (черновик создан) |
| Idempotency header | не применяется (дедуп по уникальному Suffix, INV-NWS-001) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
Name | body | string | Да | 1..256, не пустое после trim | — |
Suffix | body | string | Да | 1..128, нормализация → kebab-case ^[a-z0-9]+(-[a-z0-9]+)*$; уникален среди неудалённых (INV-NWS-001) | — |
Title | body | string | Да | 1..512, не пустое после trim | — |
Subtitle | body | string? | Нет | <= 1024 | — |
CategoryId | body | Guid | Да | != Guid.Empty; существующая категория со Status = Active (INV-NWS-002) | — |
AuthorCompanyId | body / identity claims | Guid? | Нет | для менеджера — из scope/claims; допустим только AuthorType ∈ {DeveloperManager, AgencyManager} и должен совпадать со scope актора (INV-NWS-003) | — |
ProjectRefs | body | IReadOnlyList<Guid>? | Нет | <= 20, уникальные; непустой только при AuthorType = DeveloperManager (INV-NWS-003); ссылки не проверяются синхронно (ADR-0056) | — |
AuthorUserId | identity claims | Guid | Да | Required (JWT); дом реплики — identity-replicas.md | — |
AuthorType | identity claims | PostAuthorType | Да | snapshot UserType на момент создания (enum-строка) | — |
Модель ответа PostResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
PostId | Guid | Да | Post.Id | — | Идентификатор созданного черновика. |
Suffix | string | Да | Post.Suffix (нормализованный) | — | URL-slug, база для /news/{suffix}. |
Status | PostStatus | Да | Post.Status (всегда Draft) | — | Статус черновика — enum-строка "Draft". |
Алгоритм
- Контекст и доступ. Операцию выполняет авторизованный пользователь (
User); гость → 401. Автор
фиксируется из JWT: AuthorUserId, AuthorType (snapshot UserType), для менеджера — AuthorCompanyId
из scope. Требуется право news.posts.publish. Суффикс нормализуется в kebab-case (lowercase, trim,
пробелы → дефисы).
- Проверка входа. Длины
Name/Suffix/Title/Subtitle, kebab-паттерн Suffix, CategoryId задан,
ProjectRefs — не более 20 уникальных; нарушение → VALIDATION_FAILED. Категория по CategoryId должна
существовать и быть Active; если её нет или она Archived → NEWS_CATEGORY_NOT_FOUND (снятая категория
недоступна для новых публикаций). Suffix должен быть свободен среди неудалённых постов; занят →
NEWS_SUFFIX_TAKEN (уникальный индекс страхует гонку, INV-NWS-001).
- Бизнес-правила.
- eligibility (INV-NWS-002 / INV-CAT-002):
AuthorType входит в AllowedAuthorTypes категории, иначе →
NEWS_CATEGORY_NOT_ALLOWED;
- атрибуция (INV-NWS-003):
AuthorCompanyId задаётся только при AuthorType ∈ {DeveloperManager, AgencyManager} и равен scope-компании актора; непустой ProjectRefs — только для DeveloperManager;
иначе → NEWS_ATTRIBUTION_NOT_ALLOWED.
- Создание. Создать черновик
Post (переход [*] → Draft): пустой контент, Status = Draft,
CurrentVersionNumber = 0, CommentsEnabled = true, CommentsCount = 0, ViewsCount = 0, SearchText
пуст. Денормализованный SearchText пересчитывается из Name/Title/Subtitle в том же согласованном
изменении (INV-NWS-008).
- Результат. Черновик сохранён; возвращается
PostResult { PostId, Suffix, Status = Draft } → 201.
Черновик в ленту и в поисковую проекцию news-posts не индексируется (только Published); интеграционных
событий не порождает — post-published шлётся только при публикации.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none — черновик не порождает событий (post-published — только при публикации, post-published.md) |
| External calls | none — selections не вызывается (блоков нет); identity — только claims из JWT |
| Cache / projections / search | SearchText (денорм. plaintext) пересчитывается, но пост Draft в Elasticsearch-проекцию news-posts не индексируется (только Published, search/posts.md) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | формат Name/Suffix/Title/Subtitle, ProjectRefs > 20 | «Проверьте корректность полей» | no retry |
NEWS_CATEGORY_NOT_FOUND | 404 | категория не найдена или Status = Archived | «Категория не найдена» | no retry |
NEWS_CATEGORY_NOT_ALLOWED | 403 | AuthorType ∉ Category.AllowedAuthorTypes (INV-NWS-002) | «Публикация в эту категорию недоступна для вашего типа автора» | no retry |
NEWS_ATTRIBUTION_NOT_ALLOWED | 403 | AuthorCompanyId/ProjectRefs не соответствуют типу автора или scope (INV-NWS-003) | «Недопустимая привязка автора» | no retry |
NEWS_SUFFIX_TAKEN | 409 | Suffix занят (pre-check или гонка unique-индекса, INV-NWS-001) | «Суффикс уже используется» | retry с другим суффиксом |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор аутентифицирован (
User) и несёт news.posts.publish в scope своего типа (create-personal/create-company/create-project);
- целевая категория существует и
Active (снятая с оборота недоступна для новых публикаций);
- для менеджера —
AuthorCompanyId соответствует его company-scope; непустой ProjectRefs допустим только для DeveloperManager.
Постусловия (гарантированы после успешного создания):
- создан
Post в статусе Draft: CurrentVersionNumber = 0, CommentsEnabled = true, CommentsCount = 0, ViewsCount = 0, пустой контент;
Suffix нормализован в kebab-case, занят и уникален среди неудалённых (INV-NWS-001); денормализованный SearchText пересчитан из Name/Title/Subtitle (INV-NWS-008);
- пост не в ленте и не в поисковой проекции
news-posts (индексируются только Published); PostVersion не пишется, интеграционных событий нет — черновик локален до publish, кросс-сервисных эффектов нет (identity используется только как источник claims из JWT).
Acceptance Criteria
- Создание черновика. Дано: авторизованный автор, категория
Active, AuthorType ∈ Category.AllowedAuthorTypes, Suffix свободен. Когда: POST /api/v1/posts. Тогда: 201, Post в Draft, Suffix нормализован и занят, событий/версий нет.
- Категория снята/не найдена. Дано:
CategoryId отсутствует или Archived. Когда: POST. Тогда: 404 NEWS_CATEGORY_NOT_FOUND.
- Тип автора вне категории. Дано:
AuthorType ∉ Category.AllowedAuthorTypes (INV-NWS-002). Когда: POST. Тогда: 403 NEWS_CATEGORY_NOT_ALLOWED.
- Недопустимая атрибуция. Дано: инвестор/агент с непустым
AuthorCompanyId/ProjectRefs либо менеджер с ProjectRefs вне DeveloperManager (INV-NWS-003). Когда: POST. Тогда: 403 NEWS_ATTRIBUTION_NOT_ALLOWED.
- Занятый суффикс. Дано:
Suffix уже используется неудалённым постом (pre-check или гонка unique-индекса, INV-NWS-001). Когда: POST. Тогда: 409 NEWS_SUFFIX_TAKEN, клиент повторяет с другим суффиксом.
- Невалидный вход. Дано: нарушены длины/паттерн
Suffix либо ProjectRefs > 20. Когда: POST. Тогда: 400 VALIDATION_FAILED.
- Гость. Дано: запрос без валидного JWT. Когда:
POST. Тогда: 401.
Совместимость и наблюдаемость
- Новые optional поля тела (
Subtitle, ProjectRefs, AuthorCompanyId) — additive, без breaking change;
удаление/переименование обязательных полей или смена типа — breaking (migration + rollout).
- Logs:
post-draft created (PostId, CategoryId, AuthorType, tenant); metrics:
news_post_drafts_total{result}, latency p95; traces — сквозной trace через traceparent; dashboard
news-overview; runbook — docs/06-services/news/runbooks/ (целевой путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы