Issue #2710107: Availability responses as value objects with quantity range#593
Issue #2710107: Availability responses as value objects with quantity range#593steveoliver wants to merge 25 commits intodrupalcommerce:8.x-2.xfrom
Conversation
68e6e5f to
ac287b7
Compare
cbb9eb4 to
7c3b692
Compare
There was a problem hiding this comment.
The main change is in classed responses, and returning min and max values for available and unavailable responses. Min and Max are the widest narrowest range of all indicated availability.
An explicit "unavailable" return from any checker will automatically return unavailable for the entire check.
No opinion (Neutral) responses are still ignored / passed on from AvailabilityManager::check() as available.
Added the availability manager service to add to cart form, originally thinking we'd use it within the core add to cart form, but instead not doing anything, but providing it and the currentUser(), selectStore() methods, plus the base form id as conveniences for hook_form_alter implementations.
| */ | ||
| public function getCurrentUser() { | ||
| return $this->currentUser; | ||
| } |
There was a problem hiding this comment.
providing this public method for the convenience of form alters.
| */ | ||
| public function getAvailabilityManager() { | ||
| return $this->availabilityManager; | ||
| } |
There was a problem hiding this comment.
this also for the convenience of add to cart form alters.
| * The selected store. | ||
| */ | ||
| protected function selectStore(PurchasableEntityInterface $entity) { | ||
| public function selectStore(PurchasableEntityInterface $entity) { |
There was a problem hiding this comment.
also made AddToCartForm::selectStore() public for the convenience of form alters.
There was a problem hiding this comment.
@bojanz This works, but I wonder if we want to provide a custom form alter hook for add to cart forms, passing store and user explicitly. What do you think?
There was a problem hiding this comment.
I don't like this. People should be providing their own add to cart form if they're altering this much.
There was a problem hiding this comment.
I'm sad that this function is protected now. Current cart form behavior is to show "add to cart" no matter what the availability is. So it can say out of stock, you can click the button and it doesn't add the item to the cart. I Am using hook_form_alter and the enhanced $form_object->getAvailabilityManager()->check of this PR to change the button to "Unavailable". To do that, I need to construct a $context, which requires the $store. This seems like a reasonable use of form_alter to me, if the stock AddToCart form isn't going to be availability-aware.
|
@bojanz, Check out the latest fails ... why should ->refresh() change qty from 2 to 1? |
| * @return bool|null | ||
| * TRUE if the entity is available, FALSE if it's unavailable, | ||
| * or NULL if it has no opinion. | ||
| * @return \Drupal\commerce\AvailabilityResponseInterface |
There was a problem hiding this comment.
should be: \Drupal\commerce\AvailabilityResponse\AvailabilityResponseInterface
There was a problem hiding this comment.
(I ended up just moving all the AvailabilityResponse* classes into the root commerce namespace.)
There was a problem hiding this comment.
This is changing the interface of tagged services, so breaks API.
| * | ||
| * @return bool | ||
| * TRUE if the purchasable entity is available, FALSE otherwise. | ||
| * @return \Drupal\commerce\AvailabilityResponseInterface |
There was a problem hiding this comment.
should be: \Drupal\commerce\AvailabilityResponse\AvailabilityResponseInterface
There was a problem hiding this comment.
I ended up moving these AvailabilityResponse* classes into the root Drupal\commerce namespace.
| * Checks the availability of the given purchasable entity. | ||
| * Gets an availability response from the aggregate of all checkers. | ||
| * | ||
| * Possible \Drupal\commerce\AvailabilityResponseInterface classes: |
There was a problem hiding this comment.
should be: \Drupal\commerce\AvailabilityResponse\AvailabilityResponseInterface
| if ($response instanceof AvailabilityResponseNeutral) { | ||
| continue; | ||
| } | ||
| $has_opinion = TRUE; |
There was a problem hiding this comment.
I think this should be reversed
$min = max($min, $response->getMin());
$max = min($max, $response->getMax());
As its I think it in consistent with:
if ($response instanceof AvailabilityResponseUnavailable) {
return $response;
i.e. it is the interaction of the availability checkers.
Real life example:
stock module says: max: 500
Max per order module says: max: 10 (per order)
In such case I think the maximum should be the min of the two: 10
|
Thanks @olafkarsten. Working on getting tests passing now, and after discussion with @BBGuy about his min/max comment, I agree that makes sense, and will make that change as well. |
6af3bc7 to
6f0a329
Compare
| ]), 'warning'); | ||
| $order_item->setQuantity($availability->getMax()); | ||
| $order_item->save(); | ||
| } |
There was a problem hiding this comment.
I think I get why there is no check for ->isAvailable here, but I will walk through how this would work for the custom backorder availability checker I think I need to implement.
- my checker should run first, and in the case that the item has 0 stock but can be backordered, we return an Availability response with isAvailable == true, and we might think that is all we need to do, but..
- that isAvailable TRUE doesn't tell us anything about what amount of stock it means to be available, so even though our checker knows that it means "any amount" we have to explicitly answer with a large getMax in this case.
- the edge condition here, for me, is a order item that has positive stock (deliverable immediately) AND can be backordered (N-time delivery). If, for instance, stock was 2, then 2 items were put in cart A, but not checked out, then someone else added 1 item to cart B and checked out. Cart A refresh will still (correctly) get an isAvailable response here, with a very high max. However by default there is no way to tell them that now since stock is 1 not 2, one of the items won't deliver immediately anymore. That is probably the task of a custom OrderProcessor just for updating e.g. a custom OrderItem delivery_time field and maybe dsm the customer.
There was a problem hiding this comment.
I think you are got a few separate issues (although I can c the link).
Yesterday me and @steveoliver covered some of this. I explained the idea behind stock that separated the "is in stock" & the "get total stock level" methods. That was to allow cases like yours. However as the availability checker is passed in the requested quantity, it can simply return that as the max to get around the issue. Steve has shown me its a technique he already used. I will try and address your other points in the drupal issue q as it does not directly relate to the code.
There was a problem hiding this comment.
I don't think with the current code in this PR here we're providing a way to support backorder-ability in Commerce core--the AvailabilityManager::check returns neutral, available, or unavailable. Backorderable is arguably a fourth possible return value between available and unavailable. This PR needs work to support back-orderability (based on commerce_stock or any other availability-determining contrib module), if we are to support it in Commerce core -- and I think we should. @bojanz What do you think about Commerce core supporting the logic of backorderability?
There was a problem hiding this comment.
With the use of the commerce_stock_backorderable module I posted in a sandbox, I seem to have a properly functioning backorder stock system while using this PR. Seems to me the question should be submodule vs standard support in core stock, But maybe a review of my submodule would indicate if there are some inherent flaws to that approach as a general-purpose submodule vs a project-level custom module.
There was a problem hiding this comment.
Ah, you're right - this can work! I reviewed the sandbox module and it does look like it'll need some changes in commerce_stock such as the StockCheckInterface::getOrderStockTransactionSum addition, but I think we're headed in a good direction. I'd open a PR against commerce_stock for backorderable as a submodule, that way you can start writing tests against the commerce_stock for the changes to stock and the addition/integration of backorderable. I think this PR is ready for a real review from @bojanz.
There was a problem hiding this comment.
@ransomweaver Are you still working on/using backorderable? I'd like to know how this PR affects your use cases.
There was a problem hiding this comment.
@steveoliver Yes, I'll check this out and see what it does for backorderable.
There was a problem hiding this comment.
@steveoliver commerce_stock_backorderable definitely depends on this PR. Its checker returns Drupal\commerce\AvailabilityResponse rather than a boolean. After some tweaks, everything about backorderable seems to work properly with the latest version of this PR.
In another comment here I've noted that I have a separate issue depending on this PR (https://www.drupal.org/node/2847529) having nothing to do with backorderable. This add-to-cart form availability issue I have chosen to solve with a hook_form_alter in commerce_stock_local, as that seems the best place for it. I will turn that code into a PR for commerce_stock.
6be1d5c to
b8a7444
Compare
|
This will no longer apply as a patch due to the change in the AddToCartForm constructor. Rebase please. |
| */ | ||
| public function check(PurchasableEntityInterface $entity, $quantity, Context $context) { | ||
| $min = .0000001; | ||
| $max = 9999999; |
There was a problem hiding this comment.
I remember how someone discribed a use case where several millions were a common quantity in cart. I don't remember the details, but the learning: never assume 9999999 .. is absurdly large ;). Since than I use PHP_INT_MAX for such cases.
d23417a to
0beb9be
Compare
|
Rebased and PHP_INT_MIN and PHP_INT_MAX for min and max. |
|
This PR no longer applies as a patch. Can it be rebased? |
|
This breaks on < PHP 7 |
mglaman
left a comment
There was a problem hiding this comment.
Quick review. I feel like the test coverage should be fine. But we might want more.
| * {@inheritdoc} | ||
| */ | ||
| public function buildForm(array $form, FormStateInterface $form_state) { | ||
| $form = parent::buildForm($form, $form_state); |
There was a problem hiding this comment.
Don't form alters get the form ID passed? And users could use hook_form_BASE_FORM_ID_alter
| * The selected store. | ||
| */ | ||
| protected function selectStore(PurchasableEntityInterface $entity) { | ||
| public function selectStore(PurchasableEntityInterface $entity) { |
There was a problem hiding this comment.
I don't like this. People should be providing their own add to cart form if they're altering this much.
|
|
||
| /** | ||
| * Checks the availability of the given purchasable entity. | ||
| * Gets the availability of a given entity with the given context. |
There was a problem hiding this comment.
We usually have the document block reflect the method name (this doesn't start with getXXX).
Maybe:
Checks the availability of a given purchasable entity with given context.
| ]), 'warning'); | ||
| } | ||
| elseif ($availability->getMax() < $order_item->getQuantity()) { | ||
| drupal_set_message(new PluralTranslatableMarkup(abs($availability->getMax() - $order_item->getQuantity()), '1 %item is currently unavailable. The quantity in your cart has been updated.', '@count %item are currently unavailable. The quantity in your cart has been updated.', [ |
There was a problem hiding this comment.
This should use the "availability Reason" something like:
drupal_set_message(new PluralTranslatableMarkup(abs($availability->getMax() - $order_item->getQuantity()), '1 %item %reason. The quantity in your cart has been updated.', '@count %item %reason. The quantity in your cart has been updated.', [
'%item' => $order_item->getTitle(), '%reason' => $availability->getReason(),
]), 'warning');
Also the part: "The quantity in your cart has been updated" is not always true as the quantity may have stayed the same but that's less of an issue.
|
Made changes to stock and works as expected :) A few small ux issues like not using the $availability->getReason() but I think this type of changes can be made later as they will have no impact on the API/function definition. We should get this in ASAP so we can make the stock changes needed. |
|
It looks like this issue and review process is the current bottleneck for getting stock / inventory support into Commerce 2. As the last activity was some time ago, I thought I'd ask to see if there was anything I could do to help here. |
|
One major problem with the PR as it stands is that it's breaking API. |
Rename amount to quantity, like the terms used in all other places in this file.
|
After seeking to Bojanz and finding it difficult to justify the minimum values of the response. I have spent some time rethinking this aprouch. This however leaves one other issue. Commerce needs to know if to disable the add to cart button. If we use the above logic and provide 1 to the checker, then the module may return 0 and the button will be disabled. I think that this is a different type of availability question. This second question is a simple Yes/No question. I now think that both those questions are needed for a flexible implementation of the availability manager and as far as I can see it leaves no use case were a product is available unsupported. This does not include back order and wait list as I think those should be categorized as unavailable and that's a whole new story. |
See https://www.drupal.org/node/2710107