Задача — при добавлении в корзину товаров кратных 5 шт. — предлагать выбрать любой подарок из списка. В акции участвуют товары из конкретных категорий.
Заранее оговорюсь, что не претендую на красивое решение, зато работает :)
Итак, принцип работы следующий:
— создание отдельной категории для товаров-подарков, где цена равно 0 (проверка по родителю категории будет);
— отключение аякса и перезагрузка страницы с корзиной (большой) для корректной работы плагина;
— изменение родного скрипта
— вывод украшательств в карточку товара

1. Изменение скрипта ms2


Сначала идем в системные настройки и меняем путь к скриптами минишопа. Ключ — ms2_frontend_js, значение (например) web/default_new.js
Теперь или копируем и изменяем default.js
(строка status: function (status) ),
вместо
if (status['total_count'] < 1)
пишем
if(window.location.pathname == "/cart.html")

Где прописываем свой путь к большой корзине. assets/components/minishop2/js/web/default_new.js

2. Создаем подарочные товары

Тут ничего сложного. Просто в каком-то месте (например — рядом с каталогом общим) создаем категории товаров Подарки и не публикуем ее. Внутри создаем обычные товары, но с ценой равной 0. Проверяем, чтобы эти товары не мешались где-нибудь в вызовах на страницах.
Например указываете parents = -5 (где 5 — id категории подарков).
В корзине заказов, под списком товаров и анимашками делаем вызов подарков.
{set $preset_limitpresent = $.session['preset_limitpresent']}
{if $preset_limitpresent > 0}
	<section class="container" id="resset">
		    <h3>Выберите подарок</h3>
		    <div class="row">

</div>
</div>
{/if}


3. Создаем плагин

Отлавливаем событие изменения корзины и отдельно добавление в корзину.
Я специально не вызываю
$count = $cart->status()['total_count'];
так как в этом случае подсчет будет не правильным, ведь система не разделит подарки и основные товары и в строку прогресса будет идти не то что нужно (а общая сумма).

Готовый код с комментариями ниже.
<?php
switch ($modx->event->name) {    
    case 'msOnChangeInCart': case 'msOnRemoveFromCart': case 'msOnAddToCart' : case 'msOnEmptyCart':
    $tmp = $cart->get(); //Получаем список товаров
    
    // ниже функция для округления до 5 в большую сторону. Нужно для отлавливания колиества подарков на 5 единиц.
function roundClosest($num){
    $helpVar = round($num/5);
    $helpVar2 = $helpVar * 5;
    if($num - $helpVar2 > 0.1){
        $res = ($helpVar + 1) * 5;
    }
    else{
        $res = $helpVar * 5;
    }
    return $res;
};
// а здесь мы будем склонение от количества правильное ловить
function declOfNum($num, $titles) {
    $cases = array(2, 0, 1, 1, 1, 2);
    return $num . " " . $titles[($num % 100 > 4 && $num % 100 < 20) ? 2 : $cases[min($num % 10, 5);
};
// эта часть нужна, если подарки будут даваться только за конкретные категории товаров (3 и 4). В ином случае можно почистить эти моменты.
    $pdoRes = $modx->runSnippet('pdoResources', array(	'parents' => '3,4',	'limit' => 0,        'templates' => '4',	'tpl' => '@INLINE {$id},',	'tplLast' => '{$id}'));
    $rows = explode(",",$pdoRes); 
    // назначаем начальные значения счетчиков и идем по массиву корзины
         $count_category = 0;                  
         $count_category_present = 0;   
         foreach ($tmp as &$tm)  {
          	 if ($product = $modx->getObject('msProduct', $tm['id'])) { 
          	     $parent = $product->get('parent');
// выделяем подарочную категорию
          	         if ($parent == "5") {
          	         $count_category_present = $count_category_present + $tm['count'];
          	         }
          	         else {
// и исключаем все остальные категории. Условие if внутри можно убрать, если акция на все товары магазина, а не на определенные
          	             if (in_array($parent, $rows)) { 
          	                  $count_category = $count_category + $tm['count']; 
          	             }
          	         }
                 }
          }
// и понеслась магия
            $limit_category = $count_category / 5;
            $limit_category = floor($limit_category); // получили лимит доступных товаров
            $textlimit = declOfNum($limit_category, array('подарок', 'подарка', 'подарков')); 
          if ($count_category_present >= $limit_category) { // если подарков больше, чем нужно
                 $stopvount = 0;
                        foreach ($tmp as &$tm)  {
          	              if ($producttwo = $modx->getObject('msProduct', $tm['id'])) {  
          	                  if ($producttwo->get('parent') == "5") {
          	                  $stopvount = $stopvount + $tm['count'];
          	                  if ($stopvount > $limit_category) {
          	                     $tm['count'] = 0;
          	                  }
          	                   }
                          }
                       } 
                    
          }
        
        
           $count = $count_category;
         
                 $more = roundClosest($count);
                 $more = $more - $count;
                 $limitpresent = floor($count / 5);
                 if ($more > 0) {
                     $ost = $limitpresent + 1;
                 $textmore = declOfNum($more, array('товар', 'товара', 'товаров'));
                 $textmoret = declOfNum($ost, array('подарка', 'подарков', 'подарков')); 
                 $more = 'Вам осталось '.$textmore.' до '.$textmoret;
                 }
                 else {
                    $textmoret = declOfNum($limitpresent, array('подарка', 'подарков', 'подарков')); 
                    $more = 'Вы собрали нужное количество товаров для '.$textmoret.'.';
                 }
                if ($count > 0 && $count < 6) {
                     $width = $count * 20;
                     // заполняем шкалу
                 $_SESSION['preset_width'] = $width;
                     
                }
                elseif ($count > 5) {
                     // заполняем шкалу    
                 $_SESSION['preset_width'] = '100';
                    
                }
                else {
                 $_SESSION['preset_width'] = '0';
                }
                  $_SESSION['preset_more'] = $more;
                 $_SESSION['preset_limitpresent'] = $limitpresent;
   $cart->set($tmp); //Записываем данные в корзину 
    break;
//////////////////////////////////////////////////////////  
   
   
    case 'msOnBeforeAddToCart' :
       // и почти все то же самое на событие добавления
        $tmp = $cart->get();
          if ($product->get('parent') == "5"){
function declOfNum($num, $titles) {
    $cases = array(2, 0, 1, 1, 1, 2);
    return $num . " " . $titles[($num % 100 > 4 && $num % 100 < 20) ? 2 : $cases[min($num % 10, 5);
};
           
    $pdoRes = $modx->runSnippet('pdoResources', array(	'parents' => '3,4',	'limit' => 0,        'templates' => '4',	'tpl' => '@INLINE {$id},',	'tplLast' => '{$id}'));
    $rows = explode(",",$pdoRes); 
    
            $count_category = 0;                  
            $count_category_present = 0;   
            foreach ($tmp as &$tm)  {
          	 if ($productcart = $modx->getObject('msProduct', $tm['id'])) { 
          	     $parent = $productcart->get('parent');
          	         if ($parent == "5") {
          	         $count_category_present = $count_category_present + $tm['count'];
          	         }
          	         else {          	          
          	             if (in_array($parent, $rows)) { 
          	                  $count_category = $count_category + $tm['count']; 
          	             }
          	         }
                 }
          }
            $limit_category = $count_category / 5;
            $limit_category = floor($limit_category);
            $textlimit = declOfNum($limit_category, array('товар', 'товара', 'товаров'));     
             if ($limit_category <= $count_category_present) {
                        $modx->event->output('Вы можете добавить только '.$textlimit); //прерывает добавление  
             }        
          
          }
   $cart->set($tmp); 
    break;
}


4. Добавить красоту в корзину


Верстка в корзине, где происходит «магия»
<section class="progress">
	{set $width = $.session['preset_width']}
				<h3 class="progress__title">Доберите продукты до пяти и выберите подарок!</h3>
				<div class="progress__box">	
				
					<div class="progressbar ui-progressbar ui-corner-all ui-widget ui-widget-content">
						<div class="ui-progressbar-value ui-corner-left ui-widget-header" style="width:{$width}%;height: 100%;"></div>
						
					</div>
				</div>
				<div class="progress__text">
				{$.session['preset_more']}
				</div> 
	
</section>

Выглядит все так: