Followup to #2511266: Create a service for determining variation availability
PR: https://github.com/drupalcommerce/commerce/pull/593
We need to use the AvailabilityManager to check a product at these events:
- On product load - disable add to cart if not available
- On adding to cart - do not add to cart if not available
- On checkout - do not allow checkout if not available
- This is now triggering but needs work.
This is blocking development of the stock module see #2709939: Stock availability checker service implements Commerce AvailabilityCheckerInterface
Commerce_stock() should handle checking for specific quantities of product - if you don't use that module then the availability check should always succeed unless another module causes the check to fail.
To do
the below are all the interactions between the store and availability checker. Some of those can possibly be moved to contrib .
On adding to cart
Do not add to cart if not available- Do not show the product added message if not available (need to be in commerce)
- Show a Product was not added because it is unavailable message
- Allow for the AvailabilityChecker to specify a message/action.
On product load
- Disable add to cart button if not available (Ideally be in commerce)
- Allow AvailabilityChecker to specify button state: text, enabled/disabled,action if enabled (specifying the text would do as a start)
On checkout
- Do not allow entering checkout if not available - redirect to cart
- AvailabilityChecker to specify the unavailable text
- Check the AvailabilityChecker for all products before processing payment to make sure all is in stock before taking payment.
Comments
Comment #2
jonas139 commentedComment #3
agoradesign commentedWhat if you want to allow purchasing out-of-stock products, but only show info messages and/or calculate a different delivery time, etc?
For that requirement, we could do two things:
Both seem acceptable solutions to me, but we should think of, which one's the better way, before we implement this...
Comment #4
jonas139 commentedTrue, I guess this needs some more thinking before implementing it.
Comment #5
jonas139 commentedComment #6
guy_schneerson commentedMaybe we need a second service: availability actions or extending the existing to handle the actions.
Comment #7
jonas139 commentedI guess the option to sell of out-of-stock products is a good case.
Maybe also add the option to either recalculate the availability of the product or just display a (custom) message and disable the add to cart button?
Comment #8
agoradesign commentedOr maybe we can just do it as simple as proposed in the issue description as well.
The philosophical question here is to clarify the term "available": is an out-of-stock product on a site, which wants to allow to sell out-of-stock product, "available" or not?
A slightly different thing is, if you want to output the availability information on the product on the page. Depending on how granular the information is, you want to display, asking the availabilty manager could be enough or not. If you want to just display "Available: yes/no", then you could ask the availabilty manager as well. But as soon as you want to display e.g. traffic lights, where yellow means "low on stock", or want to simply display "available" down to a specific amount, and for the rest something like "less than xx items on stock", etc, you would have to implement your own logic anyway.
So, if you e.g. want to sell out-of-stock products and display the "less than xx items on stock" information, you could use the "always in stock" availability checker anyway, because you have to implement the logic to display that message youself anyway
Comment #9
guy_schneerson commentedAlso need to be able to repurpose the button for things like "Let me know wen back in stock" and "Reserve product".
If we can give the task to a plugable service we just need to implement the basic use case and live it to contrib.
Comment #10
jonas139 commentedMy idea was that we just display the yes/no message for the availability. The traffic lights (or something similar) would be a nice-to-have feature but is not really the original issue (I guess). Furthermore I would also add the option to let the user determine whether or not the add to cart button needs to be disabled or not.
At the other hand, I really like the idea for the custom availability checker and the 'product reservation/keep me posted' service. The question is, is this really necessary for default commerce behavior?
Comment #11
bojanz commentedPreordering and the related logic (notifications) is definitely contrib material.
Our concern here is to ensure that whatever logic we add can easily be swapped (service sounds good).
Comment #12
guy_schneerson commentedwould be good to get going with a patch that simply disables the "add to cart" button and sets the text to "currently unavailable" to get things going and help test the stock and any other availability modules. We can then look at a full implementation.
the check should be:
Not sure how to interact with the cart form so if anyone knows please help.
Also a second check on validating the submitted form would be good (replacing 1 with the requested quantity).
Once we figured out the mechanics of working with the cart form, it will be easier to develop more flexible business logic.
Comment #13
steveoliver commentedUpdated issue summary based on comment #12 on add to cart form validate() checking, and adding idea about holding/decrementing available qty when added to cart.
Comment #14
kscheirerUpdated issue description. Any guidance on proper implementation?
Comment #15
kscheirerComment #16
guy_schneerson commentedThe AvailabilityManager is now triggered by the add to cart form and if the product is out of stock the item is not added (I think it may be added and then removed).
However the form is not aware of the AvailabilityManager and both AvailabilityManager->check() & AvailabilityOrderProcessor->process() don't store the result of the check so even if the form had access to the AvailabilityManager the result of the check would not be available.
The result of all this is that the user gets the message "Product X added to your cart." although it wasn't.
Comment #17
guy_schneerson commentedComment #18
guy_schneerson commentedComment #19
guy_schneerson commentedI updated the description the Issue summary to include all the points of interaction between the store and the AvailabilityManager. Some of those may be handled by contrib.
Comment #20
guy_schneerson commentedOne more observation: if we have products in our cart that are no longer available and we add a new product to the cart the availability check is run on the cart(order) and those products will be removed without prompting the user.
Comment #21
bojanz commentedYes, that was the Commerce 1.x behavior too (unsure if Commerce 1.x showed a message along with that, should be checked)
Comment #22
guy_schneerson commentedHi @bojanz Not sure I follow. Did Commerce 1.x have something like the availability manager?
The approach of stock was:
on the cart form only check the viewed/added product
on checkout: check the cart and if any items are out of stock send user to the cart page with a message explaining whats out of stock.
Not sure what is the perfect UI but at some point I am sure one of us will want a wait list type functionality and things disappearing from a cart may make this harder.
Comment #23
guy_schneerson commentedOne more issue. using stock to illustrate.
Product X has quantity 2.
User adds product x to basket
-> AvailabilityManager calls check() with $quantity = 1
-> StockAvailabilityChecker return TRUE
Cart now has 1 of Product X
User adds one more product x to basket
-> AvailabilityManager calls check() with $quantity = 2
-> StockAvailabilityChecker return TRUE
Cart now has 2 of Product X
User adds one more product x to basket
-> AvailabilityManager calls check() with $quantity = 3
-> StockAvailabilityChecker return FALSE (as it only has 2)
-> AvailabilityOrderProcessor removes product X as it was told by the availabilityManager that it is not available.
Cart now has 0 of Product X
I think that we should consider returning the number of available units instead of TRUE/FALSE.
Comment #24
steveoliver commentedFor the points outlined above, I don't think a boolean response to check() will work. Instead of a check() method, what about a getAvailability method, which returns an AvailabilityResponse value object that contains $entity, $context, $minimum, and $maximum values.
By checking that the requested quantity is still within range of availability, AvailabilityOrderProcessor::process() can accurately know when to remove the item from the order, or to update the quantity to the max available.
This could use some more thought around how to deal with NULL / no opinion, but see POC here: https://github.com/drupalcommerce/commerce/pull/593
Comment #25
guy_schneerson commented@steveoliver that will resolve most of the issues.
The only remaining issue will be:
Comment #26
steveoliver commentedComment #27
steveoliver commentedRemaining tasks:
- Finish building the availability response classes
- Work out the line between commerce core and commerce contrib for configurable actions/messaging based on ^
Work is happening at https://github.com/drupalcommerce/commerce/pull/593 -- I'll resume today or tomorrow.
Comment #28
ransomweaver commentedI'd like to contribute to shaping the discussion of how backorder is going to work with commerce_stock, as its a critical need for the app I'm building at the moment.
Use cases are always good, so I will describe the needed functionality for us, and how I have gone about it so far.
For our app--
* variation has stock
* variation may or may not be sold with 0 stock (backorder/custom order) depending on some setting for the variation.
* variations that may not be purchased, due to 0 stock and backorder not allowed:
- have add-to-cart button disabled
- display "availability: out of stock"
- are filtered out from a view of available products
* a variation that can be backordered needs to display a custom "Availability" string, supplied by the product owner on a per-variation basis. E.g. "ships in 3-4 weeks"
How I have built this so far:
- local stock service
- commerce_stock field_stock on variation, uses stock api to add stock to variations via UI or custom csv processor
- allow_no_stock_sale (backorder) bool field on variation
- no_stock_sale_message textfield on variation
- custom StockFormatter to display e.g. " 7 remaining" or "out of stock" or "ships in 3 weeks" depending on the custom fields above
Of course, the above does not actually do anything about disabling the cart button, views filtering or getting a back-orderable product into the cart.
What I would like to know/understand is how can I integrate the above with the developing Stock availability service, or how to change my approach to integrate with that service.
Comment #29
steveoliver commented@ransomweaver -- I think the approach you've taken so far makes sense--you'll need a custom formatter for the stock field if you choose to show it, and some form alteration on the add to cart form 'submit' button.
I've made progress on the availability response PR here: https://github.com/drupalcommerce/commerce/pull/593/files
AvailabilityManager::check returns a (new) AvailabilityResponseInterface (one of AvailabilityResponseNeutral, AvailabilityResponseAvailable, or AvailabilityResponseUnavailable). Each response has these six methods: isNeutral(), isAvailable(), and isUnavailable, and getMin(), getMax(), and getReason. With these methods any response from the Availability Manager service can be used to make informed decisions in about what to do or show in case of needing to remove or change the quantity of any item under consideration.
The implications of the responses, i.e. to disable the add to cart form, show other actions, etc. should come from each module(s) use cases and configuration. PR 593 makes the add to cart form able to be altered by e.g. commerce_stock or your custom implementation.
This issue should be concerned only with the API of the availability manager service -- please review the PR and let me know your thoughts.
For reference, here's what the commerce_stock local storage module might do to disable the add to cart form when an item is unavailable for a qty of 1:
Comment #30
steveoliver commentedThis is now passing Travis CI builds and is ready for a real review. See https://github.com/drupalcommerce/commerce/pull/593.
Copied from the latest PR review comment:
Comment #31
guy_schneerson commentedI really like the idea of the range although I think the min/max should be reversed see my note.
One more potential complication is that some modules may have an opinion only on the min or max but I suppose in such case they can just return an obscene large number for max and something like 0.0001 for min (as we need to support decimal quotients).
I can imaging cases where we have multiple stock fulfillment services that will have the other requirements but I think we should address those by:
a) in the stock module
b) custom/contrib modules inheriting from stock and providing the additional min/max calculation.
Comment #32
steveoliver commentedI agree on the min/max() swap Guy recommends. While working on this, as you can see by my thinking about min/max before being convinced by Guy, I have been thinking of Availability in the context of stock only ... but of course availability responses can come from other considerations, such as business rules for min/max per order, etc.
Comment #33
guy_schneerson commentedHi @ransomweaver I think we need to limit the scope of the availability manager. so looks like we are using an and for available/not available. So if one of the checkers says its not available then its not and an intersection of min/max (Steve is making this change).
The way i think you should address your needs is by inheriting from the commerce stock, that way you can create a "Local stock with back order" service. That will allow you to control the order of checks using your own business logic with full access to the inherited functionality.
would be nice to see your code if you manage to make it general. Maybe can be added to the stock module as a sub module or a another contrib.
At some point I think we will add an option to reserve stock on an "add to cart" this will need a cron job for returning the stock after a given time.
We should move most of this discussion to the stock issue Q.
Comment #34
ransomweaver commentedOk @guy, I was going to work on my checker today, but I will cease that until you guys are finished, and approach it as building my own local stock service.
Comment #35
guy_schneerson commented@ransomweaver cool, I think that once the availability manager service is finalize (this issue) the part you need to override should not change and you can start your work. Let us know how you get on.
Comment #36
ransomweaver commentedI have made a sandbox module called commerce_stock_backorderable here: https://www.drupal.org/sandbox/ransomweaver/2848270
I find it works as needed with not other alterations to commerce_stock that i can see.
Comment #37
londova commented@ransomweaver
Could you please provide the link to download the module and test it?
Thanks
Comment #38
ransomweaver commented@londova, how do you mean? Its a sandbox project, is there some other way to link to a sandbox project?
Comment #39
heddnTagging. This is blocking #2613264: Drupal 8 port of Commerce Stock. Leaving at major, since this a blocker.
Comment #40
mglamanPut PR link in summary, took a minute to find it.
Comment #41
mglamanI did a cursory review. I think it looks good so far. Just a constants fail on PHP < 7. Some of the visibility changes on methods I'm unsure of. I think users altering the form _that much_ should extend it and implement their class. This is what I have done on three client sites, now.
Comment #42
joachim commentedThat PR is changing AvailabilityCheckerInterface, which now we're at 2.0 would be breaking API.
Comment #43
steveoliver commentedTo help move this issue forward, I'll summarize the changes related to the availability manager, availability checkers, and availability responses.
AvailabilityManager::checkandAvailabilityCheckerInterface::checkhave not changed, they are stillcheck(Entity, Quantity, Context).TRUE|FALSE|NULL, but are nowAvailable,Unavailable,Neutral(AvailabilityResponseInterface) value objectsmin,maxandreasonproperties.AvailabilityManagerwill immediately return anUnavailableresponse (which may include its ownreason) received from any checker.Availableresponses, the highest minimum and lowest maximum of all checks will be returned.Unavailableresponse is returned with the minimum and maximum quantity available and a reason string--either "minimum not met" or "maximum exceeded".If this logic works for the use cases of those following this issue, I say we add a few more assertions for our expected
reasonmessages. If there are any questions or concerns, please comment. So close!Comment #44
joachim commented> But responses from availability checks are no longer TRUE|FALSE|NULL, but are now Available, Unavailable, Neutral (AvailabilityResponseInterface) value objects
Now that we are at a stable release, that breaks API, because it breaks any code that implements AvailabilityCheckerInterface::check.
Comment #45
ransomweaver commentedI think in practice, contrib code will not consume AvailabilityResponses, it will generate them. e.g. my commerce_stock_backorderable sandbox module requires this commerce PR as a patch, and it overrides StockAvailabilityChecker to take into account that the product may be backorderable, allowing the customer to buy more than the existing stock, and it tells commerce with an AvailabilityResponse.
So the situation Joachim should be concerned about is commerce receiving a TRUE|FALSE|NULL when it expects an AvailabilityResponse. Can't we make this commerce code backwards compatible and handle boolean as well as AvailabilityResponse?
Comment #46
epetvir commentedI've been trying to disable the add-to-cart button when a variation is out of stock, following the reference in #29.
This works fine for products with only one variation. However, when the add-to-cart form of a product with multiple variations is Ajax refreshed, $form_state->getBuildInfo()['callback_object'] always references the default variation.
I've worked around this by using $form_state->get('selected_variation') when it's available (ajax refresh) and reverting to build_info when the product page is loaded. This seems to work, but I don't know how good an approach it is.
Comment #47
ransomweaver commented@epetivir this is the subject of this issue https://www.drupal.org/node/2847529 and commerce_stock PR #37
Comment #48
guy_schneerson commented@ransomweaver will try and look into #37 in the next few days.
Comment #49
guy_schneerson commented@ransomweaver yes #37 looks good also created a https://github.com/BBGuy/commerce_stock/pull/38 for the other changes needed for the new AvailabilityResponse.
Both can go in once the availability manager service PR is committed to commerce.
Comment #50
cocoshogo commentedHello All,
My Observations and comments may be to late to be considered but I have a thought I would like to get some feedback. Can the availability use the Drupal State system? So that site owners could define custom Availability states, and actions to take once Stock goes into handout of that state. For example I could define that the product is still purchasable after -20 items have been sold. After that the availability goes into a Out of Stock state and is no longer purchasable. The query for the cart would be to see if the product is purchasable or something like that. Now, Im not a developer and may be ignorant about my question. But I think this would allow for better per site and per product handling of how they want to sell or back sell items.
Comment #51
steveoliver commented@cocoshogo: Yes, with the API change proposed in this issue, you would implement an availability checker service that inspects your stock and inventory business logic, and respond with AvailabilityResponse::available()/unavailable()/neutral() each time availability was checked.
Aside from the breaking API change(s) pointed out by Joachim in #44 (which of course needs discussion/planning), I wonder if the AvailabilityResponseInterface and these three specific types of responses (Available, Unavailable, Neutral), with their min and max values, and reason message, is flexible enough to respresent all possible availability responses.
For example, do we want the API to qualify 'availability' requests with what it is requesting a check for--e.g. 'order', 'backorder'?
Or, do we want to return not a reason String but a Reason type of value object, where we can respond to different reasons for a response, such that the next checker(s) can be informed with the results' passed to them in the resolver chain?
I think any comments on that topic might help spark the discussion and review needed to get this in.
Comment #52
joachim commented> For example, do we want the API to qualify 'availability' requests with what it is requesting a check for--e.g. 'order', 'backorder'?
Regarding that, I filed #2937041: AvailabilityManager needs to be aware of the order, if there is one, and there is also #2862546: Context for Availability Manager,
Comment #53
bojanz commentedIt's time to finally wrap this up. Sorry for the long wait.
I've pinged guy_schneerson to go over some of the details on Slack, but here are some questions and notes in the meantime:
0) Note the current AvailabilityManagerInterface docblock:
We want to make sure that the "available on" use case isn't made too awkward by hardcoding only stock expectations. Probably by documenting that the returned "known quantity" can be NULL.
1) Why does the new API have min/max quantities, instead of a single quantity, indicating how many items are available? What do we gain with that approach?
2) If the response now tells us the available quantity, is there a point in passing $quantity to the method at all? The idea was that were asking for a specific $quantity, but that's obviously not flexible enough.
3) Why is there a getReason() when it's not used, and documented as "Intended for developers, hence not translatable."?
We have the need for the following messages:
a) The general message that can be shown on the product (In stock / Discontinued / Out of stock / 10 remaining / Ships in 10 weeks)
b) the add to cart submit button label when the button is disabled (is this needed if we have the general message somewhere above?)
c) A message shown when the product is removed due to unavailability ("@entity has been removed from your cart because it is no longer available")
d) A message shown when the product quantity is reduced due to unavailability ("@entity has been removed from your cart because it is no longer available")
I thought we'd want to let the AvailabilityResult dictate at least some of them (a, or a/b for example)?
4) Right now we follow the Commerce 1.x logic of running the checkers on each order refresh. I think that we need to remove that, and make the runs explicit: add to cart form submit -> entering checkout -> confirming checkout (just before payment). That would help with performance too.
Related: #2757369: commerce.availability_manager needs to pass what "phase" we're checking in.
5) Do we want availability checkers to run in the admin UI, when adding a product to an order? Or does admin know best? Or should they get an informative message only?
6) Same question for the REST API, where I'm guessing the answer depends on #5. Do we validate on POST /carts/{id}/items but not on /orders/{id}/items?
6) Right now availability is a base module API, so it can't be aware of orders. We'll need to introduce an order-aware API such as #2948117: Add an API for validating whether an order is allowed to enter/complete checkout, same way we have price resolvers and order processors. I thought about whether that would make the product-level API irrelevant, but it still feels like it would have value.
It is true that we'll most likely break BC by fixing this. I'll figure out the best way to minimize that once we agree on the ideal API.
Comment #54
mglamanI've seen this where there is a maximum order amount. But I don't know if it is something we should support. I'd rather it be a simple "this how much is available" and make sure business logic on minimum and maximum buys can be respected by hooking into the system.
I'd imagine yes in both cases. Because `/orders/{id}/items` could be a decoupled admin theme.
Comment #55
tormiMinor issue summary correction.
Comment #56
olafkarsten commentedmy2cents to #53,
This is overly complex. A single quantity should be sufficient.
I guess it clearifies the responsibility, if we don't pass it along. -> "How many of the purchasable entity is available in this context?". But I think it will make in even harder in terms of BC?
to 3) I would only ask for 3a. That should be sufficient as default. I guess getReason() was meant to do so? I would rename it to something like "getDisplayMessage()" or so. I'm a fan of small API's. On the other hand, its propbably cheap and we provide a base class anyway. So we could give meaningful defaults.
to 5) We want the checkers to run. [I never met an admin that know best ;)]
I think so, too. It would be a really bad UX, if you let the user adding stuff to the cart and at checkout throw a bunch of messages and quantity adjustments at him.
Comment #57
guy_schneerson commentedThe stock module will (at list for now) use it's own code and not the availability manager service. We need to provide a working module soon. We can look at this at a later stage.
Comment #58
Wisamx commentedI am really in need for sock management right now.
I made a simple one https://github.com/wisamx/commerce_simple_stock
(getting some code from inventory and another commerce stock project)
maybe it benefits someone else.
Comment #59
guy_schneerson commentedWe now have a version with UI enforcement (add to cart form / cart page / checkout) of stock in testing and will go to alpha soon. Can be found at https://github.com/BBGuy/commerce_stock/pull/55
feedback appreciated
Comment #60
edurenye commentedhttps://github.com/drupalcommerce/commerce/pull/593
The PR does not apply anymore, it must be rebased.
Comment #61
guy_schneerson commented@edurenye I think the availability manager has been abandoned (at least for now)
Comment #62
mglamanLinking some related issues so I can triage this. #3088597: Add a constraint to purchased_entity which checks the availability manager unblocked a good chunk of this. The API-First development has actually helped make this a lot easier to consider and work with.
Comment #63
steveoliver commentedHappy to see this issue revived ;)
Comment #64
mglamanFor those following, the following two issues are being developed, now.
#3107547: Availability checks should use a value object instead of boolean return values
#2937041: AvailabilityManager needs to be aware of the order, if there is one
Comment #65
mglamanWe definitely need to triage this issue.
#3138952: Make the AvailabilityManager order aware landed which improves the availability manager. It also adds messaging support. The availability result message should cover a lot of the remaining items.