Skip to content
  1. Extras
  2. MiniShop3
  3. Development
  4. Backend API
  5. Order API

Order API

Programmatic interface for working with MiniShop3 orders from PHP.

An order in MiniShop3 is made up of several models:

  • msOrder — main order model (cost, status, delivery, payment)
  • msOrderAddress — order address and contact data
  • msOrderProduct — order lines (products)
  • msOrderLog — order change log

Order lifecycle: draftsubmitstatus changes.

Order controller (facade)

The main way to work with orders from PHP is the facade controller Order. It delegates to dedicated services but exposes a single interface.

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

// Get current order data
$data = $ms3->order->get();

// Add field
$result = $ms3->order->add('email', 'user@example.com');

// Set multiple fields
$result = $ms3->order->set([
    'email' => 'user@example.com',
    'phone' => '+79991234567',
    'first_name' => 'John',
    'delivery_id' => 1,
    'payment_id' => 1,
    'order_comment' => 'Call before delivery',
]);

// Submit order
$result = $ms3->order->submit();
// ['success' => true, 'data' => ['order_id' => 15, 'order_num' => '26/02-15', ...]]

// Clear draft
$ms3->order->clean();

Initialization

Before using the controller you must initialize with the client token:

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

In REST API and snippet context this is done automatically.

Controller methods

MethodDescription
initialize($token, $config)Initialize with client token
initDraft()Load existing draft
get()Order data (fields + address)
add($key, $value)Add/update field
set($fields)Set multiple fields
remove($key)Remove field (set to null)
validate($key, $value)Validate field value
submit($data)Submit order
clean()Clear draft
getCost($onlyCost)Total cost (cart + delivery + payment)
getCartCost()Cart cost
getDeliveryCost()Delivery cost
getPaymentCost()Payment fee
setCustomerAddress($hash)Apply saved customer address
cleanCustomerAddress()Clear address fields
getDeliveryValidationRules($deliveryId)Delivery validation rules
getDeliveryRequiresFields($deliveryId)Required delivery fields
getDraft()Get draft msOrder object

Drafts (OrderDraftManager)

A draft is an msOrder with draft status. It is created on first cart interaction and holds data until submit.

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

// Get or create draft
$draft = $draftManager->getOrCreateDraft($token, 'web');

// Get existing draft (no create)
$draft = $draftManager->getDraft($token, 'web');

// Get draft by customer ID (restore after login)
$draft = $draftManager->getDraftByCustomer($customerId, 'web');

// Draft data as array (order + address)
$data = $draftManager->toArray($draft);
// Address fields have prefix address_:
// ['email' => '...', 'address_city' => 'Moscow', 'address_street' => '...']

// Update one field
$draftManager->updateField($draft, 'order_comment', 'Call before delivery');

// Attach customer to draft
$draftManager->attachCustomer($draft, $customerId);

// Recalculate cost
$draftManager->recalculate($draft);

// Set delivery cost
$draftManager->setDeliveryCost($draft, 350.00);

// Check if cart is empty
if ($draftManager->isEmpty($draft)) {
    // No products
}

// Clear all fields
$draftManager->clean($draft);

// Delete draft (with products and address)
$draftManager->deleteDraft($draft);

Fields and validation (OrderFieldManager)

The service manages order fields, validation and fires system events on change.

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

// Add field (with validation and events)
$result = $fieldManager->add($draft, $orderData, 'email', 'user@example.com');
// ['success' => true, 'data' => [...]]

// Remove field
$fieldManager->remove($draft, $orderData, 'email');

// Validate without saving
$result = $fieldManager->validate($orderData, 'phone', '+79991234567');
// ['success' => true] or ['success' => false, 'message' => 'Error']

Validation rules

By default delivery_id and payment_id are validated as required|numeric. Extra rules come from delivery settings.

php
// Get validation rules for delivery
$rules = $fieldManager->getDeliveryValidationRules($deliveryId);
// ['city' => 'required|min:2', 'street' => 'required', ...]

// Get required fields list
$required = $fieldManager->getDeliveryRequiredFields($deliveryId);
// ['city', 'street', 'building', 'phone']

// Add custom rules
$fieldManager->setValidationRules([
    'company_name' => 'required|min:3',
]);

Cost calculation (OrderCostCalculator)

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

// Cart cost
$result = $calculator->getCartCost($draft, $token);
// ['cost' => 5000.00]

// Delivery cost
$result = $calculator->getDeliveryCost($draft, $orderData, $token);
// ['cost' => 300.00]

// Payment fee
$result = $calculator->getPaymentCost($draft, $orderData, $token);
// ['cost' => 150.00]

// Total cost
$result = $calculator->getTotalCost($draft, $orderData, $token);
// ['cost' => 5450.00, 'cart_cost' => 5000.00, 'delivery_cost' => 300.00, 'payment_cost' => 150.00]

Each method fires msOnBefore... / msOn... events so plugins can modify costs.

Manager cost recalculation — POST /api/mgr/orders/{id}/recalculate-cost

Added in 1.11.0. Service ManagerOrderCostRecalculator recalculates cart_cost, weight, delivery_cost, and total cost from saved order lines and current delivery_id / payment_id without side effects on other fields.

Request body:

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

Modes (mode):

ModeBehavior
auto (default)Recalculates only for DefaultDelivery / DefaultPayment (via price, weight_price, free_delivery_amount, percentages). For custom handlers returns warning delivery_manual_required / payment_manual_required and keeps the previous delivery_cost / fee 0 — does not call external APIs.
manualUses the provided manual_delivery_cost. Payment fee is still calculated automatically from the field.
force_providerExplicitly calls loadController()getCost() and loadHandler()getCost() in try/catch. On failure — warning delivery_provider_error / payment_provider_error, previous values kept.

Response includes order data (same as GET /api/mgr/orders/{id}) plus:

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

Applies the shared guard OrderService::clampComputedTotal() — total cannot go below zero. Delivery and payment discounts/markups (negative price, see 1.11.0) go through MiniShop3\Utils\PriceAdjustment.

Example call from JS (order card in mgr):

javascript
const response = await fetch(`/api/mgr/orders/${orderId}/recalculate-cost`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'modAuth': MODx.modxConfig.auth, // or the current mgr-auth header
  },
  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 — e.g. delivery_manual_required

Via HTTP (REST mgr API):

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

See also routing.

Order submit (OrderSubmitHandler)

OrderSubmitHandler runs the full submit flow: validation → create customer → calculate cost → generate number → change status → call payment.

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

$result = $submitHandler->submit($draft, $orderData, $token);
// Success:
// [
//     'success' => true,
//     'data' => [
//         'order_id' => 15,
//         'order_num' => '26/02-15',
//         'redirect_url' => '/thank-you?msorder=15',
//     ],
//     'message' => 'Order submitted successfully'
// ]

Submit steps

  1. Event msOnSubmitOrder
  2. Check: cart not empty
  3. Validate delivery_id, payment_id and required delivery fields
  4. Find/create customer (msCustomer)
  5. Optionally: create MODX user (setting ms3_order_register_user_on_submit)
  6. Calculate cost (cart + delivery + payment)
  7. Generate order number
  8. Save customer address
  9. Events msOnBeforeCreateOrder / msOnCreateOrder
  10. Set status to ms3_status_new (default: 2)
  11. Call $msPayment->send() — redirect to payment
  12. Return thank-you page URL

Order number generation

php
$num = $submitHandler->getNewOrderNum();
// "26/02-15" — format is configurable:
// ms3_order_format_num — date format (default 'ym')
// ms3_order_format_num_separator — separator (default '/')

Status management (OrderStatusService)

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

// Change order status
$result = $statusService->change($orderId, $newStatusId);
// true — success
// string — error message from lexicon

// Change status without notifications
$result = $statusService->change($orderId, $newStatusId, true);

Status constraints

Status propertyBehavior
final = trueCannot change to another status
fixed = trueCan only switch to status with higher position

Notifications

On status change, notifications are sent via NotificationManager:

  • To customer — email, phone from msCustomer or modUserProfile
  • To managers — from settings ms3_email_manager, ms3_phone_manager, ms3_telegram_manager

Order products (msOrderProduct)

Products in an order are stored in model msOrderProduct (table ms3_order_products).

msOrderProduct fields

FieldTypeDescription
product_idintegerProduct ID (msProduct)
order_idintegerOrder ID
product_keystringUnique line key (e.g. 123_a1b2c3d4)
namestringProduct name
countintegerQuantity
pricefloatUnit price
weightfloatUnit weight
costfloatLine cost (price × count)
optionsjsonSelected options
propertiesjsonExtra properties

Working with products in code

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

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

// Get all order products
$products = $order->getMany('Products');
foreach ($products as $product) {
    echo $product->get('name') . ': ' . $product->get('count') . ' × ' . $product->get('price');
}

// Add product
$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', 'Product');
$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();

// Recalculate order totals after changing products
$order->updateProducts();

Recalculating totals

After adding, removing or changing products call $order->updateProducts(). It recalculates cart_cost, weight and cost from all msOrderProduct.

Order address (msOrderAddress)

Each order has one related msOrderAddress (table ms3_order_addresses).

msOrderAddress fields

FieldTypeDescription
order_idintegerOrder ID
first_namestringFirst name
last_namestringLast name
phonestringPhone
emailstringEmail
countrystringCountry
indexstringPostal code
regionstringRegion
citystringCity
metrostringMetro station
streetstringStreet
buildingstringBuilding
entrancestringEntrance
floorstringFloor
roomstringApartment/office
commentstringAddress comment
text_addressstringFull address as single line
propertiesjsonExtra properties

Working with address in code

php
use MiniShop3\Model\msOrder;

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

// Read
echo $address->get('city');       // "Moscow"
echo $address->get('street');     // "Lenina"

// Update
$address->set('city', 'Saint Petersburg');
$address->save();

OrderAddressManager

Service for addresses in draft context:

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

// Apply saved customer address to draft
$result = $addressManager->setCustomerAddress($draft, $orderData, $addressHash);

// Clear all address fields
$result = $addressManager->cleanCustomerAddress($draft, $orderData);

// Save order address to customer addresses
$savedAddress = $addressManager->saveToCustomerAddresses($customerId, $orderData);

Order log (OrderLogService)

The log records all order changes: status, fields, products.

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

// Add log entry
$logService->addEntry($orderId, 'status', [
    'old' => 1,
    'new' => 2,
]);

// Add custom action entry
$logService->addEntry($orderId, 'field', [
    'key' => 'delivery_id',
    'old_value' => 1,
    'new_value' => 2,
], true);  // visible = true (shown to customer)

// Get log entries
$entries = $logService->getEntries($orderId);
// Only customer-visible
$entries = $logService->getEntries($orderId, true);
// With limit
$entries = $logService->getEntries($orderId, false, 50);

// Check if action is logged (setting ms3_order_log_actions)
if ($logService->shouldLog('status')) {
    // ...
}

Action types

ConstantValueDescription
msOrderLog::ACTION_STATUSstatusStatus change
msOrderLog::ACTION_PAYMENTpaymentPayment change
msOrderLog::ACTION_PRODUCTSproductsProducts change
msOrderLog::ACTION_ADDRESSaddressAddress change
msOrderLog::ACTION_FIELDfieldOrder field change

Setting ms3_order_log_actions controls which actions are logged (default: status,products,field,address; * to log all).

Finalize from manager (OrderFinalizeService)

OrderFinalizeService is used to finalize orders created in the manager.

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, // default; for CRM use OrderOrigin::INTEGRATION
]);

Finalize allows skip_* and works with an existing draft. There is no payment gateway call here (unlike storefront submit).

The origin parameter (OrderOrigin): manager (default), storefront, integration. With manager, msOnBeforeMgrCreateOrder / msOnMgrCreateOrder also fire. The from_manager key in events is true only for origin=manager.

Programmatic order creation (ProgrammaticOrderService)

API without an HTTP session for extras, cron, and integrations. This is not HTTP Web API: routes/web.php has no dedicated endpoint.

DIms3_programmatic_order
ClassMiniShop3\Services\Order\ProgrammaticOrderService
DraftOrderDraftManager::createSessionlessDraft() (no PHP session or cart token)
FinalizationOrderFinalizeService::finalize(..., origin=integration)
Idempotencycolumn 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', // required
    'products' => [
        ['product_id' => 15, 'count' => 2],
        // or a snapshot without a resource:
        // ['name' => 'Service', 'price' => 500, 'count' => 1, 'weight' => 0, 'options' => []],
    ],
    'customer_id' => 0,
    'delivery_id' => 1,
    'payment_id' => 1,
    'address' => [
        'first_name' => 'John',
        'email' => 'user@example.com',
        'phone' => '+79991234567',
    ],
    'order_comment' => 'From CRM',
    'context' => 'web',
    'origin' => OrderOrigin::INTEGRATION,
    // 'delivery_cost' => 300, // → cost_mode=manual on finalize
    // 'skip_notifications' => true,
    // 'skip_validation' => false,
    // 'properties' => ['source' => 'crm'],
]);

if (!$result['success']) {
    // Common messages:
    // ms3_order_err_idempotency_key_required
    // ms3_order_err_products_required
    // ms3_order_err_programmatic_create
    // or finalize / validation errors
    return $result;
}

// Success: data = { order_id, uuid, num, status_id }
// Same idempotency_key again:
//   already finalized order → success + ms3_order_programmatic_idempotent
//   draft with draft status → finalize again

Events: msOnBeforeCreateOrder / msOnCreateOrder with origin=integration and without from_manager. Events msOnBeforeMgrCreateOrder / msOnMgrCreateOrder are not fired.

Differs from POST /api/mgr/orders (manager creates an empty/partial order in the UI) and from storefront POST /api/v1/order/submit (needs token and cart).

User resolution (OrderUserResolver)

The service finds or creates the MODX user from order data.

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

// Get or create MODX user_id
$userId = $userResolver->getUserId($orderData);

// Check if user exists
$user = $userResolver->checkUserExists([
    'email' => 'user@example.com',
    'phone' => '+79991234567',
]);

// Create user
$user = $userResolver->createUser([
    'email' => 'user@example.com',
    'first_name' => 'John',
    'last_name' => 'Doe',
    'phone' => '+79991234567',
]);

Setting ms3_order_user_groups defines groups for new users (format: group_id:role_id, comma-separated).

msOrder fields

FieldTypeDefaultDescription
user_idinteger0MODX user ID
customer_idinteger0Customer ID (msCustomer)
tokenstringClient session token
uuidstringOrder UUID
createdondatetimenullCreated at
updatedondatetimenullUpdated at
numstring''Order number
costfloat0.0Total cost
cart_costfloat0.0Products cost
delivery_costfloat0.0Delivery cost
weightfloat0.0Total weight
status_idinteger0Current status ID
delivery_idinteger0Delivery method ID
payment_idinteger0Payment method ID
contextstring'web'MODX context
order_commentstringnullOrder comment
idempotency_keystringnullIdempotency key (programmatic orders, unique)
propertiesjsonnullExtra properties (incl. origin for integrations)

msOrder relations

RelationModelTypeDescription
AddressmsOrderAddresscomposite (one)Order address
ProductsmsOrderProductcomposite (many)Order products
LogmsOrderLogcomposite (many)Change log
CustomermsCustomeraggregateCustomer
StatusmsOrderStatusaggregateStatus
DeliverymsDeliveryaggregateDelivery method
PaymentmsPaymentaggregatePayment method
UsermodUseraggregateMODX user

Composite vs Aggregate

Composite relations are deleted with the order (address, products, log). Aggregate relations are references only; related objects are not deleted.

Events

EventWhen fired
msOnBeforeSaveOrder / msOnSaveOrderOrder save
msOnBeforeRemoveOrder / msOnRemoveOrderOrder remove
msOnBeforeGetCartCost / msOnGetCartCostCart cost calculation
msOnBeforeGetDeliveryCost / msOnGetDeliveryCostDelivery cost calculation
msOnBeforeGetPaymentCost / msOnGetPaymentCostPayment cost calculation
msOnBeforeAddToOrder / msOnAddToOrderAdd/update field
msOnBeforeRemoveFromOrder / msOnRemoveFromOrderRemove field
msOnBeforeValidateOrderValue / msOnValidateOrderValueValue validation
msOnErrorValidateOrderValueValidation error
msOnSubmitOrderStart submit
msOnBeforeCreateOrder / msOnCreateOrderOrder creation
msOnBeforeChangeOrderStatus / msOnChangeOrderStatusStatus change
msOnBeforeGetOrderUser / msOnGetOrderUserFind/create user
msOnBeforeMgrCreateOrder / msOnMgrCreateOrderFinalize from manager (only origin=manager)

Manager REST API

Endpoints for the Vue orders UI (mgr session, see routing):

MethodPathDescription
GET/api/mgr/orders/statsAggregates for filters and dashboard
POST/api/mgr/ordersCreate order from manager
POST/api/mgr/orders/{id}/finalizeFinalize draft
POST/api/mgr/orders/{id}/recalculate-costRecalculate via ManagerOrderCostRecalculator

Create and finalize from mgr go through OrderFinalizeService and events msOnBeforeMgrCreateOrder / msOnMgrCreateOrder.

Event parameter details: Events.