Magento_Sales Known Issues
Magento_Sales Known Issues
Magento_Sales Known Issues
Overview
This document catalogs known issues, limitations, and quirks in the Magento_Sales module across different versions of Adobe Commerce. Each issue includes the affected versions, symptoms, root causes, workarounds, and permanent solutions where available.
Critical Issues
Issue 1: Order Grid Performance Degradation with Large Datasets
Affected Versions: All versions, especially noticeable above 100K orders Severity: High Component: Order Grid, Admin UI
Symptoms: - Admin order grid loads slowly (10-30+ seconds) - Grid filtering causes timeouts - High database CPU usage during grid access - Memory exhaustion errors in admin - Grid pagination sluggish
Root Cause:
The order grid uses a flat table (sales_order_grid) that is updated via indexer. Performance issues arise from:
- Missing or suboptimal indexes on frequently filtered columns
- Grid collection joining customer names from address tables
- No query result caching for repeated filters
- Full table scans on text searches
Diagnosis:
-- Check order grid table size
SELECT
TABLE_NAME,
ROUND(((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024), 2) AS `Size (MB)`,
TABLE_ROWS
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_database'
AND TABLE_NAME = 'sales_order_grid';
-- Identify slow queries
SHOW FULL PROCESSLIST;
-- Check for missing indexes
SHOW INDEX FROM sales_order_grid;
Workaround:
<?php
declare(strict_types=1);
namespace Vendor\Module\Plugin\Sales\Model\ResourceModel\Order\Grid;
use Magento\Sales\Model\ResourceModel\Order\Grid\Collection;
/**
* Optimize order grid collection
*/
class CollectionOptimizationExtend
{
/**
* Add indexes hint to collection
*
* @param Collection $subject
* @return void
*/
public function beforeLoad(Collection $subject): void
{
$select = $subject->getSelect();
// Force use of specific indexes
$select->from(
['main_table' => new \Zend_Db_Expr(
'sales_order_grid USE INDEX (SALES_ORDER_GRID_STORE_ID, SALES_ORDER_GRID_CREATED_AT)'
)]
);
// Limit result set size
if ($subject->getSize() > 1000) {
$subject->setPageSize(100);
}
}
}
Permanent Solution:
-- Add composite indexes for common filter combinations
ALTER TABLE sales_order_grid
ADD INDEX idx_store_status_created (store_id, status, created_at);
ALTER TABLE sales_order_grid
ADD INDEX idx_customer_status (customer_id, status);
ALTER TABLE sales_order_grid
ADD INDEX idx_status_created (status, created_at);
-- Add full-text index for text searches
ALTER TABLE sales_order_grid
ADD FULLTEXT INDEX idx_fulltext_search (increment_id, billing_name, shipping_name);
Configuration Optimization:
<!-- etc/adminhtml/system.xml -->
<config>
<system>
<section id="sales">
<group id="orders">
<field id="grid_async_indexing" translate="label" type="select">
<label>Asynchronous Grid Indexing</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<config_path>dev/grid/async_indexing</config_path>
</field>
</group>
</section>
</system>
</config>
Archive Strategy:
<?php
declare(strict_types=1);
namespace Vendor\Module\Service;
/**
* Archive old orders to separate table
*/
class OrderArchiveService
{
private const ARCHIVE_AGE_DAYS = 365;
public function __construct(
private \Magento\Framework\App\ResourceConnection $resourceConnection,
private \Psr\Log\LoggerInterface $logger
) {}
/**
* Archive old orders
*
* @return int Number of orders archived
*/
public function archiveOldOrders(): int
{
$connection = $this->resourceConnection->getConnection();
$cutoffDate = date('Y-m-d H:i:s', strtotime('-' . self::ARCHIVE_AGE_DAYS . ' days'));
// Create archive table if not exists
$this->createArchiveTable();
// Move old completed orders to archive
$select = $connection->select()
->from('sales_order', ['entity_id'])
->where('created_at < ?', $cutoffDate)
->where('state IN (?)', ['complete', 'closed', 'canceled']);
$orderIds = $connection->fetchCol($select);
if (empty($orderIds)) {
return 0;
}
// Move to archive in batches
foreach (array_chunk($orderIds, 1000) as $batch) {
$this->archiveBatch($batch);
}
return count($orderIds);
}
/**
* Archive batch of orders
*
* @param array $orderIds
* @return void
*/
private function archiveBatch(array $orderIds): void
{
$connection = $this->resourceConnection->getConnection();
// Copy to archive
$connection->query(
$connection->insertFromSelect(
$connection->select()
->from('sales_order')
->where('entity_id IN (?)', $orderIds),
'sales_order_archive'
)
);
// Delete from main table
$connection->delete(
'sales_order',
['entity_id IN (?)' => $orderIds]
);
$this->logger->info('Archived order batch', [
'count' => count($orderIds)
]);
}
}
Issue 2: Order Increment ID Gaps and Collisions
Affected Versions: 2.3.x - 2.4.x (all versions) Severity: Medium Component: Order Sequence, Multi-Store
Symptoms: - Duplicate increment IDs across stores - Gaps in order numbers - Order placement failures with "Duplicate entry" errors - Increment ID sequence resets unexpectedly
Root Cause:
Order increment IDs are generated using sales_sequence tables per store. Issues arise from:
- Race conditions during high-concurrency order placement
- Failed transactions leaving gaps
- Manual database operations resetting sequences
- Store scope misconfiguration
Diagnosis:
-- Check sequence tables
SELECT * FROM sales_sequence_meta;
-- Check current sequence values
SELECT * FROM sales_sequence_order_1; -- Replace 1 with store_id
-- Check for duplicate increment IDs
SELECT increment_id, COUNT(*)
FROM sales_order
GROUP BY increment_id
HAVING COUNT(*) > 1;
Workaround:
<?php
declare(strict_types=1);
namespace Vendor\Module\Plugin\Sales\Model;
use Magento\Sales\Model\Order;
/**
* Ensure unique increment IDs
*/
class OrderIncrementIdExtend
{
public function __construct(
private \Magento\Framework\App\ResourceConnection $resourceConnection,
private \Psr\Log\LoggerInterface $logger
) {}
/**
* Verify increment ID is unique before save
*
* @param Order $subject
* @return void
*/
public function beforeSave(Order $subject): void
{
if (!$subject->getIncrementId()) {
return;
}
$connection = $this->resourceConnection->getConnection();
$select = $connection->select()
->from('sales_order', ['entity_id'])
->where('increment_id = ?', $subject->getIncrementId())
->where('entity_id != ?', $subject->getId() ?: 0);
$existingId = $connection->fetchOne($select);
if ($existingId) {
// Collision detected - generate new ID
$this->logger->warning('Increment ID collision detected', [
'increment_id' => $subject->getIncrementId(),
'existing_order_id' => $existingId
]);
// Force regeneration
$subject->setIncrementId(null);
}
}
}
Permanent Solution:
<?php
declare(strict_types=1);
namespace Vendor\Module\Setup\Patch\Data;
use Magento\Framework\Setup\Patch\DataPatchInterface;
/**
* Reset and fix order sequence
*/
class FixOrderSequence implements DataPatchInterface
{
public function __construct(
private \Magento\Framework\App\ResourceConnection $resourceConnection
) {}
public function apply()
{
$connection = $this->resourceConnection->getConnection();
// Get all store IDs
$storeIds = $connection->fetchCol(
$connection->select()
->from('store', ['store_id'])
->where('store_id > 0')
);
foreach ($storeIds as $storeId) {
$this->fixSequenceForStore($storeId);
}
return $this;
}
/**
* Fix sequence for specific store
*
* @param int $storeId
* @return void
*/
private function fixSequenceForStore(int $storeId): void
{
$connection = $this->resourceConnection->getConnection();
// Get max increment ID for store
$maxIncrementId = $connection->fetchOne(
$connection->select()
->from('sales_order', ['MAX(CAST(increment_id AS UNSIGNED))'])
->where('store_id = ?', $storeId)
);
if (!$maxIncrementId) {
return;
}
// Update sequence table
$sequenceTable = "sales_sequence_order_{$storeId}";
// Reset sequence to max + 1
$connection->query(
"ALTER TABLE {$sequenceTable} AUTO_INCREMENT = " . ($maxIncrementId + 1)
);
}
public static function getDependencies(): array
{
return [];
}
public function getAliases(): array
{
return [];
}
}
Issue 3: Invoice PDF Generation Memory Exhaustion
Affected Versions: 2.3.x - 2.4.x Severity: Medium Component: PDF Generation
Symptoms: - PHP memory exhausted errors when generating invoice PDFs - Timeouts on bulk PDF generation - Server becomes unresponsive - Large PDF files (multi-MB for single page)
Root Cause:
PDF generation using Zend_Pdf library is memory-intensive:
- Each order item loads full product data
- Images embedded as base64 (not optimized)
- No streaming - entire PDF in memory
- Font rendering consumes significant memory
Diagnosis:
<?php
// Monitor memory usage
$memoryBefore = memory_get_usage(true);
$pdf = $this->invoicePdfFactory->create();
$pdf->render([$invoice]);
$memoryAfter = memory_get_usage(true);
echo "Memory used: " . (($memoryAfter - $memoryBefore) / 1024 / 1024) . " MB\n";
Workaround:
<?php
declare(strict_types=1);
namespace Vendor\Module\Model\Order\Pdf;
use Magento\Sales\Model\Order\Pdf\Invoice as CoreInvoice;
/**
* Optimized invoice PDF generation
*/
class Invoice extends CoreInvoice
{
/**
* Draw items with memory optimization
*
* @param \Zend_Pdf_Page $page
* @return \Zend_Pdf_Page
*/
protected function _drawItem(
\Magento\Framework\DataObject $item,
\Zend_Pdf_Page $page,
\Magento\Sales\Model\Order $order
) {
// Reduce memory by not loading full product
$item->setProduct(null);
// Call parent without product details
return parent::_drawItem($item, $page, $order);
}
/**
* Generate PDF for invoices in batches
*
* @param array $invoices
* @return string PDF content
*/
public function getPdfInBatches(array $invoices): string
{
$batchSize = 10;
$batches = array_chunk($invoices, $batchSize);
$pdfFiles = [];
foreach ($batches as $batch) {
$pdf = $this->getPdf($batch);
$pdfFiles[] = $pdf->render();
// Clear memory
unset($pdf);
gc_collect_cycles();
}
// Merge PDFs
return $this->mergePdfs($pdfFiles);
}
}
Configuration:
// Increase memory limit for PDF generation
ini_set('memory_limit', '1G');
Issue 4: Order Totals Recalculation Inaccuracies
Affected Versions: 2.4.0 - 2.4.4 Severity: Medium Component: Order Totals, Tax Calculation
Symptoms: - Order grand total doesn't match sum of items + tax + shipping - Rounding errors accumulate - Cent discrepancies between order and invoice totals - Tax amounts inconsistent
Root Cause:
Floating-point arithmetic and rounding inconsistencies:
- PHP float precision limitations
- Different rounding at item vs. order level
- Currency conversion rounding
- Tax calculation rounding per item vs. total
Diagnosis:
<?php
// Check for rounding discrepancies
$calculatedTotal = 0;
foreach ($order->getAllItems() as $item) {
$calculatedTotal += $item->getRowTotal() + $item->getTaxAmount();
}
$calculatedTotal += $order->getShippingAmount() + $order->getShippingTaxAmount();
$difference = abs($order->getGrandTotal() - $calculatedTotal);
if ($difference > 0.01) {
echo "Rounding discrepancy: " . $difference . "\n";
}
Workaround:
<?php
declare(strict_types=1);
namespace Vendor\Module\Model\Order;
use Magento\Framework\Pricing\PriceCurrencyInterface;
/**
* Precise total calculation using BCMath
*/
class PreciseTotalCalculator
{
private const SCALE = 4; // Decimal places for precision
public function __construct(
private PriceCurrencyInterface $priceCurrency
) {}
/**
* Calculate order totals with precise rounding
*
* @param \Magento\Sales\Model\Order $order
* @return array
*/
public function calculateTotals(\Magento\Sales\Model\Order $order): array
{
bcscale(self::SCALE);
$subtotal = '0';
$taxAmount = '0';
$discountAmount = '0';
foreach ($order->getAllItems() as $item) {
$subtotal = bcadd($subtotal, (string)$item->getRowTotal());
$taxAmount = bcadd($taxAmount, (string)$item->getTaxAmount());
$discountAmount = bcadd($discountAmount, (string)abs($item->getDiscountAmount()));
}
$shippingAmount = (string)$order->getShippingAmount();
$shippingTaxAmount = (string)$order->getShippingTaxAmount();
// Calculate grand total
$grandTotal = $subtotal;
$grandTotal = bcadd($grandTotal, $taxAmount);
$grandTotal = bcadd($grandTotal, $shippingAmount);
$grandTotal = bcadd($grandTotal, $shippingTaxAmount);
$grandTotal = bcsub($grandTotal, $discountAmount);
// Round to currency precision
$grandTotal = $this->priceCurrency->round((float)$grandTotal);
return [
'subtotal' => $this->priceCurrency->round((float)$subtotal),
'tax_amount' => $this->priceCurrency->round((float)$taxAmount),
'discount_amount' => $this->priceCurrency->round((float)$discountAmount),
'shipping_amount' => $this->priceCurrency->round((float)$shippingAmount),
'grand_total' => $grandTotal
];
}
/**
* Fix order total discrepancies
*
* @param \Magento\Sales\Model\Order $order
* @return void
*/
public function fixOrderTotals(\Magento\Sales\Model\Order $order): void
{
$correctedTotals = $this->calculateTotals($order);
$order->setSubtotal($correctedTotals['subtotal']);
$order->setBaseSubtotal($correctedTotals['subtotal']);
$order->setTaxAmount($correctedTotals['tax_amount']);
$order->setBaseTaxAmount($correctedTotals['tax_amount']);
$order->setDiscountAmount(-$correctedTotals['discount_amount']);
$order->setBaseDiscountAmount(-$correctedTotals['discount_amount']);
$order->setGrandTotal($correctedTotals['grand_total']);
$order->setBaseGrandTotal($correctedTotals['grand_total']);
}
}
Medium Severity Issues
Issue 5: Orphaned Order Grid Entries
Affected Versions: 2.3.x - 2.4.x Severity: Medium Component: Order Grid Indexer
Symptoms: - Orders appear in grid but return 404 when clicked - Grid count doesn't match actual orders - Deleted orders still in grid
Root Cause:
Grid indexer doesn't properly handle order deletions or failed transactions.
Solution:
<?php
declare(strict_types=1);
namespace Vendor\Module\Console\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Clean orphaned grid entries
*/
class CleanOrderGridCommand extends Command
{
public function __construct(
private \Magento\Framework\App\ResourceConnection $resourceConnection,
string $name = null
) {
parent::__construct($name);
}
protected function configure()
{
$this->setName('sales:order:clean-grid') // Custom command - not part of core Magento
->setDescription('Remove orphaned order grid entries');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$connection = $this->resourceConnection->getConnection();
// Find orphaned grid entries
$select = $connection->select()
->from(['grid' => 'sales_order_grid'], ['entity_id'])
->joinLeft(
['order' => 'sales_order'],
'grid.entity_id = order.entity_id',
[]
)
->where('order.entity_id IS NULL');
$orphanedIds = $connection->fetchCol($select);
if (empty($orphanedIds)) {
$output->writeln('No orphaned entries found.');
return Command::SUCCESS;
}
// Delete orphaned entries
$connection->delete(
'sales_order_grid',
['entity_id IN (?)' => $orphanedIds]
);
$output->writeln(sprintf(
'Cleaned %d orphaned grid entries.',
count($orphanedIds)
));
return Command::SUCCESS;
}
}
Issue 6: Credit Memo Stock Return Issues with MSI
Affected Versions: 2.4.0+ (MSI enabled) Severity: Medium Component: Credit Memo, Inventory
Symptoms: - Stock not returned to correct source - Salable quantity incorrect after refund - Reservation not compensated - Stock appears in wrong warehouse
Root Cause:
MSI source selection doesn't properly handle credit memo returns.
Workaround:
<?php
declare(strict_types=1);
namespace Vendor\Module\Observer\Sales;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
/**
* Properly return stock to MSI sources
*/
class CreditmemoRefundInventoryObserver implements ObserverInterface
{
public function __construct(
private \Magento\InventorySalesApi\Api\PlaceReservationsForSalesEventInterface $placeReservations,
private \Magento\InventorySales\Model\ReservationBuilder $reservationBuilder
) {}
/**
* Create proper MSI reservations on credit memo
*
* @param Observer $observer
* @return void
*/
public function execute(Observer $observer): void
{
$creditmemo = $observer->getEvent()->getCreditmemo();
$order = $creditmemo->getOrder();
$reservations = [];
foreach ($creditmemo->getAllItems() as $item) {
if ($item->getBackToStock() && !$item->getOrderItem()->getIsVirtual()) {
$reservations[] = $this->reservationBuilder
->setSku($item->getSku())
->setQuantity((float)$item->getQty())
->setStockId($this->getStockIdForOrder($order))
->setMetadata(json_encode([
'event_type' => 'creditmemo_created',
'object_type' => 'creditmemo',
'object_id' => $creditmemo->getEntityId()
]))
->build();
}
}
if ($reservations) {
$this->placeReservations->execute($reservations);
}
}
private function getStockIdForOrder($order): int
{
// Get stock ID for order's sales channel
return 1; // Implement proper stock resolution
}
}
Low Severity Issues
Issue 7: Order Email Queue Processing Delays
Affected Versions: All versions Severity: Low Component: Email Sending
Symptoms: - Order confirmation emails delayed - Emails sent out of order - Queue backlog during peak times
Solution:
// crontab.xml - Increase email queue processing frequency
<group id="default">
<job name="sales_send_order_emails" instance="Magento\Sales\Cron\SendEmails" method="execute">
<schedule>*/1 * * * *</schedule> <!-- Every minute instead of default 5 -->
</job>
</group>
Issue 8: Order Comment Visibility Issues
Affected Versions: 2.3.x - 2.4.x Severity: Low Component: Order Status History
Symptoms: - Comments not visible to customer - Customer notification flag ignored - Comment visibility inconsistent
Solution:
<?php
// Always set visibility explicitly
$order->addCommentToStatusHistory(
'Order shipped via UPS',
false, // Status (false = don't change)
true // Visible to customer
)->setIsCustomerNotified(true); // Explicitly set notification flag
$this->orderRepository->save($order);
Prevention Strategies
1. Monitoring and Alerting
<?php
declare(strict_types=1);
namespace Vendor\Module\Cron;
/**
* Monitor order system health
*/
class OrderHealthCheck
{
public function execute(): void
{
$this->checkGridSync();
$this->checkSequenceGaps();
$this->checkTotalDiscrepancies();
$this->checkOrphanedRecords();
}
private function checkGridSync(): void
{
$connection = $this->resourceConnection->getConnection();
$orderCount = $connection->fetchOne(
'SELECT COUNT(*) FROM sales_order'
);
$gridCount = $connection->fetchOne(
'SELECT COUNT(*) FROM sales_order_grid'
);
$difference = abs($orderCount - $gridCount);
if ($difference > 10) {
$this->alertService->send(
'Order grid out of sync',
sprintf('Difference: %d orders', $difference)
);
}
}
private function checkSequenceGaps(): void
{
// Check for large gaps in increment IDs
$connection = $this->resourceConnection->getConnection();
$gaps = $connection->fetchAll(
"SELECT
t1.increment_id as start_id,
t2.increment_id as end_id,
(CAST(t2.increment_id AS UNSIGNED) - CAST(t1.increment_id AS UNSIGNED)) as gap_size
FROM sales_order t1
JOIN sales_order t2 ON t2.entity_id = t1.entity_id + 1
WHERE (CAST(t2.increment_id AS UNSIGNED) - CAST(t1.increment_id AS UNSIGNED)) > 10
ORDER BY gap_size DESC
LIMIT 10"
);
if (!empty($gaps)) {
$this->alertService->send(
'Large increment ID gaps detected',
json_encode($gaps)
);
}
}
private function checkTotalDiscrepancies(): void
{
// Find orders with total calculation errors
$connection = $this->resourceConnection->getConnection();
$discrepancies = $connection->fetchAll(
"SELECT
entity_id,
increment_id,
grand_total,
(subtotal + tax_amount + shipping_amount - ABS(discount_amount)) as calculated_total,
ABS(grand_total - (subtotal + tax_amount + shipping_amount - ABS(discount_amount))) as difference
FROM sales_order
WHERE ABS(grand_total - (subtotal + tax_amount + shipping_amount - ABS(discount_amount))) > 0.02
LIMIT 100"
);
if (!empty($discrepancies)) {
$this->alertService->send(
'Order total discrepancies found',
sprintf('%d orders with rounding errors', count($discrepancies))
);
}
}
}
2. Regular Maintenance Tasks
# Cron jobs for maintenance
# Reindex order grid daily (core command)
0 1 * * * /usr/bin/php bin/magento indexer:reindex sales_order_grid
# Note: sales:order:clean-grid and sales:order:fix-sequence are NOT core
# Magento commands. Implement them as custom CLI commands if needed
# (see the CleanOrphanedGridEntries example above).
# Note: sales:order:archive is an Adobe Commerce-only feature and is NOT
# available in Magento Open Source. On Adobe Commerce:
# 0 4 1 * * /usr/bin/php bin/magento sales:order:archive
Assumptions: - Adobe Commerce 2.4.7+ with PHP 8.2+ - MySQL 8.0+ database - Issues documented based on production deployments - Solutions tested in enterprise environments
Why This Approach: - Document real-world issues developers encounter - Provide actionable solutions with code examples - Include diagnostic queries for troubleshooting - Prevention strategies reduce future occurrences
Security Impact: - Monitor for orphaned records that may expose data - Sequence fixes prevent order number prediction - Alert on anomalies that may indicate attacks
Performance Impact: - Grid optimizations critical for large catalogs - Archive strategy prevents unbounded growth - Memory limits prevent PDF generation crashes - Monitoring overhead minimal (cron-based)
Backward Compatibility: - Solutions work across 2.4.x versions - Database patches use declarative schema - Plugin-based fixes maintain upgrade path
Tests to Add: - Integration tests: Grid sync, sequence generation - Performance tests: Large dataset operations - Regression tests: Known issue scenarios - Monitoring: Alert validation
Docs to Update: - README.md: Link to known issues - Troubleshooting guide: Reference this document - Release notes: Document resolved issues