justinholtweb / craft-weight
Weight-based shipping rates for Craft Commerce — tiered per-weight pricing, handling fees, destination and subtotal conditions, box packing and dimensional weight.
Package info
github.com/justinholtweb/craft-weight
Type:craft-plugin
pkg:composer/justinholtweb/craft-weight
Requires
- php: ^8.2
- ext-json: *
- craftcms/cms: ^5.3.0
- craftcms/commerce: ^5.0.0
README
Weight-based shipping rates for Craft Commerce 5 — tiered per-weight pricing, handling fees, destination and subtotal conditions, box packing and dimensional weight.
It is a Craft-native answer to WooCommerce's Weight Based Shipping: the same rule model store owners already know, plus the packing engine that plugin never had and a simulator that shows you every number behind a price.
What it does
Each method you create appears at checkout as its own shipping option. Behind it is an ordered list of rules, and every rule that matches contributes to the price:
Handling flat 3.50
First 2kg ≤ 2kg flat 4.50
Above 2kg > 2kg flat 4.50 + 1.20 per 0.5kg over the first 2kg
Free over 75 subtotal ≥ 75 → free
A 6kg order comes to 17.60: `3.50 + 4.50 + (6 − 2 = 4kg → 8 × 0.5kg steps @ 1.20) = 3.50 + 4.50
- 9.60`. The simulator will show you exactly that, line by line.
Conditions a rule can carry
- Weight — a range, with independent inclusivity at each end, so 5kg lands in one band and not two. Judged against the weight actually being billed: packed weight, including the box and any dimensional uplift.
- Subtotal — a range, optionally after discounts and optionally including tax.
- Quantity — a range.
- Destination — only these places, or everywhere except these places.
US,GB,US:NC,CA:ON. - Postal codes — exact codes,
902*wildcards and10000-19999numeric ranges. - Shipping categories — contains any of / only / none of.
- Anything else — Craft's own condition builder, with everything Craft and Commerce can already ask a cart, plus largest item dimension and distinct item count.
What a rule can charge
A flat handling fee, a cost per unit of weight (continuous or in whole steps, after a free allowance), a cost per item, a percentage of the subtotal, and its own minimum and maximum. There are per-shipping-category overrides too — frozen goods at a higher rate per kilo than dry goods.
What a rule can do instead of charging
- Make shipping free — the package costs nothing and no rule below it runs.
- Make this method unavailable — how "we don't ship anything over 30kg" is expressed.
- Required — the method isn't offered at all unless this rule matches.
- Stop — nothing below this rule is considered once it matches.
Packing
Tell a method how the cart becomes parcels:
- Don't pack — one notional package for the whole cart. The default, and what WooCommerce does.
- Split by maximum package weight — no parcel over the limit.
- Pack into boxes — first-fit-decreasing into the box library you define, with inner dimensions, a maximum contents weight, the empty box's own weight and a packaging surcharge.
Then tell it whether to rate the cart as a whole or rate each parcel and add the results up — which is how a per-parcel carrier price gets modelled honestly.
Dimensional weight: give a method a divisor and each package is billed on the heavier of
its real weight and length × width × height ÷ divisor.
Price
$59, with a $49/year renewal for updates. One edition — everything below is in it. There is no free tier and no feature you can buy your way into later.
| Methods and ordered rules | ✓ |
| All four charge components, clamps, free / disable / required / stop | ✓ |
| Weight, subtotal, quantity, destination, postal-code, category conditions | ✓ |
| Craft condition-builder rules | ✓ |
| Split by maximum package weight | ✓ |
| Box packing and per-package rating | ✓ |
| Dimensional weight | ✓ |
| Per-shipping-category charges | ✓ |
| Carrier rate presets, and Postie support | ✓ |
| Simulator, Twig variable, cart API, console commands, export and import | ✓ |
Carriers and Postie
Weight ships importable presets shaped like the common carriers' own rate structures — Royal Mail, UPS Ground, FedEx Home, USPS Priority (both flat-rate and by weight), DHL Express and Australia Post. Every amount in them is a placeholder. The structure is the tedious part and it rarely changes; the numbers are the part you already have on a contract, and republishing a carrier's rate card would be both somebody else's copyright and stale within a quarter.
php craft weight/presets # list them php craft weight/presets/import royal-mail-uk # create its methods, disabled
Where Postie is installed alongside, two things happen on their own:
- Weight's box library drives Postie's packing, so the simulator and the carrier request cannot disagree about how many parcels there are.
- A rule can ask what the carriers said, which lets a table rate be a backstop for an API outage rather than a permanent competitor to a live rate.
Weight never edits Postie's prices. Exactly one plugin computes any given total.
Requirements
Craft CMS 5.3+, Craft Commerce 5.0+, PHP 8.2+. Postie is optional and is not a dependency.
Installation
composer require justinholtweb/craft-weight php craft plugin/install weight
Templating
{% set quotes = craft.weight.quotes() %}
{% for quote in quotes %}
{% if quote.available %}
<label>
<input type="radio" name="shippingMethodHandle" value="{{ quote.method.shippingMethodHandle }}">
{{ quote.method.customerLabel }} —
{{ quote.isFree ? 'Free' : quote.total|commerceCurrency(cart.currency) }}
{% if quote.packageCount > 1 %}
<small>{{ quote.packageCount }} parcels</small>
{% endif %}
</label>
{% endif %}
{% endfor %}
{% set progress = craft.weight.progress() %}
{% if progress and progress.message %}
<p>{{ progress.message }}</p>
{% endif %}
craft.weight.quotes(order = cart) |
every method's price, cheapest first, keyed by handle |
craft.weight.quote(handle, order = cart) |
one method's price |
craft.weight.packages(handle, order = cart) |
how the cart would be packed |
craft.weight.progress(order = cart) |
how far the cart is from free shipping |
JSON
GET /actions/weight/cart/quote returns the same numbers for the requester's own cart, along with
the free-shipping progress. Anonymous, because it only ever describes your own basket.
Console
php craft weight/methods # every method and its rules php craft weight/methods/export --file=w.json # methods, rules and boxes as JSON php craft weight/methods/import --file=w.json # ...and back, matching on handle php craft weight/rates/quote <orderNumber> # what every method costs for one real cart php craft weight/rates/table standard 0.5 20 0.5 --country=GB # a rate ladder, which is what you actually want # to eyeball before going live php craft weight/presets # carrier-shaped rate structures php craft weight/presets/show <name> # print one as importable JSON php craft weight/presets/import <name> # create its methods, disabled
Methods, rules and boxes live in the database rather than project config — the same call Commerce
makes for its own shipping methods — so export and import are how they move between
environments. docs/example-config.json is a working example.
How it decides
Two invariants hold the plugin together:
services\Rater::quote()is the only place a price is computed. The Commerce shipping method, the simulator, the cart API and the console all read the sameQuote, so a preview can never disagree with a checkout.services\Packer::pack()is the only place a cart becomes packages.
Both memoize on a full cart signature — items, quantities, weights, prices, address, coupon — never on the order number alone, which would answer a recalculated cart from a stale quote.
Line items that are non-shippable or already carry free shipping are excluded from packing, so the weight being charged for is the weight actually being shipped.
About the box packer
Three-dimensional bin packing is NP-hard and nobody solves it exactly at checkout. Weight's packer is first-fit-decreasing with a volume-and-dimension fit test: an item goes in a box if each of its dimensions fits the box's, the box has volume left and the box has weight left. It does not model the geometry of what is already in the box, so it errs towards more boxes rather than fewer — the direction that overcharges no carrier — and every decision it makes is visible in the simulator.
Documentation
justinholt.com/plugins/craft-weight/docs —
Installation,
Configuration,
Usage,
Troubleshooting and the
FAQ. The same pages live in docs/ in this
repository, which is where they are authored.
Support
Trademarks
Weight is an independent plugin built by Justin Holt. It is not affiliated with, endorsed by, or sponsored by Automattic Inc., Verbb, or any shipping carrier.
“WooCommerce” is a trademark of Automattic Inc., used here only to identify the rule model Weight is modelled on. “Postie” is a trademark of Verbb, used here only to identify the plugin Weight can run alongside. Carrier and service names — Royal Mail, UPS, FedEx, USPS, DHL, Australia Post — are trademarks of their respective owners and are used only to identify the rate structures the presets mirror. No preset contains any carrier's published rates.
Licence
Proprietary. See LICENSE.md.