August 04, 2022

An Update on Bundles in Solidus

I've worked with a number of stores that either use the solidus_product_assembly extension or have some kind of custom bundle/pack/assembly system. I've been working to improve Solidus' support for bundles* and wanted to provide a small update on that work, since I've been deep in the code for store that has a custom bundle system.

All of these systems fundamentally do the same thing: certain variants don't map one to one to the fulfilled items. When you purchase a unit of one of these variants, you are charged for that variant in your cart, but receive the items that make up that bundle in your shipment. To put it in technical Solidus terms, line items containing these special variants generate inventory units of different variant than the line item variant.

Supporting this requires some Solidus customizations. Fortunately, extension points exist for most of these customizations. I made the three main extension points configurable in a recent pull request.

These three extension points are documented here, but they don't cover everything.

Normally when you go through checkout, when you reach the delivery step, the checkout controller looks at the shipments generated for the order and presents you with items that didn't make it into shipments. This could because they are not shippable to your address due to their shipping category or some other detail of the shipping configuration. When you advance passed that step to payment, those items are removed.

Bundles make it so the stock differentiator can't tell which items are missing, because the line items don't match up directly with variants in the shipments. Unfortunately, solidus_product_assembly has taken the approach of remove all that functionality. It introduces a Deface override that removes the "Unshippable Items" section and nukes the method that removes those items from your order. This means that if your store configuration allows for there to be unshippable items, the customer won't be told they are there, they won't be removed, and the customer will be charged for them, but never receive them.

This is a totally solvable problem. The simplest solution would be to make the stock differentiator a configurable class that can be replaced by individual stores or extensions, like many of the other classes in the Spree::Stock namespace, though I think we can probably do better.

If you were basing a custom implementation of bundles off of solidus_product_assembly, you might see this override and think that you would have to do something similar. Fortunately, you do not. Solidus v2.10.4 included this change which ensures that unstocking inventory works correctly, regardless if the line item match up directly with the inventory units. This is great. The unstocking should only care about inventory units and their variants.

This leads me to another discovery; there are more places we can make Solidus handle a bundles. The default availability validator currently contains this code, which assumes that line items always match the inventory units. This isn't necessary, and I've started a discussion regarding fixing this. The fact that we only actually need to know about bundles in one of the two availability validation cases makes me think we might be able to make things even simpler.

Looking at all this had led me to a conclusion: I think it should be possible to create a single extension point that handles the mapping of line items/variants to their composite parts and make all the code I've talked about in here rely on that single point, while maintaining all existing functionality. I'll flesh out a proposal for how that might work later, but all this work has convinced me that it's possible.

*For the purposes of this article, "bundle", "pack", and "assembly" all mean the same thing.