Skip to content
  1. Компоненты
  2. MiniShop3
  3. Разработка
  4. Backend API
  5. API заказа

API заказа ​

Заказ собирается из четырёх моделей:

  • msOrder: стоимость, статус, доставка, оплата
  • msOrderAddress: адрес и контакты
  • msOrderProduct: позиции
  • msOrderLog: журнал изменений

Жизненный цикл: черновик (draft) → оформление (submit) → смена статусов.

Контроллер Order (фасад) ​

Контроллер Order ($ms3->order) — точка входа к сервисам заказа из PHP.

Результат приходит в конверте

Методы контроллера, которые возвращают array, отдают конверт ['success' => bool, 'message' => string, 'data' => array]. Это тот же Utils::success() / Utils::error(), что и в Web API. Нужные значения лежат в data. У get() поля заказа вложены ещё на уровень: data['order'].

Значение напрямую отдают только: initialize(), initDraft(), remove() и hasPayment() (bool); getDraft() (объект msOrder или null); getUserId() (int); getNewOrderNum() (string).

Особый случай — restrictDraft(): он объявлен как void. Метод пересчитывает черновик (OrderDraftManager::recalculate()), и об успехе судят не по возвращаемому значению, а по состоянию заказа.

php
$ms3 = $modx->services->get('ms3');

// Получить данные текущего заказа
$response = $ms3->order->get();
$order = $response['data']['order'];

// Добавить поле
$result = $ms3->order->add('email', 'user@example.com');

// Установить несколько полей
$result = $ms3->order->set([
    'email' => 'user@example.com',
    'phone' => '+79991234567',
    'first_name' => 'Иван',
    'delivery_id' => 1,
    'payment_id' => 1,
    'order_comment' => 'Позвоните перед доставкой',
]);

// Оформить заказ
$result = $ms3->order->submit();
// Ответ оплаты. У DefaultPayment в data есть order_id, order_num, payment_link.
// Ключ редиректа — redirect, в query msorder лежит uuid заказа.

// Очистить черновик
$ms3->order->clean();

Инициализация

До первого вызова контроллеру нужен токен покупателя:

php
$ms3->order->initialize($token);
$ms3->order->initDraft();

В REST API и сниппетах это происходит автоматически.

Методы контроллера ​

МетодОписание
initialize($token, $config)Инициализация с токеном покупателя
initDraft()Загрузка существующего черновика
get()Данные заказа (поля + адрес)
add($key, $value)Добавить/обновить поле
set($fields)Установить несколько полей
remove($key)Удалить поле (сбросить в null)
validate($key, $value)Валидация значения поля
submit($data)Оформить заказ
clean()Очистить черновик
getCost($onlyCost)Полная стоимость (корзина + доставка + оплата)
getCartCost()Стоимость корзины
getDeliveryCost()Стоимость доставки
getPaymentCost()Комиссия оплаты
setCustomerAddress($hash)Применить сохранённый адрес покупателя
cleanCustomerAddress()Очистить адресные поля
getDeliveryValidationRules($deliveryId)Правила валидации доставки
getDeliveryRequiresFields($deliveryId)Обязательные поля доставки. Имя не опечатка: у контроллера и Web API метод называется Requires, а у OrderFieldManager — getDeliveryRequiredFields()
getDraft()Объект черновика msOrder
hasPayment($deliveryId, $paymentId)Допустима ли пара «доставка и оплата»
getUserId()ID пользователя MODX для текущего заказа. Если его нет, регистрирует
getNewOrderNum()Устарел. Показывает следующий номер, не занимая его; при оформлении номер выдаёт OrderNumberGenerator::runWithNextNumber()
restrictDraft($draft)Пересчитать стоимость черновика через OrderDraftManager::recalculate()

Черновики (OrderDraftManager) ​

Черновик: объект msOrder в статусе draft. Появляется при первом обращении покупателя к корзине и хранит данные до оформления.

php
$draftManager = $modx->services->get('ms3_order_draft_manager');

// Получить или создать черновик
$draft = $draftManager->getOrCreateDraft($token, 'web');

// Получить существующий черновик (без создания)
$draft = $draftManager->getDraft($token, 'web');

// Получить черновик по ID клиента (для восстановления после логина)
$draft = $draftManager->getDraftByCustomer($customerId, 'web');

// Данные черновика как массив (заказ + адрес)
$data = $draftManager->toArray($draft);
// Адресные поля возвращаются с префиксом address_:
// ['email' => '...', 'address_city' => 'Москва', 'address_street' => '...']

// Обновить одно поле
$draftManager->updateField($draft, 'order_comment', 'Позвоните перед доставкой');
// С 1.14.2-beta1 ключ пишется только в одну таблицу: заказ приоритетнее
// адреса. Для общих колонок (id, createdon, updatedon, properties) адрес
// больше не обновляется — properties адреса ставьте напрямую:
// $draft->Address->set('properties', [...]); $draft->Address->save();

// Привязать клиента к черновику
$draftManager->attachCustomer($draft, $customerId);

// Пересчитать стоимость
$draftManager->recalculate($draft);

// Установить стоимость доставки
$draftManager->setDeliveryCost($draft, 350.00);

// Проверить, пуста ли корзина
if ($draftManager->isEmpty($draft)) {
    // Нет товаров
}

// Очистить все поля
$draftManager->clean($draft);

// Удалить черновик полностью (с товарами и адресом)
$draftManager->deleteDraft($draft);

Остальные публичные методы:

МетодОписание
findDraftByToken($token, $ctx)Черновик по токену, без создания
createDraft($token, $ctx)Новый черновик
ensureAddress($draft)Адрес черновика, создаёт запись, если её нет
syncToken($draft, $newToken)Записать новый токен в черновик
getSessionCustomerId()ID покупателя из сессии
bindDraftToCustomer($token, $customerId, $ctx)Привязать черновик токена к покупателю
transferDraftToToken($fromToken, $toToken, $customerId, $ctx)Перенести черновик на новый токен после смены сессии
fillAddressFromArray($order, $addressData)Заполнить адрес из массива
addProductFromSnapshot($order, $snapshot, $origin)Добавить позицию из снимка, без ресурса товара

Поля и валидация (OrderFieldManager) ​

OrderFieldManager пишет поля черновика и вызывает события при изменении.

Служебные поля через add() не записать

С 1.14.2-beta1 add() и remove() отклоняют служебные колонки черновика — те же, что закрыты для Web API (см. оформление заказа). Запрет статический: он не зависит от того, пришёл вызов по HTTP или из дополнения. Срабатывает он до события msOnBeforeAddToOrder, поэтому плагин, который раньше перехватывал запись в status_id или customer_id, до события не доедет.

Служебные поля из PHP пишутся напрямую: OrderDraftManager::updateField(), attachCustomer(), setDeliveryCost() — они через фильтр не идут.

php
$fieldManager = $modx->services->get('ms3_order_field_manager');

// Добавить поле (с валидацией и событиями)
$result = $fieldManager->add($draft, $orderData, 'email', 'user@example.com');
// ['success' => true, 'data' => [...]]

// Удалить поле
$fieldManager->remove($draft, $orderData, 'email');

// Валидация без сохранения
$result = $fieldManager->validate($orderData, 'phone', '+79991234567');
// ['success' => true] или ['success' => false, 'message' => 'Ошибка']

Правила валидации ​

По умолчанию delivery_id и payment_id проверяет правило required|numeric. Остальные правила приходят из настроек доставки.

php
// Получить правила валидации для доставки
$rules = $fieldManager->getDeliveryValidationRules($deliveryId);
// $rules['data']['validation_rules'] — ['city' => 'required|min:2', ...]

// Обязательные поля: ключи ассоциативного массива «поле → правила»
$required = $fieldManager->getDeliveryRequiredFields($deliveryId);
// array_keys($required['data']['requires'])

// Добавить свои правила
$fieldManager->setValidationRules([
    'company_name' => 'required|min:3',
]);

Расчёт стоимости (OrderCostCalculator) ​

php
$calculator = $modx->services->get('ms3_order_cost_calculator');

// Стоимость корзины. Сумма в $result['data']['cost']
$result = $calculator->getCartCost($draft, $token);

// Стоимость доставки
$result = $calculator->getDeliveryCost($draft, $orderData, $token);

// Комиссия оплаты
$result = $calculator->getPaymentCost($draft, $orderData, $token);

// Полный итог. В data: cost, cart_cost, delivery_cost, payment_cost
// и поля статуса корзины. С аргументом onlyCost остаётся только cost.
$result = $calculator->getTotalCost($draft, $orderData, $token);

Каждый метод вызывает пару событий msOnBefore... / msOn.... Через них плагин меняет стоимость.

Пересчёт стоимости в админке — POST /api/mgr/orders/{id}/recalculate-cost ​

С 1.11.0 ManagerOrderCostRecalculator пересчитывает cart_cost, weight, delivery_cost и итоговый cost по сохранённым позициям и текущим delivery_id / payment_id. Другие поля заказа он не трогает.

Тело запроса:

json
{
  "mode": "auto",
  "manual_delivery_cost": 500.0
}

Режимы (mode):

РежимПоведение
auto (по умолчанию)Пересчитывает только для DefaultDelivery / DefaultPayment (поля price, weight_price, free_delivery_amount и проценты). Для своих обработчиков возвращает delivery_manual_required / payment_manual_required, оставляет прежнюю delivery_cost и комиссию 0. Внешние API не вызывает.
manualБерёт переданный manual_delivery_cost. Комиссию оплаты считает автоматически, по полю.
force_providerЯвно вызывает loadController() → getCost() и loadHandler() → getCost() в try/catch. Сбой доставки оставляет прежнюю delivery_cost и пишет delivery_provider_error. Сбой оплаты ставит комиссию 0, пересобирает cost и пишет payment_provider_error (#897)

Ответ повторяет данные заказа из GET /api/mgr/orders/{id} и добавляет:

json
{
  "breakdown": {
    "cart_cost": 5000.0,
    "weight": 1.5,
    "delivery_cost": 300.0,
    "payment_cost": 150.0,
    "cost": 5450.0
  },
  "warnings": ["delivery_manual_required"]
}

Итог проходит через OrderService::clampComputedTotal(). Ниже нуля он не уйдёт. Скидки и наценки доставки и оплаты (отрицательные price, см. 1.11.0) считает MiniShop3\Utils\PriceAdjustment.

Вызов из JS (карточка заказа в админке):

javascript
const response = await fetch(`/api/mgr/orders/${orderId}/recalculate-cost`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'modAuth': MODx.modxConfig.auth, // или актуальный mgr-auth заголовок
  },
  body: JSON.stringify({
    mode: 'manual',
    manual_delivery_cost: 500,
  }),
})
const json = await response.json()
// json.data.breakdown — cart_cost / delivery_cost / payment_cost / cost
// json.data.warnings — например delivery_manual_required

Через HTTP (Manager API):

bash
curl -X POST 'https://example.com/api/mgr/orders/42/recalculate-cost' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: …' \
  -d '{"mode":"auto"}'

См. также API Router.

Оформление заказа (OrderSubmitHandler) ​

OrderSubmitHandler проводит полный цикл оформления — от валидации до вызова оплаты.

php
$submitHandler = $modx->services->get('ms3_order_submit_handler');

$result = $submitHandler->submit($draft, $orderData, $token);
// Успех — массив, который вернул $msPayment->send().
// У DefaultPayment message = ms3_payment_link_created, в data:
// payment_link (URL страницы успеха, msorder = номер заказа),
// order_id, order_num.
// Если в data нет redirect, обработчик дописывает его:
// страница ms3_order_redirect_thanks_id, query msorder = uuid заказа.

Порядок действий при оформлении ​

  1. Событие msOnSubmitOrder. Поля, которые плагин вернул в data['data'], дописываются через OrderFieldManager::add(), поэтому служебные колонки отбрасываются и здесь — см. Какие поля фронтенд менять не вправе
  2. Проверка: корзина не пуста (ms3_order_err_empty)
  3. Заданы delivery_id и payment_id
  4. Способ оплаты существует и активен (ms3_order_err_payment_not_found)
  5. Выбранная пара «доставка + оплата» допустима (DeliveryService::getDeliveryPaymentPairError())
  6. Обязательные поля выбранной доставки заполнены. Поле считается заполненным, если пришло само значение, значение с префиксом address_ или ключ уже лежит в properties['_validated']
  7. Поиск или создание покупателя (msCustomer) — необязательный шаг: без email и телефона покупатель не создаётся, а оформление продолжается с данными только в msOrderAddress
  8. Опционально: создание пользователя MODX (настройка ms3_order_register_user_on_submit). В отличие от шага 7, неудача здесь прерывает оформление ошибкой ms3_err_user_nf
  9. Расчёт стоимости (корзина + доставка + оплата)
  10. Проверка остатка inventory()->assertOrderAvailable() до выдачи номера
  11. Номер и суммы пишет OrderNumberGenerator::runWithNextNumber() — выдача номера и сохранение идут под блокировкой
  12. Адрес покупателя сохраняется в адресную книгу, только если в properties есть save_address и у заказа есть customer_id
  13. События msOnBeforeCreateOrder / msOnCreateOrder
  14. Остаток проверяется ещё раз, и статус меняется на ms3_status_new (по умолчанию: 2)
  15. Вызов $msPayment->send()
  16. Если в ответе оплаты нет data.redirect, обработчик дописывает URL страницы «Спасибо» с uuid заказа

С шага 11 заказ держит номер. Что будет с номером при сбое:

Где сбойНомер заказа
Шаги 13–14, включая повторную проверку остатка перед сменой статусаСнимается через revertAllocatedNumber(), но только при включённом складском учёте
Шаги 15–16 — вызов оплаты и редиректОстаётся за заказом. Обработчик снимает лишь складской резерв (abortInventoryHold()) и отдаёт ошибку

Генерация номера заказа ​

getNewOrderNum() устарел

Метод помечен @deprecated и на каждый вызов пишет предупреждение в журнал MODX. Он только подсматривает следующий номер и не занимает его. Между чтением номера и сохранением заказа тот же номер может уйти другому заказу. Выдавать номер нужно через OrderNumberGenerator::runWithNextNumber(), где выдача и сохранение идут под одной блокировкой.

Метод оставлен для совместимости со старыми плагинами. Для показа «какой номер будет у следующего заказа» он годится, для записи — нет.

php
$num = $submitHandler->getNewOrderNum();
// Формат номера задают две настройки:
// ms3_order_format_num — формат даты (по умолчанию 'ym')
// ms3_order_format_num_separator — разделитель (по умолчанию '/')

Управление статусами (OrderStatusService) ​

php
$statusService = $modx->services->get('ms3_order_status');

// Сменить статус заказа
$result = $statusService->change($orderId, $newStatusId);
// true — успех
// string — текст ошибки из лексикона

// Сменить статус без уведомлений
$result = $statusService->change($orderId, $newStatusId, true);

Ограничения статусов ​

Свойство статусаПоведение
final = trueНельзя сменить на другой статус
fixed = trueМожно переключить только на статус с большей position

Поверх этих двух правил работает список разрешённых переходов: системная настройка ms3_order_status_transitions. Пока она пуста, действуют только final и fixed. Если список задан, переход должен быть в нём и не нарушать final и fixed.

Формат: пары «из статуса : в статус», через запятую или массивом JSON:

text
2:3,3:4,2:5
json
[[2,3],[3,4]]

Если значение настройки разобрать не удалось, блокируется любая смена статуса с ошибкой ms3_err_status_transitions_invalid. Опечатка в списке не игнорируется: она останавливает обработку заказов.

Уведомления ​

При смене статуса NotificationManager сам рассылает уведомления:

  • Покупателю: email, телефон из msCustomer или modUserProfile
  • Менеджерам: из настроек ms3_email_manager, ms3_phone_manager, ms3_telegram_manager

Позиции заказа (msOrderProduct) ​

Позиции лежат в таблице ms3_order_products.

Поля msOrderProduct ​

ПолеТипОписание
product_idintegerID товара (msProduct)
order_idintegerID заказа
product_keystringУникальный ключ позиции (напр. 123_a1b2c3d4)
namestringНазвание товара
countintegerКоличество
pricefloatЦена за единицу
weightfloatВес за единицу
costfloatСтоимость позиции (price × count)
optionsjsonВыбранные опции
propertiesjsonДополнительные свойства

Программная работа с позициями ​

php
use MiniShop3\Model\msOrder;
use MiniShop3\Model\msOrderProduct;

$order = $modx->getObject(msOrder::class, $orderId);

// Получить все позиции заказа
$products = $order->getMany('Products');
foreach ($products as $product) {
    echo $product->get('name') . ': ' . $product->get('count') . ' × ' . $product->get('price');
}

// Добавить позицию
$item = $modx->newObject(msOrderProduct::class);
$item->set('order_id', $orderId);
$item->set('product_id', $productId);
$item->set('product_key', $productId . '_' . md5(json_encode($options)));
$item->set('name', 'Товар');
$item->set('count', 2);
$item->set('price', 1500.00);
$item->set('cost', 3000.00);
$item->set('weight', 0.5);
$item->set('options', $options);
$item->save();

// Пересчитать итоги заказа после изменения позиций
$order->updateProducts();

Пересчёт итогов

После добавления, удаления или изменения позиций вызывайте $order->updateProducts(). Метод пересчитывает cart_cost, weight и cost по всем msOrderProduct заказа.

Адрес заказа (msOrderAddress) ​

У заказа один объект msOrderAddress (таблица ms3_order_addresses).

Поля msOrderAddress ​

ПолеТипОписание
order_idintegerID заказа
createdondatetimeДата создания
updatedondatetimeДата обновления
first_namestringИмя
last_namestringФамилия
phonestringТелефон
emailstringEmail
countrystringСтрана
indexstringПочтовый индекс
regionstringРегион
citystringГород
metrostringСтанция метро
streetstringУлица
buildingstringДом
entrancestringПодъезд
floorstringЭтаж
roomstringКвартира/офис
commentstringКомментарий к адресу
text_addressstringПолный адрес одной строкой
propertiesjsonДополнительные свойства

Программная работа ​

php
use MiniShop3\Model\msOrder;

$order = $modx->getObject(msOrder::class, $orderId);
$address = $order->getOne('Address');

// Чтение
echo $address->get('city');       // "Москва"
echo $address->get('street');     // "Ленина"

// Обновление
$address->set('city', 'Санкт-Петербург');
$address->save();

OrderAddressManager ​

Сервис пишет адреса черновика.

php
$addressManager = $modx->services->get('ms3_order_address_manager');

// Применить сохранённый адрес клиента к черновику
$result = $addressManager->setCustomerAddress($draft, $orderData, $addressHash);

// Очистить все адресные поля
$result = $addressManager->cleanCustomerAddress($draft, $orderData);

// Сохранить адрес заказа в адреса клиента
$savedAddress = $addressManager->saveToCustomerAddresses($customerId, $orderData);

Журнал заказа (OrderLogService) ​

Журнал пишет смену статуса, правку полей и работу с позициями.

php
$logService = $modx->services->get('ms3_order_log');

// Добавить запись в журнал
$logService->addEntry($orderId, 'status', [
    'old' => 1,
    'new' => 2,
]);

// Добавить запись о произвольном действии
$logService->addEntry($orderId, 'field', [
    'key' => 'delivery_id',
    'old_value' => 1,
    'new_value' => 2,
], true);  // visible = true (видна клиенту)

// Получить записи журнала
$entries = $logService->getEntries($orderId);
// Только видимые клиенту
$entries = $logService->getEntries($orderId, true);
// С лимитом
$entries = $logService->getEntries($orderId, false, 50);

// Прежний метод: add($orderId, $entry, $action, $visible = true)
$logService->add($orderId, ['old' => 1, 'new' => 2], 'status');

// Проверить, логируется ли действие (настройка ms3_order_log_actions)
if ($logService->shouldLog('status')) {
    // ...
}

Типы действий ​

КонстантаЗначениеОписание
msOrderLog::ACTION_STATUSstatusСмена статуса
msOrderLog::ACTION_PAYMENTpaymentИзменение оплаты
msOrderLog::ACTION_PRODUCTSproductsИзменение позиций
msOrderLog::ACTION_ADDRESSaddressИзменение адреса
msOrderLog::ACTION_FIELDfieldИзменение поля заказа

Настройка ms3_order_log_actions задаёт, какие действия попадают в журнал. По умолчанию: status,products,field,address. Значение * записывает все действия.

Финализация из админки (OrderFinalizeService) ​

OrderFinalizeService оформляет заказы, которые менеджер создал в админке.

php
use MiniShop3\Services\Order\OrderOrigin;

$finalizeService = $modx->services->get('ms3_order_finalize');

$result = $finalizeService->finalize($orderId, [
    'skip_validation' => false,
    'skip_notifications' => false,
    'create_customer' => true,
    'force_create_customer' => false,
    'origin' => OrderOrigin::MANAGER, // по умолчанию; для CRM — OrderOrigin::INTEGRATION
]);

Финализация работает с уже созданным черновиком. Платёжный сервис здесь не вызывается, в отличие от submit на фронтенде.

Параметр origin (OrderOrigin): manager (по умолчанию), storefront, integration. При manager дополнительно вызываются msOnBeforeMgrCreateOrder / msOnMgrCreateOrder. Ключ from_manager в событиях равен true только при origin=manager.

Программное создание заказа (ProgrammaticOrderService) ​

API без HTTP-сессии: для дополнений, cron и интеграций. Это не Web API. В routes/web.php отдельного эндпоинта нет.

Ключms3_programmatic_order
КлассMiniShop3\Services\Order\ProgrammaticOrderService
ЧерновикOrderDraftManager::createSessionlessDraft() (без PHP-сессии и токена корзины)
ФинализацияOrderFinalizeService::finalize(..., origin=integration)
Идемпотентностьколонка ms3_orders.idempotency_key (unique, nullable)
php
use MiniShop3\Services\Order\OrderOrigin;

$orders = $modx->services->get('ms3_programmatic_order');

$result = $orders->create([
    'idempotency_key' => 'crm-invoice-10042', // обязательно
    'products' => [
        ['product_id' => 15, 'count' => 2],
        // или снимок без ресурса:
        // ['name' => 'Услуга', 'price' => 500, 'count' => 1, 'weight' => 0, 'options' => []],
    ],
    'customer_id' => 0,
    'delivery_id' => 1,
    'payment_id' => 1,
    'address' => [
        'first_name' => 'Иван',
        'email' => 'user@example.com',
        'phone' => '+79991234567',
    ],
    'order_comment' => 'Из CRM',
    'context' => 'web',
    'origin' => OrderOrigin::INTEGRATION,
    // 'delivery_cost' => 300, // → cost_mode=manual при finalize
    // 'skip_notifications' => true,
    // 'skip_validation' => false,
    // 'properties' => ['source' => 'crm'],
]);

if (!$result['success']) {
    // Частые message:
    // ms3_order_err_idempotency_key_required
    // ms3_order_err_products_required
    // ms3_order_err_programmatic_create
    // либо ошибки finalize / валидации
    return $result;
}

// Успех: data = { order_id, uuid, num, status_id }
// Повтор того же idempotency_key:
//   уже оформленный заказ → success + ms3_order_programmatic_idempotent
//   черновик со статусом draft → повторная финализация

События: msOnBeforeCreateOrder / msOnCreateOrder с origin=integration и без from_manager. События msOnBeforeMgrCreateOrder / msOnMgrCreateOrder не вызываются.

Это не POST /api/mgr/orders: менеджер там создаёт пустой или частичный заказ в админке. И не POST /api/v1/order/submit на фронтенде: нужны токен и корзина.

Разрешение пользователей (OrderUserResolver) ​

Сервис находит или создаёт пользователя MODX по данным заказа.

php
$userResolver = $modx->services->get('ms3_order_user_resolver');

// Получить или создать MODX user_id
$userId = $userResolver->getUserId($orderData);

// Проверить существование пользователя
$user = $userResolver->checkUserExists([
    'email' => 'user@example.com',
    'phone' => '+79991234567',
]);

// Создать пользователя
$user = $userResolver->createUser([
    'email' => 'user@example.com',
    'first_name' => 'Иван',
    'last_name' => 'Иванов',
    'phone' => '+79991234567',
]);

Настройка ms3_order_user_groups задаёт группы для новых пользователей. Формат: group_id:role_id, через запятую.

Поля msOrder ​

ПолеТипПо умолчаниюОписание
user_idinteger0ID пользователя MODX
customer_idinteger0ID покупателя (msCustomer)
tokenstring—Токен сессии покупателя
uuidstring—Уникальный UUID заказа
createdondatetimenullДата создания
updatedondatetimenullДата обновления
numstringnullНомер заказа. У черновика из админки тоже null
costfloat0.0Итоговая стоимость
cart_costfloat0.0Стоимость товаров
delivery_costfloat0.0Стоимость доставки
weightfloat0.0Общий вес
status_idinteger0ID текущего статуса
delivery_idinteger0ID способа доставки
payment_idinteger0ID способа оплаты
contextstring'web'Контекст MODX
order_commentstringnullКомментарий к заказу
idempotency_keystringnullКлюч идемпотентности (программные заказы, unique)
propertiesjsonnullДополнительные свойства (в т.ч. origin для интеграций)

Связи msOrder ​

СвязьМодельТипОписание
AddressmsOrderAddresscomposite (one)Адрес заказа
ProductsmsOrderProductcomposite (many)Позиции заказа
LogmsOrderLogcomposite (many)Журнал изменений
msCustomermsCustomeraggregateПокупатель. Алиас именно такой, getOne('Customer') вернёт null
StatusmsOrderStatusaggregateСтатус
DeliverymsDeliveryaggregateСпособ доставки
PaymentmsPaymentaggregateСпособ оплаты
UsermodUseraggregateПользователь MODX

Composite vs Aggregate

Composite-связи удаляются каскадно при удалении заказа (адрес, позиции, журнал). Aggregate-связи только ссылаются на объекты и не удаляют их.

События ​

СобытиеКогда вызываетсяКто вызывает
msOnBeforeSaveOrder / msOnSaveOrderСохранение заказа — любое, включая сохранение черновикаmsOrder (модель)
msOnBeforeRemoveOrder / msOnRemoveOrderУдаление заказаmsOrder (модель)
msOnBeforeGetOrderCost / msOnGetOrderCostСборка итога заказаOrderCostCalculator
msOnBeforeGetCartCost / msOnGetCartCostРасчёт стоимости корзиныOrderCostCalculator
msOnBeforeGetDeliveryCost / msOnGetDeliveryCostРасчёт стоимости доставкиOrderCostCalculator
msOnBeforeGetPaymentCost / msOnGetPaymentCostРасчёт комиссии оплатыOrderCostCalculator
msOnBeforeAddToOrder / msOnAddToOrderДобавление/изменение поляOrderFieldManager
msOnBeforeRemoveFromOrder / msOnRemoveFromOrderУдаление поляOrderFieldManager
msOnBeforeValidateOrderValue / msOnValidateOrderValueВалидация значенияOrderFieldManager
msOnErrorValidateOrderValueОшибка валидацииOrderFieldManager
msOnSubmitOrderНачало оформленияOrderSubmitHandler
msOnBeforeCreateOrder / msOnCreateOrderСоздание заказа — все три источника, см. примечание нижеOrderSubmitHandler, OrderFinalizeService
msOnBeforeCreateOrderProduct / msOnCreateOrderProductДобавление позиции в заказManagerOrderProductsService (админка), OrderDraftManager (программное создание)
msOnBeforeUpdateOrderProduct / msOnUpdateOrderProductИзменение позиции — только в админкеManagerOrderProductsService
msOnBeforeRemoveOrderProduct / msOnRemoveOrderProductУдаление позиции — только в админкеManagerOrderProductsService
msOnBeforeEmptyOrder / msOnEmptyOrderОчистка заказаOrderDraftManager
msOnBeforeChangeOrderStatus / msOnChangeOrderStatusСмена статусаOrderStatusService
msOnBeforeGetOrderUser / msOnGetOrderUserПоиск/создание пользователяOrderUserResolver
msOnBeforeMgrCreateOrder / msOnMgrCreateOrderФинализация из админки (только origin=manager)OrderFinalizeService

Покупатель работает с корзиной, а не с позициями заказа. Поэтому ни одно из шести событий позиции на фронтенде не возникает — даже при добавлении товара в корзину, хотя физически там создаётся msOrderProduct (CartItemManager). Изменения корзины ловят её собственные события: msOnBeforeAddToCart, msOnBeforeChangeInCart, msOnBeforeRemoveFromCart (см. События корзины).

Вызов из OrderDraftManager — это addProductFromSnapshot(), и приходит он только из ProgrammaticOrderService (sessionless создание заказа для интеграций и cron). В параметрах такого вызова есть дополнительный ключ origin, которого нет у вызова из админки. Подробный разбор параметров — на странице событий позиций.

Параметры msOnBeforeCreateOrder и msOnCreateOrder ​

Заказ создают два разных сервиса, и набор параметров у них не совпадает. Плагин, рассчитывающий на один источник, на другом получит null вместо ожидаемого объекта.

ИсточникПараметры
Оформление на фронтенде (OrderSubmitHandler)handler — сам обработчик, msOrder — черновик, customFields — свои поля из properties['_validated']
Финализация из админки и программное создание (OrderFinalizeService)service — сам сервис, msOrder — заказ, origin — manager или integration, from_manager — true только при origin=manager

Разбирать оба случая удобно по наличию ключа:

php
$order = $params['msOrder'] ?? null;
$origin = $params['origin'] ?? 'storefront'; // ключа нет — значит оформление с фронтенда

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

Manager REST API ​

Эндпоинты Vue-интерфейса заказов. Нужна сессия mgr. См. API Router.

МетодПутьОписание
GET/api/mgr/ordersСписок заказов
GET/api/mgr/orders/filtersЗначения фильтров грида
GET/api/mgr/orders/statsАгрегаты для фильтров и дашборда
GET/api/mgr/orders/{id}Карточка заказа
PUT/api/mgr/orders/{id}Обновление заказа
DELETE/api/mgr/orders/{id}Удаление заказа
DELETE/api/mgr/orders/bulkМассовое удаление
POST/api/mgr/ordersСоздание заказа из админки
POST/api/mgr/orders/{id}/finalizeФинализация черновика
POST/api/mgr/orders/{id}/recalculate-costПересчёт через ManagerOrderCostRecalculator
GET/api/mgr/orders/{id}/logsЖурнал заказа
GET/api/mgr/orders/{id}/productsПозиции заказа
POST/api/mgr/orders/{id}/productsДобавить позицию
PUT/api/mgr/orders/{id}/products/{product_id}Изменить позицию
DELETE/api/mgr/orders/{id}/products/{product_id}Удалить позицию
GET/api/mgr/orders/{id}/shipmentОтгрузка заказа
PUT/api/mgr/orders/{id}/shipmentОбновить отгрузку

Чтение списка, карточки, фильтров, статистики, позиций, журнала и отгрузки требует msorder_list. Запись, включая оба DELETE, требует msorder_save. Отдельного msorder_remove на этих маршрутах нет.

Правка позиций сверяется с остатком

При включённом учёте остатков (ms3_inventory_enabled) добавление, изменение количества и удаление позиции приводят резерв склада к итоговому количеству по каждому товару, а остаток двигают на разницу. С 1.14.3-beta1 это делается до сохранения строки: если остатка не хватает, строка не сохраняется, резерв и остаток остаются прежними.

Отказ приходит с кодом 409 Conflict и одним из двух ключей:

КлючКогда
ms3_err_inventory_shortОстатка не хватает на запрошенное количество
ms3_err_inventory_adjust_unsupportedСкладской сервис подменён своим и не реализует AdjustableInventoryInterface, а заказ уже держит остаток

В data отказа приходят подробности, в том числе флаг can_override — есть ли у текущего пользователя право msorder_inventory_override. Интерфейс по нему решает, предлагать ли повтор операции:

json
{
  "success": false,
  "message": "Недостаточно остатка",
  "code": 409,
  "errors": null,
  "error_code": "conflict",
  "data": {
    "product_id": 15,
    "available": 3,
    "required": 10,
    "can_override": true
  }
}

Чтобы провести правку в минус, передайте в теле force: true. Если права нет, ответ будет 403 Forbidden с тем же сообщением: флаг force сам по себе ничего не разрешает. При ms3_err_inventory_adjust_unsupported значение can_override всегда false — здесь дело не в правах, а в подменённом сервисе.

POST /api/mgr/orders создаёт черновик (ms3_status_draft, запасной id 1) и события msOnBeforeMgrCreateOrder / msOnMgrCreateOrder не вызывает. Эти события вызывает POST /api/mgr/orders/{id}/finalize, когда OrderFinalizeService работает с origin=manager.

Параметры событий: События.