Free Ride for Craft CMS

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.

  1. 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.
  2. Block rules veto. If any block rule matches, nothing else counts — no free method, no waiver, no progress message.
  3. 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
eligiblethe cart already has free shipping
blockeda block rule matched
isAchievablethere is something the customer can do about it
remainingAmount / remainingAmountFormattedmoney still to spend
remainingQty / remainingWeightitems or weight still to add
percent0–1 along the nearest threshold
messagethe line to show — empty when there's nothing honest to say
rulethe rule it's about

Twig

CallReturns
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.