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 area | admin (m2m, ADR-0053) |
| Feature group | categories |
| Статус | draft |
| Документ | docs/06-services/news/api/admin/manage-categories.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | Operator(Admin) через admin API; транспорт — ExternalSystem (m2m client-credentials, ADR-0053) |
| Auth policy / Permissions | policy 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-контракт
| Операция | Method | Route | Success | Idempotency |
|---|---|---|---|---|
| Create | POST | /api/v1/admin/categories | 201 (+ Location: /api/v1/admin/categories/{id}) | Idempotency-Key (Guid, обязателен) |
| Update | PUT | /api/v1/admin/categories/{id} | 200 | по id (естественная) |
| Archive | POST | /api/v1/admin/categories/{id}/archive | 200 | по id (естественная; повтор — no-op) |
| List | GET | /api/v1/admin/categories | 200 | не применяется (чтение) |
Correlation — сквозной trace через заголовок traceparent; заголовок X-Admin-User-Id
(Guid оператора) обязателен для мутаций.
Входные данные / параметры запроса
Типы, ограничения и дом полей — domain/post-category.md.
Slug неизменяем после создания (INV-CAT-001) — присутствует только в create.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Операция |
|---|---|---|---|---|---|
AdminUserId | header X-Admin-User-Id | Guid | Да (мутации) | != Guid.Empty | create/update/archive |
IdempotencyKey | header Idempotency-Key | Guid | Да | != Guid.Empty | create |
Slug | body | string | Да | 1..64, pattern ^[a-z0-9]+(-[a-z0-9]+)*$, уникален (INV-CAT-001) | create |
Title | body | string | Да | 1..128 после trim | create/update |
Description | body | string? | Нет | <= 512 | create/update |
AllowedAuthorTypes | body | IReadOnlyList<PostAuthorType> | Да | 1..5 уникальных enum-строк (VO CategoryAuthorTypes); иначе NEWS_CATEGORY_AUTHOR_TYPES_INVALID | create/update |
SortOrder | body | int | Да | >= 0 (default 0) | create/update |
Id | route | Guid | Да | != Guid.Empty; существование — шаг 2 | update/archive |
Status | query | CategoryStatus? | Нет | enum Active/Archived; фильтр (admin видит все статусы) | list |
SearchTerm | query | string? | Нет | <= 128; ILIKE по Slug/Title | list |
Page / PageSize | query | int | Нет | 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)
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
Id | Guid | Да | PostCategory.Id | Идентификатор категории. |
Slug | string | Да | PostCategory.Slug | Стабильный kebab-ключ. |
Status | string (enum CategoryStatus) | Да | PostCategory.Status | Active / Archived после мутации. |
SortOrder | int | Да | PostCategory.SortOrder | Порядок вывода. |
Created | bool | Да | вычисление (create-ветка) | true только для create. |
UpdatedAt | DateTimeOffset | Да | PostCategory.UpdatedAt | Момент мутации. |
AdminCategoryModel (элемент списка)
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
Id | Guid | Да | PostCategory.Id | — |
Slug | string | Да | PostCategory.Slug | — |
Title | string | Да | PostCategory.Title | — |
Description | string? | Нет | PostCategory.Description | — |
AllowedAuthorTypes | IReadOnlyList<PostAuthorType> | Да | CategoryAuthorTypes.Types | enum-строки. |
SortOrder | int | Да | PostCategory.SortOrder | — |
Status | string (enum CategoryStatus) | Да | PostCategory.Status | Admin видит Active и Archived. |
CreatedAt / UpdatedAt | DateTimeOffset | Да | сущность | — |
Список возвращается постранично: 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, диф полей).
- 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. - Update. Найти категорию по
Id; если не найдена →NEWS_CATEGORY_NOT_FOUND(404). Проверить те же ограничения полей. ОбновитьTitle/Description/AllowedAuthorTypes/SortOrder;Slugне меняется (INV-CAT-001). Ответ —CategoryMutationResult { Created = false },200. - Archive. Найти категорию по
Id; если не найдена →NEWS_CATEGORY_NOT_FOUND. ПеревестиActive → Archived(INV-CAT-003 — существующие посты сохраняютCategoryId, физического удаления нет). Если категория ужеArchived— операция идемпотентна: вернуть текущее состояние (200). Физическое удаление категории с постами не предоставляется (NEWS_CATEGORY_IN_USE); штатная замена — архивирование. - List. Вернуть страницу категорий с фильтром по
Status(админ видитActiveиArchived) иSearchTerm(ILIKEпоSlug/Title), сортировкаSortOrder↑, затемTitle↑ (индексix_post_categories_status). Проекция —AdminCategoryModel.
Справочник не порождает доменных/интеграционных событий (reference-данные): фронт перечитывает публичный список.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (справочник; фронт перечитывает публичный список) |
| External calls | none |
| Cache / projections / search | none в news; публичный кэш категорий на фронте инвалидируется его собственным TTL/перечиткой |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | slug pattern/длины/SortOrder/enum | Ошибка валидации запроса | no retry |
NEWS_CATEGORY_AUTHOR_TYPES_INVALID | 400 | AllowedAuthorTypes пуст, > 5 или с дублями (VO) | Список типов авторов некорректен | no retry |
FORBIDDEN | 403 | Нет news.categories.manage / клиент не pin-admin-system (INV-CAT-004) | Доступ запрещён | no retry |
NEWS_CATEGORY_NOT_FOUND | 404 | update/archive: категория по Id не найдена | Категория не найдена | no retry |
NEWS_CATEGORY_SLUG_DUPLICATE | 409 | create: Slug уже занят (INV-CAT-001) | Категория с таким slug уже существует | no retry |
NEWS_CATEGORY_IN_USE | 409 | Физическое удаление категории с постами (INV-CAT-003) — hard-delete в admin-API не предоставляется; штатная замена — POST /{id}/archive | Категория используется постами; заархивируйте | no retry |
Предусловия и постусловия
Предусловия (истинны до сценария):
- запрос идёт от m2m-клиента
pin-admin-system(policyAdminSystemOnly, ADR-0053) с оператором вX-Admin-User-Id; для любой мутации — permissionnews.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,AllowedAuthorTypes1..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. Dashboardnews-overview; runbook — сверка публичного кэша категорий (не применяется для CRUD — правки редки).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: PostCategory
- API (1): GET /api/v1/categories — Справочник категорий постов + eligibility актора
- Use Cases (1): UC-NWS-007. Категории по типам авторов (eligibility публикации)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность, инварианты INV-CAT-001..004 и enum'ы (
CategoryStatus,PostAuthorType, VOCategoryAuthorTypes): domain/post-category.md. - Публичное чтение с eligibility-фильтром: public/categories/list-categories.md.
- Permission:
identity/domain/permission.md(news.categories.manage). - ADR-0053 (admin m2m + deep audit), ADR-0052 (не tenanted справочник); сервис — README.md.