Skip to content
  1. Extras
  2. MiniShop3
  3. Manager interface
  4. Settings
  5. Deliveries

Delivery methods ​

Settings → Deliveries

Open Extras → MiniShop3 → Settings → Deliveries.

Delivery fields ​

FieldTypeDescription
namestringDelivery method name
descriptiontextDescription for the customer
pricestringDelivery surcharge: a fixed amount, a percent of the cart total (5%), or a negative value acting as a discount. The allowed percent runs from −100 to 100; anything else zeroes the surcharge and is logged. The interface labels the field «Additional cost». Stored as varchar(11)
weight_pricefloatCost per unit of weight
distance_pricefloatCost per unit of distance. The calculation does not use it now (reserved field)
free_delivery_amountfloatOrder total for free delivery
logostringPath to the image
positiontinyint unsignedSort order. The model map declares the column as tinyint(1) unsigned, so the ceiling is 255 rows. Payment methods carry the same column as int — a historical inconsistency
activeboolActive flag
classstringPHP handler class
validation_rulesJSONField validation rules
propertiesJSONJSON properties for the handler's own data

The delivery dialog ​

The form is split into tabs:

  • Delivery — name, description, logo, active flag;
  • Settings — the handler class, the surcharge, the per-weight price, the free-delivery threshold, the validation rules;
  • Payment methods — link toggles. The tab appears only when editing: an unsaved delivery has no id to link a payment to.

Links are written the moment a toggle flips, through POST and DELETE /api/mgr/deliveries/{id}/payments, without waiting for the save button.

Payment linkage ​

You link each delivery method to the payment methods it needs.

  • Cash only for pickup
  • Online payment for courier delivery
  • Your own combinations for different regions

When the customer picks a delivery, the payment list is filtered.

Cost calculation ​

Delivery cost is calculated with this formula:

text
Total cost = weight_price × weight + (price as an amount or a percent of the cart)

The order weight is the sum of weight × count across all lines. A negative weight_price is zeroed and logged.

When the free-delivery threshold is set (free_delivery_amount above zero) and the order total reaches it, the delivery cost is 0. The field defaults to 0, so an empty threshold means free delivery never applies.

Custom calculation ​

For a more complex rule, create your own handler class. There are two contracts, and they are independent:

InterfaceMethodPurpose
DeliveryProviderInterfacegetCost()Cost calculation. The class goes into the delivery method class field
ShipmentProviderInterfaceverifyWebhook(), parseWebhook()Receiving carrier notifications: signature check and parsing the event into a ShipmentWebhookEvent

Notifications are received by the public core route POST /api/v1/delivery/webhook/{delivery_id} — no token is required there, and the handler verifies authenticity itself. A class that only calculates implements the first interface and knows nothing about webhooks.

A cost example:

php
<?php
namespace MyComponent\Delivery;

use MiniShop3\Controllers\Delivery\DeliveryProviderInterface;
use MiniShop3\Model\msDelivery;
use MiniShop3\Model\msOrder;

class CustomDelivery implements DeliveryProviderInterface
{
    public function getCost(msOrder $order, msDelivery $delivery, float $cost): float
    {
        // Your calculation
        $cartCost = $order->get('cart_cost');
        $weight = $order->get('weight');

        if ($cartCost > 10000) {
            return 0; // Free from 10000 RUB
        }

        if ($weight > 5000) {
            return 500 + ($weight - 5000) * 0.1; // Surcharge for a heavy order
        }

        return 300; // Base cost
    }
}

Set the class in the class field: MyComponent\Delivery\CustomDelivery.

Order field validation ​

For each delivery method you set the required fields and the rules. A courier needs a full address. Pickup needs a phone.

Visual builder ​

Visual mode ​

  1. Click Add field
  2. Pick a field from the list (groups: Order, Address)
  3. Add validation rules for the field
  4. For rules that take a parameter, set the value

Rules show up as tags. Click the cross to remove one.

JSON mode ​

The switch opens manual JSON editing:

json
{
  "phone": "required",
  "email": "required|email",
  "city": "required|min:2",
  "street": "required|min:3",
  "building": "required"
}

In this mode you copy rules between deliveries, set a regex, and move JSON between installs.

Custom validation fields ​

Besides the standard order and address fields, you can add your own. An agreement checkbox is one example:

json
{
  "phone": "required",
  "email": "required|email",
  "agreement": "required|accepted"
}

Fields outside the standard set (agreement and others) stay on the order draft between the order/add and order/submit steps. When the order is created they go to msOnBeforeCreateOrder / msOnCreateOrder through the customFields parameter.

Checkboxes

On the storefront, checkboxes send input.checked ('1' or '0'), not a static value attribute. That is what makes the accepted rule work.

Available validation fields ​

Order fields ​

FieldDescription
order_commentOrder comment

Address fields ​

FieldDescription
first_nameFirst name
last_nameLast name
phonePhone
emailEmail
countryCountry
indexPostal code
regionRegion
cityCity
metroMetro station
streetStreet
buildingBuilding
entranceEntrance
floorFloor
roomApartment or office
commentAddress comment
text_addressFull address as text

Validation rules ​

Validation is done by the built-in PipeRuleValidator (src/Services/Validation/PipeRuleValidator.php). No external library is required. Rules are combined with |.

Basic rules ​

RuleDescription
requiredRequired field
nullableThe field may be null
presentThe field must be present, even when empty
acceptedThe value must be "yes", "on", "1", or true

Data types ​

RuleDescription
emailValid email
urlValid URL
ipIP address (v4 or v6)
ipv4IPv4 address
ipv6IPv6 address
numericNumeric value
integerInteger
booleanBoolean
arrayArray
jsonValid JSON

String rules ​

RuleDescription
alphaLetters only
alpha_numLetters and digits
alpha_dashLetters, digits, hyphen, underscore
alpha_spacesLetters and spaces
uppercaseUppercase letters only
lowercaseLowercase letters only

Rules with parameters ​

RuleDescriptionSyntax
minMinimum string length or numeric valuemin:3
maxMaximum string length or numeric valuemax:100
betweenValue inside a rangebetween:1,10
digitsExact digit countdigits:6
digits_betweenDigit count inside a rangedigits_between:4,8
inValue from a listin:pickup,courier,post
not_inValue not in the listnot_in:test,demo
sameMatches another fieldsame:email_confirm
differentDiffers from another fielddifferent:old_password
regexMatches a regular expressionregex:/^[0-9]{6}$/
extensionFile extensionextension:jpg,png
mimesFile MIME typemimes:jpg,png

Date rules ​

RuleDescriptionSyntax
dateValid date in the given formatdate:Y-m-d
afterDate after the given oneafter:2024-01-01
beforeDate before the given onebefore:2025-12-31

Conditional rules ​

RuleDescriptionSyntax
required_ifRequired when another field equals a valuerequired_if:delivery,courier
required_unlessRequired when another field is not that valuerequired_unless:delivery,pickup
required_withRequired when another field is setrequired_with:phone
required_withoutRequired when another field is not setrequired_without:email
required_with_allRequired when all of the fields are setrequired_with_all:city,street
required_without_allRequired when none of the fields are setrequired_without_all:phone,email

Some rules have no message of their own

The checks work, but PipeRuleValidator::DEFAULT_MESSAGES has no text for after, before, digits_between and all six required_*. On failure the customer sees the generic «The :attribute has unsupported validation rule», and a text of your own cannot be set (see Error messages).

Configuration examples ​

Identical rule strings collapse

OrderFieldManager::validate() merges the sets through array_unique(array_merge(...)), and array_unique works by value. When two fields carry the same rule string, only the first survives in the set — the others lose their format checks (min, email, regex). A required field is still enforced at submit through another path.

The examples below contain such repeats; for the rules to apply to every field the strings must differ — required|min:2 and required|min:3, for instance. The defect is filed as modx-pro/MiniShop3#949.

Courier delivery ​

A full address is required:

json
{
  "first_name": "required|min:2",
  "last_name": "required|min:2",
  "phone": "required|regex:/^\\+?[0-9]{10,15}$/",
  "email": "required|email",
  "city": "required|min:2",
  "street": "required|min:3",
  "building": "required",
  "room": "required_if:building_type,apartment"
}

Pickup ​

The minimum needed to reach the customer:

json
{
  "first_name": "required|min:2",
  "phone": "required"
}

Postal delivery ​

A postal code and a full address are required:

json
{
  "first_name": "required",
  "last_name": "required",
  "phone": "required",
  "index": "required|digits:6",
  "region": "required",
  "city": "required",
  "street": "required",
  "building": "required"
}

Parcel locker ​

Contact details only:

json
{
  "first_name": "required",
  "phone": "required|regex:/^\\+?[0-9]{10,15}$/",
  "email": "required|email"
}

Fields in the builder ​

The field list in the rules builder is not static: on top of the built-in set it pulls msOrder and msOrderAddress fields from Form fields and from Extra fields. Technical columns such as id and the service timestamps are filtered out.

So a custom order field created in the utilities shows up in the builder on its own — there is nothing extra to declare.

Error messages ​

The default messages are English and do not follow the interface language. Order fields use four texts hard-coded in OrderFieldManager::validate() — for required, numeric, min and email; the rest come from PipeRuleValidator::DEFAULT_MESSAGES.

Custom messages cannot be set

setValidationMessages() and setValidationRules() are overwritten at the start of every validate() call, and the delivery properties are never read by validation: rules come only from the validation_rules column. There is no working way to replace the texts in the current code — the defect is filed as modx-pro/MiniShop3#950.

Manager API ​

The manager endpoints. The whole group is protected by the mssetting_save permission.

MethodRouteDescription
GET/api/mgr/deliveriesList. Filters filter_name and filter_active
POST/api/mgr/deliveriesCreate
GET / PUT / DELETE/api/mgr/deliveries/{id}Read, update, delete
DELETE/api/mgr/deliveries/bulkBulk delete
POST/api/mgr/deliveries/sortOrder after a drag
PUT/api/mgr/deliveries/positionsSet positions from a list
POST / DELETE/api/mgr/deliveries/{id}/paymentsLink and unlink a payment method
GET/api/mgr/deliveries-activeActive deliveries for the order form dropdowns. Returns an allowlist of fields, without properties or other secrets

Payments mirror the set: /api/mgr/payments, /sort, /positions, /bulk, /{id}/deliveries and GET /api/mgr/payments-active (which takes delivery_id to keep only the linked ones).

Web API ​

Both GET requests carry the shop token: a Bearer or MS3Token header, or the ms3_token cookie. A query parameter is not accepted — TokenMiddleware strips it from the query string. Passing a token by hand is not required though: on a route without one the server mints an anonymous token, so the request does not fail.

Rules come back only for an active delivery

The rules query carries an active = 1 condition. For a disabled delivery method both endpoints return an empty set, and that is indistinguishable from "this delivery has no rules of its own".

Get validation rules ​

http
GET /api/v1/order/delivery/validation-rules?delivery_id=1

Response:

json
{
  "success": true,
  "data": {
    "validation_rules": {
      "phone": "required",
      "city": "required|min:2",
      "street": "required"
    }
  }
}

Get required fields ​

http
GET /api/v1/order/delivery/required-fields?delivery_id=1

Response (a map of field to rules, only those that include required):

json
{
  "success": true,
  "data": {
    "requires": {
      "phone": "required",
      "city": "required",
      "street": "required"
    }
  }
}

These two requests refresh the order form when the delivery method changes.