Usage
How a decision is made
Craft's condition builder only does AND: every condition on a rule has to pass. The list of rules is the OR.
- Every enabled rule in the store is evaluated, in list order. A rule outside its date window or not enabled for the order's site is skipped, and the trace says why.
- Block rules veto. If any block rule matches, nothing else counts — no free method, no waiver, no progress message.
- Otherwise the first matching granting rule decides what the customer is told and whether other methods are hidden.
So "free over $75, but never on the canoes, and free express over $200" is three rules, and you can read the whole policy top to bottom on one screen.
Every one of those questions is answered in one place. The shipping method, the waiver, the progress API, the simulator and the console all read the same decision, so a preview can never disagree with a checkout.
Offer a free shipping method
A matching rule adds a $0 method to the cart's shipping options, with the rule's customer-facing
name and description. Its handle is freeride-<rule handle>, which is what lands on
order.shippingMethodHandle when the customer picks it.
Several method rules can match at once — "Free Standard" and "Free Local Pickup", say. They are real alternatives, and the customer picks between them.
A completed order keeps its free method even if the rule is later edited, disabled or expires, so receipts and the order screen can still name it.
Hiding the paid methods
Turn on Hide other shipping methods and, when that rule is the first match, every other option is removed — free is the only choice. This includes methods registered by other shipping plugins.
Waive the cost of the chosen method
The customer picks a method as usual, and its cost is cancelled with a negative shipping
adjustment named after the rule. Pick Methods to waive to limit it — "free express over $200"
waives only express — or leave it empty to waive whatever they chose.
A waiver doesn't have to be the first match to apply. If a method rule matches first and a waiver rule matches further down, the customer gets the free method as an option and the waiver on the paid method they chose — so put conditions on each that mean what you intend.
The waiver is applied straight after Commerce's own shipping adjuster, before tax, so nobody is taxed on shipping they weren't charged for.
Block free shipping
A veto. Oversized items, excluded regions, clearance stock — anything that should disqualify an
order however much it spends. Typical conditions: Largest Item Dimension is greater than 48,
Cart Product Types includes any of Furniture, Shipping State / Province is one of
AK, HI.
Progress
{% set progress = craft.freeride.progress() %}
{% if progress.eligible %}
<p>{{ progress.message }}</p>
{% elseif progress.isAchievable %}
<p>{{ progress.message }}</p>
<progress value="{{ progress.percent }}" max="1"></progress>
{% endif %}
Progress only reports a distance it can stand behind. A cart is "$12 away" only when exactly one condition on a granting rule failed, and that condition is a threshold the customer can cross: a number condition on an order attribute — Item Subtotal, Discounted Item Subtotal, Total Qty, Total Weight — with is greater than or equal to, is greater than, or the minimum of is between. A cart shipping somewhere the rule excludes is not $12 away from anything, and it is told nothing. A blocked cart is told nothing, whatever it spends.
When several rules are within reach, money beats quantity beats weight, and the nearest wins.
| Property | |
|---|---|
eligible | the cart already has free shipping |
blocked | a block rule matched |
isAchievable | there is something the customer can do about it |
remainingAmount / remainingAmountFormatted | money still to spend |
remainingQty / remainingWeight | items or weight still to add |
percent | 0–1 along the nearest threshold |
message | the line to show — empty when there's nothing honest to say |
rule | the rule it's about |
Twig
| Call | Returns |
|---|---|
craft.freeride.progress(order) | the progress object above |
craft.freeride.eligible(order) | whether free shipping applies right now |
craft.freeride.message(order) | just the message |
craft.freeride.explain(order) | the whole decision, rule by rule and condition by condition |
craft.freeride.rules(storeId) | the store's enabled rules, in evaluation order |
Every argument is optional. Without one, they read the current cart.
The JSON endpoint
For carts that update without a page load, request actions/freeride/cart/progress with an
Accept: application/json header. It reads the current visitor's cart and nothing else.
const res = await fetch('/actions/freeride/cart/progress', { headers: { Accept: 'application/json' } });
{
"eligible": false, "blocked": false, "achievable": true,
"rule": "freeOver50", "ruleName": "Free over $50",
"remainingAmount": 12, "remainingAmountFormatted": "$12.00",
"remainingQty": null, "remainingWeight": null,
"percent": 0.76, "message": "Spend $12.00 more for free shipping"
}
The simulator
Free Ride → Simulator takes an order or cart number, short number, reference or ID and shows the decision: which rule matched or blocked, the shipping options the order would see with their prices, and every rule with every condition, pass or fail. From the terminal:
php craft freeride/simulate/order <number|reference|id>
Console
php craft freeride/rules # list, in evaluation order
php craft freeride/rules/export --file=rules.json # stdout without --file
php craft freeride/rules/import --file=rules.json # matching handles are updated
All three take --store=<handle>, defaulting to the current store.