GET /api/developers/v1/promotions — Мои покупки продвижения
Назначение
Возвращает менеджеру компании его покупки продвижения (PromotionPurchase,
promotion-purchase.md) с контекстом пакета (код/название) — экран
«Продвижение → Мои покупки» ЛК (оплата не требуется — решение №10, PaymentStatus = NotRequired).
Читается для отслеживания активных бустов, срока действия и статуса (в т.ч. Cancelled/Expired с
причиной).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | promotion |
| API area | developers |
| Feature group | promotions |
| Статус | draft |
| Документ | docs/06-services/promotion/api/developers/list-my-promotions.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер компании — JWT identity) |
| Auth policy / Permissions | permission promotion.purchases.view в scope организации |
| Scope (RBAC) | организация (компания-покупатель) |
| Record-level | не применяется (изоляция — tenant) |
| Tenant isolation | TenantId = CompanyId из identity claims (= PromotionPurchase.TenantId, ADR-0052); выборка покупок автоматически ограничивается компанией актора — компания видит только свои покупки |
| Audit | none (чтение собственных данных) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/developers/v1/promotions |
| Success status | 200 |
| Idempotency header | не применяется (чтение) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required (= CompanyId) | tenant isolation |
Status | query | string? (enum PromotionPurchaseStatus) | Нет | ∈ {Pending, Active, Expired, Cancelled}; null — все | — |
Kind | query | string? (enum PromotionPackageKind) | Нет | ∈ {ProjectBoost, PropertyObjectBoost, PostBoost}; фильтр по PackageKind; null — все | — |
TargetKind | query | string? (enum PromotionTargetKind) | Нет | ∈ {Project, PropertyObject, Post}; фильтр «есть цель такого типа»; null — любой | — |
Page | query | int | Нет (default 1) | >= 1 | — |
PageSize | query | int | Нет (default 50) | 1..200 | — |
Enum-значения — дом promotion-purchase.md /
promotion-package.md; неизвестное значение enum → ValidationError.
Модель ответа: страница MyPromotionItem
Постраничный список (Items, TotalCount, Page, PageSize). Пагинация — offset (покупок у компании
десятки); при росте истории — keyset по (StartsAt desc, Id) без смены контракта.
Модель MyPromotionItem (проекция PromotionPurchase)
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
PurchaseId | Guid | Да | PromotionPurchase.Id | — | Покупка. |
PackageId | Guid | Да | PromotionPurchase.PackageId | — | Купленный пакет. |
PackageCode | string | Да | PromotionPurchase.PackageCode (снапшот) | — | Стабильный код (project-boost-week). |
PackageName | string | Да | PromotionPackage.Name (батч-джойн шага 3) | — | Отображаемое название пакета. |
PackageKind | string (enum PromotionPackageKind) | Да | PromotionPurchase.PackageKind (снапшот) | — | ProjectBoost / PropertyObjectBoost / PostBoost. |
Targets | PromotionTargetModel[] | Да | PromotionPurchaseTarget.Target (owned-коллекция) | — | Цели покупки ({ TargetKind, TargetId }). |
Status | string (enum PromotionPurchaseStatus) | Да | PromotionPurchase.Status | — | Pending/Active/Expired/Cancelled. |
PaymentStatus | string (enum PromotionPaymentStatus) | Да | PromotionPurchase.PaymentStatus | — | Сейчас всегда NotRequired (решение №10). |
BoostWeight | int | Да | PromotionPurchase.BoostWeight (снапшот) | — | Вес буста покупки (1..100). |
Price | PromotionPriceModel | Да | PromotionPurchase.Price (owned снапшот) | — | Снапшот цены (информационно; оплата не требуется). |
StartsAt | DateTimeOffset | Да | PromotionPurchase.StartsAt | — | Начало действия. |
EndsAt | DateTimeOffset | Да | PromotionPurchase.EndsAt | — | Конец действия (job expire-promotions → Expired). |
CancelReason | string? | Нет (обязателен при Status=Cancelled) | PromotionPurchase.CancelReason | — | Причина отмены; null в прочих статусах. |
Version | long | Да | PromotionPurchase.Version | — | Для optimistic-check при отмене (cancel-promotion.md). |
PromotionPriceModel ({ Amount: decimal(14,2) >= 0, CurrencyId: short }) — проекция снапшота цены
PromotionPrice (дом — promotion-package.md); CurrencyId — ссылка
на запись справочника валют handbook (RUB — ADR-0048), display-код/знак клиент берёт из lookup справочника.
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер компании; tenant — его компания; требуется permission
promotion.purchases.view. - Проверка входа.
Status/Kind/TargetKind∈ соответствующих enum,Page/PageSizeв границах → иначеValidationError. Принадлежность гарантирует tenant-изоляция (компания видит только свои покупки). - Выборка. Отобрать страницу покупок компании по фильтрам:
Status;PackageKind = Kind; «есть цель типаTargetKind»; сортировкаStartsAt desc, затемStatus; пагинация(Page, PageSize). Цели покупок читаются вместе с покупкой. Для отображаемого названия пакета подтянуть пакеты по их id, включая архивные:Nameв покупку не снапшотится, а пакет мог быть архивирован при живой покупке (PackageCode— из снапшота покупки). - Маппинг. Поля
PromotionPurchase→ одноимённые поляMyPromotionItem;PackageName← название найденного пакета (fallback"(пакет архивирован)");Targets← цели покупки;Price→PromotionPriceModel;CancelReason— только приStatus = Cancelled, иначеnull. - Результат. Страница
MyPromotionItem→200; freshness — source of truth этого контекста;Expiredпроставляет jobexpire-promotions(свежесть статуса — в пределах интервала job-а).
Граничные случаи. Компания без покупок (или без совпадений по фильтрам) → 200 с пустым Items,
TotalCount = 0 (не ошибка). Page за пределами данных → пустая страница с корректным TotalCount.
Фильтры комбинируются по И (Status ∧ Kind ∧ TargetKind). Покупка с истёкшим EndsAt, ещё не
обработанная job-ом, показывается как Active до ближайшего цикла истечения (свежесть статуса — в пределах
интервала job-а); поле Version в item-е нужно для optimistic-check при последующей отмене.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Невалидные enum (Status/Kind/TargetKind), paging | Ошибка валидации запроса | no retry |
UNAUTHORIZED | 401 | Нет/просрочен JWT | Требуется авторизация | no retry |
FORBIDDEN | 403 | Нет permission promotion.purchases.view в scope организации | Доступ запрещён | no retry |
Доменных PROMOTION_*-ошибок нет: операция читает только собственные данные tenant-а.
Совместимость и наблюдаемость
- Новые optional поля item-а (будущие платёжные атрибуты, новые
TargetKind) — additive; удаление/ переименование/смена типа — breaking. Unknown enum в фильтре →ValidationError. - Logs:
my-promotions-listed(tenantId, фильтры, count); metrics:promotion_operations_total{operation="list-my-promotions"}, duration; traces — сквозной trace черезtraceparent. Dashboard promotion-overview.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: PromotionPurchase
- API (3): GET /api/developers/v1/promotion-packages — Доступные пакеты продвижения, POST /api/developers/v1/promotions — Покупка пакета продвижения (без оплаты), POST /api/developers/v1/promotions/{purchaseId}/cancel — Отмена покупки продвижения
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: promotion-purchase.md (дом полей, enum-ы, инварианты,
событие
PromotionPurchaseChangedKafkaEvent→pin.promotion.purchase-changed.v1); пакет — promotion-package.md. - Соседние операции: list-promotion-packages.md (доступные пакеты), purchase-promotion.md (покупка), cancel-promotion.md (отмена).
- ADR-0052 (tenant = CompanyId), ADR-0060; решение №10 technical-assignment.md.