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

UC-OVR-009. Продвижение поста / объекта недвижимости

Назначение

Cross-service сценарий (ADR-0060): застройщик/автор покупает пакет продвижения для полиморфной цели (Project / PropertyObject / Post), а контексты-потребители пересчитывают денормализованный вклад продвижения в ранжирование у себя. Оркеструет promotion (покупка/жизненный цикл); catalog бустит проекты/объекты, news — посты; notifications уведомляет покупателя. Монетизация продвижения — спроектирована, реализация оплаты отложена.

Metadata

ПолеЗначение
Статусdraft
Owning teampromotion
Primary capabilitycontent-promotion-boost
Risk levelmedium

Scope И Участники

УчастникРоль
Менеджер застройщика / АвторАктор: покупает пакет продвижения для цели
promotionВладелец пакетов/покупок; единственный источник факта продвижения; жизненный цикл (актив/истечение/отмена)
catalogConsumer: PromotionBoost карточек Project/PropertyObject
newsConsumer: Post.PromotionBoost для постов
notificationsConsumer: уведомление покупателю об активации/истечении

Preconditions

  • Пакеты продвижения заведены администратором (UC-PRM-002).
  • Цель существует: проект/объект (chessboardcatalog) или пост (news).

Основной Сценарий

  1. Покупатель выбирает пакет и цель (TargetKind = Project/PropertyObject/Post) и покупает продвижение (purchase-promotion.md, UC-PRM-001).
  2. promotion создаёт PromotionPurchase (активна на срок пакета) и публикует pin.promotion.purchase-changed.v1 (promotion-purchase-changed.md).
  3. Consumer фильтрует цели по TargetKind:
    • catalog (Project/PropertyObject) пересчитывает PromotionBoost карточек (promotion-boost.md) — сумма BoostWeight активных покупок влияет на ранжирование выдачи;
    • news (Post) наполняет Post.PromotionBoost (news/consumers/promotion-boost.md).
  4. notifications уведомляет покупателя об активации.
  5. Истечение/отмена. Фоновый процесс promotion истекает покупки (UC-PRM-003) → повторный purchase-changed (статус); catalog/news снимают вклад буста.

Sequence

Постусловие

После успешной покупки гарантированно:

  • в promotion создан PromotionPurchase в статусе Active (в no-payment режиме — сразу; PaymentStatus = NotRequired), с иммутабельными снапшотами PackageCode/PackageKind/BoostWeight/цели/срока (StartsAt..EndsAt);
  • опубликовано pin.promotion.purchase-changed.v1 (Status = Active, key = PurchaseId);
  • потребители по фильтру TargetKind применили вклад буста: catalog пересчитал PromotionBoost карточек Project/PropertyObject, newsPost.PromotionBoost (< 1 мин); буст входит слагаемым в ранжирование, но не переопределяет явные пользовательские сортировки;
  • покупатель уведомлён об активации (< 5 мин).

После истечения/отмены: публикуется повторный purchase-changed (Status = Expired/Cancelled); потребители снимают вклад буста; покупатель уведомлён. Списание средств — вне контракта (эквайринг отложен).

Альтернативные Сценарии И Отказы

СитуацияПоведение
Цель удалена/скрытаConsumer игнорирует буст для несуществующей цели; вклад не применяется
Несколько активных покупок на одну цельВклад суммируется (BoostWeight активных покупок), Version-guard от переупорядочивания
Неизвестный TargetKind для потребителяFallback: строка сохраняется + warning; чужой TargetKind просто игнорируется (напр. Post для catalog)
Отмена продвиженияcancel-promotionpurchase-changed (Cancelled) → снятие вклада у consumers
Истечение опередило активацию (переупорядочивание)Version-guard: событие с версией ≤ применённой игнорируется — «залипший»/преждевременно снятый буст исключён
Повторная доставка purchase-changedИдемпотентно по MessageId + Version-guard — вклад не удваивается/не снимается ошибочно
Оплата (эквайринг)Отложена; покупка моделируется (PaymentStatus = NotRequired), списание средств — вне контракта

Согласованность И Идемпотентность

  • Единственный источник факта продвиженияpromotion; catalog/news держат лишь денормализованную проекцию (PromotionBoost), которую пересчитывают на каждое событие. Между покупкой и применением буста — окно eventual-consistency (< 1 мин).
  • Идемпотентность и порядок. Ключ PurchaseId даёт порядок событий одной покупки; потребители дедуплицируют по MessageId и держат монотонный Version-guard (событие с версией ≤ применённой игнорируется). Терминальные статусы Expired/Cancelled переходов не имеют — число событий на покупку конечно.
  • Суммирование вклада. Эффективный PromotionBoost цели = сумма BoostWeight активных покупок; истечение/отмена уменьшают сумму. BoostWeight — снапшот, правка пакета его не меняет (INV-2).
  • Жизненный цикл событием, не клиентом. Истечение инициирует фоновый процесс promotion (now() >= EndsAt), публикуя purchase-changed; потребители снимают вклад по событию.
  • Буст — лишь слагаемое ранжирования (защита от накрутки): веса пакетов контролирует администратор, буст не подменяет явные сортировки пользователя.

Acceptance Criteria (Given/When/Then)

  • Happy (покупка → буст). Дано: пакет продвижения заведён, цель (Project/PropertyObject/Post) существует. Когда: покупатель покупает продвижение. Тогда: создан PromotionPurchase (Active), опубликован purchase-changed, соответствующий потребитель (catalog/news) применил PromotionBoost, покупатель уведомлён.
  • Истечение. Дано: активная покупка с EndsAt в прошлом. Когда: срабатывает фоновое истечение. Тогда: purchase-changed (Expired), потребители сняли вклад буста, покупатель уведомлён.
  • Отмена. Дано: активная покупка. Когда: покупатель/админ отменяет. Тогда: purchase-changed (Cancelled), вклад снят.
  • Суммирование. Дано: две активные покупки на одну цель. Когда: обе применены. Тогда: PromotionBoost = сумма их BoostWeight.
  • Переупорядочивание. Дано: Expired доставлен раньше/повторно относительно применённого состояния. Когда: потребитель обрабатывает. Тогда: Version-guard отбрасывает устаревшее событие, буст корректен.
  • Оплата отложена. Дано: no-payment режим. Когда: покупка создаётся. Тогда: PaymentStatus = NotRequired, списания средств нет.

Source Of Truth И Данные

ДанныеВладелецConsistency rule
PromotionPackage / PromotionPurchasepromotionЕдинственный источник факта; жизненный цикл событием
PromotionBoost карточекcatalogДенормализованная проекция; идемпотентно MessageId + Version-guard
Post.PromotionBoostnewsТо же; фильтр по TargetKind = Post

Контракты И Зависимости

Наблюдаемость

  • critical metrics: активные покупки по TargetKind; лаг применения буста consumer'ами; доля истёкших.
  • trace path: promotion (покупка/истечение) → шина событий → catalog/news/notifications.
  • alerts: буст не снят после истечения; рассинхрон PromotionBoost с фактом в promotion.

Риски И Guardrails

  • Риск: «залипший» буст после истечения. Control: истечение публикует событие; Version-guard у consumers.
  • Риск: накрутка ранжирования. Control: буст — лишь слагаемое ранжирования; веса пакетов контролирует администратор.

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

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