- Проверено Modstore
- Бесплатные будущие обновления
- Работа на тестовом и публичном домене
- 12 месяцев тех. поддержки
О компоненте
Полнофункциональная система промо-кодов для магазинов на MiniShop3: гибкие правила применения, ручная и пакетная генерация, современная админка на Vue 3 + PrimeVue 4, готовая фронтенд-форма для покупателя. Скидка учитывается прямо в позициях заказа — чек 54-ФЗ, email-уведомления и личный кабинет показывают правильные суммы без каких-либо доработок шаблонов.
Скидки
- Тип: процент или фиксированная сумма в рублях.
- Область применения: вся корзина или только подпадающие под правила позиции.
- Корректный учёт в позициях. Цена со скидкой прописывается в каждой позиции заказа, поэтому сумма в чеке ОФД, в email-уведомлении и в личном кабинете покупателя всегда совпадает с оплаченной — никаких правок шаблонов не требуется.
- Точная сумма скидки. При распределении по нескольким товарам копейки автоматически компенсируются, чтобы итог совпадал с заявленной суммой до рубля.
Условия применения (правила)
- Включить или исключить конкретные товары.
- Ограничить категориями или брендами.
- Универсальный фильтр по любому полю карточки товара (цена, остаток, артикул, вес, собственные поля и т.д.) с разными операторами сравнения: равно, не равно, больше, меньше, диапазон, содержит и др.
- Фильтр по опциям товара — цвет, размер, вариант и другие.
- Несколько правил объединяются по «И», значения внутри одного правила — по «ИЛИ».
Жизненный цикл кода
- Период действия — даты начала и окончания.
- Общий лимит применений.
- Минимальная сумма заказа для применения.
- Включён / выключен.
- Автоматические статусы: запланирован, истёк, исчерпан, отключён. Отключённый код для покупателя выглядит как «не найден» — факт его существования не раскрывается.
Генерация кодов
- Ручное создание одного кода.
- Пакетная генерация по маске. Синтаксис маски: # — цифра, ? — буква, * — цифра или буква, обычные символы — как есть. Например, SALE-####-?? даст коды вида SALE-4729-KM.
- Набор символов без похожих друг на друга (нет 0 / O, 1 / I / L) — покупатель не ошибётся при вводе.
- Защита от дублирования, живое превью в админке.
Админ-панель
Современный интерфейс на Vue 3 + PrimeVue 4.
- Список промо-кодов с постраничной выборкой, сортировкой, фильтрами и групповыми действиями (включить / выключить / удалить сразу несколько кодов).
- Редактор кода с тремя вкладками: основные параметры, правила применения, статистика.
- Раздел «Аналитика»: ключевые показатели (количество применений, общая сумма скидки) и детальный журнал использований с фильтрами по датам и статусу.
- Вкладка «Промо-код» прямо в карточке заказа — менеджер может применить или снять код у конкретного заказа.
Фронтенд для покупателя
- Готовая форма ввода кода через сниппет ms3PromoCodeForm. Состояние «код применён» рендерится сразу на сервере — нет «мигания» при загрузке страницы.
- Шаблон формы легко переопределяется через собственный чанк.
- Headless JS-API для собственных интеграций (SPA, кастомные темы).
- Автоматический перерасчёт итогов корзины после применения или снятия кода.
- Папка переопределений assets/components/ms3promocode-overrides/ — можно заменить любой JS- или CSS-файл пакета своей версией без правок исходников.
Ручное редактирование заказа в админке
- Если менеджер добавил, изменил или удалил позицию в заказе, к которому уже применён промо-код — скидка автоматически пересчитается и распределится заново.
- Если новый состав заказа больше не подходит под условия кода (например, сумма стала меньше минимальной) — код автоматически снимается, а факт снятия записывается в историю заказа.
Учёт и статистика
- Каждое применение фиксируется в базе с привязкой к заказу, покупателю и подробной разбивкой скидки по позициям.
- Отмена заказа откатывает применение кода и уменьшает счётчик использований. Возврат заказа из отменённого статуса — восстанавливает.
- Данные учёта доступны в разделе «Аналитика» с возможностью фильтрации и экспорта.
Требования
- MODX Revolution 3.0 и выше
- MiniShop3 1.10 и выше
- PHP 8.2 и выше
- VueTools — для админки
- pdoTools — для шаблонов фронтенд-формы
1.0.0-beta1 — 2026-04-21
Первый публичный релиз. Ниже — архитектурные решения, точки расширения и неочевидные моменты, которые стоит знать при сопровождении и развитии кода.
Архитектурная картина
Два слоя данных:
msOrder.properties.promo_code— снимок применённого кода (JSON):id,code,discount_type,discount_value,discount_amount,breakdown,original_cart_cost,applied_at.msOrderProduct.properties.ms3promocode— снимок по каждой позиции:original_price,discount_amount,promo_code_id. Ставится только на те позиции, что получили часть скидки.
Модель применения — уценка позиций. Скидка распределяется по matching-позициям пропорционально их стоимости, затем на каждой позиции снижается price (и производится cost = price * count). Оригинальная цена сохраняется в properties.ms3promocode.original_price и восстанавливается при снятии или пересчёте.
Почему так, а не отдельная строка -500 ₽ в итогах:
msOrder.cart_cost = SUM(msOrderProduct.cost)у MS3 рассчитывается и вsubmit, и вfinalize. При уценённомpriceитоги корректны автоматически — никаких дополнительных хуков наmsOnGetCartCostне нужно (он был удалён).- Чек 54-ФЗ формируется по
msOrderProduct.price— и автоматически совпадает с оплаченной суммой по каждой позиции. - Email-уведомления и личный кабинет покупателя сразу показывают уценённые цены без доработки шаблонов.
Компенсация копеек. При fixed-скидке и count > 1 округление new_price = target_cost / count может потерять или прибавить копейку. ApplicationService::applyToLineItems() после основного цикла считает leftover и корректирует одну позицию (приоритет — count=1, иначе максимальная уже применённая) так, чтобы итоговая скидка совпала с заявленной до копейки.
Inversion at read-time. ApplicationService::findMatchingItems() при построении breakdown читает оригинальные цены из properties.ms3promocode.original_price, если он есть. Это сохраняет корректность повторной валидации: запуск validate() на корзине с уже применённым кодом вернёт тот же discount_amount, что и при первом apply().
Ключевые сервисы
Все сервисы регистрируются в DI-контейнере через bootstrap.php:
| Сервис | Ключ DI | Назначение |
|---|---|---|
PromoCodeService |
ms3promocode_promo_code_service |
CRUD + case-insensitive поиск по коду + инкремент/декремент счётчика. |
ValidationService |
ms3promocode_validation_service |
Lifecycle (disabled/scheduled/expired/exhausted) + min_order. Возвращает {valid, reason, message}. Сообщения поддерживают плейсхолдеры ([[+min_amount]]). |
DiscountCalculator |
ms3promocode_discount_calculator |
Чистая логика: расчёт суммы и proportional distribution. Без зависимостей от MODX — тестируется изолированно. |
RuleEngine + Rule strategies |
ms3promocode_rule_engine |
Проверка matching для scope=matching. 6 стратегий: ProductIds, ExcludeProductIds, ParentIds, VendorIds, ProductData, ProductOption. Секции объединяются по AND, значения внутри — по OR. |
CodeGenerator |
ms3promocode_code_generator |
Генерация по маске (# цифра, ? буква, * оба, литералы), unambiguous-charset, collision guard. |
ApplicationService |
ms3promocode_application_service |
Оркестратор: apply / validate / remove / syncAfterCartChange / validateCodeOnly / getCurrent. Фасад для контроллеров и плагина. |
UsageTracker |
ms3promocode_usage_tracker |
Запись / отмена / восстановление применений в ms3_promo_code_usages + синхронизация денормализованного msPromoCode.used_count. |
Интеграция с MiniShop3
Плагин — ms3promocode.plugin.php. Подписан на:
| Событие | Что делаем |
|---|---|
OnLoadWebDocument |
Регистрируем JS/CSS фронтенда по пресету ms3promocode.frontend_assets_preset. Честный fallback на overrides-директорию. |
msOnAddToCart / msOnChangeInCart / msOnRemoveFromCart |
Получаем draft через reflection Cart::$draft, вызываем syncAfterCartChange. |
msOnCreateOrderProduct / msOnUpdateOrderProduct / msOnRemoveOrderProduct |
То же самое при ручном редактировании состава заказа через старую (legacy) админку MS3. В новой Vue-админке эти события не дёргаются (см. workaround через Resync ниже). |
msOnCreateOrder |
UsageTracker::recordApplication() — фиксируем применение в ms3_promo_code_usages + инкремент used_count. |
msOnChangeOrderStatus |
Если переход в ms3promocode.cancel_statuses → cancelApplication + декремент; обратно → reinstateApplication. |
msOnManagerCustomCssJs |
На странице редактирования заказа (page=order) инжектим order-tab.js и window.ms3PromoCodeOrderTabConfig. |
Событие msOnGetCartCost не используется — при модели «уценка позиций» вычитать discount ещё раз из агрегата означало бы двойную скидку.
xPDO-кэш связей. Повсюду, где раньше был $order->getMany('Products'), теперь используется $this->loadProducts($order) (в ApplicationService), который идёт через $modx->getCollection(msOrderProduct::class, ['order_id' => ...]). getMany() кэширует relations — в события msOnAddToCart MS3 передаёт тот же msOrder, что был до мутации, и его кэш устарел на одну позицию.
Workaround: Vue-админка MS3
Новая Vue-админка MS3 ходит на свой OrdersController::addProduct/updateProduct/deleteProduct, который сохраняет msOrderProduct через $modx->newObject()→save() / remove() напрямую, минуя MODX-процессоры и события msOn{Create,Update,Remove}OrderProduct. Отслеживается в modx-pro/MiniShop3#207.
Пока PR не мерджен — в компоненте рабочий обход:
- Процессор
Mgr/Order/Resync— принимаетorder_id, проверяетproperties.promo_codeи вызываетsyncAfterCartChange. - В
order-tab.jsустановлен перехватwindow.fetch. На каждый успешныйPOST/PUT/PATCH/DELETEпо URL-паттерну/api/mgr/orders/{id}/products*(включая закодированный в query-параметреroute=` — MS3 заворачивает запросы в MODX-connector) хук делает `await resync(orderId)до возврата response клиенту. Vue послеawait A.post(...)делаетA.get(...)для обновления данных и видит уже пересчитанные цены. - XHR-обёртка оставлена как fallback на случай, если где-то ещё используется
XMLHttpRequest.
После мержа upstream — откатываем fetch-hook, оставляем только подписку плагина на события, уже сделанную в плагине.
Web API
- Router:
core/components/ms3promocode/config/routes/web.php. - Stub: ставится resolver'ом в
core/config/ms3.routes.d/web/50-ms3promocode.php— онrequire-ит реальный файл из пакета. Это позволяет менять роуты без переустановки пакета. - TokenMiddleware / CORS / rate limiting — от общего MS3-entry
assets/components/minishop3/api.php. - Endpoints:
POST /api/v1/promo/apply|remove|validate,GET /api/v1/promo/current. - Контроллер
PromoCodeControllerмаскируетreason=disabled→reason=not_foundперед отдачей клиенту, чтобы не раскрывать существование временно отключённого кода. Менеджерский API маскировки не делает.
Manager API
Собственный коннектор assets/components/ms3promocode/connector.php с $processorMap. Процессоры:
Mgr/PromoCode/*: GetList / Get / Create / Update / Remove / Generate / PreviewMask.Mgr/Order/*: ApplyPromo / RemovePromo / GetPromo / Resync.Mgr/Product/Search,Mgr/Category/Search,Mgr/Vendor/GetList,Mgr/ProductData/GetFields,Mgr/ProductOption/GetKeys|GetValues— helpers для конструктора правил.Mgr/Analytics/GetKpi|GetLog.
Тонкости Update-процессора: nullable-числа (min_order_amount, date_start, date_end) очищаются пустой строкой. Фронтенд (vueManager/src/services/request.js) для этого отправляет null как пустую строку — без такого преобразования URLSearchParams вообще теряет ключ.
Фронтенд покупателя
ms3promocode.headless.js— headless-ядро, глобалwindow.ms3PromoCode. Единственный_initPromiseрешает race condition:initialized=trueвыставляется после_refreshCurrent, чтобы UI-слой не ловилgetCurrent() === nullдо загрузки состояния из сессии.ui/PromoCodeUI.js— опциональный UI, монтируется на[data-ms3-promocode="form"]. По умолчанию послеapply/removeделаетwindow.location.reload()(гарантированно обновляет цены позиций в корзине). Отключаетсяdata-ms3pc-auto-reload="false"для AJAX-корзин.- Сниппет
ms3PromoCodeFormотдаёт SSR-hydrated состояние: получает draft черезms3_order_draft_manager+ms3_token_service, читаетApplicationService::getCurrent()и рендерит блокemptyлибоappliedуже с правильнымhidden. Нет «мигания» при загрузке страницы. - Чанк
ms3promocode_formпереопределяется через свойство&tplсниппета. - Overrides: любой файл в
assets/components/ms3promocode-overrides/прозрачно заменяет ассет из пакета.




Последние обсуждения в сообществе MODX.pro