POST /api/admin/v1/promotion-packages — Создание пакета продвижения (admin)
Назначение
Заводит новый платформенный пакет продвижения (promotion-package.md)
в каталоге платформы: администратор платформы задаёт вид продвижения (Kind), вес буста, длительность,
лимит целей и прейскурантную цену. Оплата не реализуется (решение №10) — застройщик может купить пакет,
но без оплаты. Пакет создаётся в статусе Draft — невидим покупателям до отдельной публикации
(Draft → Active); витрина застройщика отдаёт только Active
(../developers/list-promotion-packages.md).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | promotion |
| API area | admin (ADR-0053; m2m) |
| Feature group | admin-promotions |
| Статус | draft |
| Документ | docs/06-services/promotion/api/admin/create-promotion-package.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | ExternalSystem (администратор платформы; машинный доступ admin API — ADR-0053) |
| Auth policy / Permissions | policy AdminSystemOnly; permission promotion.packages.admin (identity/domain/permission.md) |
| Scope (RBAC) | платформа (пакеты — каталог платформы, не tenanted — ADR-0052 п.3) |
| Record-level | не применяется |
| Tenant isolation | не применяется — пакет не привязан к tenant |
| Audit | обязателен: глубокий аудит админ-мутаций — актор (X-Admin-User-Id), Code, снимок созданных полей, результат; отметка автора создания (CreatedByUserId) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/admin/v1/promotion-packages |
| Success status | 201 (создан; Location: /api/admin/v1/promotion-packages/{id}); идемпотентный повтор — 201 с сохранённым результатом (шаг 1) |
| Idempotency header | Idempotency-Key (Guid; обязателен — admin API ретраит по таймауту) |
| Correlation | сквозной trace через заголовок traceparent; X-Admin-User-Id (Guid админа) — обязателен |
Входные данные / параметры запроса
Типы/ограничения/инварианты полей — дом promotion-package.md; здесь — источник и обязательность запроса.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
Code | body | string | Да | varchar(64), pattern ^[a-z0-9][a-z0-9-]*$; unique среди живых (шаг 2, INV-1) | — |
Name | body | string | Да | 1..256 после trim | — |
Description | body | string? | Нет | ≤ 2000 | — |
Kind | body | string (enum PromotionPackageKind) | Да | ∈ {ProjectBoost, PropertyObjectBoost, PostBoost} | — |
BoostWeight | body | int | Да | 1..100 | — |
DurationDays | body | int | Да | 1..365 | — |
MaxTargetsPerPurchase | body | int? | Условно | 1..500; обязателен при Kind ∈ {PropertyObjectBoost, PostBoost}, null при ProjectBoost (INV-2, кросс-поле шага 2) | — |
Price.Amount | body | decimal | Да | decimal(14,2), >= 0 (0 = бесплатный пакет) | — |
Price.CurrencyId | body | short? | Нет (default — RUB) | ссылка на запись справочника валют; резолв через internal resolve handbook (INV-5): неизвестный/архивный id → PROMOTION_CURRENCY_UNKNOWN; допустима только RUB (ADR-0048) | — |
AdminUserId | header X-Admin-User-Id | Guid | Да | != Guid.Empty | отметка автора (CreatedByUserId) |
IdempotencyKey | header Idempotency-Key | Guid | Да | != Guid.Empty | — |
Price — модель { Amount, CurrencyId } (дом VO PromotionPrice —
promotion-package.md). Status/Id/Version клиентом не
задаются. Оплата не реализуется (решение №10) — Price информационная.
Модель ответа PromotionPackageResult
Проекция созданного PromotionPackage (дом полей — promotion-package.md).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PromotionPackage.Id | — | Идентификатор созданного пакета. |
Code | string | Да | PromotionPackage.Code | — | Стабильный код. |
Name | string | Да | PromotionPackage.Name | — | Название. |
Kind | string (enum PromotionPackageKind) | Да | PromotionPackage.Kind | — | ProjectBoost / PropertyObjectBoost / PostBoost. |
BoostWeight | int | Да | PromotionPackage.BoostWeight | — | Вес буста (1..100). |
DurationDays | int | Да | PromotionPackage.DurationDays | — | Срок действия покупки (1..365). |
MaxTargetsPerPurchase | int? | Нет | PromotionPackage.MaxTargetsPerPurchase | — | Лимит целей (заполнен при multi-target Kind). |
Price | PromotionPriceModel | Да | PromotionPackage.Price (owned) | — | { Amount: decimal, CurrencyId: short }. |
Status | string (enum PromotionPackageStatus) | Да | PromotionPackage.Status | — | При создании — всегда Draft. |
Version | long | Да | PromotionPackage.Version | — | При создании — 1. |
Алгоритм
- Контекст и доступ. Операцию выполняет администратор платформы через admin API (машинный доступ,
ADR-0053); требуется permission
promotion.packages.admin. Пакеты — платформенный каталог, к tenant не привязаны. Идемпотентность: повтор с тем жеIdempotency-Keyвозвращает ранее созданный результат (201), без второго пакета; каноничным считается тело первого запроса — расхождение полей в повторе игнорируется (пакет повторно не правится). Ключ актуален на окно ретраев admin-клиента (таймаут-повтор). - Проверка входа. Проверить формат и диапазоны:
Code— pattern^[a-z0-9][a-z0-9-]*$и≤ 64;Name1..256;Description≤ 2000;Kind∈ {ProjectBoost,PropertyObjectBoost,PostBoost};BoostWeight1..100;DurationDays1..365;Price.Amount >= 0. Валюта (INV-5): не заданныйPrice.CurrencyIdзаменяется дефолтом RUB; заданный — резолвится в справочнике валют через internal resolvehandbook(неизвестный или архивный id →PROMOTION_CURRENCY_UNKNOWN, 422), затем guard RUB-only (ADR-0048): любая другая валюта →ValidationError. Справочник недоступен → fail-closedPROMOTION_HANDBOOK_UNAVAILABLE(503; админ повторяет с тем жеIdempotency-Key). Нарушение остальных правил →ValidationError. Кросс-полеMaxTargetsPerPurchase(INV-2): приKind ∈ {PropertyObjectBoost, PostBoost}обязателен, приnull→PROMOTION_PACKAGE_MAX_TARGETS_REQUIRED; приKind = ProjectBoostдолжен отсутствовать, иначе →PROMOTION_PACKAGE_MAX_TARGETS_NOT_APPLICABLE; значение вне1..500→ValidationError. - Уникальность кода (INV-1). Убедиться, что среди живых (не архивных) пакетов нет пакета с таким
Code; иначе →PROMOTION_PACKAGE_CODE_DUPLICATE(409). Гонка двух одновременных создателей одного кода ловится уникальным ограничением каталога и даёт тот же код ошибки. - Создание. Создать пакет со снапшотом заданных полей и ценой
Price {Amount, CurrencyId}. Начальный переход[*] → Draft(Status = Draft),Version = 1, автор — администратор (CreatedByUserId). Пакет невидим покупателям до отдельной публикации. - Результат. Вернуть карточку созданного пакета (
Status = Draft,Version = 1) —201+Location; ошибки → коды таблицы ниже. Интеграционные и доменные события при создании пакета не публикуются (события есть только у покупок).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Pattern Code, длины Name/Description, диапазоны BoostWeight/DurationDays/MaxTargetsPerPurchase/Price.Amount, enum Kind; Price.CurrencyId резолвится, но не RUB (ADR-0048) | Ошибка валидации запроса | no retry |
PROMOTION_CURRENCY_UNKNOWN | 422 | Price.CurrencyId не найден в справочнике валют или запись архивна (internal resolve, INV-5) | Валюта не найдена в справочнике | no retry (исправить валюту) |
PROMOTION_PACKAGE_MAX_TARGETS_REQUIRED | 400 | Kind ∈ {PropertyObjectBoost, PostBoost} и MaxTargetsPerPurchase == null | Для multi-target пакета обязателен лимит целей | no retry |
PROMOTION_PACKAGE_MAX_TARGETS_NOT_APPLICABLE | 400 | Kind = ProjectBoost и MaxTargetsPerPurchase задан | Лимит целей неприменим к пакету уровня проекта | no retry |
UNAUTHORIZED | 401 | Нет/невалиден m2m-токен или X-Admin-User-Id | Требуется авторизация | no retry |
FORBIDDEN | 403 | Клиент не pin-admin-system / нет promotion.packages.admin | Доступ запрещён | no retry |
PROMOTION_PACKAGE_CODE_DUPLICATE | 409 | Code уже существует среди живых пакетов (pre-check или unique violation в гонке) | Пакет с таким кодом уже существует | no retry (сменить Code) |
PROMOTION_HANDBOOK_UNAVAILABLE | 503 | Справочник недоступен при резолве Price.CurrencyId (fail-closed) | Сервис временно недоступен | retry (тот же Idempotency-Key) |
Совместимость и наблюдаемость
- Новые optional поля пакета (будущие платёжные атрибуты) — additive; удаление/переименование/смена типа,
ужесточение pattern
Code— breaking. Неизвестное значение enumKind/Statusв теле →ValidationError. - Logs:
admin-promotion-package-created(packageId,Code,Kind,adminUserId; без секретов); metrics:promotion_admin_operations_total{operation="create-promotion-package"}, duration; traces — сквозной trace черезtraceparent. Каждый вызов — в глубоком аудите. Dashboard promotion-overview.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: PromotionPackage
- API (4): GET /api/admin/v1/promotion-packages — Управленческий список пакетов (admin), GET /api/developers/v1/promotion-packages — Доступные пакеты продвижения, POST /api/developers/v1/promotions — Покупка пакета продвижения (без оплаты), PUT /api/admin/v1/promotion-packages/{packageId} — Изменение/публикация/архивация пакета (admin)
- Use Cases (1): UC-PRM-002. Администратор управляет пакетами продвижения
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей, статусы, инварианты): promotion-package.md.
- Витрина застройщика (только
Active): ../developers/list-promotion-packages.md; правка/публикация/архивация — update-promotion-package.md; управленческий список — list-promotion-packages.md. - Валидация валюты: internal resolve справочника — ../../../handbook/api/internal/resolve-references.md; RUB-only — ADR-0048.
- ADR-0052 (платформенная витрина, не tenanted), ADR-0053 (admin API, машинный доступ + аудит), ADR-0056
(simplicity-first), ADR-0060 (контекст
promotion); technical-assignment.md (решение №10).