Skip to content

Introduce new middleware stock coordinator#6484

Draft
sofiabesenski4 wants to merge 22 commits into
solidusio:mainfrom
SuperGoodSoft:introduce-new-middleware-stock-coordinator
Draft

Introduce new middleware stock coordinator#6484
sofiabesenski4 wants to merge 22 commits into
solidusio:mainfrom
SuperGoodSoft:introduce-new-middleware-stock-coordinator

Conversation

@sofiabesenski4

@sofiabesenski4 sofiabesenski4 commented May 14, 2026

Copy link
Copy Markdown
Contributor

WIP

This refactoring of the existing SimpleCoordinator leaves it in a state that retains the original functionality, and higher level shape, but encapsulates the lower level logic into middleware components.

Next steps will be to reconsider how the middleware components are encapsulated, setting up the middleware orchestration, and adding unit specs to prevent regressions.

Checklist

Check out our PR guidelines for more details.

The following are mandatory for all PRs:

The following are not always needed:

  • 📖 I have updated the README to account for my changes.
  • 📑 I have documented new code with YARD.
  • 🛣️ I have opened a PR to update the guides.
  • ✅ I have added automated tests to cover my changes.
  • 📸 I have attached screenshots to demo visual changes.

We want to support additional keyword arguments to this class and a
positional argument interferes with this.

Co-authored-by: Jared Norman <jared@super.gd>
Co-authored-by: Adam Mueller <adam@super.gd>
Co-authored-by: Noah Silvera <noah@super.gd>
@github-actions github-actions Bot added the changelog:solidus_core Changes to the solidus_core gem label May 14, 2026
@codecov

codecov Bot commented May 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.46%. Comparing base (a0746e6) to head (851a690).
⚠️ Report is 15 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6484      +/-   ##
==========================================
+ Coverage   89.68%   93.46%   +3.78%     
==========================================
  Files         991      534     -457     
  Lines       20832    10532   -10300     
==========================================
- Hits        18683     9844    -8839     
+ Misses       2149      688    -1461     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Co-authored-by: Noah Silvera <noah@super.gd>
Co-authored-by: Adam Mueller <adam@super.gd>
@sofiabesenski4 sofiabesenski4 force-pushed the introduce-new-middleware-stock-coordinator branch from 6b44d32 to 4a86af5 Compare May 14, 2026 20:34
sofiabesenski4 and others added 9 commits May 14, 2026 13:37
Co-authored-by: Noah Silvera <noah@super.gd>
Co-authored-by: Adam Mueller <adam@super.gd>
This starts the pattern of delegating low level operations to middleware
objects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sofiabesenski4 sofiabesenski4 force-pushed the introduce-new-middleware-stock-coordinator branch from 4a86af5 to ca9ae0d Compare May 14, 2026 20:38
Comment on lines +6 to +9
context[:availability] = Spree::Stock::Availability.new(
variants: context[:desired].variants,
stock_locations: context[:stock_locations]
)

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.

This is definitely an example of how overusing this approach makes the code worse. This has seriously obfuscated the dependency chain of the availability and desired variants objects. If we have some dependent computed stuff like this, maybe we just make this methods on the context object or something? It's worth considering this further.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed! I didn't put too much thought into how things shook out entirely at this point in the draft. Will definitely revisit before opening for review.

sofiabesenski4 and others added 10 commits May 21, 2026 15:00
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These are pure derived values with tight dependency chains, not
independent pipeline stages. Expressing them as memoized methods
on the Context makes their relationships explicit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Will fix up later
@sofiabesenski4

Copy link
Copy Markdown
Contributor Author

@jarednorman ignore the tedious back + forth refactoring that is happening here. I will fixup a lot of these commits and make the history cohesive.

Is this shape in the final diff more aligned with the pattern you were thinking about?

@jarednorman jarednorman left a comment

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.

Yeah, this is much closer. I think we can do even better than this, but this is the right direction, I think.

I do think the "shape" of this problem fits less well than with the order recalculator, so I want to be careful about this change. We may find a different design that fits this part of the code better (and solves the original problem of parameterization) while refactoring this, and that's totally fine.

@jarednorman jarednorman changed the title WIP: Introduce new middleware stock coordinator Introduce new middleware stock coordinator Jun 4, 2026

@Noah-Silvera Noah-Silvera left a comment

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.

Hey Sofia! I took a pass on this and had some ideas around how the middleware pattern might make more sense for this specific use case!

In summary, IMO the middleware pattern will only makes things easier in this case to understand and override if the following is true:

  • It reduces the obfuscated relationships and tracing rabbit holes introduced by configurable classes
  • It produces structured outputs at each step that are easy to locate and understand in the codebase without having to fully understand the previous middleware
  • The middleware are tiny, iterative pieces that can be individually replaced & injected between to allow for maximum flexibility in the building process.

I added some comments to this end, LMK what you think of them! Curious about your thoughts and if we can come to some solution in between!

Dropping the configurable classes entirely in favour of new code/a new pattern might be a dealbreaker, but I'm not sure! cc: @jarednorman @adammathys for thoughts too

"Spree::Stock::Middleware::Package",
"Spree::Stock::Middleware::Shipment"
]
class_name_attribute :estimator_class, default: "::Spree::Stock::Estimator"

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.

My first thought is we should let the middleware list be overridden, or use class name attributes to choose which classes exist in the middleware, but not both.

Using both just increases the possible matrix of overrides that users can configure, and makes the code path hard to trace

My gut is that this should just be a "opt in"feature, and you have to port your existing overrides to middlewares if you want to use. I think we should just get rid of configurable classes being used inside middleware or monkey patching middleware; it's better to have smaller middlewares that users can just override wholesale

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?

inventory_units || Spree::Config.stock.inventory_unit_builder_class.new(order).units

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

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

sorted_stock_locations = Spree::Config.stock.location_sorter_class.new(@filtered_stock_locations).sort
@stock_locations = sorted_stock_locations

@inventory_units_by_variant = @inventory_units.group_by(&:variant)

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.

absolute nit of a nit - this should be in the next commit right?

end
end

def split_packages(initial_packages)

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.

IMO splitter chain should be a separate middleware that splits packages after they are created

def call(context)
context[:shipments] = context[:packages].map do |package|
shipment = package.shipment = package.to_shipment
shipment.shipping_rates = Spree::Config.stock.estimator_class.new.shipping_rates(package)

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.

I'm still leaning towards inlining these classes and having users replace Spree::Stock::Middleware::BuildShipment in the middleware list if they want to change behavior

shipment
end

yield context

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.

Love the yield pattern!

@@ -11,8 +11,6 @@ class StockConfiguration < Spree::Preferences::Configuration
"Spree::Stock::Middleware::InventoryUnit",

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.

One of the things that this way of defining middleware obfuscates is the outputs of each class. Since the outputs could theoretically be anything, it be useful to define them to make the relationships easier to understand. It would also allow us to define getter and setter methods on the defined outputs

WDYT? I'm not sold on this, but it was just an idea
e.g.

  middleware = [
    {
        class: "Spree::Stock::Middleware::InventoryUnit",
        outputs: [:inventory_units]
    },
    {
        class: "Spree::Stock::Middleware::InventoryUnitGroup",
        outputs: [:inventory_units_by_variant]
    },
    {
        class: "Spree::Stock::Middleware::LoadStockLocations",
        outputs: [:stock_locations]
    },
    {
        class: "Spree::Stock::Middleware::SortStockLocations",
        outputs: [:stock_locations]
    },
    ...
]

@@ -6,13 +6,24 @@ class Coordinator
class Context

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.

TBH I prefer them as independent middleware pipeline stages

These are pure derived values with tight dependency chains, not
independent pipeline stages.

You are correct! But also, this is true for the whole pipeline! Almost every step is essential, and produces output required by the next. I'd rather implicitly assume that in the chain, or use a more strict configuration format (even defining inputs in addition to outputs), and have smaller middleware, easier to override, replace, or insert other middleware in between

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

Labels

changelog:solidus_core Changes to the solidus_core gem

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants