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

POST /api/admin/v1/promotion-packages — Создание пакета продвижения (admin)

Назначение

Заводит новый платформенный пакет продвижения (promotion-package.md) в каталоге платформы: администратор платформы задаёт вид продвижения (Kind), вес буста, длительность, лимит целей и прейскурантную цену. Оплата не реализуется (решение №10) — застройщик может купить пакет, но без оплаты. Пакет создаётся в статусе Draftневидим покупателям до отдельной публикации (Draft → Active); витрина застройщика отдаёт только Active (../developers/list-promotion-packages.md).

Metadata

ПолеЗначение
Сервис/контекстpromotion
API areaadmin (ADR-0053; m2m)
Feature groupadmin-promotions
Статусdraft
Документdocs/06-services/promotion/api/admin/create-promotion-package.md

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

ПроверкаЗначение
Actor typeExternalSystem (администратор платформы; машинный доступ admin API — ADR-0053)
Auth policy / Permissionspolicy 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-контракт

ПолеЗначение
MethodPOST
Route/api/admin/v1/promotion-packages
Success status201 (создан; Location: /api/admin/v1/promotion-packages/{id}); идемпотентный повтор — 201 с сохранённым результатом (шаг 1)
Idempotency headerIdempotency-Key (Guid; обязателен — admin API ретраит по таймауту)
Correlationсквозной trace через заголовок traceparent; X-Admin-User-Id (Guid админа) — обязателен

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

Типы/ограничения/инварианты полей — дом promotion-package.md; здесь — источник и обязательность запроса.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
CodebodystringДаvarchar(64), pattern ^[a-z0-9][a-z0-9-]*$; unique среди живых (шаг 2, INV-1)
NamebodystringДа1..256 после trim
Descriptionbodystring?Нет≤ 2000
Kindbodystring (enum PromotionPackageKind)Да∈ {ProjectBoost, PropertyObjectBoost, PostBoost}
BoostWeightbodyintДа1..100
DurationDaysbodyintДа1..365
MaxTargetsPerPurchasebodyint?Условно1..500; обязателен при Kind ∈ {PropertyObjectBoost, PostBoost}, null при ProjectBoost (INV-2, кросс-поле шага 2)
Price.AmountbodydecimalДаdecimal(14,2), >= 0 (0 = бесплатный пакет)
Price.CurrencyIdbodyshort?Нет (default — RUB)ссылка на запись справочника валют; резолв через internal resolve handbook (INV-5): неизвестный/архивный id → PROMOTION_CURRENCY_UNKNOWN; допустима только RUB (ADR-0048)
AdminUserIdheader X-Admin-User-IdGuidДа!= Guid.Emptyотметка автора (CreatedByUserId)
IdempotencyKeyheader Idempotency-KeyGuidДа!= Guid.Empty

Price — модель { Amount, CurrencyId } (дом VO PromotionPricepromotion-package.md). Status/Id/Version клиентом не задаются. Оплата не реализуется (решение №10) — Price информационная.

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

Проекция созданного PromotionPackage (дом полей — promotion-package.md).

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаPromotionPackage.IdИдентификатор созданного пакета.
CodestringДаPromotionPackage.CodeСтабильный код.
NamestringДаPromotionPackage.NameНазвание.
Kindstring (enum PromotionPackageKind)ДаPromotionPackage.KindProjectBoost / PropertyObjectBoost / PostBoost.
BoostWeightintДаPromotionPackage.BoostWeightВес буста (1..100).
DurationDaysintДаPromotionPackage.DurationDaysСрок действия покупки (1..365).
MaxTargetsPerPurchaseint?НетPromotionPackage.MaxTargetsPerPurchaseЛимит целей (заполнен при multi-target Kind).
PricePromotionPriceModelДаPromotionPackage.Price (owned){ Amount: decimal, CurrencyId: short }.
Statusstring (enum PromotionPackageStatus)ДаPromotionPackage.StatusПри создании — всегда Draft.
VersionlongДаPromotionPackage.VersionПри создании — 1.

Алгоритм

  1. Контекст и доступ. Операцию выполняет администратор платформы через admin API (машинный доступ, ADR-0053); требуется permission promotion.packages.admin. Пакеты — платформенный каталог, к tenant не привязаны. Идемпотентность: повтор с тем же Idempotency-Key возвращает ранее созданный результат (201), без второго пакета; каноничным считается тело первого запроса — расхождение полей в повторе игнорируется (пакет повторно не правится). Ключ актуален на окно ретраев admin-клиента (таймаут-повтор).
  2. Проверка входа. Проверить формат и диапазоны: Code — pattern ^[a-z0-9][a-z0-9-]*$ и ≤ 64; Name 1..256; Description ≤ 2000; Kind ∈ {ProjectBoost, PropertyObjectBoost, PostBoost}; BoostWeight 1..100; DurationDays 1..365; Price.Amount >= 0. Валюта (INV-5): не заданный Price.CurrencyId заменяется дефолтом RUB; заданный — резолвится в справочнике валют через internal resolve handbook (неизвестный или архивный id → PROMOTION_CURRENCY_UNKNOWN, 422), затем guard RUB-only (ADR-0048): любая другая валюта → ValidationError. Справочник недоступен → fail-closed PROMOTION_HANDBOOK_UNAVAILABLE (503; админ повторяет с тем же Idempotency-Key). Нарушение остальных правил → ValidationError. Кросс-поле MaxTargetsPerPurchase (INV-2): при Kind ∈ {PropertyObjectBoost, PostBoost} обязателен, при nullPROMOTION_PACKAGE_MAX_TARGETS_REQUIRED; при Kind = ProjectBoost должен отсутствовать, иначе → PROMOTION_PACKAGE_MAX_TARGETS_NOT_APPLICABLE; значение вне 1..500ValidationError.
  3. Уникальность кода (INV-1). Убедиться, что среди живых (не архивных) пакетов нет пакета с таким Code; иначе → PROMOTION_PACKAGE_CODE_DUPLICATE (409). Гонка двух одновременных создателей одного кода ловится уникальным ограничением каталога и даёт тот же код ошибки.
  4. Создание. Создать пакет со снапшотом заданных полей и ценой Price {Amount, CurrencyId}. Начальный переход [*] → Draft (Status = Draft), Version = 1, автор — администратор (CreatedByUserId). Пакет невидим покупателям до отдельной публикации.
  5. Результат. Вернуть карточку созданного пакета (Status = Draft, Version = 1) — 201 + Location; ошибки → коды таблицы ниже. Интеграционные и доменные события при создании пакета не публикуются (события есть только у покупок).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Pattern Code, длины Name/Description, диапазоны BoostWeight/DurationDays/MaxTargetsPerPurchase/Price.Amount, enum Kind; Price.CurrencyId резолвится, но не RUB (ADR-0048)Ошибка валидации запросаno retry
PROMOTION_CURRENCY_UNKNOWN422Price.CurrencyId не найден в справочнике валют или запись архивна (internal resolve, INV-5)Валюта не найдена в справочникеno retry (исправить валюту)
PROMOTION_PACKAGE_MAX_TARGETS_REQUIRED400Kind ∈ {PropertyObjectBoost, PostBoost} и MaxTargetsPerPurchase == nullДля multi-target пакета обязателен лимит целейno retry
PROMOTION_PACKAGE_MAX_TARGETS_NOT_APPLICABLE400Kind = ProjectBoost и MaxTargetsPerPurchase заданЛимит целей неприменим к пакету уровня проектаno retry
UNAUTHORIZED401Нет/невалиден m2m-токен или X-Admin-User-IdТребуется авторизацияno retry
FORBIDDEN403Клиент не pin-admin-system / нет promotion.packages.adminДоступ запрещёнno retry
PROMOTION_PACKAGE_CODE_DUPLICATE409Code уже существует среди живых пакетов (pre-check или unique violation в гонке)Пакет с таким кодом уже существуетno retry (сменить Code)
PROMOTION_HANDBOOK_UNAVAILABLE503Справочник недоступен при резолве Price.CurrencyId (fail-closed)Сервис временно недоступенretry (тот же Idempotency-Key)

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

  • Новые optional поля пакета (будущие платёжные атрибуты) — additive; удаление/переименование/смена типа, ужесточение pattern Code — breaking. Неизвестное значение enum Kind/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.

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

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

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