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 team | PIN Marketplace |
| Domain | Merchandising & Promotion |
| Data classification | Internal (пакеты/покупки — внутренние; PII нет; Price — прейскурант) |
| Runtime criticality | medium (влияет на ранжирование витрины/ленты у потребителей; отказ → буст устаревает, потери данных нет) |
Ownership
| Artifact | Owner/Rule |
|---|---|
| PostgreSQL schema | promotion |
| API areas | developers (покупка/отмена/список — покупатель = компания), admin (CRUD пакетов, m2m ADR-0053) |
| Owned entities | PromotionPackage, PromotionPurchase (+ дочерний PromotionPurchaseTarget) |
| Produced events | pin.promotion.purchase-changed.v1 |
| Consumed events | none (контекст — источник; резолвинг владельца цели при покупке — синхронный internal read, не Kafka) |
| Search indexes | none (полнотекстового поиска нет) |
| Analytics facts | none |
Ответственность И Границы
- Делает: каталог промо-пакетов (структуры + статусы, 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 | Полный отказ контекста |
| Kafka | queue | pin.promotion.purchase-changed.v1 (out, outbox) | Покупки создаются; событие буста задерживается (лаг-алерты) |
catalog | service (internal read) | Резолвинг владельца целей Project/PropertyObject при покупке (INV-4) | Покупка на эти цели невозможна (fail-closed, DSC-PRM-1) |
news | service (internal read) | Резолвинг владельца целей Post при покупке (INV-4) | Покупка на посты невозможна (fail-closed) |
handbook | service (internal resolve) | Валидация Price.CurrencyId на admin-write (INV-5 пакета; деньги в событиях — CurrencyId + CurrencySnapshot, snapshots.md) | Admin-мутации пакетов с валютой — fail-closed PROMOTION_HANDBOOK_UNAVAILABLE |
identity | service (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) | promotion | domain/promotion-package.md |
PromotionPurchase (+PromotionPurchaseTarget) | Покупка пакета компанией; полиморфные цели; снапшоты | promotion | domain/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): управлениеPromotionPackage—POST/PUT·DELETE/GET/api/admin/v1/promotion-packages. - Internal / Public / Callbacks: not applicable.
Events И Consumers
| Type | Name | Topic | Назначение | Link |
|---|---|---|---|---|
| Produced | PromotionPurchaseChangedKafkaEvent | pin.promotion.purchase-changed.v1 | Буст целей у потребителей; уведомления; будущий биллинг | events/promotion-purchase-changed.md |
| Consumed (downstream) | PromotionPurchaseChangedKafkaEvent | pin.promotion.purchase-changed.v1 | catalog — PromotionBoost карточек Project/PropertyObject | ../catalog/consumers/promotion-boost.md |
| Consumed (downstream) | PromotionPurchaseChangedKafkaEvent | pin.promotion.purchase-changed.v1 | news — Post.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 мин) |
| Dashboard | promotion-overview (операции покупки/отмены, лаг outbox pin.promotion.purchase-changed.v1, DLQ, backlog job-а истечения) |
| Alerts | job expire-promotions не отработал > 2 интервалов; outbox backlog pin.promotion.purchase-changed.v1 > 1000; DLQ > 0; рост PROMOTION_TARGET_NOT_OWNED (недоступность резолвинга владельца) |
| Runbooks | лаг outbox; poison-покупка в job-е истечения; сверка SEED-каталога пакетов (будущие пути по конвенции) |
| Feature flags | promotion.enabled (скрыть покупку до готовности: list-promotion-packages → PurchaseEnabled = false, purchase-promotion → PROMOTION_DISABLED) |
Jobs (Pin.Promotion.Background): expire-promotions (истечение покупок Active → Expired,
FOR UPDATE SKIP LOCKED — background/expire-promotions.md).
Безопасность
- Actor types: developer/agency-manager (developers-область в scope компании), администратор платформы (admin-область, admin API), system (job истечения).
- Permission model:
identity/domain/permission.md—promotion.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+ flowPending → 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.
Связанные Документы
- Домены: promotion-package · promotion-purchase.
- Событие: events/promotion-purchase-changed.md; job — background/expire-promotions.md; use-cases — use-cases/.
- Потребители буста:
../catalog/consumers/promotion-boost.md,../news/. - Решения: ADR-0060 (выделение контекста), ADR-0018 (границы возможностей), ADR-0052 (tenancy = CompanyId), ADR-0053 (admin API, машинный доступ), ADR-0056 (simplicity-first, polling, оплата отложена); technical-assignment.md (решение №10), service-boundaries.md, glossary.md.