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

UC-PRM-001. Покупка продвижения (компания)

Назначение

Описывает сценарий: менеджер компании (застройщик/агентство) покупает платформенный пакет продвижения для набора целей (проект / объекты недвижимости / посты) без оплаты (решение №10). Связан с контекстом promotion.

Metadata

ПолеЗначение
IDUC-PRM-001
Контекстpromotion (см. docs/06-services/promotion/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольДевелопер / Агентство (менеджер компании)
Permissions / scopepromotion.purchases.manage в scope организации
TenantTenantId = CompanyId (ADR-0052)
Record-levelне применяется (владение целью проверяет контекст-владелец)

Предусловие

  • Пакет PromotionPackage существует в статусе Active (опубликован админ-панелью — UC-PRM-002).
  • Feature flag promotion.enabled = true.
  • Цели (Project/PropertyObject/Post) принадлежат компании актора и существуют в контекстах-владельцах.

Триггер

ПолеЗначение
ИсточникUI button / API call
Триггерящий элементэкран «Продвижение» ЛК компании → кнопка «Купить пакет»

Основной Поток

  1. Менеджер открывает экран «Продвижение», видит доступные пакеты (GET /api/developers/v1/promotion-packages).
  2. Выбирает пакет и цели (набор Targets[] { TargetKind, TargetId }, ограниченный PackageKind).
  3. Фронт шлёт POST /api/developers/v1/promotions с PackageId и Targets[].
  4. Система проверяет доступ (permission), feature flag promotion.enabled и принадлежность компании.
  5. Система проверяет предусловия: пакет Active (INV-3), TargetKind/кол-во по PackageKind (INV-1), владение целями через контекст-владелец (INV-4, fail-closed), отсутствие пересечения (INV-5).
  6. Покупка создаётся со снапшотом пакета и сразу активируется (Pending → Active, PaymentStatus = NotRequired); в одном согласованном изменении публикуется PromotionPurchaseChangedKafkaEvent (Status = Active).
  7. Ответ 201 с PromotionPurchaseResult (Status = Active, EndsAt).
  8. Фронт показывает активную покупку сразу (source of truth — promotion, лага нет). Потребители catalog/news получают событие асинхронно и поднимают PromotionBoost целей у себя — до этого момента (окно eventual-consistency, целевой SLA < 1 мин) покупка уже Active, но карточка проекта/объекта/пост ещё показывается без нового буста. notifications асинхронно шлёт уведомление «продвижение запущено».

Межсервисная последовательность

ШагКто → комуСинхронностьЧтоПромежуточное состояние
1promotion → контекст-владелец (catalog/news)синхронно (internal read)резолвинг владельца каждой цели (INV-4); при недоступности — fail-closedпокупка ещё не создана
2promotion (сам)синхронно, атомарносоздание+активация покупки и запись события в одном согласованном изменениипокупка Active, событие ждёт публикации
3promotion → топик pin.promotion.purchase-changed.v1асинхронно (at-least-once)публикация Status = Active, key = PurchaseIdу потребителей буст ещё старый
4топик → catalog/newsасинхронно (< 1 мин)пересчёт PromotionBoost карточек/поста (сумма BoostWeight активных покупок)буст целей поднят
5топик → notificationsасинхронно (< 5 мин)уведомление менеджеру

Альтернативные Потоки

A1. Пакет недоступен

  1. Пакет не найден или Status != Active (INV-3) → PROMOTION_PACKAGE_NOT_AVAILABLE (409). Покупка не создаётся, событий нет. Завершается.

A2. Цель не принадлежит компании / контекст-владелец недоступен

  1. Синхронный резолвинг владельца цели вернул несовпадение владельца, несуществование цели или был недоступен (fail-closed, DSC-PRM-1) → PROMOTION_TARGET_NOT_OWNED (403). Покупка не создаётся. Завершается.

A3. Продвижение уже активно (пересечение / повтор)

  1. Для (PackageId, цель) уже есть покупка в статусе Pending/Active (INV-5) → PROMOTION_ALREADY_ACTIVE (409). Повтор той же операции упирается сюда же (естественная идемпотентность). Завершается.

A4. Покупка временно отключена

  1. Feature flag promotion.enabled = falsePROMOTION_DISABLED (409) до всякой загрузки. Витрина пакетов при этом остаётся видимой (PurchaseEnabled = false). Завершается.

A5. Цели не соответствуют виду пакета

  1. Набор TargetKind/количество целей не допустим для PackageKind (INV-1: ProjectBoost — ровно 1, PropertyObjectBoost/PostBoost1..MaxTargetsPerPurchase), либо цели не distinct → PROMOTION_TARGETS_INVALID (422). Завершается.

A6. Конкурентная покупка той же цели

  1. Две одновременные покупки на одну цель: одна фиксируется, вторая упирается в INV-5 + уникальное ограничение целей → проигравшая получает PROMOTION_ALREADY_ACTIVE (409). Двойного буста нет.

Постусловие

  • При успехе: создана покупка PromotionPurchase в статусе Active (PaymentStatus = NotRequired) с целями PromotionPurchaseTarget; снапшот BoostWeight/Price/PackageKind зафиксирован; счётчик покупок компании увеличен; опубликовано PromotionPurchaseChangedKafkaEvent (Status = Active); в пределах окна консистентности буст целей поднят у потребителей catalog/news.
  • При любом отказе (A1–A6): покупка не создана, событий нет, состояние потребителей не изменилось.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
PROMOTION_DISABLED409Feature flag выключенПоказать «покупка временно недоступна»
PROMOTION_PACKAGE_NOT_AVAILABLE409Пакет не ActiveОбновить список пакетов
PROMOTION_TARGETS_INVALID422Цели не соответствуют PackageKindПоправить выбор целей
PROMOTION_TARGET_NOT_OWNED403Цель чужая/недоступнаУбрать цель из выбора
PROMOTION_ALREADY_ACTIVE409Дубль активной покупкиПоказать существующую покупку

Доменные События

СобытиеКогда поднимаетсяSubscribers
пересчёта буста в промо-контексте нет (у консюмеров, ADR-0060)

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
PromotionPurchaseChangedKafkaEventpin.promotion.purchase-changed.v1Активация покупки (Status = Active)catalog, news (буст), notifications, будущий billing

Acceptance Criteria

#GivenWhenThen
AC1 (happy)Пакет Active, цели принадлежат компании, пересечений нет, promotion.enabled = trueменеджер покупает пакет для допустимого набора целейпокупка создана Active, PaymentStatus = NotRequired, EndsAt = StartsAt + DurationDays, Version = 1, 201
AC2 (снапшот)Есть действующая покупка; администратор меняет BoostWeight/Price пакетаправка пакета сохранена (Active → Active)покупка сохраняет прежние BoostWeight/Price/PackageKind (INV-2); событий пересчёта нет
AC3 (атомарность)Условия happy-path выполненыпокупка активируетсяPromotionPurchaseChangedKafkaEvent (Status = Active, key = PurchaseId) публикуется в одном согласованном изменении со сменой статуса; в пределах окна < 1 мин буст поднят у catalog/news
AC4 (INV-1)Пакет ProjectBoostменеджер передаёт 2 цели или цель типа PropertyObjectотказ PROMOTION_TARGETS_INVALID (422); покупка не создана
AC5 (INV-3)Пакет в статусе Draft/Archived (или не существует)менеджер покупает этот пакетотказ PROMOTION_PACKAGE_NOT_AVAILABLE (409)
AC6 (INV-4)Одна из целей принадлежит другой компании либо контекст-владелец недоступенменеджер покупает набор с этой цельюотказ PROMOTION_TARGET_NOT_OWNED (403, fail-closed); покупка не создана
AC7 (INV-5)На (PackageId, цель) уже есть Active-покупкаменеджер покупает тот же пакет на ту же цельотказ PROMOTION_ALREADY_ACTIVE (409); двойного буста нет
AC8 (flag)promotion.enabled = falseменеджер вызывает покупкуотказ PROMOTION_DISABLED (409); витрина пакетов остаётся видимой
AC9 (идемпотентность)Первая покупка прошла успешноменеджер повторяет тот же запросвторая упирается в INV-5 → PROMOTION_ALREADY_ACTIVE; второй покупки/события нет

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

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

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