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

Entity / Aggregate: PromotionPackage

Дом полей платформенного пакета продвижения (контекст promotion). Все проекции (модели ответов admin/ developers API, снапшот в promotion-purchase.md, event payload) ссылаются сюда и перечисляют только дельту, а не переописывают поля.

Назначение

PromotionPackage — каталог пакетов продвижения платформы: различные пакеты (продвигать проект застройщика чаще; продвигать объекты проекта чаще), которые застройщик может купить, но без оплаты (решение №10 — структуры монетизации проектируются, реализация оплаты отложена). Пакет описывает вид продвижения (Kind), допустимый набор целей (PromotionTargetKind), вес буста, длительность действия покупки и будущую прейскурантную цену.

Пакет — платформенная сущность, не tenanted (каталог платформы, ADR-0052 п.3): управляет им администратор платформы (admin API, ADR-0053), а не застройщик/агентство. Покупка пакета — отдельный агрегат PromotionPurchase, который снапшотит Kind/BoostWeight/Price на момент приобретения (INV снапшота — правки пакета не трогают действующие покупки).

Классификация

ПолеЗначение
Контекстpromotion
KindAggregateRoot
Source of truthэтот контекст
Таблицаpromotion.promotion_packages
Tenant isolationне tenanted (каталог платформы, ADR-0052 п.3)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор пакета.
CodestringДаvarchar(64), unique ux_promotion_packages_code where deleted_at is null, pattern ^[a-z0-9][a-z0-9-]*$adminСтабильный код (project-boost-week, property-object-boost-month, post-boost-week) для SEED/аналитики.
NamestringДаvarchar(256), min 1adminОтображаемое название пакета в ЛК покупателя.
Descriptionstring?Нетvarchar(2000)adminМаркетинговый текст пакета.
KindPromotionPackageKindДаvarchar(16), index ix_promotion_packages_kindadminВид продвижения; определяет допустимый PromotionTargetKind (таблица ниже).
BoostWeightintДа1..100 (check ck_promotion_packages_boost)adminВес буста, добавляемый целям на время действия покупки (снапшотится в покупку).
DurationDaysintДа1..365 (check)adminСрок действия покупки: EndsAt = StartsAt + DurationDays.
MaxTargetsPerPurchaseint?Нет (обязателен при multi-target Kind)1..500 (check)adminМаксимум целей в одной покупке для PropertyObjectBoost/PostBoost; null при ProjectBoost (ровно 1 цель).
PricePromotionPrice (owned)Дасм. VOadminПрейскурантная цена. Оплата не реализуется (решение №10) — цена информационная и снапшотится в покупку для будущего биллинга.
StatusPromotionPackageStatusДаvarchar(16), index ix_promotion_packages_statusappDraft / Active / Archived.
CreatedAt / CreatedByUserId / UpdatedAt / UpdatedByUserId / DeletedAt / DeletedByUserIdстандартappАудит + архивация (soft-delete).
VersionlongДа>= 1appВерсия для optimistic-check в admin-мутациях.

Enum PromotionPackageKind → допустимые PromotionTargetKind

Дом PromotionTargetKindpromotion-purchase.md (VO PromotionTarget). Набор допустимых целей покупки ограничивается видом пакета (проверка — INV-1 покупки).

KindДопустимый TargetKindКол-во целей в покупкеConsumer буста
ProjectBoostProjectровно 1catalog — карточка проекта + все Published-объекты проекта
PropertyObjectBoostPropertyObject1..MaxTargetsPerPurchasecatalog — карточки перечисленных объектов недвижимости
PostBoostPost1..MaxTargetsPerPurchasenewsPost.PromotionBoost перечисленных постов

Enum PromotionPackageStatus

ЗначениеОписание
DraftЧерновик: невидим покупателям, покупка невозможна.
ActiveДоступен к покупке в ЛК покупателя.
ArchivedСнят с продажи (DeletedAt заполнен); действующие покупки доживают срок (FK restrict).

Value objects

PromotionPrice (owned; используется в пакете и снапшотом в покупке)

ПолеТипОбязательностьОграниченияКолонка
Amountdecimal(14,2)Да>= 0 (0 = бесплатный пакет)price_amount
CurrencyIdshortДаdefault — RUB; логическая ссылка на запись справочника валют (handbook, без FK); допустим только RUB (ADR-0048 — типизированная ссылка, не мультивалютность)price_currency_id

CurrencyId валидируется на write-path admin-мутаций через internal resolve справочника (resolve-references.md): неизвестный/архивный id → PROMOTION_CURRENCY_UNKNOWN (INV-5). В событиях покупок цена уходит как CurrencyId + CurrencySnapshot (единый дом snapshot-моделей — snapshots.md); отдельных строковых полей кода валюты контракты не несут.

Модели JSONB

JSONB-модели не используются.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
PromotionPackagePromotionPurchasereferenceFK package_idчтение пакета при покупке (снапшот полей)restrict (архив пакета не трогает покупки)

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-1 CodeUniqueCode уникален среди живых пакетов (where deleted_at is null)проверка + уникальный индексPROMOTION_PACKAGE_CODE_DUPLICATE
INV-2 MaxTargetsMatchesKindMaxTargetsPerPurchase задан ⇔ Kind ∈ {PropertyObjectBoost, PostBoost}; null при ProjectBoostпроверка при созданииPROMOTION_PACKAGE_MAX_TARGETS_RULE
INV-3 ImmutableIdentityCode и Kind неизменяемы после созданияполя не принимаются операцией правкиPROMOTION_PACKAGE_IMMUTABLE_FIELD
INV-4 StatusTransitionпереходы только по матрице (см. update-promotion-package.md)проверка перехода по матрицеPROMOTION_PACKAGE_INVALID_STATUS_TRANSITION
INV-5 CurrencyRefPrice.CurrencyId существует в справочнике валют и не архивен (internal resolve handbook); допустима только RUB (ADR-0048)admin create/updatePROMOTION_CURRENCY_UNKNOWN (неизвестный/архивный id); не-RUB → ValidationError

Переходы статусов (из / в, условие, инициатор, эффект)

Дом матрицы допустимых переходов и её проверки — update-promotion-package.md (guard PROMOTION_PACKAGE_INVALID_STATUS_TRANSITION). Здесь — бизнес-смысл каждого перехода.

ИзВУсловиеИнициаторЭффект
[*]Draftсоздание пакета (пройдены INV-1 CodeUnique, INV-2 MaxTargets⇔Kind)администратор (create)пакет заведён, но невидим покупателям; покупка невозможна
DraftDraftправка полей без смены статусаадминистратор (update)обновлены Name/Description/BoostWeight/DurationDays/MaxTargetsPerPurchase/Price; Code/Kind неизменяемы (INV-3)
DraftActiveпубликацияадминистратор (update)пакет появляется на витрине застройщика (Active), становится покупаемым немедленно (source of truth — лага нет)
DraftArchivedархивация черновикаадминистратор (delete/update)soft-delete (DeletedAt/DeletedByUserId); покупок на пакет нет
ActiveActiveправка полей действующего пакетаадминистратор (update)новые значения применяются только к будущим покупкам; действующие покупки держат снапшот (INV-2 покупки) — событий пересчёта нет
ActiveArchivedснятие с продажиадминистратор (delete/update)soft-delete; пакет пропадает с витрины; действующие покупки доживают срок (FK restrict, истекают штатно)
ActiveDraftне поддерживается (unpublish снятого с продажи нет)отказ PROMOTION_PACKAGE_INVALID_STATUS_TRANSITION
Archivedлюбойтерминально: архивный пакет неотличим от несуществующегоотказ PROMOTION_PACKAGE_NOT_FOUND (soft-deleted → 404)

Изменение пакета не порождает интеграционных событий и не затрагивает потребителей буста: единственный канал влияния на потребителей — события покупок (promotion-purchase.md).

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
Изменения самого пакета интеграционных событий не эмитят (v1): интеграционные события есть только у покупок (promotion-purchase.md, PromotionPurchaseChangedKafkaEvent). Доменных событий пакет тоже не поднимает.

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK; ux_promotion_packages_code unique where deleted_at is null; ix_promotion_packages_kind; ix_promotion_packages_status
Партиционирование / retentionnone; пакеты живут бессрочно (архив — soft-delete)
Concurrencyоптимистическая блокировка + Version (optimistic-check в admin-мутациях)
Permissions / PIIчтение витрины (только Active) — developers API (promotion.packages.view); управление пакетами — только admin API (promotion.packages.admin, ADR-0053). PII нет; Price — прейскурант, не секрет
Auditотметка автора + Version; глубокий аудит админ-мутаций; архив — soft-delete

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

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

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