Magento 2 Flat Rate Shipping Bug: 'For Shipment with Matching Items' Free Shipping Fails
As e-commerce migration experts at Shopping Mover, we constantly monitor the pulse of the Magento community, especially critical bugs that can impact merchant operations. A recent GitHub issue (Issue #41431) has brought to light a significant bug in Magento 2's Flat Rate shipping method concerning free shipping cart rules. This insight delves into the problem, its technical roots, and provides an immediate workaround for affected Magento Open Source and Adobe Commerce users.
The Magento 2 Flat Rate Free Shipping Conundrum
Merchants often configure cart rules to offer free shipping based on specific conditions, such as a minimum order subtotal. A common setup involves a cart rule with the "Free Shipping" option set to "For shipment with matching items". The expectation is straightforward: if the cart meets the rule's conditions, the Flat Rate shipping cost should be zeroed out.
However, users have discovered that when the Flat Rate shipping method is enabled, it continues to charge the full shipping price, even when a qualifying free shipping rule is active. For example, if a rule grants free shipping for orders over $50, and the cart subtotal is $68, a Flat Rate of $10 (for two items at $5 each) is still applied, instead of the expected $0.00.
Unpacking the Technical Discrepancy
The core of this issue lies in how different Magento shipping carriers interpret free shipping flags. When a cart rule with "For shipment with matching items" is applied, Magento's Calculator::applyFreeShippingRule() correctly sets the free_shipping flag on the shipping address ($address->setFreeShipping(true)). This indicates that the entire shipment should be free.
The problem arises within the Flatrate carrier's logic. Specifically, its getFreeBoxesCount() method, which determines how many items should be charged, only checks the free_shipping flag at the item level ($item->getFreeShipping()). Since the rule only flags the address and not individual items, Flatrate incorrectly calculates zero "free boxes" and proceeds to charge the full price.
This behavior contrasts sharply with other core Magento shipping carriers:
- The
Tableratecarrier correctly checks$item->getAddress()->getFreeShipping(). - The
Freeshippingcarrier checks$request->getFreeShipping().
This inconsistency points to a historical oversight. The issue author noted that an address check was removed from both carriers in a 2017 commit (cdefefa948, MAGETWO-63844) but was later restored for Tablerate, leaving Flatrate with the outdated logic. This bug has been reported in similar forms before (e.g., #35967, #37945) but was closed without a permanent fix.
The bug has been confirmed on Magento 2.4.9 Open Source and, as validated by community member lbajsarowicz, on a clean 2.4-develop branch using PHP 8.3, MySQL 8.4, and OpenSearch 2.19. The provided standalone PHP script clearly demonstrates the issue:
getObjectManager();
$om->get(\Magento\Framework\App\State::class)->setAreaCode('adminhtml');
$repo = $om->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
$p = $om->create(\Magento\Catalog\Model\Product::class);
$p->setTypeId('simple')->setAttributeSetId(4)->setSku('t41431_sku')->setName('t41431')->setPrice(34)
->setWebsiteIds([1])->setVisibility(4)->setStatus(1)->setWeight(1)
->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_in_stock' => 1]);
$p = $repo->save($p);
$om->get(\Magento\Framework\Registry::class)->register('isSecureArea', true, true);
$rule = $om->create(\Magento\SalesRule\Model\Rule::class);
$rule->setName('t41431')->setIsActive(1)->setWebsiteIds([1])->setCustomerGroupIds([0, 1, 2, 3])
->setCouponType(1)->setSimpleFreeShipping(2)->setSimpleAction('by_percent')->setDiscountAmount(0);
$rule->loadPost(['conditions' => [
'1' => ['type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, 'aggregator' => 'all', 'value' => '1'],
'1--1' => ['type' => \Magento\SalesRule\Model\Rule\Condition\Address::class,
'attribute' => 'base_subtotal', 'operator' => '>=', 'value' => '50'],
]]);
$rule->save();
foreach ([2 => 'for shipment with matching items', 1 => 'for matching items only'] as $mode => $label) {
$om->create(\Magento\SalesRule\Model\Rule::class)->load($rule->getId())->setSimpleFreeShipping($mode)->save();
$q = $om->create(\Magento\Quote\Model\QuoteFactory::class)->create();
$q->setStoreId(1)->setCustomerIsGuest(true)->setCustomerGroupId(0)->setCustomerEmail('[email protected]');
$q->addProduct($om->create(\Magento\Catalog\Model\Product::class)->load($p->getId()), 2);
$a = $q->getShippingAddress();
$a->setCountryId('US')->setRegionId(12)->setPostcode('90210')->setCity('LA')->setStreet('1 Main')
->setFirstname('a')->setLastname('b')->setTelephone('5555555')->setCollectShippingRates(true);
$q->getBillingAddress()->addData($a->getData());
$q->collectTotals();
$a->collectShippingRates();
$q->collectTotals();
$out = [];
foreach ($a->getAllShippingRates() as $r) {
if ($r->getCarrier() === 'flatrate') { $out[] = $r->getCode() . '=' . $r->getPrice(); }
}
echo "$mode ($label): address free_shipping=" . var_export((bool)$a->getFreeShipping(), true) . ' rates=' . implode(',', $out) . PHP_EOL;
}
$om->create(\Magento\SalesRule\Model\Rule::class)->load($rule->getId())->delete();
$repo->delete($p);
The result of this script confirms: with simple_free_shipping=2 ("for shipment with matching items"), the address free_shipping flag is true, but the Flat Rate is still 10. With simple_free_shipping=1 ("for matching items only"), the rate correctly becomes 0.
Immediate Workaround for Merchants
Until a permanent fix is implemented, the most effective workaround is to adjust your free shipping cart rules:
- Navigate to Marketing > Cart Price Rules.
- Edit your relevant free shipping rule.
- Under the "Actions" tab, change the "Free Shipping" setting from "For shipment with matching items" to "For matching items only".
While this resolves the Flat Rate issue, it's important to understand that "For matching items only" might behave slightly differently in complex scenarios involving mixed carts (e.g., some items matching the rule, others not). However, for most standard free shipping offers, this adjustment will achieve the desired outcome with Flat Rate.
A Call for Developer Contribution
This issue highlights a clear opportunity for community contribution. The fix would involve modifying the Flatrate carrier to properly honor the address-level free shipping flag, bringing its behavior in line with other core carriers. Developers could propose a pull request that checks $request->getFreeShipping() or $item->getAddress()->getFreeShipping() within the Flatrate carrier's logic to correctly apply free shipping when the rule is set to "For shipment with matching items."
For merchants undergoing Magento migrations or managing complex shipping configurations, understanding such nuances is crucial. At Shopping Mover, we emphasize staying informed about these community insights to ensure smooth operations and effective problem-solving for your e-commerce platform.