Problem/Motivation
The AvailabilityManager performs checks to see if a purchasable entity is available. Right now the availability of a purchasable entity is a simple boolean value. TRUE it is available and FALSE it is not available. Both the AvailabilityManagerInterface and AvailabilityCheckerInterface operate this way. Individual AvailabilityCheckerInterface implementations may return NULL if there is no opinion on a purchasable entity's availability.
In #3088597: Add a constraint to purchased_entity which checks the availability manager we discovered it would be much more useful to be able to return a specific error message about why a purchasable entity is not available.
For example, that is how the AccessResult value object works - they are allowed, neutral, or forbidden and optionally have a message.
We also must account for backward compatibility. Anyone using the availability manager will expect truthy values. Returning a value object will now cause logic checks only implement NOT checks to always be false, leading to false positives on availability.
Proposed resolution
Backwards compatibility
We’re not using strict types and return types, so we’re super flexible here! We can update \Drupal\commerce\AvailabilityCheckerInterface::check to allow returning AvailabilityManagerResult|bool|null.
The current check method on the availability manager must convert AvailabilityResult objects into a boolean for return.
We then add a new method that allows leveraging the AvailabilityResult object. For lack of better ideas, let’s call it checkWithReason for now. Because it can return a reason ¯\_(ツ)_/¯.
This will always return an AvailabilityManagerResult. If a checker returns a boolean value, we convert it.
This allows a progressive enhancement without breaking backward compatibility. We can also add some kind of note in both methods whenever we hit a boolean:
get_class($checker) . ' returned a boolean which is deprecated in Commerce 2.x, availability checkers will must return \Drupal\commerce\AvailableResultNeutral or \Drupal\commerce\AvailableResultForbidden in 3.x'
Value object
Create an AvailabilityResult value object to represent the result of the availability check. It should return if the purchasable entity is available and reasonings. Any additional properties can be bikeshed and added later (such as available onhand, etc.)
We can have AvailabilityResultUnavailable, and AvailabilityResultNeutral objects. Unline the access result objects, we will consider neutral to be available. Or we just allow AvailabilityCheckerInterface to return NULL as neutral and then AvailabilityManagerInterface returns an available value object if neutral. Most AvailabilityCheckerInterface would return NULL unless explicitly denying availability.
There is no reason for an "available" class, as we won't halt processing checkers if there is an allowed availability.
We can review the value response as proposed in a previous pull request: https://github.com/drupalcommerce/commerce/blob/329fabe2da6bab6cee33a903...
Remaining tasks
- Decide on the approach to backward compatbility
- Write and communicate a change record for the backward compatibility break
- Commerce Stock is in alpha, but we would need to coordinate these breaking changes.
- Decide if we should have a value object representing "null/neutral" or just boolean available/unavailable
- Bikeshed the name of the value object and its properties
User interface changes
N/A
API changes
\Drupal\commerce\AvailabilityCheckerInterface::check may return a boolean value or an AvailabilityResult
\Drupal\commerce\AvailabilityManagerInterface::check will remain the same. It will convert AvailabilityResults to boolean, if returned.
\Drupal\commerce\AvailabilityManagerInterface now has checkWithReason that returns an AvailabilityResult
Data model changes
N/A
Release notes snippet
| Comment | File | Size | Author |
|---|---|---|---|
| #21 | interdiff-3107547-19-21.txt | 6.64 KB | mglaman |
| #21 | availability_checks-3107547-21.patch | 10.59 KB | mglaman |
Comments
Comment #2
mglamanComment #3
mglamanThis issue summary is a mess. Cleaning up.
Comment #4
mglamanComment #5
mglamanComment #6
mglamanComment #7
mglamanComment #8
olafkarsten commentedDejavu: #2710107: Finalize the availability manager service for more background.
The value object from steveoliver is a good start. As discussed in the comments of his PR, we can omit the min qty and get away with one qty value (see here).
For commerce stock it would be fine, if we could return an own message plus the qty that is available. AvailabilityResult is a good name.
I will reach out for guy schneerson to join the discussion.
Comment #9
mglaman@olafkarsten :) yeah, I'm going to take time to triage that issue. Especially now that we have #3088597: Add a constraint to purchased_entity which checks the availability manager.
Comment #10
olafkarsten commented@mglaman The constraint idea is a really beautiful one. Thanks for getting that done. :)
It makes sense to triage the old issue. The comments on github and the issue are a good source. I think it was a comprehensive discussion back then, that covered a lot of use cases, not only stock. IIRC at that time commerce was in beta, much to do for bojanz and nobody wanted to break BC for commerce stock ... ;)
Comment #11
guy_schneerson commentedHi @mglaman
I did like the idea of the min/max but agree with @olafkarsten that returning an absolute quantity value is good enough for stock.
As far the add to cart functionality no need to worry about stock backward compatibility as it currently does not use the availability manager. As long as the availability manager will support an "allowed quantity" and providing "a reason why not available" message, I think we should be able to easily move over to using it.
One area I don't remember much about is if/how the availability manager behaves on cart and checkout as those have other required behaviors. On the cart page, for example, we may not wish to automatically adjust the quantity, but I suspect this may be using existing stock validation and not down for the availability manager to handle.
Also backing @olafkarsten on the point of issues/2710107 and commerce/pull/593 containing useful information.
Comment #12
ransomweaver commentedI agree with @guy_schneerson about just requiring an allowed quantity. By way of example, I have a commerce_stock_backorderable module that overrides the commerce_stock availabilityChecker as a service. Here is the check function:
As you can see for the available min I am just using 0, so I have no problem with changing min/max to just an allowed quantity.
Comment #13
mglamanWe recently went through this internally. I'll post an updated issue summary.
We're going to avoid the backward compatibility break identified here:
checkwill always return a boolean. We'll provide a new method that returns the result object. Folks can then opt-in and use the new method to receive the result object and be able to return why something isn't available.Comment #14
mglamanComment #15
mglamanComment #16
mglamanHere is a first patch. I didn't expand any tests, yet, or add support for returning the value object.
Comment #17
mglamanComment #18
mglamanReading #11 and #12 again, I wonder about the
availableproperty. It seems like the available quantity is only required when a purchasable entity is not available. It's not something that is always needed.If we did make it a value returned on the availability result, we would have to merge all returned values from availability checkers.
So it seems like on top of
AvailableResultForbidden::$reasonit would be beneficial to haveAvailableResultForbidden::$availableso that the consumer of that result could have more context.Instead of just a reason of "Sorry, there are only 10." It could also provide the available count. This is more useful in a decoupled context as well, so the frontend doesn't need to parse the reason to find the number allowed.
For displaying allowed stock, that should be a more explicit call that doesn't rely on availability checks.
Comment #19
mglamanFix
@trigger_errorto use E_USER_DEPRECATED constant and lint problems. Forgot we require 7.0 minimum, not 7.1.Comment #21
mglamanThis adds the new method and a base test.
Comment #22
mglamanComment #23
mglamanNeeds better description. The interface needs to explain it is preferred to return an AvailableResult but we also allow bool|null.
This needs to get cleaned up to match the proper deprecated messaging. We also need a CR to link to.
Needs a better name.
checkWithResult
checkResult
I couldn't find where this service is actually used and tested. Which means we need a new test, I think.
Comment #24
ransomweaver commentedRegarding #18, the crux of the issue is in AvailabilityOrderProcessor::process
In this function we need to e.g. take a quantity of three for an item and if availabilityManager->check says there is only one available, we need to remove two from the order and tell the customer about it. So this product is "available", just not i the desired quantity. I guess this could be an example of "forbidden" where there is one available, and we do something about that here in ::process
If we look at its current state, https://git.drupalcode.org/project/commerce/-/blob/8.x-2.x/modules/order...
I don't get the feeling that it can do anything like that, which must be why I'm using PR 593
https://github.com/drupalcommerce/commerce/blob/329fabe2da6bab6cee33a903...
So yea, where PR 593 says $availability->getMax() < $order_item->getQuantity() and it triggers a change in quantity, we could instead be responding to AvailableResultForbidden and using AvailableResultForbidden::$available to change the quantity appropriately.
And if its not forbidden, yes, we don't care what the quantity is -- just return from the function.
Comment #25
jsacksick commentedNote that the patch from #3138952: Make the AvailabilityManager order aware also introduces new value objects for returning an availability result.
Comment #26
mglamanFixed in #3138952: Make the AvailabilityManager order aware, which leverages a more robust AvailabilityManager API