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

POST /api/v1/posts — Создать черновик поста

Точка входа авторинга: пользователь создаёт черновик поста ленты новостей (Status = Draft) с метаданными, но без контента-блоков. Публикация, версионирование и интеграционное событие post-published выполняются отдельной операцией POST /api/v1/posts/{id}/publish — черновик в ленту и поиск не попадает.

Назначение

Завести авторский черновик: зафиксировать автора (snapshot из identity), категорию, суффикс-slug и заголовки, создать Post в статусе Draft с пустым контентом.

Metadata

ПолеЗначение
Сервис/контекстnews
API areaauthoring
Feature groupauthoring
Статусdraft
Документdocs/06-services/news/api/authoring/create-post-draft.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (инвестор, агент, менеджер застройщика/агентства, сотрудник PIN)
Auth policy / Permissionsnews.posts.publish (канонический каталог разбивает по scope: news.posts.create-personal/create-company/create-project, permission.md)
Scope (RBAC)self для инвестора/агента/сотрудника; scope компании для DeveloperManager/AgencyManager (AuthorCompanyId из claims)
Record-levelAuthorUserId = actor (менеджер — в scope AuthorCompanyId), INV-NWS-007
Tenant isolationTenantId = AuthorCompanyId (nullable); null для Investor/Agent/PinEmployee (ADR-0052 п.4)
Auditcreate — actor-stamping CreatedByUserId; PII нет (авторские ФИО — реплика identity-replicas.md)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/posts
Success status201 (черновик создан)
Idempotency headerне применяется (дедуп по уникальному Suffix, INV-NWS-001)
Correlationсквозной trace через traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
NamebodystringДа1..256, не пустое после trim
SuffixbodystringДа1..128, нормализация → kebab-case ^[a-z0-9]+(-[a-z0-9]+)*$; уникален среди неудалённых (INV-NWS-001)
TitlebodystringДа1..512, не пустое после trim
Subtitlebodystring?Нет<= 1024
CategoryIdbodyGuidДа!= Guid.Empty; существующая категория со Status = Active (INV-NWS-002)
AuthorCompanyIdbody / identity claimsGuid?Нетдля менеджера — из scope/claims; допустим только AuthorType ∈ {DeveloperManager, AgencyManager} и должен совпадать со scope актора (INV-NWS-003)
ProjectRefsbodyIReadOnlyList<Guid>?Нет<= 20, уникальные; непустой только при AuthorType = DeveloperManager (INV-NWS-003); ссылки не проверяются синхронно (ADR-0056)
AuthorUserIdidentity claimsGuidДаRequired (JWT); дом реплики — identity-replicas.md
AuthorTypeidentity claimsPostAuthorTypeДаsnapshot UserType на момент создания (enum-строка)

Модель ответа PostResult

ПолеТипОбязательностьИсточникMaskingОписание
PostIdGuidДаPost.IdИдентификатор созданного черновика.
SuffixstringДаPost.Suffix (нормализованный)URL-slug, база для /news/{suffix}.
StatusPostStatusДаPost.Status (всегда Draft)Статус черновика — enum-строка "Draft".

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь (User); гость → 401. Автор фиксируется из JWT: AuthorUserId, AuthorType (snapshot UserType), для менеджера — AuthorCompanyId из scope. Требуется право news.posts.publish. Суффикс нормализуется в kebab-case (lowercase, trim, пробелы → дефисы).
  2. Проверка входа. Длины Name/Suffix/Title/Subtitle, kebab-паттерн Suffix, CategoryId задан, ProjectRefs — не более 20 уникальных; нарушение → VALIDATION_FAILED. Категория по CategoryId должна существовать и быть Active; если её нет или она ArchivedNEWS_CATEGORY_NOT_FOUND (снятая категория недоступна для новых публикаций). Suffix должен быть свободен среди неудалённых постов; занят → NEWS_SUFFIX_TAKEN (уникальный индекс страхует гонку, INV-NWS-001).
  3. Бизнес-правила.
    • 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.
  4. Создание. Создать черновик Post (переход [*] → Draft): пустой контент, Status = Draft, CurrentVersionNumber = 0, CommentsEnabled = true, CommentsCount = 0, ViewsCount = 0, SearchText пуст. Денормализованный SearchText пересчитывается из Name/Title/Subtitle в том же согласованном изменении (INV-NWS-008).
  5. Результат. Черновик сохранён; возвращается PostResult { PostId, Suffix, Status = Draft }201. Черновик в ленту и в поисковую проекцию news-posts не индексируется (только Published); интеграционных событий не порождает — post-published шлётся только при публикации.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsnone — черновик не порождает событий (post-published — только при публикации, post-published.md)
External callsnone — selections не вызывается (блоков нет); identity — только claims из JWT
Cache / projections / searchSearchText (денорм. plaintext) пересчитывается, но пост Draft в Elasticsearch-проекцию news-posts не индексируется (только Published, search/posts.md)
Notificationsnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400формат Name/Suffix/Title/Subtitle, ProjectRefs > 20«Проверьте корректность полей»no retry
NEWS_CATEGORY_NOT_FOUND404категория не найдена или Status = Archived«Категория не найдена»no retry
NEWS_CATEGORY_NOT_ALLOWED403AuthorType ∉ Category.AllowedAuthorTypes (INV-NWS-002)«Публикация в эту категорию недоступна для вашего типа автора»no retry
NEWS_ATTRIBUTION_NOT_ALLOWED403AuthorCompanyId/ProjectRefs не соответствуют типу автора или scope (INV-NWS-003)«Недопустимая привязка автора»no retry
NEWS_SUFFIX_TAKEN409Suffix занят (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/ (целевой путь по конвенции).

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

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

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