Skip to content

SourceItemsSave accepts inventory_source_item rows for product types where source-item management is disallowed (configurable, bundle, grouped) #3455

Description

@damienwebdev

Preconditions and environment

  • Magento Open Source 2.4.7-p9. Defect is independent of source topology (reproduced in single-source MSI mode but the missing validator applies to all SourceItemsSaveInterface::execute callers regardless).
  • Magento Inventory module versions:
    • magento/module-inventory 1.2.5 (registers the SourceItemValidatorChain and contains the existing validators that omit a product-type check)
    • magento/module-inventory-api 1.2.5 (defines SourceItemValidatorChain and SourceItemsSaveInterface)
    • magento/module-inventory-configuration 1.2.4 (suggested target for the new validator's DI wiring)
    • magento/module-inventory-configuration-api 1.2.3 (declares IsSourceItemManagementAllowedForProductTypeInterface)
    • magento/module-inventory-bundle-product 1.2.4 (provides the only existing type-aware validator, ShipmentTypeValidator, which scopes to bundles)
  • PHP 8.3, MariaDB.
  • At least one configurable (or bundle, or grouped) product enabled in the catalog. Note its SKU — for the repro below, call it <CONFIGURABLE_SKU>.

Steps to reproduce

  1. Confirm no row exists in inventory_source_item for the configurable's SKU:

    SELECT * FROM inventory_source_item WHERE sku = '<CONFIGURABLE_SKU>';
    -- expect: 0 rows
  2. Create a Stock Sources CSV containing the configurable's SKU:

    source_code,sku,status,quantity
    default,<CONFIGURABLE_SKU>,0,0
  3. Admin → System → Data Transfer → Import. Entity Type = Stock Sources. Upload the CSV, submit.

  4. The import reports success. Re-query:

    SELECT source_code, sku, status, quantity
    FROM   inventory_source_item
    WHERE  sku = '<CONFIGURABLE_SKU>';

Expected result

The import (and Magento\InventoryApi\Api\SourceItemsSaveInterface::execute more generally) rejects the row with a per-row validation error identifying the SKU and product type, on the basis that Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForProductTypeInterface::execute('configurable') returns false. No write occurs.

Actual result

The import succeeds with a "rows imported" success message. The re-query in step 4 returns one row:

source_code | sku                  | status | quantity
default     | <CONFIGURABLE_SKU>   | 0      | 0.0000

A new inventory_source_item row exists for a SKU whose product type is declared (by IsSourceItemManagementAllowedForProductTypeInterface) to not be source-managed. The row persists indefinitely and has no UI surface for removal (configurable products do not expose a sources/quantity grid in the product edit form). Removal requires direct SQL or a developer-built command.

The same defect reproduces for bundle and grouped products.

Additional information

Root cause — missing validator. The validator chain at vendor/magento/module-inventory/etc/di.xml against Magento\InventoryApi\Model\SourceItemValidatorChain includes only:

  • Magento\Inventory\Model\SourceItem\Validator\SkuValidator
  • Magento\Inventory\Model\SourceItem\Validator\SourceCodeValidator
  • Magento\Inventory\Model\SourceItem\Validator\QuantityValidator
  • Magento\InventoryBundleProduct\Model\SourceItem\Validator\ShipmentTypeValidator (added by module-inventory-bundle-product for a bundle-specific check)

None of these consult Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForProductTypeInterface. SourceItemsSave::execute (vendor/magento/module-inventory/Model/SourceItem/Command/SourceItemsSave.php) proceeds through Magento\Inventory\Model\ResourceModel\SaveMultiple::execute and inserts the row into inventory_source_item unconditionally.

Why the orphan row appears harmless today. The legacy-sync plugin Magento\InventoryCatalog\Plugin\CatalogInventory\UpdateSourceItemAtLegacyStockItemSavePlugin::aroundSave gates its cataloginventory_stock_item / cataloginventory_stock_status writes on the same IsSourceItemManagementAllowedForProductType check, so the orphan row does not propagate to the legacy tables for the configurable. The storefront-visibility pipeline (Magento\CatalogInventory\Helper\Stock, the catalog product collection plugins, the products GraphQL resolver) reads cataloginventory_stock_status and is therefore unaffected today.

Why it matters anyway.

  • The orphan row appears in raw exports (inventory_source_item table dumps, MSI export endpoints).
  • It is visible in admin source-item filters and any custom report that joins inventory_source_item directly.
  • It will be consumed by any future or third-party MSI feature that does not also gate on IsSourceItemManagementAllowedForProductType (a fair assumption given the gate exists in legacy-sync but is not enforced at write time).
  • It cannot be removed via admin UI because configurable products don't expose a sources/quantity grid — the row has no UI surface for editing or deletion.
  • The invariant being violated — "rows in inventory_source_item exist only for product types where source-item management is allowed" — is the invariant the rest of MSI relies on when it decides to gate certain code paths by checking IsSourceItemManagementAllowedForProductType instead of by checking inventory_source_item row presence. The gate exists because the invariant is supposed to hold by construction at write time. It doesn't.

Suggested fix — new validator. Add a validator to SourceItemValidatorChain that rejects source items whose SKU resolves to a product type with IsSourceItemManagementAllowedForProductTypeInterface::execute(<typeId>) === false. Skeleton:

namespace Magento\InventoryConfiguration\Model\SourceItem\Validator;

use Magento\Framework\Validation\ValidationResult;
use Magento\Framework\Validation\ValidationResultFactory;
use Magento\InventoryApi\Api\Data\SourceItemInterface;
use Magento\InventoryApi\Model\SourceItemValidatorInterface;
use Magento\InventoryCatalogApi\Model\GetProductTypesBySkusInterface;
use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForProductTypeInterface;

class ProductTypeManagementAllowedValidator implements SourceItemValidatorInterface
{
    public function __construct(
        private readonly GetProductTypesBySkusInterface $getProductTypesBySkus,
        private readonly IsSourceItemManagementAllowedForProductTypeInterface $isAllowed,
        private readonly ValidationResultFactory $validationResultFactory,
    ) {}

    public function validate(SourceItemInterface $sourceItem): ValidationResult
    {
        $sku = (string) $sourceItem->getSku();
        $type = $this->getProductTypesBySkus->execute([$sku])[$sku] ?? null;
        if ($type !== null && !$this->isAllowed->execute($type)) {
            return $this->validationResultFactory->create([
                'errors' => [
                    __('Source items are not supported for product type "%1" (SKU "%2").', $type, $sku),
                ],
            ]);
        }
        return $this->validationResultFactory->create(['errors' => []]);
    }
}

Wire it into the validator chain via vendor/magento/module-inventory-configuration/etc/di.xml:

<type name="Magento\InventoryApi\Model\SourceItemValidatorChain">
    <arguments>
        <argument name="validators" xsi:type="array">
            <item name="productTypeManagementAllowed"
                  xsi:type="object">Magento\InventoryConfiguration\Model\SourceItem\Validator\ProductTypeManagementAllowedValidator</item>
        </argument>
    </arguments>
</type>

This enforces the invariant at the only place it can be enforced authoritatively: the write boundary.

Release note

Add validation to prevent inventory_source_item rows from being created for product types that do not support MSI source-item management (configurable, bundle, grouped). The Stock Sources import (and any other caller of SourceItemsSaveInterface::execute) now rejects rows whose SKU resolves to a non-source-managed product type, preserving the invariant that the rest of the MSI codebase relies on.

Triage and priority

S3 — Average. The orphan rows do not break the storefront-visibility pipeline today (the legacy-sync plugin gates them out), so the visible impact is limited to admin reports, raw exports, and third-party MSI consumers. However, the rows violate an architectural invariant the rest of MSI assumes by construction, have no UI surface for removal, and will silently surface as a defect in any future or third-party code that consumes inventory_source_item without also gating on product type. Best treated as a data-integrity bug fixed at the write boundary rather than worked around by every consumer.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions