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

Admin CRUD категорий постов — POST/PUT/GET /api/v1/admin/categories (m2m)

Четыре операции admin-области над справочником категорий PostCategory: create / update / archive / list. Дом полей, инварианты INV-CAT-001..004 и enum'ы — domain/post-category.md; здесь — только операционная дельта.

Назначение

Администратор платформы (m2m, ADR-0053) ведёт платформенный SEED-справочник категорий новостной ленты PostCategory: заводит новые категории, правит их отображение и состав допустимых типов авторов (AllowedAuthorTypes), архивирует вышедшие из оборота и читает полный список (включая Archived) для панели администрирования. Справочник читается публично и кэшируется фронтом (public/categories/list-categories.md); правки редки, поэтому справочник не порождает интеграционных событий — фронт перечитывает публичный список. Feature group — categories.

Metadata

ПолеЗначение
Сервис/контекстnews
API areaadmin (m2m, ADR-0053)
Feature groupcategories
Статусdraft
Документdocs/06-services/news/api/admin/manage-categories.md

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

ПроверкаЗначение
Actor typeOperator(Admin) через admin API; транспорт — ExternalSystem (m2m client-credentials, ADR-0053)
Auth policy / Permissionspolicy AdminSystemOnly (клиент pin-admin-system); permission news.categories.manage (INV-CAT-004) — identity/domain/permission.md
Scope (RBAC)платформа — справочник не tenanted (ADR-0052 п.3); доступность категорий компании решается workspace-настройкой, не строкой словаря
Record-levelне применяется
Tenant isolationне применяется (платформенный news.post_categories)
Auditобязателен: deep audit admin-мутаций — actor (X-Admin-User-Id), CategoryId, диф полей, результат; у сущности отдельных *ByUserId полей нет, актор фиксируется журналом admin API

HTTP-контракт

ОперацияMethodRouteSuccessIdempotency
CreatePOST/api/v1/admin/categories201 (+ Location: /api/v1/admin/categories/{id})Idempotency-Key (Guid, обязателен)
UpdatePUT/api/v1/admin/categories/{id}200по id (естественная)
ArchivePOST/api/v1/admin/categories/{id}/archive200по id (естественная; повтор — no-op)
ListGET/api/v1/admin/categories200не применяется (чтение)

Correlation — сквозной trace через заголовок traceparent; заголовок X-Admin-User-Id (Guid оператора) обязателен для мутаций.

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

Типы, ограничения и дом полей — domain/post-category.md. Slug неизменяем после создания (INV-CAT-001) — присутствует только в create.

ПолеИсточникТипОбязательностьВалидация / ОграниченияОперация
AdminUserIdheader X-Admin-User-IdGuidДа (мутации)!= Guid.Emptycreate/update/archive
IdempotencyKeyheader Idempotency-KeyGuidДа!= Guid.Emptycreate
SlugbodystringДа1..64, pattern ^[a-z0-9]+(-[a-z0-9]+)*$, уникален (INV-CAT-001)create
TitlebodystringДа1..128 после trimcreate/update
Descriptionbodystring?Нет<= 512create/update
AllowedAuthorTypesbodyIReadOnlyList<PostAuthorType>Да1..5 уникальных enum-строк (VO CategoryAuthorTypes); иначе NEWS_CATEGORY_AUTHOR_TYPES_INVALIDcreate/update
SortOrderbodyintДа>= 0 (default 0)create/update
IdrouteGuidДа!= Guid.Empty; существование — шаг 2update/archive
StatusqueryCategoryStatus?Нетenum Active/Archived; фильтр (admin видит все статусы)list
SearchTermquerystring?Нет<= 128; ILIKE по Slug/Titlelist
Page / PageSizequeryintНетPage >= 1 (def 1); PageSize 1..100 (def 50)list

PostAuthorType (дом — domain/post-category.md): DeveloperManager, AgencyManager, Agent, Investor, PinEmployee (enum строкой; unknown → ValidationError).

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

CategoryMutationResult (create/update/archive)

ПолеТипОбязательностьИсточникОписание
IdGuidДаPostCategory.IdИдентификатор категории.
SlugstringДаPostCategory.SlugСтабильный kebab-ключ.
Statusstring (enum CategoryStatus)ДаPostCategory.StatusActive / Archived после мутации.
SortOrderintДаPostCategory.SortOrderПорядок вывода.
CreatedboolДавычисление (create-ветка)true только для create.
UpdatedAtDateTimeOffsetДаPostCategory.UpdatedAtМомент мутации.

AdminCategoryModel (элемент списка)

ПолеТипОбязательностьИсточникОписание
IdGuidДаPostCategory.Id
SlugstringДаPostCategory.Slug
TitlestringДаPostCategory.Title
Descriptionstring?НетPostCategory.Description
AllowedAuthorTypesIReadOnlyList<PostAuthorType>ДаCategoryAuthorTypes.Typesenum-строки.
SortOrderintДаPostCategory.SortOrder
Statusstring (enum CategoryStatus)ДаPostCategory.StatusAdmin видит Active и Archived.
CreatedAt / UpdatedAtDateTimeOffsetДасущность

Список возвращается постранично: Items (страница AdminCategoryModel), TotalCount, Page, PageSize.

Алгоритм

Четыре ветки над справочником PostCategory: create / update / archive / list. Все выполняет администратор платформы (m2m-клиент pin-admin-system, policy AdminSystemOnly) с оператором из заголовка X-Admin-User-Id; для любой мутации требуется permission news.categories.manage, иначе — FORBIDDEN (INV-CAT-004). Справочник платформенный (не tenanted). Каждая мутация фиксируется в deep-audit (актор, CategoryId, диф полей).

  1. Create. Проверить вход: Slug соответствует kebab-паттерну и длине, Title 1..128, Description <= 512, AllowedAuthorTypes — 1..5 уникальных значений PostAuthorType (иначе NEWS_CATEGORY_AUTHOR_TYPES_INVALID), SortOrder >= 0; нарушение формата → ValidationError. Повтор с тем же Idempotency-Key возвращает ранее созданную категорию (201). Если Slug уже занят → NEWS_CATEGORY_SLUG_DUPLICATE (409, INV-CAT-001; уникальный индекс страхует гонку). Иначе создать категорию со статусом Active; Slug фиксируется неизменяемым (INV-CAT-001). Ответ — CategoryMutationResult { Created = true }, 201 + Location.
  2. Update. Найти категорию по Id; если не найдена → NEWS_CATEGORY_NOT_FOUND (404). Проверить те же ограничения полей. Обновить Title/Description/AllowedAuthorTypes/SortOrder; Slug не меняется (INV-CAT-001). Ответ — CategoryMutationResult { Created = false }, 200.
  3. Archive. Найти категорию по Id; если не найдена → NEWS_CATEGORY_NOT_FOUND. Перевести Active → Archived (INV-CAT-003 — существующие посты сохраняют CategoryId, физического удаления нет). Если категория уже Archived — операция идемпотентна: вернуть текущее состояние (200). Физическое удаление категории с постами не предоставляется (NEWS_CATEGORY_IN_USE); штатная замена — архивирование.
  4. List. Вернуть страницу категорий с фильтром по Status (админ видит Active и Archived) и SearchTerm (ILIKE по Slug/Title), сортировка SortOrder ↑, затем Title ↑ (индекс ix_post_categories_status). Проекция — AdminCategoryModel.

Справочник не порождает доменных/интеграционных событий (reference-данные): фронт перечитывает публичный список.

Диаграмма

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

ТипДетали
Integration eventsnone (справочник; фронт перечитывает публичный список)
External callsnone
Cache / projections / searchnone в news; публичный кэш категорий на фронте инвалидируется его собственным TTL/перечиткой
Notificationsnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400slug pattern/длины/SortOrder/enumОшибка валидации запросаno retry
NEWS_CATEGORY_AUTHOR_TYPES_INVALID400AllowedAuthorTypes пуст, > 5 или с дублями (VO)Список типов авторов некорректенno retry
FORBIDDEN403Нет news.categories.manage / клиент не pin-admin-system (INV-CAT-004)Доступ запрещёнno retry
NEWS_CATEGORY_NOT_FOUND404update/archive: категория по Id не найденаКатегория не найденаno retry
NEWS_CATEGORY_SLUG_DUPLICATE409create: Slug уже занят (INV-CAT-001)Категория с таким slug уже существуетno retry
NEWS_CATEGORY_IN_USE409Физическое удаление категории с постами (INV-CAT-003) — hard-delete в admin-API не предоставляется; штатная замена — POST /{id}/archiveКатегория используется постами; заархивируйтеno retry

Предусловия и постусловия

Предусловия (истинны до сценария):

  • запрос идёт от m2m-клиента pin-admin-system (policy AdminSystemOnly, ADR-0053) с оператором в X-Admin-User-Id; для любой мутации — permission news.categories.manage (INV-CAT-004);
  • create: Slug соответствует kebab-паттерну и свободен (INV-CAT-001); update/archive: категория с Id существует;
  • AllowedAuthorTypes — 1..5 уникальных значений PostAuthorType.

Постусловия (гарантированы после мутации):

  • create: заведена категория со Status = Active, неизменяемым Slug (INV-CAT-001); update: обновлены Title/Description/AllowedAuthorTypes/SortOrder (Slug не тронут); archive: Active → Archived при сохранении CategoryId у существующих постов (INV-CAT-003, физического удаления нет);
  • каждая мутация записана в deep-audit (актор, CategoryId, диф полей);
  • интеграционных/доменных событий нет — справочник reference-данные, фронт перечитывает публичный список.

Операция полностью локальна для news: синхронных кросс-сервисных вызовов и исходящих событий нет; единственный внешний эффект — публичный кэш категорий на фронте инвалидируется его собственным TTL/перечиткой.

Acceptance Criteria

  • Создание категории. Дано: pin-admin-system с news.categories.manage, свободный валидный Slug, AllowedAuthorTypes 1..5. Когда: POST /api/v1/admin/categories. Тогда: 201 + Location, CategoryMutationResult { Created = true, Status = Active }.
  • Идемпотентное создание. Дано: повтор с тем же Idempotency-Key. Когда: POST. Тогда: возвращается ранее созданная категория (201), дубля нет.
  • Дубль slug. Дано: Slug уже занят (INV-CAT-001). Когда: POST. Тогда: 409 NEWS_CATEGORY_SLUG_DUPLICATE.
  • Обновление. Дано: существующая категория. Когда: PUT /{id} с новыми Title/AllowedAuthorTypes. Тогда: 200, Created = false, Slug не изменён.
  • Архивирование. Дано: Active-категория с постами. Когда: POST /{id}/archive. Тогда: 200, Archived, посты сохраняют CategoryId (INV-CAT-003).
  • Повторное архивирование. Дано: категория уже Archived. Когда: POST /{id}/archive. Тогда: 200 идемпотентно (текущее состояние).
  • Некорректные типы авторов. Дано: AllowedAuthorTypes пуст, > 5 или с дублями. Когда: POST/PUT. Тогда: 400 NEWS_CATEGORY_AUTHOR_TYPES_INVALID.
  • Категория не найдена. Дано: Id отсутствует. Когда: PUT/archive. Тогда: 404 NEWS_CATEGORY_NOT_FOUND.
  • Попытка hard-delete с постами. Дано: категория используется постами. Когда: удаление. Тогда: 409 NEWS_CATEGORY_IN_USE — штатная замена — архивирование.
  • Нет прав. Дано: клиент не pin-admin-system или без news.categories.manage (INV-CAT-004). Когда: любая мутация. Тогда: 403 FORBIDDEN.
  • Листинг для панели. Дано: оператор. Когда: GET /api/v1/admin/categories?status&searchTerm. Тогда: 200, страница AdminCategoryModel (включая Archived), сортировка SortOrder ↑, Title ↑.

Совместимость и наблюдаемость

  • Новые optional поля запроса/ответа — additive; удаление/переименование/смена типа или enum-значения PostAuthorType/CategoryStatus — breaking (migration + rollout). Slug неизменяем — публичный ключ ленты.
  • Logs: admin-news-category-created/updated/archived (CategoryId, Slug, AdminUserId, изменённые поля); metrics: news_admin_category_operations_total{operation}, duration; traces — сквозной trace через traceparent; каждая мутация — в deep audit. Dashboard news-overview; runbook — сверка публичного кэша категорий (не применяется для CRUD — правки редки).

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

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

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