Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b67737d
Make SimpleCoordinator inventory_units keyword arg
sofiabesenski4 May 14, 2026
8b98fa7
Port SimpleCoordinator code into new Coordinator
sofiabesenski4 May 14, 2026
dbb8066
Update to use the @ instance variable syntax
sofiabesenski4 May 14, 2026
b762d99
Introduce InventoryUnit Stock middleware
sofiabesenski4 May 14, 2026
f861dcd
Delegate InventoryUnit grouping to middleware
sofiabesenski4 May 14, 2026
7b98c26
Delegate Stock StockLocation logic to middleware
sofiabesenski4 May 14, 2026
a4a3a6d
Delegate Desired logic to middleware
sofiabesenski4 May 14, 2026
c6f14a4
Delegate Availability logic to middleware
sofiabesenski4 May 14, 2026
2feb441
Delegate Allocate logic to middleware
sofiabesenski4 May 14, 2026
26e9893
Delegate Package logic to middleware
sofiabesenski4 May 14, 2026
ca9ae0d
Delegate Shipment logic to middleware
sofiabesenski4 May 14, 2026
cec1968
Add Runner and Context for stock coordination middleware
sofiabesenski4 May 21, 2026
a5893c3
Add Spree::Core::ClassConstantizer::List for ordered class registries
jarednorman Apr 24, 2026
898446b
Move Spree::Core::ClassConstantizer::Set to its own file
jarednorman Apr 24, 2026
c4a3a33
Add add_class_list helper to Spree::Core::EnvironmentExtension
jarednorman Apr 24, 2026
d449631
Register stock coordination middleware list in config
sofiabesenski4 May 21, 2026
bb9e0ed
Delegate middleware chaining to Runner via config
sofiabesenski4 May 21, 2026
3028892
Extract generic Spree::MiddlewareRunner
sofiabesenski4 May 21, 2026
8547546
Move desired and availability to computed methods on Context
sofiabesenski4 May 21, 2026
558d149
WIP: Maybe remove redundant assignment
sofiabesenski4 Jun 2, 2026
27c1073
WIP: Fix linting
sofiabesenski4 Jun 2, 2026
851a690
FIXUP Simplify coordinator logic
sofiabesenski4 Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion core/app/models/spree/exchange.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ def display_amount

def perform!
begin
shipments = Spree::Config.stock.coordinator_class.new(@order, @reimbursement_objects.map(&:build_exchange_inventory_unit)).shipments
shipments = Spree::Config.stock.coordinator_class.new(
@order,
inventory_units: @reimbursement_objects.map(&:build_exchange_inventory_unit)
).shipments
rescue Spree::Order::InsufficientStock
raise UnableToCreateShipments.new("Could not generate shipments for all items. Out of stock?")
end
Expand Down
4 changes: 2 additions & 2 deletions core/app/models/spree/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,9 @@ def create_proposed_shipments
end

def create_shipments_for_line_item(line_item)
units = Spree::Config.stock.inventory_unit_builder_class.new(self).missing_units_for_line_item(line_item)
inventory_units = Spree::Config.stock.inventory_unit_builder_class.new(self).missing_units_for_line_item(line_item)

Spree::Config.stock.coordinator_class.new(self, units).shipments.each do |shipment|
Spree::Config.stock.coordinator_class.new(self, inventory_units:).shipments.each do |shipment|
shipments << shipment
end
end
Expand Down
98 changes: 98 additions & 0 deletions core/app/models/spree/stock/coordinator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true

module Spree
module Stock
class Coordinator
def initialize(order, inventory_units: nil)
context = {order:, inventory_units:}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once of the goals of the initial work to make this more flexible was to allow arbitrary context to be passed into the coordinator that can be used to guide it's behavior.

Instead of turning inventory_units: into a keyword argument, what about making order and inventory_units optional, and adding a context keyword argument that can be passed instead that contains both attributes? That would allow arbitrary extension of the arguments passed into the stock coordinator

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea, but we are dealing with the existing API, so that would be a trickier change to make.

@order = order

Middleware::InventoryUnit.new.call(context)
@inventory_units = context[:inventory_units]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand @jarednorman's point correctly, I agree we should have getters/setters for the methods on context to reduce obfuscation/add some runtime guarantees.

Maybe make context an object that extends hash? then custom attributes can be set? I'm not sure, this might be overkill and a hash like it is rn might be fine

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context object should definitely be a PORO, not something that extends hash. We want something that exposes an API that reflects how it's meant to be used.


@splitters = Spree::Config.environment.stock_splitters

@filtered_stock_locations = Spree::Config.stock.location_filter_class.new(load_stock_locations, order).filter
sorted_stock_locations = Spree::Config.stock.location_sorter_class.new(@filtered_stock_locations).sort
@stock_locations = sorted_stock_locations

@desired = Spree::StockQuantities.new(@inventory_units_by_variant.transform_values(&:count))
@availability = Spree::Stock::Availability.new(
variants: @desired.variants,
stock_locations: @stock_locations
)

@allocator = Spree::Config.stock.allocator_class.new(@availability)
end

def shipments
@shipments ||= begin
@packages = build_packages
shipments = build_shipments

# Make sure we don't add the proposed shipments to the order
@order.shipments = @order.shipments - shipments

shipments
end
end

private

def load_stock_locations
Spree::StockLocation.all
end

def build_shipments
# Turn the Stock::Packages into a Shipment with rates
@packages.map do |package|
shipment = package.shipment = package.to_shipment
shipment.shipping_rates = Spree::Config.stock.estimator_class.new.shipping_rates(package)
shipment
end
end

def build_packages
# Allocate any available on hand inventory and remaining desired inventory from backorders
on_hand_packages, backordered_packages, leftover = @allocator.allocate_inventory(@desired)

raise Spree::Order::InsufficientStock.new(items: leftover.quantities) unless leftover.empty?

packages = @stock_locations.map do |stock_location|
# Combine on_hand and backorders into a single package per-location
on_hand = on_hand_packages[stock_location.id] || Spree::StockQuantities.new
backordered = backordered_packages[stock_location.id] || Spree::StockQuantities.new

# Skip this location it has no inventory
next if on_hand.empty? && backordered.empty?

# Turn our raw quantities into a Stock::Package
package = Spree::Stock::Package.new(stock_location)
package.add_multiple(get_units(on_hand), :on_hand)
package.add_multiple(get_units(backordered), :backordered)

package
end.compact

# Split the packages
split_packages(packages)
end

def split_packages(initial_packages)
initial_packages.flat_map do |initial_package|
stock_location = initial_package.stock_location
Spree::Stock::SplitterChain.new(stock_location, @splitters).split([initial_package])
end
end

def get_units(quantities)
# Change our raw quantities back into inventory units
quantities.flat_map do |variant, quantity|
@inventory_units_by_variant[variant].shift(quantity)
end
end

end
end
end

15 changes: 15 additions & 0 deletions core/app/models/spree/stock/middleware/inventory_unit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Spree
module Stock
module Middleware
class InventoryUnit
def call(context)
order = context[:order]

context[:inventory_units] = context[:inventory_units] ||
Spree::Config.stock.inventory_unit_builder_class.new(order).units

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as I mentioned in a previous comment, WDYT of getting rid of this configuration altogether in favor of putting the work in this class? Reducing obfuscation, but at the cost of a potential harder migration?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth entertaining, at least.

end
end
end
end
end

9 changes: 8 additions & 1 deletion core/app/models/spree/stock/simple_coordinator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ class SimpleCoordinator
:filtered_stock_locations, :inventory_units_by_variant, :desired,
:availability, :allocator, :packages

def initialize(order, inventory_units = nil)
def initialize(order, inventory_units_deprecated = nil, inventory_units: nil)
if inventory_units_deprecated
Spree.deprecator.warn "Using the `inventory_units` positional " \
"argument is deprecated in favor of using the keyword argument. "

inventory_units ||= inventory_units_deprecated
end

@order = order
@inventory_units =
inventory_units || Spree::Config.stock.inventory_unit_builder_class.new(order).units
Expand Down
Loading