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

Promotion Service (Pin.Promotion)

Назначение

promotion владеет продвижением платформы (решение №10): каталогом платформенных промо-пакетов (PromotionPackage) и их покупками компаниями (PromotionPurchase с полиморфными целями Project/PropertyObject/Post). Контекст — source of truth продвижения: принимает покупку без оплаты (PaymentStatus = NotRequired), снапшотит вес/цену/вид пакета, истекает покупки фоновым job-ом и публикует единое событие буста pin.promotion.purchase-changed.v1.

Денормализацию буста контекст НЕ выполняет — это делают консюмеры-потребители: catalog (карточки проекта/объекта) и news (Post.PromotionBoost). Продвижение выделено из catalog в отдельный контекст по ADR-0060 (сквозная бизнес-возможность: буст нужен и каталогу, и ленте новостей; владелец возможности не должен совпадать с одним из потребителей — ADR-0018). Оплата отложена (решение №10) — структуры цены заложены под будущий эквайринг.

Metadata

ПолеЗначение
Статусdraft
Owning teamPIN Marketplace
DomainMerchandising & Promotion
Data classificationInternal (пакеты/покупки — внутренние; PII нет; Price — прейскурант)
Runtime criticalitymedium (влияет на ранжирование витрины/ленты у потребителей; отказ → буст устаревает, потери данных нет)

Ownership

ArtifactOwner/Rule
PostgreSQL schemapromotion
API areasdevelopers (покупка/отмена/список — покупатель = компания), admin (CRUD пакетов, m2m ADR-0053)
Owned entitiesPromotionPackage, PromotionPurchase (+ дочерний PromotionPurchaseTarget)
Produced eventspin.promotion.purchase-changed.v1
Consumed eventsnone (контекст — источник; резолвинг владельца цели при покупке — синхронный internal read, не Kafka)
Search indexesnone (полнотекстового поиска нет)
Analytics factsnone

Ответственность И Границы

  • Делает: каталог промо-пакетов (структуры + статусы, admin-CRUD), покупку пакета компанией без оплаты (снапшот BoostWeight/Price/PackageKind), полиморфные цели (Project/PropertyObject/Post), истечение покупок (polling-job), публикацию PromotionPurchaseChangedKafkaEvent на каждый переход статуса.
  • НЕ делает: денормализацию PromotionBoost в карточки/посты (у потребителей catalog/news, ADR-0060); расчёт ранжирования (остаётся у потребителей); оплату продвижения (отложена — решение №10; будущий billing-консюмер); хранение самих проектов/объектов/постов (source of truth — catalog/news).
  • Source of truth: пакеты продвижения и их покупки (со снапшотами и полиморфными целями).
  • Реплики/проекции: нет — контекст не денормализует чужие данные (цели — логические ссылки без FK).

Зависимости

ЗависимостьТипЗачем НужнаFailure mode
PostgreSQL (schema promotion)dbПакеты, покупки, цели, outboxПолный отказ контекста
Kafkaqueuepin.promotion.purchase-changed.v1 (out, outbox)Покупки создаются; событие буста задерживается (лаг-алерты)
catalogservice (internal read)Резолвинг владельца целей Project/PropertyObject при покупке (INV-4)Покупка на эти цели невозможна (fail-closed, DSC-PRM-1)
newsservice (internal read)Резолвинг владельца целей Post при покупке (INV-4)Покупка на посты невозможна (fail-closed)
handbookservice (internal resolve)Валидация Price.CurrencyId на admin-write (INV-5 пакета; деньги в событиях — CurrencyId + CurrencySnapshot, snapshots.md)Admin-мутации пакетов с валютой — fail-closed PROMOTION_HANDBOOK_UNAVAILABLE
identityservice (JWT/permissions)Авторизация акторов, CompanyId (tenant покупок)Developers-область недоступна
Администратор платформы (admin API)consumer (машинный доступ, ADR-0053)Управление пакетамиНедоступны admin-правки пакетов

Проекты

src/services/promotion/
Pin.Promotion.Api/ # developers + admin API areas
Pin.Promotion.Application/ # обработчики операций
Pin.Promotion.Domain/ # агрегаты PromotionPackage / PromotionPurchase
Pin.Promotion.Infrastructure/ # PostgreSQL
Pin.Promotion.Background/ # job: expire-promotions
Pin.Promotion.Migrator/ # миграции + SEED пакетов продвижения
Pin.Promotion.Contracts/ # pin.promotion.* event contracts

Основные Сущности

EntityНазначениеSource Of TruthДокумент
PromotionPackageПлатформенный каталог промо-пакетов (не tenanted, admin-CRUD)promotiondomain/promotion-package.md
PromotionPurchase (+PromotionPurchaseTarget)Покупка пакета компанией; полиморфные цели; снапшотыpromotiondomain/promotion-purchase.md

ER-обзор

API (карта областей)

  • Developers (promotion.packages.view, promotion.purchases.view/manage; покупатель — компания застройщик/агентство): GET /api/developers/v1/promotion-packages (доступные пакеты), POST /api/developers/v1/promotions (покупка без оплаты), GET /api/developers/v1/promotions (мои покупки), POST /api/developers/v1/promotions/{id}/cancel (отмена).
  • Admin (машинный доступ, ADR-0053; promotion.packages.admin): управление PromotionPackagePOST/PUT·DELETE/GET /api/admin/v1/promotion-packages.
  • Internal / Public / Callbacks: not applicable.

Events И Consumers

TypeNameTopicНазначениеLink
ProducedPromotionPurchaseChangedKafkaEventpin.promotion.purchase-changed.v1Буст целей у потребителей; уведомления; будущий биллингevents/promotion-purchase-changed.md
Consumed (downstream)PromotionPurchaseChangedKafkaEventpin.promotion.purchase-changed.v1catalogPromotionBoost карточек Project/PropertyObject../catalog/consumers/promotion-boost.md
Consumed (downstream)PromotionPurchaseChangedKafkaEventpin.promotion.purchase-changed.v1newsPost.PromotionBoost для Post../news/domain/post.md

Промо-контекст сам чужих Kafka-событий не потребляет; резолвинг владельца целей при покупке — синхронный internal read catalog/news (INV-4, DSC-PRM-1).

Интеграционный поток буста (кто кому что и когда)

  • Синхронно (на покупке): promotion → catalog/news — резолвинг владельца каждой цели (INV-4, fail-closed при недоступности). Это единственный синхронный внешний вызов контекста.
  • Асинхронно (на каждом переходе покупки): promotion → топик pin.promotion.purchase-changed.v1 (key = PurchaseId, at-least-once) — активация (Active), отмена (Cancelled), истечение (Expired).
  • Потребители событий: catalog (буст карточек Project/PropertyObject), news (Post.PromotionBoost), notifications (уведомления покупателю), будущий billing (счёт по Price).
  • Окно eventual-consistency: покупка/отмена/истечение видны в промо-контексте сразу (source of truth); денормализованный буст у потребителей обновляется по событию — целевой SLA < 1 мин. В этом окне «Мои покупки» уже показывают новый статус, а витрина/лента у потребителя — ещё прежний вклад. Порядок per-purchase гарантирован key-ом PurchaseId; потребители держат монотонный Version-guard (запоздавшее или повторное событие не откатывает состояние).

Runtime И Эксплуатация

ОбластьТребование
Health checks/health/live, /health/ready (PostgreSQL, Kafka), /health/startup (миграции применены)
SLO/SLAпокупка p99 < 300 мс; лаг публикации события буста < 1 мин; точность истечения — в пределах интервала job-а (1–5 мин)
Dashboardpromotion-overview (операции покупки/отмены, лаг outbox pin.promotion.purchase-changed.v1, DLQ, backlog job-а истечения)
Alertsjob expire-promotions не отработал > 2 интервалов; outbox backlog pin.promotion.purchase-changed.v1 > 1000; DLQ > 0; рост PROMOTION_TARGET_NOT_OWNED (недоступность резолвинга владельца)
Runbooksлаг outbox; poison-покупка в job-е истечения; сверка SEED-каталога пакетов (будущие пути по конвенции)
Feature flagspromotion.enabled (скрыть покупку до готовности: list-promotion-packagesPurchaseEnabled = false, purchase-promotionPROMOTION_DISABLED)

Jobs (Pin.Promotion.Background): expire-promotions (истечение покупок Active → Expired, FOR UPDATE SKIP LOCKEDbackground/expire-promotions.md).

Безопасность

  • Actor types: developer/agency-manager (developers-область в scope компании), администратор платформы (admin-область, admin API), system (job истечения).
  • Permission model: identity/domain/permission.mdpromotion.packages.view/admin, promotion.purchases.view/manage.
  • Sensitive data classes: PII нет; Price — прейскурант (internal, не секрет). Событие исключает actor-ids и CancelReason.
  • Audit: пакеты — глубокий аудит админ-мутаций + отметка автора + Version + soft-delete; покупки — отметка автора + Version, не удаляются (финансовая история), переходы — публикуемое событие.
  • Tenant isolation: ADR-0052 — PromotionPackage не tenanted (платформенный каталог); PromotionPurchase привязан к компании (TenantId = CompanyId); job истечения — системный актор (cross-tenant scan).

Известные Ограничения И Assumptions

  • Оплата не реализуется (решение №10) — PaymentStatus = NotRequired; включение эквайринга потребует ADR + billing-консюмер pin.promotion.purchase-changed.v1 + flow Pending → Paid.
  • Резолвинг владельца цели (INV-4) требует internal read-контракта у catalog/news — контракт assumption (DSC-PRM-1, api/developers/purchase-promotion.md); до согласования — fail-closed.
  • Отмена по делистингу цели не в job-е истечения (цель в чужом контексте): вклад снимает потребитель по факту делистинга у себя; покупка доживает срок и истекает штатно.
  • Топик события переименован из pin.catalog.promotions.v1 (ADR-0060); статус draft, прод-потребителей нет — переезд без dual-publish.

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