Кастомная система ручных скидок менеджера в админке (с фиксацией исходной цены и скрытыми метками клиентов)

Возникала необходимость вручную предоставить клиенту персональную скидку при обработке заказа в панели управления. Стандартный miniShop2 позволяет просто переписать поле «Стоимость» (cost), но у этого подхода есть две проблемы:

Человеческий фактор:
Если менеджер ошибся или скидку нужно изменить, исходная стоимость заказа теряется. Повторный пересчет идет от уже уменьшенной суммы.
Скрытые пометки:
Менеджеру нужно видеть статус клиента (например, «Постоянный покупатель 10%»), но выводить это в имя пользователя нельзя — клиент увидит пометку в системных письмах.

В этой статье мы создадим изолированную систему ручных скидок (от 1 до 20%) с защитой от перезаписи базовой цены, возможностью полной отмены скидки (сброс на 0) и выводом скрытых служебных заметок из профиля пользователя.

Шаг 1. Подготовка базы данных

Для хранения процента скидки и неприкосновенной первоначальной стоимости добавим две колонки в таблицу заказов modx_ms2_orders.
Выполните SQL-запрос

ALTER TABLE `modx_ms2_orders` 
ADD `discount_percent` INT(3) NOT NULL DEFAULT '0',
ADD `discount_original_cost` DECIMAL(15,2) NOT NULL DEFAULT '0.00';
Шаг 2. Серверный обработчик (PHP)

Создаем файл по адресу assets/components/minishop2/discount.php. Этот скрипт изолирован от ядра, работает через подготовленные выражения PDO, защищает исходную цену от изменений при повторных пересчетах, а при отправке 0 полностью откатывает заказ к исходному состоянию. Также он умеет читать скрытое поле comment из профиля покупателя.

<?php
define('MODX_API_MODE', true);
$modxRoot = '/var/www/www-root/data/www/site.ru/';
/**
обратите внимание на путь! возможно Вам подойдет конструкция ниже
$modxRoot = dirname(__DIR__, 3) . '/'; 
if (!file_exists($modxRoot . 'config.core.php')) {
    $modxRoot = $_SERVER['DOCUMENT_ROOT'] . '/';
}
**/
require_once $modxRoot . 'config.core.php';
require_once MODX_CORE_PATH . 'model/modx/modx.class.php';

$modx = new modX();
$modx->initialize('mgr');

header('Content-Type: application/json; charset=utf-8');

if (!$modx->hasPermission('save_document')) {
    http_response_code(403);
    echo json_encode(['success' => false, 'message' => 'Нет прав'], JSON_UNESCAPED_UNICODE);
    exit;
}

$orderId = isset($_POST['order_id']) ? (int)$_POST['order_id'] : 0;
$percent = isset($_POST['percent']) ? (int)$_POST['percent'] : 0;

if (!$orderId) {
    echo json_encode(['success' => false, 'message' => 'Не указан ID заказа'], JSON_UNESCAPED_UNICODE);
    exit;
}

$table = $modx->getTableName('msOrder');

$stmt = $modx->prepare("SELECT id, user_id, cost, discount_original_cost, discount_percent FROM {$table} WHERE id = ? LIMIT 1");
$stmt->execute([$orderId]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$order) {
    echo json_encode(['success' => false, 'message' => 'Заказ не найден'], JSON_UNESCAPED_UNICODE);
    exit;
}

// Извлечение комментария из профиля покупателя
$userComment = '';
if (!empty($order['user_id'])) {
    $userTable = $modx->getTableName('modUserProfile');
    $userStmt = $modx->prepare("SELECT comment FROM {$userTable} WHERE internalKey = ? LIMIT 1");
    $userStmt->execute([$order['user_id']]);
    $userProfile = $userStmt->fetch(PDO::FETCH_ASSOC);
    if ($userProfile && !empty($userProfile['comment'])) {
        $userComment = trim($userProfile['comment']);
    }
}

if ($percent === -1) {
    echo json_encode([
        'success' => true,
        'order_id' => $orderId,
        'original_cost' => (float)$order['discount_original_cost'],
        'discount_percent' => (int)$order['discount_percent'],
        'cost' => (float)$order['cost'],
        'user_comment' => $userComment
    ], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($percent < 0 || $percent > 10) {
    echo json_encode(['success' => false, 'message' => 'Скидка должна быть от 0 до 10%'], JSON_UNESCAPED_UNICODE);
    exit;
}

$originalCost = (float)$order['discount_original_cost'];
if ($originalCost <= 0) {
    $originalCost = (float)$order['cost'];
}

if ($originalCost <= 0) {
    echo json_encode(['success' => false, 'message' => 'Стоимость заказа равна 0'], JSON_UNESCAPED_UNICODE);
    exit;
}

// Если менеджер ввел 0 — сбрасываем и очищаем скидку
if ($percent === 0) {
    $newCost = $originalCost;
    $resetOriginalCost = 0;

    $update = $modx->prepare("UPDATE {$table} SET discount_original_cost = ?, discount_percent = ?, cost = ? WHERE id = ? LIMIT 1");
    $result = $update->execute([$resetOriginalCost, $percent, $newCost, $orderId]);
} else {
    $discount = round($originalCost * $percent / 100, 2);
    $newCost = round($originalCost - $discount, 2);

    $update = $modx->prepare("UPDATE {$table} SET discount_original_cost = ?, discount_percent = ?, cost = ? WHERE id = ? LIMIT 1");
    $result = $update->execute([$originalCost, $percent, $newCost, $orderId]);
}

if (!$result) {
    http_response_code(500);
    echo json_encode(['success' => false, 'message' => 'Не удалось сохранить скидку'], JSON_UNESCAPED_UNICODE);
    exit;
}

echo json_encode([
    'success' => true,
    'order_id' => $orderId,
    'original_cost' => $originalCost,
    'discount_percent' => $percent,
    'cost' => $newCost,
    'user_comment' => $userComment
], JSON_UNESCAPED_UNICODE);


Шаг 3. Плагин интерфейса

Создаем плагин в MODX, выбираем событие OnManagerPageInit. Он находит существующий fieldset стоимости доставки, внедряет под него кастомный блок разметки, а также отправляет асинхронный AJAX-запрос для вывода примененных расчетов и комментария из таблицы пользователя.

<?php
if ($modx->event->name != 'OnManagerPageInit') {
    return;
}
$modx->regClientStartupScript('
<script>
Ext.onReady(function() {
    function addDiscountField() {
        var w = Ext.getCmp("minishop2-window-order-update");
        if (!w || !w.fp) {
            return;
        }
        if (Ext.getCmp("ms-order-discount-percent")) {
            return;
        }
        var deliveryField = w.find("name", "delivery_cost")[0];
        if (!deliveryField) {
            return;
        }
        
        var orderId = w.record.id;
// здесь maxValue 20% - можно менять на свой
        var discountField = new Ext.form.NumberField({
            id: "ms-order-discount-percent",
            name: "discount_percent",
            fieldLabel: "Скидка, %",
            minValue: 0, 
            maxValue: 20,
            allowDecimals: false,
            allowNegative: false,
            width: 60,
            value: 0
        });
        
        var discountButton = new Ext.Button({
            text: "Применить скидку",
            style: "margin-left:10px;",
            handler: function() {
                var percent = parseInt(
                    discountField.getValue(),
                    10
                );
                if (isNaN(percent) || percent < 0 || percent > 10) {
                    Ext.Msg.alert(
                        "Ошибка",
                        "Введите скидку от 0 (отмена) до 10%"
                    );
                    return;
                }
                Ext.Ajax.request({
                    url: MODx.config.base_url +
                        "assets/components/minishop2/discount.php",
                    method: "POST",
                    params: {
                        order_id: orderId,
                        percent: percent
                    },
                    success: function(response) {
                        var data;
                        try {
                            data = Ext.decode(
                                response.responseText
                            );
                        } catch (e) {
                            Ext.Msg.alert(
                                "Ошибка",
                                response.responseText
                            );
                            return;
                        }
                        if (!data.success) {
                            Ext.Msg.alert(
                                "Ошибка",
                                data.message ||
                                "Ошибка применения скидки"
                            );
                            return;
                        }
                        window.location.reload();
                    },
                    failure: function(response) {
                        window.location.reload();
                    }
                });
            }
        });
        var oldFieldset = deliveryField.ownerCt.ownerCt.ownerCt;
        if (!oldFieldset || !oldFieldset.el) {
            return;
        }
        var discountFieldset = Ext.DomHelper.insertAfter(
            oldFieldset.el,
            {
                tag: "fieldset",
                id: "ms-order-discount-fieldset",
                cls: "x-fieldset",
                style:
                    "padding:15px 15px 12px;" +
                    "margin-top:10px;" +
                    "width:668px;" +
                    "box-sizing:border-box;"
            },
            true
        );
        discountFieldset.createChild({
            tag: "legend",
            html: "Скидка",
            style: "font-weight:bold;"
        });
        var discountRow = discountFieldset.createChild({
            tag: "div",
            style:
                "display:flex;" +
                "align-items:center;" +
                "height:30px;"
        });
        discountRow.createChild({
            tag: "span",
            html: "Процент скидки:",
            style:
                "display:inline-block;" +
                "margin-right:10px;" +
                "line-height:24px;"
        });
        
        discountField.render(discountRow);
        discountButton.render(discountRow);
        discountFieldset.dom.style.width = "668px";

        Ext.Ajax.request({
            url: MODx.config.base_url + "assets/components/minishop2/discount.php",
            method: "POST",
            params: { order_id: orderId, percent: -1 },
            success: function(response) {
                var data;
                try { data = Ext.decode(response.responseText); } catch (e) {}
                if (!data || !data.success) return;

                // 1. Показываем примененную скидку, если она больше нуля
                if (parseInt(data.discount_percent, 10) > 0) {
                    discountField.setValue(data.discount_percent);
                    Ext.DomHelper.append(discountRow, {
                        tag: "span",
                        html: "Применено: " + data.discount_percent + "% (Оригинальная сумма: " + data.original_cost + " руб.)",
                        style: "display:inline-block; margin-left:15px; color:#23a127; font-weight:bold; line-height:24px;"
                    });
                } else {
                    discountField.setValue(0);
                }

                // 2. Добавляем оранжевую плашку комментария рядом с пользователем
                if (data.user_comment && data.user_comment.length > 0) {
                    var userField = w.find("name", "user_id")[0];
                    if (userField && userField.ownerCt && userField.ownerCt.el) {
                        userField.ownerCt.el.setStyle("display", "flex");
                        userField.ownerCt.el.setStyle("align-items", "center");
                        Ext.DomHelper.append(userField.ownerCt.el, {
                            tag: "span",
                            html: data.user_comment,
                            style: "display:inline-block; margin-left:15px; padding:3px 10px; background:#e67e22; color:#fff; font-weight:bold; border-radius:3px; font-size:12px; line-height:16px; white-space:nowrap;position: absolute;bottom: -25px;left: -15px;"
                        });
                    }
                }
            }
        });
    }
    var task = new Ext.util.DelayedTask(function() {
        addDiscountField();
    });
    task.delay(1000);
    Ext.getBody().on("click", function() {
        task.delay(300);
    });
});
</script>
');
Как это работает для менеджера

Администратор заходит в профиль клиента и пишет в системное поле «Комментарий» пометку: Скидка 10%/оставляет хорошие чаевые/классный человек и пр. Клиент об этом не знает. При открытии любого заказа этого пользователя в шапке окна (снизу от его имени) появится оранжевый бейдж с текстом из комментария.

Менеджер вводит нужный процент (от 1 до 10) в блоке под стоимостью доставки и нажимает «Применить». Скрипт фиксирует исходную цену, рассчитывает скидку, перезаписывает cost и выводит зеленый текст с логом расчетов.
Если скидку нужно отменить, менеджер вводит 0, нажимает «Применить» — стоимость заказа возвращается к первоначальному доскидочному значению

Готово!
varanika
varanika
4 часа назад
modx.pro
24

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

Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
0