Skip to content

[Issue] Fix division by zero in Weee item price renderer when qty_ordered is 0 #41162

Description

@m2-assistant

This issue is automatically created based on existing pull request: #41160: Fix division by zero in Weee item price renderer when qty_ordered is 0


Description (*)

Magento\Weee\Block\Item\Price\Renderer computes a per-unit base price by dividing the row total by the ordered quantity before checking whether Weee/FPT is even enabled:

$qty = $orderItem->getQtyOrdered();
$basePriceExclTax = $orderItem->getBaseRowTotal() / $qty;

if (!$this->weeeHelper->isEnabled($this->getStoreId())) {
    return $basePriceExclTax;
}

This appears identically in getBaseUnitDisplayPriceExclTax() (line 246) and getBaseFinalUnitDisplayPriceExclTax() (line 467).

If a sales_order_item row has qty_ordered = 0 while base_row_total is non-zero, the division throws DivisionByZeroError. Because it happens before the isEnabled() check, the admin order, invoice and credit memo views return HTTP 500 even on stores where Weee/FPT is switched off entirely, and since the order view will not open at all, the offending line cannot be corrected from the admin UI.

On reachability, to be straight about it: I could not find a default flow that creates this data shape. Storefront checkout and admin order create both enforce a quantity of at least 1, and order create renders through the cart-zone block rather than this one, so a stock install driven through the UI will not hit it. We hit it because a third-party admin order-editing extension recalculated a fully refunded line to qty_ordered = 0 while leaving base_row_total untouched. That extension bug is ours to fix.

What makes this worth fixing in core anyway is that the class is inconsistent with itself. It already guards this exact division in eight other places (lines 109, 133, 225, 255, 353, 373, 451, 475); these two are the only unguarded ones left, and both sit before the isEnabled() check, so a merchant with FPT switched off entirely still loses the page. Magento\Sales\Model\OrderRepository::save() applies no quantity validation, so core will persist the shape for any API consumer, and then core cannot render it.

Note that the natural-looking guard $qty ?: 1 does not fix this. getQtyOrdered() returns a numeric string such as '0.0000', which PHP treats as truthy:

$ php -r 'var_dump((bool)"0.0000");'
bool(true)

so $qty ?: 1 still evaluates to '0.0000' and still divides by zero. The guard has to be numeric.

This PR fixes both methods:

$qty = (float)$orderItem->getQtyOrdered();
$basePriceExclTax = $qty > 0
    ? $orderItem->getBaseRowTotal() / $qty
    : (float)$orderItem->getBaseRowTotal();

Why not just call getItemQtyForUnitPriceCalculation()?

That helper already exists in this class and already ends return $qty > 0 ? $qty : 1.0;, and both of these methods already use it, guarded, a few lines further down. Swapping it in for the unguarded division would be a smaller diff, but it would also change behaviour beyond the crash: the helper reads $item->getQty() for InvoiceItem and CreditMemoItem, whereas the code being fixed deliberately unwraps to the order item and reads getQtyOrdered(). On a partially invoiced order those are different numbers, so using the helper here would change displayed unit prices on invoice and credit memo views. This PR keeps the existing quantity semantics and changes only the divide-by-zero case.

The fallback value is chosen to agree with that helper's convention: when the quantity is zero the helper yields 1.0, and dividing the base row total by 1.0 gives the base row total, which is exactly what this patch returns.

For context on the surrounding convention, the same class already guards divisions this way in eight other places (lines 109, 133, 225, 255, 353, 373, 451, 475). These two are the only unguarded divisions left in it.

Related Pull Requests

Same fix submitted to Mage-OS: mage-os/mageos-magento2#329

Fixed Issues (if relevant)

N/A. No existing issue was found. Searched magento/magento2 issues and PRs for DivisionByZeroError Weee, getBaseUnitDisplayPriceExclTax, getQtyOrdered division and weee division by zero; every division-by-zero result was for an unrelated file. Search coverage is not proof of absence, so please close this as a duplicate if a prior report exists.

Manual testing scenarios (*)

  1. Take an order whose sales_order_item row has qty_ordered = 0 and a non-zero base_row_total. This can be set directly for the purposes of reproducing:
    UPDATE sales_order_item SET qty_ordered = 0 WHERE item_id = <a row with base_row_total > 0>;
  2. Open that order in the admin panel (Sales > Orders > View), which renders Magento\Weee\Block\Item\Price\Renderer.
    • Before: the page returns HTTP 500 and var/log/exception.log records DivisionByZeroError: Division by zero at Renderer.php:247 (or :468 for the final-price method). This reproduces whether or not Weee/FPT is enabled for the store, since the division precedes the isEnabled() check.
    • After: the page renders, showing the base row total as that line's unit price instead of throwing.
  3. Repeat on the invoice and credit memo views for the same order, which render the same block.
  4. Regression: run Magento\Weee\Test\Unit\Block\Item\Price\RendererTest. The existing cases (non-zero quantity, Weee enabled and disabled, include-Weee flag on and off) are unchanged.

Questions or comments

Two things I would welcome direction on.

First, the fallback. Returning the base row total keeps the class's own : 1.0 convention, but returning 0.0 is arguably more honest for a line with no quantity. I went with the former for consistency; happy to change it.

Second, whether the maintainers would prefer the two methods to be refactored onto getItemQtyForUnitPriceCalculation() as a follow-up, accepting the invoice and credit memo quantity change described above. That felt out of scope for a crash fix, but it would leave the class with one quantity source instead of two.

Contribution checklist (*)

  • Pull request has a meaningful description of its purpose
  • All commits are accompanied by meaningful commit messages
  • All new or changed code is covered with unit/integration tests. Added testGetBaseUnitDisplayPriceExclTaxWithZeroQtyOrdered and testGetBaseFinalUnitDisplayPriceExclTaxWithZeroQtyOrdered to RendererTest, each covering both an integer 0 and the numeric-string '0.0000' that getQtyOrdered() actually returns. Each asserts a value the unpatched code cannot return, because the same line throws first.
  • README.md files for modified modules are updated and included in the pull request if any README.md predefined sections require an update. N/A, no README change is needed.
  • All automated tests passed successfully (all builds are green). Not yet run: the changed files are php -l clean and the new tests were written against the existing suite's mocks and #[DataProvider] style, but I have not executed the suite locally. Relying on CI here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: OrderComponent: order statusIssue: ConfirmedGate 3 Passed. Manual verification of the issue completed. Issue is confirmedPriority: P2A defect with this priority could have functionality issues which are not to expectations.Reported on 2.4.xIndicates original Magento version for the Issue report.Reproduced on 2.4.xThe issue has been reproduced on latest 2.4-develop branch

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions