A store I worked on was quoting shipping at checkout from product weight alone. The numbers looked fine. Every order shipped. Nothing errored.
The margin on bulky items was quietly gone.
Couriers do not bill dead weight. They bill the greater of dead weight and volumetric weight, where volumetric weight is L × B × H ÷ 5000 for most Indian carriers. A large, light parcel (a pillow, a lampshade, anything foam-packed) has a volumetric weight several times its actual weight. Rate on the scale reading alone and the store eats the difference on every one of those orders.
That part is not a secret. It is printed in the courier's own terms. The interesting part is why a competent team ships the wrong calculation anyway.
The field that is marked optional
Open a courier's rate calculator and you will usually find a form roughly like this:
- Pickup pincode
- Delivery pincode
- Weight (kg)
- Dimensions (Optional)
Leave the dimensions blank and the calculator does not complain. It does not warn you. It returns a rate.
It also returns, in the breakdown, Volumetric Weight: 0.00 KG and an applicable weight equal to the dead weight you typed. The quote that comes back is the dead-weight quote: for a bulky parcel, roughly half of what the courier will actually invoice.
So the calculator is not lying. It answered the question you asked. You asked "what does a 2.86 kg parcel of unspecified shape cost," and the only honest answer to that is a dead-weight rate, because shape is the missing variable.
But nobody reads it that way. You read it as "what does my parcel cost," and the interface encouraged you to, by marking the input that changes the answer as optional.
Why "optional" is the wrong word
There are two kinds of optional input, and interfaces routinely conflate them.
The first kind genuinely does not affect the result. A note on the order. A nickname on the address. Omit it and the number is identical.
The second kind affects the result enormously, but the system has a fallback so it does not have to stop. Dimensions are this kind. Omitting them does not remove volumetric weight from the pricing model. It substitutes zero and carries on. Zero is a valid number. It is arithmetically fine. It is also the single most optimistic value the field can take, so the resulting quote is not merely wrong, it is wrong in the direction that looks best.
The general shape:
An input marked optional whose omission silently selects a default is not optional. It is a required input with a hidden, favourable answer pre-filled.
This is the same class of failure as a config flag that defaults to the permissive setting, or an ORM that silently coerces a bad string to 0. No error, valid-looking output, wrong result. You only find out downstream, when reality disagrees with your number.
What it costs
Run the arithmetic on a single parcel. A box 40 × 35 × 30 cm weighing 2.86 kg:
volumetric = (40 × 35 × 30) / 5000
= 42000 / 5000
= 8.4 kg
applicable = max(2.86, 8.4) = 8.4 kgYou quoted for 2.86 kg. You get invoiced for 8.4. On a courier slab priced per half-kilo, that is not a rounding error. It is roughly triple the weight you charged for.
Now notice what makes it hard to catch. The loss does not appear as a failed order or a support ticket. It appears as a courier invoice that is somewhat higher than expected, every month, spread across the subset of orders containing bulky items. There is no single event to investigate. It reads as "shipping is expensive," which is a sentence every merchant already believes.
Fixing it in the catalog, not the checkout
The temptation is to patch the shipping calculation: clamp it, add a fudge factor, apply a percentage uplift to bulky categories. Don't. A fudge factor is a second wrong number chosen to cancel the first one, and it stops cancelling the moment the product mix changes.
The real fix is upstream and boring: dimensions are catalog data, and they are mandatory.
In Magento, product.weight exists out of the box and length, width and height do not. Whatever you do, the rating code needs all four. Something along these lines:
public function applicableWeight(float $deadWeight, ?array $dims): float
{
if ($dims === null) {
throw new LocalizedException(
__('Cannot rate a shipment without dimensions.')
);
}
$volumetric = ($dims['l'] * $dims['b'] * $dims['h']) / self::DIVISOR;
return max($deadWeight, $volumetric);
}The throw is the important line, and it is the one that gets argued about. A missing dimension is not a case to default through. It is a product that cannot be shipped correctly, and you would much rather learn that while adding the product than three weeks later on an invoice.
Two details worth pinning down before you write this:
The divisor is per-carrier, and it belongs in config. 5000 is common for Indian domestic courier services, but it is a carrier term, not a law of physics. Different carriers and different service classes use different divisors. Hardcode it and you have built the same silent-wrong-number bug one level up.
Dimensions are the shipped box, not the product. A folded garment ships flat, a rolled poster ships in a tube, and multiple items may consolidate into one carton. If your packing logic combines items, the volumetric calculation has to run against the resulting box, not against the sum of the products. Getting the divisor right and the box wrong leaves you exactly where you started.
The check that generalises
The shipping bug is worth fixing. The habit behind it is worth more.
When a calculator, an API, or an admin form gives you a number you are going to make decisions with, ask one question before you trust it: which inputs did I leave empty, and what did the system substitute?
If the answer is "nothing, it ignores them," fine. If the answer is "zero," or "the default plan," or "the aggregate across all locations," you did not get an answer to your question. You got an answer to an easier one, and the difference between those two questions is where the money goes.
The rate calculator was never broken. It was doing arithmetic on the numbers it was given. The blank field was the bug.
Source / further reading: https://github.com/mage-os/mageos-magento2/releases/tag/3.3.0
