PUT /api/admin/v1/promotion-packages/{packageId} — Изменение/публикация/архивация пакета (admin)
Назначение
Правка и архивация платформенного пакета продвижения
(promotion-package.md): администратор платформы через admin API
правит поля пакета (Name, Description, BoostWeight, DurationDays,
MaxTargetsPerPurchase, Price) и/или переводит его по жизненному циклу PromotionPackageStatus:
публикация Draft → Active (пакет становится виден и покупаем —
../developers/list-promotion-packages.md) и архивация
→ Archived (снят с продажи, soft-delete). Code и Kind неизменяемы (INV-3).
Ключевое ограничение — INV-2 SnapshotImmutable покупки
(promotion-purchase.md): правки пакета не меняют уже созданные
PromotionPurchase (те держат снапшот BoostWeight/Price/PackageKind), поэтому операция не
публикует интеграционных событий и не затрагивает потребителей буста. Требуется глубокий аудит.
Архивация — симметричная операция DELETE /api/admin/v1/promotion-packages/{packageId} (дельта
описана здесь): soft-delete пакета (DeletedAt/DeletedByUserId, Status = Archived) без body. Пакет
снят с продажи; действующие покупки доживают срок (FK restrict).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | promotion |
| API area | admin (ADR-0053) |
| Feature group | admin-promotions |
| Статус | draft |
| Документ | docs/06-services/promotion/api/admin/update-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 | не применяется (пакет — платформенная сущность) |
| Audit | обязателен: глубокий аудит админ-мутаций — актор (X-Admin-User-Id), packageId, диф полей, переход статуса; отметка автора правки (UpdatedByUserId)/(при архиве) автора архивации (DeletedByUserId) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT (архив-дельта: DELETE, тот же route) |
| Route | /api/admin/v1/promotion-packages/{packageId} |
| Success status | 200 |
| Idempotency header | Idempotency-Key (Guid; обязателен) |
| Correlation | сквозной trace через заголовок traceparent; X-Admin-User-Id (Guid админа) — обязателен |
Входные данные / параметры запроса
Типы/ограничения полей — дом promotion-package.md. Архив-дельта
(DELETE) body не принимает.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
PackageId | route | Guid | Да | != Guid.Empty; существование — шаг 2 | — |
Name | body | string | Да | 1..256 после trim | — |
Description | body | string? | Нет | ≤ 2000 символов | — |
BoostWeight | body | int | Да | 1..100 | — |
DurationDays | body | int | Да | 1..365 | — |
MaxTargetsPerPurchase | body | int? | Условно | 1..500; обязателен при Kind ∈ {PropertyObjectBoost, PostBoost}, null при ProjectBoost (INV-2 пакета) | — |
Price | body | PromotionPriceModel | Да | Amount — decimal(14,2) >= 0; CurrencyId — short?, default RUB; резолв через internal resolve handbook (INV-5): неизвестный/архивный id → PROMOTION_CURRENCY_UNKNOWN; допустима только RUB (ADR-0048) | — |
Status | body | string (enum PromotionPackageStatus) | Да | ∈ {Draft,Active,Archived}; переход — матрица ниже | — |
ExpectedVersion | body | long? | Нет | >= 1; optimistic-check против PromotionPackage.Version (защита «два админа»), дополняет RowVersion | — |
AdminUserId | header X-Admin-User-Id | Guid | Да | != Guid.Empty | отметка автора |
IdempotencyKey | header Idempotency-Key | Guid | Да | != Guid.Empty | — |
Code и Kind в контракт обновления не входят (INV-3 неизменяемы). Если присланы и отличаются →
PROMOTION_PACKAGE_IMMUTABLE_FIELD (400).
Матрица переходов Status (guard шага 2/4)
| Из \ В | Draft | Active | Archived |
|---|---|---|---|
Draft | ✅ правка | ✅ публикация | ✅ архив черновика |
Active | ⛔ (unpublish не поддержан) | ✅ правка | ✅ архивация |
Archived | ⛔ терминальный (soft-deleted → NotFound) | ⛔ | ⛔ |
Недопустимый переход → PROMOTION_PACKAGE_INVALID_STATUS_TRANSITION (409).
Модель ответа PromotionPackageResult
Дом модели-результата мутации пакета (переиспользуется create-promotion-package.md).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PromotionPackage.Id | — | Пакет. |
Code | string | Да | PromotionPackage.Code | — | Стабильный код (неизменяем). |
Name | string | Да | PromotionPackage.Name | — | Название после правки. |
Description | string? | Нет | PromotionPackage.Description | — | Маркетинговый текст. |
Kind | string (enum PromotionPackageKind) | Да | PromotionPackage.Kind | — | Неизменяем. |
BoostWeight | int | Да | PromotionPackage.BoostWeight | — | Вес буста. |
DurationDays | int | Да | PromotionPackage.DurationDays | — | Срок действия покупки. |
MaxTargetsPerPurchase | int? | Нет | PromotionPackage.MaxTargetsPerPurchase | — | Лимит целей (multi-target Kind). |
Price | PromotionPriceModel | Да | PromotionPackage.Price (owned) | — | { Amount: decimal, CurrencyId: short } — прейскурант (оплата не требуется — решение №10). |
Status | string (enum PromotionPackageStatus) | Да | PromotionPackage.Status | — | Новый статус после перехода. |
Version | long | Да | PromotionPackage.Version | — | Версия после мутации (Version++). |
ArchivedAt | DateTimeOffset? | Нет | PromotionPackage.DeletedAt | — | Момент архивации; null для Draft/Active. |
Алгоритм
- Контекст и доступ. Операцию выполняет администратор платформы через admin API; требуется permission
promotion.packages.admin. Пакет — платформенная сущность, к tenant не привязан. Повтор с тем жеIdempotency-Keyвозвращает ранее сохранённый результат (200), без повторной правки. - Проверка входа. Диапазоны/длины/enum:
Name 1..256,Description ≤ 2000,BoostWeight 1..100,DurationDays 1..365,MaxTargetsPerPurchase 1..500,Price.Amount >= 0,Status ∈enum → иначеValidationError. Валюта (INV-5): не заданныйPrice.CurrencyIdзаменяется дефолтом RUB; заданный — резолвится в справочнике валют через internal resolvehandbook(неизвестный/архивный id →PROMOTION_CURRENCY_UNKNOWN, 422), затем guard RUB-only (ADR-0048): другая валюта →ValidationError; справочник недоступен → fail-closedPROMOTION_HANDBOOK_UNAVAILABLE(503). - Существование и предусловия. Найти пакет по
PackageId(архивированный считается ненайденным); отсутствует →PROMOTION_PACKAGE_NOT_FOUND(404). Неизменяемость (INV-3): присланныеCode/Kind, отличные от сохранённых →PROMOTION_PACKAGE_IMMUTABLE_FIELD(400). Правило лимита целей (INV-2 пакета): приKind ∈ {PropertyObjectBoost, PostBoost}MaxTargetsPerPurchaseзадан, приProjectBoost— отсутствует, иначе →PROMOTION_PACKAGE_MAX_TARGETS_RULE(400). Переход(текущий Status → целевой Status)допустим по матрице, иначе →PROMOTION_PACKAGE_INVALID_STATUS_TRANSITION(409). При заданномExpectedVersion, не совпавшем с текущей версией пакета →CONCURRENCY_CONFLICT(409). Архивация (DELETE): целевойStatus = Archivedподставляется автоматически; тот же чеклист. Действующие покупки не читаются — INV-2: правка/архив пакета их не касается (покупки держат снапшот, доживают срок, FKrestrict). - Правила и переходы. Применить правку полей (
Code/Kindне меняются). Переход статуса:- целевой == текущий — чистая правка полей;
Draft → Active— публикация,Status = Active;Draft/Active → Archived— архивация:Status = Archived, пакет помечается архивным (тот же путь —DELETE). В обоих путяхVersion++, автор правки — администратор (UpdatedByUserId). INV-2: снапшот-поля покупок не трогаются, событий пересчёта нет.
- Сохранение. Сохранить изменённый пакет (при архивации — с отметкой архивации
DeletedAt/DeletedByUserId). Конкурентность: гонка со вторым админом фиксируется по версии — повтор с тем жеIdempotency-Keyпосле перечитывания. Потребители буста не затрагиваются (INV-2). Интеграционные и доменные события правка/архив пакета не публикует (pin.promotion.purchase-changed.v1шлют переходы покупок, не пакетов). - Результат. Вернуть карточку пакета (
Version++, новыйStatus;ArchivedAtпри архиве) →200; ошибки → коды таблицы ниже.
Видимость и постусловие. Пакеты — source of truth этого контекста, лага нет: публикация Draft → Active
делает пакет покупаемым на витрине застройщика немедленно после сохранения; архивация → Archived
немедленно убирает его из витрины (Active-фильтр), но уже созданные покупки остаются Active и
доживают срок (FK restrict) — правка/архив пакета их снапшот не трогает (INV-2) и не публикует
никаких событий, поэтому денормализованный буст у потребителей catalog/news не меняется. Проигравший
в гонке двух администраторов (несовпавший ExpectedVersion или конфликт версии) получает
CONCURRENCY_CONFLICT и повторяет с тем же Idempotency-Key после перечитывания актуальной версии.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Диапазоны/длины/enum полей | Ошибка валидации запроса | no retry |
PROMOTION_PACKAGE_IMMUTABLE_FIELD | 400 | Попытка изменить Code/Kind | Поле пакета неизменяемо | no retry |
PROMOTION_PACKAGE_MAX_TARGETS_RULE | 400 | MaxTargetsPerPurchase не согласован с Kind | Лимит целей задаётся только для multi-target пакета | no retry |
PROMOTION_CURRENCY_UNKNOWN | 422 | Price.CurrencyId не найден в справочнике валют или запись архивна (internal resolve, INV-5) | Валюта не найдена в справочнике | no retry (исправить валюту) |
FORBIDDEN | 403 | Клиент не pin-admin-system / нет promotion.packages.admin | Доступ запрещён | no retry |
PROMOTION_PACKAGE_NOT_FOUND | 404 | Пакет отсутствует или уже архивирован (soft-deleted) | Пакет продвижения не найден | no retry |
PROMOTION_PACKAGE_INVALID_STATUS_TRANSITION | 409 | Переход вне матрицы (напр. Active → Draft) | Недопустимый переход статуса пакета | no retry |
CONCURRENCY_CONFLICT | 409 | ExpectedVersion не совпал или конфликт optimistic-блокировки | Данные изменены параллельно; обновите форму | retry с тем же Idempotency-Key после перечитывания |
PROMOTION_HANDBOOK_UNAVAILABLE | 503 | Справочник недоступен при резолве Price.CurrencyId (fail-closed) | Сервис временно недоступен | retry (тот же Idempotency-Key) |
Совместимость и наблюдаемость
- Новые optional поля body/результата — additive; удаление/переименование/смена типа, снятие
неизменяемости
Code/Kind, расширение матрицы переходов — breaking. Неизвестное значениеStatus→ValidationError. - Logs:
admin-promotion-package-updated(packageId,code,from → toстатус, набор изменённых полей без значенияPrice,adminUserId); metrics:promotion_admin_operations_total{operation="update-promotion-package"}(архивация —operation="archive-promotion-package"), duration; traces — сквозной trace черезtraceparent. Каждый вызов — в глубоком аудите. Dashboard promotion-overview.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: PromotionPackage
- API (2): GET /api/admin/v1/promotion-packages — Управленческий список пакетов (admin), POST /api/admin/v1/promotion-packages — Создание пакета продвижения (admin)
- Use Cases (1): UC-PRM-002. Администратор управляет пакетами продвижения
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей, статусы, INV-2/3): promotion-package.md; покупка (INV-2 SnapshotImmutable) — promotion-purchase.md.
- Соседние admin-операции: create-promotion-package.md (Create, дом
PromotionPackageResult), list-promotion-packages.md (список). - Витрина застройщика: ../developers/list-promotion-packages.md.
- Валидация валюты: internal resolve справочника — ../../../handbook/api/internal/resolve-references.md; RUB-only — ADR-0048.
- ADR-0053 (admin API, машинный доступ + аудит), ADR-0052 (пакеты не tenanted), ADR-0056
(simplicity-first), ADR-0060 (контекст
promotion); technical-assignment.md решение №10.