magento / magento/inventory

Composite parent stock status is not maintained in multi source mode, and the three obvious fixes each break something else

Open
#3,466 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Issue: ready for confirmation
Dominant language
PHP
Stars
357
Forks
262
PR merge metrics
No merged PRs in 30d

Description

### Summary

Composite parent products (configurable, bundle, grouped) are not maintained in multi source mode, and the three obvious ways to fix that each break something else. I have measured all three against `2.4-develop` + `magento/inventory@develop` and am filing the evidence rather than a pull request, because a correct fix looks like a design decision rather than a bug fix.

### The gap

Every composite parent stock recompute is gated on `IsSingleSourceMode`:

* `InventoryCatalog/Plugin/InventoryApi/UpdateCompositeProductStockStatusOnSourceItemsSave::afterExecute()`
* `InventoryCatalog/Plugin/Inventory/UpdateCompositeProductStockStatusOnDecrementSourceItemQty::afterExecute()`
* `InventoryCatalog/Model/UpdateInventory::execute()`
* `InventoryCatalog/Plugin/CatalogInventory/Observer/ParentItemProcessor/SkipParentItemProcessorOnMultipleSourceMode::aroundProcess()`

`IsSingleSourceMode` counts *enabled* sources, so enabling a second source stops composite parent maintenance entirely, even if that source is assigned to no stock, website or product.

### Why the obvious fixes do not work

**1. Remove the gate.** This reintroduces #3350. Measured with a configurable whose children are out of stock on the `default` source only, while the `eu` sources backing stock `10` still hold stock:

| Build | Parent salable in stock 10 |
|---|---|
| Unmodified `develop` | yes |
| Gate removed | **no — #3350 reproduced** |

This matches the original reporter's own note that the later plugin was added "with additional checks that single source mode and a single store are being used". The gate *is* the fix for #3350.

**2. Remove the gate and stop the legacy row vetoing non-default stocks.** The three composite indexer `SelectBuilder`s fold the parent's default-stock `cataloginventory_stock_item` row into every non-default stock's `is_salable`. Lifting that veto breaks `InventoryCatalog/Test/Integration/CompositeProductReindexOnNonDefaultStockTest`, which asserts for all three composite types that an admin setting `is_in_stock = false` must propagate to a non-default stock. That behaviour looks deliberate.

**3. Lift the veto only when the row records a merchant decision.** This is what I tried first, using `stock_status_changed_auto = 0` as the signal. It does not work, because the flag does not carry that meaning. After an admin sets out of stock via `StockRegistryInterface::updateStockItemBySku()`, the row is, for all three composite types:

```
{"is_in_stock": "0", "stock_status_changed_auto": "1"}
```

The flag is `1` both when core moved the status itself and when a human did, so nothing in the row distinguishes them.

### Root cause

`ChangeParentStockStatus` derives the parent from **default-scope** legacy stock items, exactly as #3350's reporter put it: it "does not take the inventory setup into account at all". A composite parent's salability is inherently per-stock, but its legacy stock item is a single default-stock value, and there is no signal separating an automatic status change from a merchant decision. Any correct fix therefore needs either a stock-aware parent recompute or a new signal — both larger than a bug fix.

### Steps to reproduce the gap

1. Set up a second source and a second stock, with a website assigned to it.
2. Create a configurable product with two simple variations.
3. Assign the variations to the non-default source with stock.
4. Change a variation's source item quantity or status.
5. Observe that the parent's `cataloginventory_stock_item` row is not recomputed, because the recompute is gated off.

### Suggested next step

A maintainer decision on which of these is intended:

1. The parent legacy row stays default-stock only, and non-default stock salability is derived purely from the index — which requires deciding what should happen to an admin out-of-stock on the parent.
2. Parent salability becomes stock-aware, with per-stock parent status.
3. The current behaviour is intended and the gap in multi source mode is accepted, in which case documenting it would help.

### Attached

I have opened a pull request adding two tests that pass against `develop` unchanged: a regression guard that fails the moment the gate is lifted (there is no such guard today), and unit coverage of the gated plugin.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the four composite stock recompute entry points named in the issue, then read ChangeParentStockStatus and the three composite indexer SelectBuilders. Run InventoryCatalog/Test/Integration/CompositeProductReindexOnNonDefaultStockTest and review the attached regression and gated-plugin tests. Done requires a maintainer decision on stock-aware parent status, merchant overrides, or documenting the accepted gap.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.