Skip to content

[5.x] Trigger event to override inventory locations per order#4276

Open
yoannisj wants to merge 4 commits into
craftcms:5.xfrom
yoannisj:feature/4273-per-order-inventory-locations
Open

[5.x] Trigger event to override inventory locations per order#4276
yoannisj wants to merge 4 commits into
craftcms:5.xfrom
yoannisj:feature/4273-per-order-inventory-locations

Conversation

@yoannisj
Copy link
Copy Markdown

Description

This PR adds the following changes in order to allow modules and plugins to select a subset of the store's inventory locations, when Craft-Commerce is accessing a given purchasable's stock. When this is done in the context of an order, then this selection can be made based on the order's details.

  • trigger a new event craft\commerce\services\InventoryLocations:: EVENT_REGISTER_INVENTORY_LOCATIONS_FOR_PURCHASABLE when accessing a given purchasable's stock, which allows modules and plugins to select a subset of the store’s inventory locations
  • expose the order details in the newly triggered event, when the purchasable's stock is accessed in the context of an order
  • add first argument $order to \craft\commerce\base\Purchasable::getStock()` to aggregate stock in order-specific inventory locations only
  • add first argument $order to \craft\commerce\base\Purchasable::hasStock()` to check for stock in order-specific inventory locations only
  • verify stock available in order-specific inventory locations when checking a purchasable’s availability

Related issues

The changes make it possible for modules or plugins to implement the use-cases described in #4273 and most use-cases from #4247 .

- trigger event when getting the inventory locations for given purchasable `craft\commerce\services\InventoryLocations:: EVENT_REGISTER_INVENTORY_LOCATIONS_FOR_PURCHASABLE ` which allows modules and plugins to select a subset of the store’s inventory locations based on the order details
- add first argument `$order` to \craft\commerce\base\Purchasable::getStock()` to aggregate stock in order-specific inventory locations only
- add first argument `$order` to \craft\commerce\base\Purchasable::hasStock()` to check for stock in order-specific inventory locations only
- verify stock available in order-specific inventory locations when checking a purchasable’s availability
@yoannisj yoannisj requested a review from a team as a code owner April 10, 2026 20:36
@yoannisj
Copy link
Copy Markdown
Author

I just discovered an error in this PR's code, and will push a fix later today.

@yoannisj yoannisj changed the title Trigger event to override inventory locations per order [5.x] Trigger event to override inventory locations per order Apr 21, 2026
@denisyilmaz
Copy link
Copy Markdown

@lukeholder is there any chance to get this released in the next version? Our project is relying on this event listener and we would need to know if this is coming or not.

yoannisj and others added 2 commits May 8, 2026 16:15
- Fix RegisterInventoryLocationsForPurchasableEvent docblock, @SInCE,
  @Property type, dead instanceof check, and trailing newline
- Update the event docblock example on InventoryLocations to reference
  the correct class and constant, and use the setter
- Match the constant value to its name
  (registerInventoryLocationsForPurchasable)
- Skip event construction when no handlers are attached
- Restore brace style and remove duplicate $order assignment in
  Purchasable::populateLineItem
- Drop the unused $order parameter from
  Inventory::getInventoryItemByPurchasable and stray blank line
- Normalize Order|null to ?Order, fix @SInCE to 5.6.5 on new symbols
@lukeholder
Copy link
Copy Markdown
Member

lukeholder commented Jun 3, 2026

Hi @yoannisj — really appreciate this, it potentially will unlock #4273/#4247.

I pushed a follow-up commit (ab492c8) cleaning up the event class, docblocks, brace style, the dead instanceof check, the _stockForOrders @since, the placeholder PHPDoc, and gated the event behind hasEventHandlers() so it's a no-op when nothing is listening. I also renamed the event value to registerInventoryLocationsForPurchasable to match the constant name and dropped the unused $order param from Inventory::getInventoryItemByPurchasable(). phpstan and ecs both pass on the changed files.

Before we merge there are a few design-level questions I'd love your take on, since they could cause subtle issues in production:

  1. Per-order stock cache invalidation. Purchasable::$_stockForOrders is keyed only by $order->id. If anything about the order changes within a request (shipping address, customer, etc.) and a listener keys its location filter on that data, the second call returns the stock computed from the earlier state. Could we either invalidate this cache on order recalculation, or key it on a hash that includes the relevant order state?

  2. Unsaved carts fall through to the store-wide cache. For a brand-new cart, $order?->id is null, so getStock($order) takes the $this->_stock branch and returns store-wide stock (event never fires for that lookup). Should we still run the event-aware path when we have an order but no id, and just skip caching?

  3. PurchasableInterface consistency. Purchasables::isPurchasableAvailable() only calls getStock($order) when the purchasable is a Purchasable; third-party PurchasableInterface implementations fall through to ArrayHelper::getValue($purchasable, 'stock') (store-wide). Should getStock(?Order $order = null) be added to PurchasableInterface so it works consistently across implementations?

  4. BC for subclasses. Adding ?Order $order = null to public getStock(), hasStock(), and getInventoryLevels() is BC-safe for callers but LSP-breaks any subclass that overrides them (custom Variants, etc.). It'd be worth a heads-up in the upgrade notes so plugin/site authors update their signatures alongside this release. This might mean this would need to wait for a breaking change.

  5. Order-completion drift / the @todo. The // @todo: Fix the list of inventory locations on order completion… you left in getInventoryLocationsForPurchasable (now removed in my cleanup, but the concern stands) is real: Inventory::orderCompleteHandler now uses $purchasable->getInventoryLevels($order) for committed-stock selection. If a listener's location selection depends on order state that changed between cart and completion, deductions could land in a different location than the cart was validated against. Persisting the resolved location list on the order at completion (as your todo suggested) would close this. Can we look to add this?

1, 3, and 5 feel most important to address before this goes out. I also think it should probably be in a major version, not in the next release. Let me know your thoughts and I'm happy to help land any of the remaining changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants