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

Event Contract: pin.promotion.purchase-changed (PromotionPurchaseChangedKafkaEvent)

Версионируемый контракт интеграционного события «изменился статус покупки продвижения». Единственное produced-событие контекста promotion (README, раздел «Events И Consumers»). Consumer-документы ссылаются сюда, не переописывая payload.

Назначение

Фиксирует каждый переход статуса агрегата PromotionPurchase (покупка пакета продвижения компанией): активацию без оплаты, отмену и истечение срока. Публичное: даёт downstream-контекстам снапшот покупки без синхронного запроса в promotion — потребители буста пересчитывают денормализованный вклад у себя (catalog для целей Project/PropertyObject, news для Post), notifications шлёт покупателю уведомление об активации/истечении, будущий биллинг (решение №10 — оплата отложена) получает заложенные поля цены. Переименован из pin.catalog.promotions.v1 (ADR-0060: продвижение вынесено из catalog в отдельный контекст).

Metadata

ПолеЗначение
Event typepin.promotion.purchase-changed
C# typePromotionPurchaseChangedKafkaEvent
Topicpin.promotion.purchase-changed.v1 (единственный produced-топик контекста)
Partitions / key3 партиции (низкий трафик продвижения) / key = PurchaseId (= PromotionPurchase.Id) — per-purchase порядок
ContractVersion1
Producerpromotion: активация purchase-promotion, отмена cancel-promotion, истечение job expire-promotions
Статусdraft

Событие несёт идентификатор сообщения (для дедупликации), время события и ссылку на агрегат (AggregateId = PurchaseId, AggregateType = "PromotionPurchase"). Actor-ids в событие не входят. Публикуется атомарно со сменой статуса покупки.

Payload PromotionPurchaseChangedKafkaEvent

Дом полей — entity: PromotionPurchase; здесь перечислено только то, что реально в событии. Строгая типизация: enum сериализуются строкой, голого object/json нет; коллекция целей — типизированная вложенная модель.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
PurchaseIdGuidДаPromotionPurchase.IdImmutableКлюч агрегата (= Kafka key = AggregateId).
TenantIdGuidДаPromotionPurchase.TenantId (= CompanyId)ImmutableКомпания-покупатель (ADR-0052).
PackageIdGuidДаPromotionPurchase.PackageIdImmutableКупленный пакет.
PackageCodestringДаPromotionPurchase.PackageCode (снапшот)ImmutableСтабильный код (project-boost-week) — для аналитики consumer-ов без lookup.
PackageKindstring (enum PromotionPackageKind)ДаPromotionPurchase.PackageKind (снапшот)Additive only (новые значения)ProjectBoost / PropertyObjectBoost / PostBoost.
TargetsPromotionTargetModel[]Да (>= 1)коллекция PromotionPurchaseTarget.TargetAdditive onlyПолиморфные цели покупки (см. вложенную модель).
Statusstring (enum PromotionPurchaseStatus)ДаPromotionPurchase.StatusAdditive onlyPending / Active / Expired / Cancelled — новое зафиксированное состояние.
PaymentStatusstring (enum PromotionPaymentStatus)ДаPromotionPurchase.PaymentStatusAdditive onlyСейчас всегда NotRequired (решение №10); Pending/Paid/Refunded — заготовка эквайринга.
BoostWeightintДаPromotionPurchase.BoostWeight (снапшот)Additive onlyВес буста покупки (INV-2 — не меняется правкой пакета).
StartsAtDateTimeOffsetДаPromotionPurchase.StartsAtAdditive onlyНачало действия (момент активации).
EndsAtDateTimeOffsetДаPromotionPurchase.EndsAtAdditive onlyКонец действия (StartsAt + DurationDays).
PricePromotionPriceSnapshotДаPromotionPurchase.Price (owned VO, снапшот)Additive onlyСнапшот цены для будущего биллинга: CurrencyId + CurrencySnapshot (snapshots.md).
VersionlongДаPromotionPurchase.VersionImmutable semanticsДоменная версия; монотонный guard у потребителей.

Вложенная модель PromotionTargetModel

Проекция owned-VO PromotionTarget (дом — promotion-purchase.md). Потребитель фильтрует цели по TargetKind: catalog берёт Project/PropertyObject, newsPost.

ПолеТипОбязательностьОграниченияОписание
TargetKindstring (enum PromotionTargetKind)ДаProject / PropertyObject / PostТип продвигаемой цели.
TargetIdGuidДа!= Guid.EmptyИдентификатор цели в контексте-владельце (логическая ссылка).

Вложенная модель PromotionPriceSnapshot

ПолеТипОбязательностьОграниченияОписание
AmountdecimalДа>= 0 (decimal(14,2); 0 — бесплатный пакет)Прейскурантная сумма на момент покупки.
CurrencyIdshortДассылка на запись справочника валют handbook; допустима только RUB (ADR-0048)Типизированная ссылка на валюту (PromotionPrice.CurrencyId).
CurrencyCurrencySnapshotДадом модели — handbook/domain/snapshots.mdDisplay-снапшот валюты (IsoCode/Name/Sign), зафиксированный на момент публикации; отдельного строкового кода валюты payload не несёт.

Поля агрегата, не входящие в payload: CancelReason (свободный текст — потенциально внутренние заметки), CreatedByUserId/UpdatedByUserId (actor — PII, см. Безопасность), RowVersion.

Когда публикуется

На каждый переход статуса покупки, всегда в одном согласованном изменении со сменой статуса покупки.

Условие (источник)Переход состоянияСогласованность
Покупка без оплаты, purchase-promotionPending → Active (в no-payment режиме — в той же операции создания; публикуется со Status = Active, PaymentStatus = NotRequired)смена статуса и событие — в одном согласованном изменении
Отмена покупателем/админом, cancel-promotionActive → Cancelled / Pending → Cancelled (CancelReason задан, но в payload не входит)то же
Истечение срока, job expire-promotionsActive → Expired (now() >= EndsAt, FOR UPDATE SKIP LOCKED)то же

Терминальные статусы Expired/Cancelled переходов не имеют (INV-6) — по покупке публикуется конечное число событий. Порядок per-purchase гарантирован key-ом PurchaseId; потребители держат Version-guard.

Потребители (consumers)

Сверено с README (раздел «Events И Consumers») и service-boundaries.md.

Consumer-контекстConsumerSide effectsИдемпотентностьЗадержка (SLA)
catalogPromotionBoostConsumerДля целей Project/PropertyObject пересчитывает PromotionBoost карточек проекта/объекта (сумма BoostWeight активных покупок); истечение/отмена снимают вклад — promotion-boost.mdMessageId + Version-guard< 1 мин
newsPromotionBoostConsumer (news)Для целей Post наполняет Post.PromotionBoost (ADR-0060; дом — post.md)MessageId + Version-guard< 1 мин
notificationsPromotionPurchaseChangedConsumerУведомление менеджеру компании: Active — «продвижение запущено» (EndsAt), Expired — «срок истёк», Cancelled — «отменено»MessageId + Version-guard< 5 мин
billing (будущий)BillingPromotionPurchaseChangedConsumer (планируется)Выставление счёта по Price при включении эквайринга (решение №10)MessageIdn/a (не реализован)

Гарантии доставки и порядка

Единый свод гарантий, на которые опираются потребители (catalog/news/notifications/будущий billing):

  • Доставка — at-least-once. Событие публикуется после фиксации перехода статуса; при сбое публикации возможен повтор. Потребитель обязан дедуплицировать по идентификатору сообщения (MessageId).
  • Порядок — per-purchase. Партиционирование по key = PurchaseId гарантирует порядок событий одной покупки внутри партиции. Между разными покупками порядок не гарантирован.
  • Защита от переупорядочивания/дублей — Version-guard. Потребитель применяет событие, только если его Version больше последнего применённого по этой покупке; запоздавшее/повторное событие с Version <= уже применённого — игнорируется (буст не откатывается назад).
  • Ограниченность. По каждой покупке конечное число событий: Active → (Cancelled или Expired); терминальные статусы переходов не имеют (INV-6). «Бесконечного» потока по одной покупке нет.
  • Отсутствие потери при недоступности consumer-а. Событие сохраняется в топике; отставший потребитель доберёт его при восстановлении (в пределах retention топика), затем снимет/поднимет буст.

Версионирование и совместимость (реальность SDK)

  • ContractVersion = 1; при несовпадении версии события с ожидаемой consumer логирует warning и продолжает (upcasting/schema-migration нет). Топик версию не кодирует автоматически — по конвенции суффикс .v1.
  • Additive-only для minor: новые optional-поля с безопасным default — без bump ContractVersion.
  • Breaking (удаление/переименование/смена типа/смысла) — новый ContractVersion = 2 + новый топик pin.promotion.purchase-changed.v2 + dual-publish на период миграции + deadline миграции consumers + sunset .v1.
  • Unknown enum (Status/PackageKind/PaymentStatus/TargetKind) — fallback: сохранить строку как есть + warning, не падать (новый TargetKind для нового потребителя — просто игнорируется чужими консюмерами).

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

ПолеКласс данныхПравило
PurchaseId, TenantId, PackageId, PackageCode, PackageKind, Targets, Status, PaymentStatus, BoostWeight, StartsAt, EndsAt, VersionInternalinclude
Price.Amount, Price.CurrencyId, Price.Currency (CurrencySnapshot)Internal (коммерческая цена пакета, не секрет)include; доступ к топику — m2m ACL Kafka (catalog/news/notifications/billing)
CancelReasonInternal (свободный текст)exclude — в payload не входит
CreatedByUserId / UpdatedByUserId (actor)PIIexclude — в payload не входит

PII в событии нет: TenantId — идентификатор компании (CompanyId), не физлица; персональные actor-ids исключены. Событие НЕ содержит secrets, tokens, credentials, платёжных реквизитов (Price — прейскурант пакета, не карта/счёт) и unbounded вложений (Targets ограничен MaxTargetsPerPurchase).

Наблюдаемость публикации

Публикация наблюдается метриками очереди публикации (не отдельным producer-кодом):

  • метрики: глубина/лаг очереди неопубликованных событий, счётчик опубликованных сообщений по топику pin.promotion.purchase-changed.v1, размер DLQ;
  • dashboard promotion-overview (README, «Runtime И Эксплуатация»); alert — DLQ > 0, рост лага публикации;
  • trace каждой публикации коррелируется с исходной операцией/job.

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

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

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