Skip to content
ms3PromoCode
ms3PromoCode
Промо-коды для MiniShop3 с гибкими правилами, генерацией по маске и интеграцией в позиции заказа
  • MODX 3
  • PHP 8.2
  • miniShop3
  1. Компоненты
  2. ms3PromoCode
  3. Разработка
  4. Модели и БД

Модели и БД ​

Две таблицы под управлением Phinx-миграций — применяются автоматически при установке и обновлении пакета.

Таблицы ​

ms3_promo_codes ​

Промо-коды. InnoDB, utf8mb4_unicode_ci.

ПолеТипDefaultОписание
idINT UNSIGNED, PKAUTO_INCREMENTИдентификатор
codeVARCHAR(64)—Сам код (uppercase, unique)
nameVARCHAR(255), NOT NULL—Отображаемое название для админки
descriptionTEXT, nullableNULLПроизвольное описание для менеджера
discount_typeENUM('percent','fixed')—Тип скидки. Константы: msPromoCode::TYPE_PERCENT, TYPE_FIXED
discount_valueDECIMAL(12,2)0.00Размер скидки
discount_scopeENUM('all','matching')'all'Область применения. Константы: msPromoCode::SCOPE_ALL, SCOPE_MATCHING
rulesJSON, nullableNULLКонфигурация правил (см. Правила)
start_dateDATETIME, nullableNULLНачало действия (NULL — без ограничения)
end_dateDATETIME, nullableNULLОкончание действия (NULL — без ограничения)
max_usesINT UNSIGNED0Лимит применений (0 — без лимита)
used_countINT UNSIGNED0Денормализованный счётчик (инкрементируется при создании Usage, уменьшается при отмене)
min_order_amountDECIMAL(12,2), nullableNULLМинимальная сумма заказа
activeTINYINT(1)11 — включён, 0 — отключён
created_atDATETIMECURRENT_TIMESTAMPПроставляется автоматически
updated_atDATETIMECURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPОбновляется автоматически при любой записи

Индексы:

  • uniq_code — UNIQUE на code.
  • idx_active — на active.
  • idx_dates — композитный на (start_date, end_date).
  • idx_created — на created_at.

ms3_promo_code_usages ​

Журнал применений. InnoDB, utf8mb4_unicode_ci.

ПолеТипDefaultОписание
idINT UNSIGNED, PKAUTO_INCREMENT
promo_code_idINT UNSIGNED—Ссылка на ms3_promo_codes.id (логическая, см. ниже)
order_idINT UNSIGNED—Ссылка на ms3_orders.id
customer_idINT UNSIGNED, nullableNULLСсылка на ms3_customers.id
user_idINT UNSIGNED, nullableNULLСсылка на modx_users.id
order_costDECIMAL(12,2)0.00Стоимость заказа со скидкой (на момент применения)
discount_amountDECIMAL(12,2)0.00Применённая сумма скидки
breakdownJSON, NOT NULL—{"product_key": discount_amount, ...} — разбивка по позициям
applied_atDATETIMECURRENT_TIMESTAMP
cancelled_atDATETIME, nullableNULLЕсли установлена — применение отменено

Про ссылки, помеченные выше

Ссылки логические: FOREIGN KEY на уровне схемы не создаётся, целостность поддерживает код компонента (UsageTracker). В метаданных xPDO объявлена одна связь — aggregates.PromoCode; Order / Customer / User там нет (см. BACKLOG).

Индексы:

  • idx_promo_code — на promo_code_id.
  • idx_order — на order_id.
  • idx_customer — на customer_id.
  • idx_applied — на applied_at.
  • idx_active_usages — композитный на (promo_code_id, cancelled_at).

xPDO модели ​

КлассОписание
ms3PromoCode\Model\msPromoCodeСам код
ms3PromoCode\Model\msPromoCodeUsageЗапись применения

msPromoCode ​

php
$code = $modx->getObject(\ms3PromoCode\Model\msPromoCode::class, ['code' => 'SALE10']);

// Стандартные геттеры
$code->get('name');                 // 'Весенняя распродажа'
$code->get('discount_type');        // 'percent'
$code->get('discount_value');       // 10.00
$code->get('used_count');           // 42
$code->get('rules');                // массив (поле phptype="json")

// Связь «к Usages» (composite, cardinality='many')
// При удалении msPromoCode xPDO удаляет и связанные Usages
$usages = $code->getMany('Usages');

// Вычислить статус
$status = $code->getStatus();
// Возможные значения — константы msPromoCode:
// STATUS_ACTIVE, STATUS_SCHEDULED, STATUS_EXPIRED, STATUS_EXHAUSTED, STATUS_DISABLED

// rules как массив (всегда массив, даже если в БД пусто)
$rules = $code->getRules();

// toArray() всегда добавляет вычисляемое поле 'status' рядом с 'active'
$data = $code->toArray();
// [..., 'active' => 1, 'status' => 'active', ...]

Сигнатура getStatus ​

php
getStatus(?int $now = null): string

Приоритет проверок: disabled → scheduled → expired → exhausted → active. У отключённого кода статус всегда disabled, даже если срок истёк. Параметр $now — timestamp для подстановки времени (удобно в тестах); по умолчанию берётся текущее.

Константы msPromoCode ​

php
// Типы скидки
msPromoCode::TYPE_PERCENT       // 'percent'
msPromoCode::TYPE_FIXED         // 'fixed'

// Область применения
msPromoCode::SCOPE_ALL          // 'all'
msPromoCode::SCOPE_MATCHING     // 'matching'

// Статусы жизненного цикла
msPromoCode::STATUS_ACTIVE
msPromoCode::STATUS_SCHEDULED
msPromoCode::STATUS_EXPIRED
msPromoCode::STATUS_EXHAUSTED
msPromoCode::STATUS_DISABLED

msPromoCodeUsage ​

php
$usage = $modx->getObject(\ms3PromoCode\Model\msPromoCodeUsage::class, ['order_id' => 1234]);

$usage->get('discount_amount');       // 500.00
$usage->get('breakdown');             // массив
$usage->get('cancelled_at');          // null или datetime

// Единственная связь xPDO
$promo = $usage->getOne('PromoCode'); // → msPromoCode

// Связанные msOrder / msCustomer / modUser достаются вручную по id:
$order    = $modx->getObject(\MiniShop3\Model\msOrder::class,    $usage->get('order_id'));
$customer = $modx->getObject(\MiniShop3\Model\msCustomer::class, $usage->get('customer_id'));
$user     = $modx->getObject(\MODX\Revolution\modUser::class,    $usage->get('user_id'));

Вспомогательные методы ​

php
// Отменено ли применение
$usage->isCancelled();                // bool — по заполненности cancelled_at

// Распределение скидки по позициям как массив
$usage->getBreakdown();               // ['product_key' => float, ...]

Снимки в properties ​

Применённый к корзине код хранится дважды:

  1. msOrder.properties.promo_code — снимок на уровне заказа.
  2. msOrderProduct.properties.ms3promocode — снимок на уровне позиции (только для позиций, получивших часть скидки).

msOrder.properties.promo_code ​

json
{
    "id": 12,
    "code": "SALE10",
    "discount_type": "percent",
    "discount_value": 10,
    "discount_amount": 500.00,
    "breakdown": {
        "ms-prod-key-1": 300.00,
        "ms-prod-key-2": 200.00
    },
    "original_cart_cost": 5000.00,
    "applied_at": "2026-04-21T12:34:56+00:00"
}

msOrderProduct.properties.ms3promocode ​

json
{
    "original_price": 1500.00,
    "discount_amount": 300.00,
    "promo_code_id": 12
}

Зачем дублировать

  • Снимок в msOrder — для UI («применён код X со скидкой Y»), для аналитики и как источник правды при повторном применении.
  • Снимки в msOrderProduct позволяют восстановить исходные цены при снятии кода. Иначе пришлось бы брать Product.price из каталога — а цена могла измениться после оформления заказа.

Связи (xPDO relations) ​

msPromoCode 1 ─── ∞ msPromoCodeUsage
     (composite Usages ↔ aggregate PromoCode)

Связей с msOrder / msCustomer / modUser в xPDO нет: поля order_id / customer_id / user_id работают как логические ссылки, объект достаётся через $modx->getObject(...).

Миграции ​

Файлы — в core/components/ms3promocode/migrations/, запускаются через Phinx.

МиграцияЧто делает
20260418000001_create_promo_codes_table.phpСоздание таблицы ms3_promo_codes
20260418000002_create_promo_code_usages_table.phpСоздание таблицы ms3_promo_code_usages

При установке миграции запускаются автоматически через resolver_02_migrations.php.

При удалении пакета таблицы остаются — чтобы сохранить историю применений на случай переустановки или ручного экспорта. Если компонент больше не нужен, удалите таблицы вручную:

sql
DROP TABLE IF EXISTS modx_ms3_promo_code_usages;
DROP TABLE IF EXISTS modx_ms3_promo_codes;

Префикс таблиц

В примерах SQL используется стандартный префикс modx_. Подставьте свой, если он отличается.

Производительность ​

Все нужные индексы перечислены в описаниях таблиц выше. При истории от миллиона строк имеет смысл добавить партиционирование по applied_at (по годам или месяцам) — вручную, в стандартную конфигурацию не входит.