[СДЕЛАЙ САМ] Промокоды для minishop2 с помощью MIGX.

Приветствую, начнём с того, что есть много прекрасных дополнений реализующих данных функционал, и я советую всем использовать именно их, поскольку функциональность их значительно выше, а цена вполне демократичная. Но если бюджет сильно ограничен, а промокоды очень хочется и самому придумывать реализацию не с руки, то читайте дальше.

Нам понадобится:
1. minshop2;
2. MIGX;
3. msOrderDiscount (если хотите после оформления заказа менять размер скидки);

Начнём с того, что создадим новую конфигурацию в компоненте MIGX с названием promocode со следующими полями:
1. code — сюда будем записывать сам промокод;
2. count — доступное количество активаций;
3. date — дата окончания срока действия;
4. percent — процент скидки;
Для ленивых прилагаю json с конфигурацией.
{
  "formtabs":[
    {
      "MIGX_id":28,
      "caption":"",
      "print_before_tabs":"0",
      "fields":[
        {
          "MIGX_id":106,
          "field":"code",
          "caption":"\u041f\u0440\u043e\u043c\u043e\u043a\u043e\u0434",
          "description":"",
          "description_is_code":"0",
          "inputTV":"",
          "inputTVtype":"",
          "validation":"",
          "configs":"",
          "restrictive_condition":"",
          "display":"",
          "sourceFrom":"config",
          "sources":"",
          "inputOptionValues":"",
          "default":"",
          "useDefaultIfEmpty":"0",
          "pos":1
        },
        {
          "MIGX_id":107,
          "field":"count",
          "caption":"\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0438\u0439",
          "description":"",
          "description_is_code":"0",
          "inputTV":"",
          "inputTVtype":"",
          "validation":"",
          "configs":"",
          "restrictive_condition":"",
          "display":"",
          "sourceFrom":"config",
          "sources":"",
          "inputOptionValues":"",
          "default":"",
          "useDefaultIfEmpty":"0",
          "pos":2
        },
        {
          "MIGX_id":108,
          "field":"date",
          "caption":"\u0414\u0430\u0442\u0430 \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f",
          "description":"",
          "description_is_code":"0",
          "inputTV":"",
          "inputTVtype":"date",
          "validation":"",
          "configs":"",
          "restrictive_condition":"",
          "display":"",
          "sourceFrom":"config",
          "sources":"",
          "inputOptionValues":"",
          "default":"",
          "useDefaultIfEmpty":"0",
          "pos":3
        },
        {
          "MIGX_id":109,
          "field":"percent",
          "caption":"\u041f\u0440\u043e\u0446\u0435\u043d\u0442 \u0441\u043a\u0438\u0434\u043a\u0438",
          "description":"\u043f\u0440\u043e\u0441\u0442\u043e \u0447\u0438\u0441\u043b\u043e",
          "description_is_code":"0",
          "inputTV":"",
          "inputTVtype":"",
          "validation":"",
          "configs":"",
          "restrictive_condition":"",
          "display":"",
          "sourceFrom":"config",
          "sources":"",
          "inputOptionValues":"",
          "default":"",
          "useDefaultIfEmpty":"0",
          "pos":4
        }
      ],
      "pos":1
    }
  ],
  "contextmenus":"",
  "actionbuttons":"",
  "columnbuttons":"",
  "filters":"",
  "extended":{
    "migx_add":"",
    "disable_add_item":"",
    "add_items_directly":"",
    "formcaption":"",
    "update_win_title":"",
    "win_id":"",
    "maxRecords":"",
    "addNewItemAt":"bottom",
    "media_source_id":"",
    "multiple_formtabs":"",
    "multiple_formtabs_label":"",
    "multiple_formtabs_field":"",
    "multiple_formtabs_optionstext":"",
    "multiple_formtabs_optionsvalue":"",
    "actionbuttonsperrow":4,
    "winbuttonslist":"",
    "extrahandlers":"",
    "filtersperrow":4,
    "packageName":"",
    "classname":"",
    "task":"",
    "getlistsort":"",
    "getlistsortdir":"",
    "sortconfig":"",
    "gridpagesize":"",
    "use_custom_prefix":"0",
    "prefix":"",
    "grid":"",
    "gridload_mode":1,
    "check_resid":1,
    "check_resid_TV":"",
    "join_alias":"",
    "has_jointable":"yes",
    "getlistwhere":"",
    "joins":"",
    "hooksnippets":"",
    "cmpmaincaption":"",
    "cmptabcaption":"",
    "cmptabdescription":"",
    "cmptabcontroller":"",
    "winbuttons":"",
    "onsubmitsuccess":"",
    "submitparams":""
  },
  "permissions":"",
  "fieldpermissions":"",
  "columns":[
    {
      "MIGX_id":1,
      "header":"\u041f\u0440\u043e\u043c\u043e\u043a\u043e\u0434",
      "dataIndex":"code",
      "width":"",
      "sortable":"false",
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":"this.textEditor"
    },
    {
      "MIGX_id":2,
      "header":"\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0438\u0439",
      "dataIndex":"count",
      "width":"",
      "sortable":"false",
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":"this.textEditor"
    },
    {
      "MIGX_id":3,
      "header":"\u0414\u0430\u0442\u0430 \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f",
      "dataIndex":"date",
      "width":"",
      "sortable":"false",
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"this.renderDate",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":"this.textEditor"
    },
    {
      "MIGX_id":4,
      "header":"\u041f\u0440\u043e\u0446\u0435\u043d\u0442 \u0441\u043a\u0438\u0434\u043a\u0438",
      "dataIndex":"percent",
      "width":"",
      "sortable":"false",
      "show_in_grid":1,
      "customrenderer":"",
      "renderer":"",
      "clickaction":"",
      "selectorconfig":"",
      "renderchunktpl":"",
      "renderoptions":"",
      "editor":"this.textEditor"
    }
  ],
  "category":""
}

Теперь у ресурса с id=1 создадим поле типа migx с названием promocodes и установим для него только что созданную конфигурацию.

Затем напишем сниппет validatePromocode, который будет проверять валидность введенного промокода
<?php
if(!$value){return false;}
$res = $modx->getObject('modResource', 1); // получаем ресурс, у которого есть поле с промокодами
$allCodes = json_decode($res->getTVValue('promocodes'),1); // получаем поле с промокодами
$isset = false; // флаг существования промокода 
$output = '';
    
foreach($allCodes as $key => $item){
    if($item['code'] == trim($value)){ // если текущий промокод равен введённому
        $isset = true;
        $now = time(); // текущее время в UNIX формате
        $date = strtotime($item['date']) ?: $now; // дата окончания срока действия промокода, если не указана промокод действует вечно
        if(($date - $now) < 0){ // если срок действия введённого промокода закончился
            return  'Истёк срок действия промокода.';      
        }
        if($item['count'] <= 0){ // если количество акциваций равно 0
            return  'Превышен лимит активаций.';    
        }
        if($changeCount === true){
            $allCodes[$key]['count'] = (($item['count'] - 1) > 0) ? ($item['count'] - 1) : 0; // уменьшаем количество на единицу
            $res->setTVValue('promocodes', json_encode($allCodes, JSON_UNESCAPED_UNICODE)); // сохраняем новые значения промокодов в TV
            $res->save(); // сохраняем ресурс
        }
        return array('percent'=> $item['percent'], 'value' => $value); // массив возвращаемых из плагина данных
    }
}
    
if(!$isset){ // если введённого промокода нет среди созданных админом
    return  'Несуществующий промокод.'; 
}
Далее нам понадобится плагин на событие msOnAddToOrder, в котором мы будем запускать созданный ранее сниппет и отправлять на фронт полученные данные. Я прокомментировал почти каждую строчку кода, надеюсь всё понятно.
<?php
if($key == 'promocode'){ // если ввели промокод
    $result = $modx->runSnippet('validatePromocode', array('value' => $value));
    if(!is_array($result)){ // если есть ошибки
       $modx->event->output($result);
    }
    $values = & $modx->event->returnedValues; // х.з. как это назвается, но в этот массив мы добавим наши значения
    $values['value'] = json_encode($result, JSON_UNESCAPED_UNICODE); // записываем наши значения в массив возвращаемых значений
}
Кроме этого нам нужен плагин на событие msOnBeforeCreateOrder, в котором мы будем пересчитывать стоимость заказа перед его сохранением.
<?php
if($_POST['promocode']){ // если есть промокод
    $delivery_cost =  $msOrder->get('delivery_cost'); // получаем стомость доставки
    $old_cost =  $msOrder->get('cart_cost'); // получаем старую стомость заказа
    $result = $modx->runSnippet('validatePromocode', array('value' => $_POST['promocode'], 'changeCount' => true)); // проверяем валидность промокода
    if(!is_array($result)){ // если есть ошибки
        $modx->log(1, 'PLUGIN recalcOrderCost' . $result); // выводим их в лог и больше ничего не делаем
    }
    else{ // если ошибок нет
        $percent = (int)$result['percent']; // получаем процент скидки
        $new_cost = $old_cost - $old_cost * $percent / 100; // рассчитываем новую стоимость заказа
        $msOrder->set('old_cost', $old_cost); // сохраняем старую стоимость заказа для корректной работы  msOrderDiscount
        $msOrder->set('cost', $new_cost + $delivery_cost); // устанавливаем новую стоимость заказа
        $msOrder->set('cart_cost', $new_cost); // устанавливаем новую стоимость покупок
        $msOrder->set('discount_size', $percent.'%'); // записываем размер скидки (требуется установить компонент msOrderDiscount)        
         $msOrder->set('comment',  'Применён промокод ' . strip_tags($_POST['promocode'])); // устанавливаем comment с названием промокода в заказ
    }
}
Осталось немного дополнить стандартный JavaScript. Для этого копируем assets/components/minishop2/js/web/default.js, новый файл называем assets/components/minishop2/js/web/custom.js. Меняем системную настройку ms2_frontend_js на [[+jsUrl]]web/custom.js. Затем открываем custom.js на редактирование и добавляем следующий код
//ПРОМОКОДЫ
        
        // функция получения cookie
        function getCookie(name) {
            let matches = document.cookie.match(new RegExp(
                "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
            ));
            return matches ? decodeURIComponent(matches[1]) : undefined;
        }

        // функция очистки cookie от данных о промокоде
        function clearPromocodeCookie() {
            recalcOrderCost(getCookie('old_cost'), 0); // пересчитываем стоимость заказа
            document.querySelector('.jsDiscount').classList.add('d-none'); // скрываем блок с сообщением о скидке
            // очищаем куки
            document.cookie = 'discount=';
            document.cookie = 'old_cost=';
            document.cookie = 'promocode=';
            document.cookie = 'new_cost=';
        }

        // функция пересчёта стоимости заказа
        function recalcOrderCost(cost, percent) {
            let newCost = cost - cost * percent / 100, // рассчитываем стоимость со скидкой
                delivery_cost = 0; // стоимость доставки
            if(document.querySelector('.jsDeliveryCost')){
                delivery_cost = Number(document.querySelector('.jsDeliveryCost').innerText.replace('/\s+/', '')); // получаем стоимость доставки если есть
            }
            document.getElementById('ms2_order_cost').innerText = miniShop2.Utils.number_format(newCost + delivery_cost, 0, '.', ' '); // форматируем цену
            document.querySelector('.jsCost').innerText =  miniShop2.Utils.number_format(newCost, 0, '.', ' '); 
            // устанавливаем куки
            document.cookie = 'old_cost=' + cost;
            document.cookie = 'new_cost=' + newCost;
        }

        miniShop2.Callbacks.add('Order.add.response.success', 'orders_add_ok', function (response) {
            if (response.data.promocode) { // если промокод введён
                let promocode = JSON.parse(response.data.promocode), // получаем данные из ответа сервера
                    cost = Number(document.querySelector('.jsCost').dataset.cost), // получаем текущую стоимость заказа
                    percent = Number(promocode.percent); // получаем процент скидки
                
                document.querySelector('input[name="promocode"]').value = promocode.value; // прописываем промокод после проверки
                document.getElementById('discount').innerText = percent; // прописываем размер скидки по промокоду
 
                recalcOrderCost(cost, percent); // пересчитываем стоимость заказа

                document.cookie = 'discount=' + percent; // записываем % в куки
                document.cookie = 'promocode=' + promocode.value; // записываем промокод в куки
                document.querySelector('.jsDiscount').classList.remove('d-none'); // показываем блок с сообщением о скидке
            }
        });

        miniShop2.Callbacks.add('Order.getcost.response.success', 'order_getcost', function (response) {
            if (getCookie('promocode')) { // если промокод есть в куках
                let cost = Number(response.data.cart_cost), // получаем текущую стоимость заказа
                    delivery_cost = Number(response.data.delivery_cost), // получаем стоимость доставки
                    percent = Number(getCookie('discount')); // получаем процент скидки из куков
                recalcOrderCost(cost, percent); // пересчитываем стоимость заказа
            }
        });

        miniShop2.Callbacks.add('Order.submit.response.success', 'clear_cookie', function (response) {
            clearPromocodeCookie(); // очищаем куки
        });

        if (document.querySelector('input[name="promocode"]')) {
            document.querySelector('input[name="promocode"]').addEventListener('change', function (e) {
                if (!e.target.value) { // если промокод удалили
                    clearPromocodeCookie(); // пересчитываем стоимость заказа
                }
            });
        }
        
    });
Код подробно прокомментирован, поэтому надеюсь вопрос не возникнет.
В чанк оформления заказа добавляем поле для ввода промокода
<input type="text" placeholder="Промокод" name="promocode" value="{$.cookie.promocode}" class="input">

И блок для вывода информации о скидке
<h5 class="mb-md-0 text-red jsDiscount {!$.cookie.discount ? 'd-none': ''}">
    Скидка:  
    <span id="discount">{$.cookie.discount ?: 0}</span> 
    <small>%</small>
</h5>
Блок вывода стоимости заказа тоже немного изменим
{set $orderCost = $.cookie.new_cost ?: $order.cart_cost}          
        <h3>
            <span class="jsCost" id="ms2_order_cart_cost" data-cost="{$order.cart_cost | replace: ' ' : ''}">{$orderCost ?: 0}</span> {'ms2_frontend_currency' | lexicon} +
            <span class="jsDeliveryCost" id="ms2_order_delivery_cost">{$order.delivery_cost ?: 0}</span> {'ms2_frontend_currency' | lexicon} =
            <span id="ms2_order_cost">{$order.cost ?: 0}</span> {'ms2_frontend_currency' | lexicon}
        </h3>
Функционал конечно простой, скидка может быть указана только в процентах, но зато бесплатно.
На этом всё!
Артур
25 мая 2021, 14:37
modx.pro
1
272
-3
Поблагодарить автора Отправить деньги

Комментарии: 23

Александр Мельник
25 мая 2021, 21:24
0
А почему кто-то минусует? Можно услышать аргументацию?
    Артур
    25 мая 2021, 21:37
    0
    Уверяю тебя, у Павлика богатая фантазия, он всегда там находит убедительные аргументы в пользу своего мнения:-) С этим бессмысленно бороться.
      Евгений Шеронов
      25 мая 2021, 22:56
      +2
      Минус ставить не буду, так как видно, что за этим стоит большая работа и желание поделиться.

      По сути проблемы что здесь, что в других топиках от недостатка опыта.

      Чтобы пользователь оформил бесплатный заказ ему достаточно поставить себе одну куку с отрицательной стоимостью доставки)

      Куки для таких данных не желательно использовать. Уж лучше писать в сессию и всем управлять на сервере.
        Артур
        25 мая 2021, 23:03
        0
        Спасибо за комментарий. Я действительно не подумал про возможность установить куки вручную и таким образом оформить бесплатный заказ, буду иметь в виду.
          Артур
          26 мая 2021, 20:48
          0
          И ещё я тут вспомнил, почему куки решил использовать, из плагина minishop2 достаточно проблематично вернуть данные, т.е. конечно можно, но выглядит некрасиво. А куки… даже если кто-то установит их вручную и оформит заказ со 100% скидкой и что? Он его всё равно не получит))) Так что, за что вы мне тут минусов понаставили даже ума не приложу.
            Aleksandr Huz
            26 мая 2021, 22:00
            0
            Как вариант за большую «простынь кода», лучше залить его на гит и давать ссылки на код.
              Артур
              26 мая 2021, 22:05
              0
              Ну как вариант. Правда я сам новичок и пишу код здесь для таких же новичков, которым с гитом может быть пока сложно работать. А ещё простыню я прячу под cut.
      Артем
      26 мая 2021, 22:59
      +1
      $msOrder->set('cost', $_COOKIE['new_cost'] + $delivery_cost);
      Не, ну это не то что выстрел в ногу, это самоподрыв уже какой-то получается.
      Исправляй эту херню, пока тебя тут не сожрали.
        Артур
        26 мая 2021, 23:04
        0
        Упс! Реальный косяк. Исправлю, если конечно правильно уловил мысль, надо перед $_COOKIE['new_cost'] хотя бы приведение к float поставить, да?
          Артем
          26 мая 2021, 23:49
          0
          Тебя не смущает, что пользователь сам устанавливает цену своего заказа?
          Или тебя волнует только то, что он может прислать не число?)
            Артур
            27 мая 2021, 06:54
            0
            Ты про то, что новая цена в куках с фронта передаётся? Ну в общем да, пожалуй стоит переписать, я один хрен там весь список получаю и в цикле его перебираю.
              Артур
              27 мая 2021, 07:16
              0
              Думаю стоит вынести проверку промокода в отдельный сниппет, и вызывать в обоих плагинах. Ну и с фронта брать только сам код, остальное буду рассчитывать в том же сниппите.
            Артур
            27 мая 2021, 10:12
            0
            Новый вариант лучше? Теперь вроде как без разницы что там в куках придёт)))
              Николай Савин
              27 мая 2021, 10:37
              +2
              Новый вариант лучше? Теперь вроде как без разницы что там в куках придёт)))
              Артур без разницы. У тебя проблема изначально в неверной архитектуре.
              Ресурсы предназначены для страниц, то есть для того, у чего есть адрес, для того что выводится посетителям на фронте.
              Не нужно пихать в них любые служебные настройки, просто потому что это просто и быстро.
              Разберись с тем как создать отдельную табличку (или набор табличек), как написать модель для таблички, чтобы MODX работал с ней как с родной и храни там данные. Для этого есть готовый инструмент modExtra.
              Вот честно слово — ты больше пользы принесешь и себе и другим малоопытным посетителям этого форума, если разберешься в этом и напишешь простым языком пару статей об этом.

              из плагина minishop2 достаточно проблематично вернуть данные
              Здесь тоже самое. Ты не прав. Писать велосипеды, костылить и говорить, что есть какая то проблема просто потому что ты не разобрался как такое делать — это так себе занятие. Тоже можешь разобраться и написать исчерпывающую статью для коллег. На форуме материала достаточно. Если что то не получается и не знаешь как сделать — спроси.
                Артур
                27 мая 2021, 11:50
                0
                Я знаю как сделать табличку и работать с ней, правда не через modExtra, а через CMPGenerator и уже писал об этом. И я знаю, что ресурсы они для фронта. Проблема в том, что если делать табличку в БД, то для её заполнения нужен интерфейс в админке, а для этого надо разобраться как этот интерфейс сделать на ExtJs, и это уже ни фига не для новичков. И в ресурс я засунул не служебные настройки, а как раз интерфейс — табличку с промокодами. Я могу предположить, что тот же MIGX может работать с таблицами, но как он это делает я не знаю и информацию об этом нигде не встречал.

                По поводу возврата данных из плагина
                $values = & $modx->event->returnedValues; 
                $values['value'] = json_encode($result, JSON_UNESCAPED_UNICODE);
                Мне кажется это не самый правильный способ вернуть массив из плагина, а по-другому никак, потому что в minishop2 вот так
                $validated = $this->validate($key, $value);
                            if ($validated !== false) {
                                $this->order[$key] = $validated;
                                $response = $this->ms2->invokeEvent('msOnAddToOrder', array(
                                    'key' => $key,
                                    'value' => $validated,
                                    'order' => $this,
                                ));
                               
                                if (!$response['success']) {
                                    return $this->error($response['message']);
                                }
                                $validated = $response['data']['value'];
                Хотя так сходу не скажу как я бы это реализовал.
                И нет, я не говорю, что это супер-пупер решение и вы все неправы, наоборот призываю всех использовать готовые компоненты, если есть такая возможность, а нищим выбирать не приходится)))
                  Николай Савин
                  27 мая 2021, 12:08
                  +1
                  для её заполнения нужен интерфейс в админке, а для этого надо разобраться как этот интерфейс сделать на ExtJs
                  В том же modExtra есть пример такого интерфейса, который при минимальных знаниях JS можно скорректировать, изменив поля на свои.
                  На следующем шаге можно вспомнить какие из знакомых тебе компонентов выглядят похоже и посмотреть исходники. Ну например как сделаны таблички минишопа. Подсмотреть исходный код, добавить его себе в modExtra компонент и вуаля — у тебя есть готовый интерфейс.

                  И в ресурс я засунул не служебные настройки, а как раз интерфейс — табличку с промокодами.
                  Все равно мусоришь ненужными в этом месте данными. Ты во первых занимаешь ресурсы базы данных лишней записью. Во вторых когда выбираешь свою информацию с промокодами получаешь кучу не нужных полей, что так же сказывается на памяти.

                  Теперь подумай — а если у тебя, или того кто повторит твое решение будет несколько тысяч промо-кодов — это тебе судя по коду их все нужно вытащить в память, в цикле перебрать и каждый проверить на соответствие. Тогда как при хранении их в табличке ты одним запросом вытащишь нужный тебе промо-код.

                  По поводу возврата данных из плагина
                  Слушай, ну а кто сказал что данные нужно вообще именно из плагина возвращать?
                  Может быть проще расширить нужный класс минишопа и дописать дополнительный метод.
                    Артур
                    27 мая 2021, 12:30
                    0
                    У меня сейчас к великому моему сожалению нет времени разбираться в исходниках других компонентов, но как я сказал раньше, в планах на ближайшее будущее это есть.
                    И если кто-то будет использовать это решение для тысяч промокодов, я ему сочувствую, даже моих небольших знаний хватит, чтобы понять, что данное решение для небольшого количества промокодов.
                      Николай Савин
                      27 мая 2021, 12:34
                      +3
                      А писать статьи каждую неделю, которые все без исключения хейтят у тебя значит время есть. Ок
                        Артур
                        27 мая 2021, 12:36
                        0
                        Это занимает совсем немного времени, его недостаточно для того чтобы разобраться в написании компонентов. Чтобы понять как там всё устроено, надо потратить не один день.
                      Артур
                      27 мая 2021, 12:34
                      0
                      Слушай, ну а кто сказал что данные нужно вообще именно из плагина возвращать?
                      Может быть проще расширить нужный класс минишопа и дописать дополнительный метод.
                      Никто не говорил, я сам так решил, как раз чтобы класс не расширять. Я понимаю, что можно расширить, но это сложнее, чем мой вариант.
                    Артур
                    27 мая 2021, 11:54
                    0
                    И в скором времени я освою написание полноценных компонентов и порадую вас ими)))
                      Николай Савин
                      27 мая 2021, 11:55
                      0
                      Звучит как угроза честно говоря
                        Артур
                        27 мая 2021, 12:09
                        0
                        Мне кажется отдельные личности знатно так угорают над моим кодом, что же будет когда я допы начну клепать)))
                Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
                23