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.

Maintainers

Package info

github.com/justinholtweb/craft-weight

Type:craft-plugin

pkg:composer/justinholtweb/craft-weight

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

5.0.0 2026-08-23 17:54 UTC

This package is auto-updated.

Last update: 2026-08-23 19:00:54 UTC


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 and 10000-19999 numeric 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:

  1. 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 same Quote, so a preview can never disagree with a checkout.
  2. 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/docsInstallation, Configuration, Usage, Troubleshooting and the FAQ. The same pages live in docs/ in this repository, which is where they are authored.

Support

justin@justinholt.com

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.