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

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

Шаг 3. Плагин интерфейса
Создаем плагин в MODX, выбираем событие OnManagerPageInit. Он находит существующий fieldset стоимости доставки, внедряет под него кастомный блок разметки, а также отправляет асинхронный AJAX-запрос для вывода примененных расчетов и комментария из таблицы пользователя.
Администратор заходит в профиль клиента и пишет в системное поле «Комментарий» пометку: Скидка 10%/оставляет хорошие чаевые/классный человек и пр. Клиент об этом не знает. При открытии любого заказа этого пользователя в шапке окна (снизу от его имени) появится оранжевый бейдж с текстом из комментария.
Менеджер вводит нужный процент (от 1 до 10) в блоке под стоимостью доставки и нажимает «Применить». Скрипт фиксирует исходную цену, рассчитывает скидку, перезаписывает cost и выводит зеленый текст с логом расчетов.
Если скидку нужно отменить, менеджер вводит 0, нажимает «Применить» — стоимость заказа возвращается к первоначальному доскидочному значению
Готово!
Человеческий фактор:
Если менеджер ошибся или скидку нужно изменить, исходная стоимость заказа теряется. Повторный пересчет идет от уже уменьшенной суммы.
Скрытые пометки:
Менеджеру нужно видеть статус клиента (например, «Постоянный покупатель 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, нажимает «Применить» — стоимость заказа возвращается к первоначальному доскидочному значению
Готово!
Техническая поддержка MODX
Сайт лежит, тормозит или остался без разработчика?
Переезд с MODX 2 на 3, PHP 7 на 8, скорость и безопасность. Поддержка со сроками и ответственностью, а не совет в чате.
Подробнее
Реклама
0
Комментарии: 0