Skip to content

Issue #2710107: Availability responses as value objects with quantity range#593

Open
steveoliver wants to merge 25 commits intodrupalcommerce:8.x-2.xfrom
steveoliver:2710107-availability-response
Open

Issue #2710107: Availability responses as value objects with quantity range#593
steveoliver wants to merge 25 commits intodrupalcommerce:8.x-2.xfrom
steveoliver:2710107-availability-response

Conversation

@steveoliver
Copy link
Copy Markdown
Contributor

@bojanz bojanz force-pushed the 8.x-2.x branch 2 times, most recently from 68e6e5f to ac287b7 Compare January 12, 2017 04:31
@steveoliver steveoliver force-pushed the 2710107-availability-response branch 3 times, most recently from cbb9eb4 to 7c3b692 Compare January 19, 2017 18:20
Copy link
Copy Markdown
Contributor Author

@steveoliver steveoliver left a comment

Choose a reason for hiding this comment

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

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;
}
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.

providing this public method for the convenience of form alters.

*/
public function getAvailabilityManager() {
return $this->availabilityManager;
}
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.

this also for the convenience of add to cart form alters.

Comment thread modules/cart/src/Form/AddToCartForm.php Outdated
* The selected store.
*/
protected function selectStore(PurchasableEntityInterface $entity) {
public function selectStore(PurchasableEntityInterface $entity) {
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.

also made AddToCartForm::selectStore() public for the convenience of form alters.

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.

@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?

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 don't like this. People should be providing their own add to cart form if they're altering this much.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@steveoliver
Copy link
Copy Markdown
Contributor Author

@bojanz, Check out the latest fails ... why should ->refresh() change qty from 2 to 1?

Copy link
Copy Markdown

@olafkarsten olafkarsten left a comment

Choose a reason for hiding this comment

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

Some nitpicks. :/

* @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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

should be: \Drupal\commerce\AvailabilityResponse\AvailabilityResponseInterface

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.

(I ended up just moving all the AvailabilityResponse* classes into the root commerce namespace.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

should be: \Drupal\commerce\AvailabilityResponse\AvailabilityResponseInterface

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.

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:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

should be: \Drupal\commerce\AvailabilityResponse\AvailabilityResponseInterface

if ($response instanceof AvailabilityResponseNeutral) {
continue;
}
$has_opinion = TRUE;
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 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

@steveoliver
Copy link
Copy Markdown
Contributor Author

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.

@steveoliver steveoliver force-pushed the 2710107-availability-response branch from 6af3bc7 to 6f0a329 Compare January 24, 2017 19:28
@steveoliver steveoliver changed the title Issue #2710107: Availability as a value object (range), not boolean Issue #2710107: Availability responses as value objects with quantity range Jan 24, 2017
]), 'warning');
$order_item->setQuantity($availability->getMax());
$order_item->save();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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 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.

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.

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?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

@steveoliver steveoliver Feb 7, 2017

Choose a reason for hiding this comment

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

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.

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.

@ransomweaver Are you still working on/using backorderable? I'd like to know how this PR affects your use cases.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@steveoliver Yes, I'll check this out and see what it does for backorderable.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@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.

@bojanz bojanz force-pushed the 8.x-2.x branch 7 times, most recently from 6be1d5c to b8a7444 Compare April 27, 2017 20:46
@ransomweaver
Copy link
Copy Markdown

This will no longer apply as a patch due to the change in the AddToCartForm constructor. Rebase please.

Comment thread src/AvailabilityManager.php Outdated
*/
public function check(PurchasableEntityInterface $entity, $quantity, Context $context) {
$min = .0000001;
$max = 9999999;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@steveoliver steveoliver force-pushed the 2710107-availability-response branch from d23417a to 0beb9be Compare May 3, 2017 16:26
@steveoliver
Copy link
Copy Markdown
Contributor Author

Rebased and PHP_INT_MIN and PHP_INT_MAX for min and max.

@ransomweaver
Copy link
Copy Markdown

This PR no longer applies as a patch. Can it be rebased?

@mglaman
Copy link
Copy Markdown
Contributor

mglaman commented Sep 15, 2017

This breaks on < PHP 7

    1)
    Drupal\Tests\commerce_cart\Functional\AddToCartFormTest::testProductAddToCartForm
    Exception: Notice: Use of undefined constant PHP_INT_MIN - assumed
    &#039;PHP_INT_MIN&#039;
    Drupal\commerce\AvailabilityManager-&gt;check()() (Line: 35)

Copy link
Copy Markdown
Contributor

@mglaman mglaman left a comment

Choose a reason for hiding this comment

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

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);
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.

Don't form alters get the form ID passed? And users could use hook_form_BASE_FORM_ID_alter

Comment thread modules/cart/src/Form/AddToCartForm.php Outdated
* The selected store.
*/
protected function selectStore(PurchasableEntityInterface $entity) {
public function selectStore(PurchasableEntityInterface $entity) {
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 don't like this. People should be providing their own add to cart form if they're altering this much.

Comment thread src/AvailabilityCheckerInterface.php Outdated

/**
* Checks the availability of the given purchasable entity.
* Gets the availability of a given entity with the given 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.

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.', [
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.

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.

@BBGuy
Copy link
Copy Markdown
Contributor

BBGuy commented Oct 24, 2017

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.

@bgronek
Copy link
Copy Markdown

bgronek commented Feb 1, 2018

@BBGuy @steveoliver @mglaman

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.

@joachim-n
Copy link
Copy Markdown

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.
@BBGuy
Copy link
Copy Markdown
Contributor

BBGuy commented Mar 9, 2018

After seeking to Bojanz and finding it difficult to justify the minimum values of the response. I have spent some time rethinking this aprouch.
The reason is that while products may have a minimum and maximum quantities that they can support, that range is not continues. take for example this Electronic Capacitor that can only be ordered in multiples of 5.
I now believe that the solution is to allow the availability checker to provide a number that's valid for purchase alongside an optional message that will give context. This will allow for a module to react on the above product with a request of 17 by returning 15 and a message of "Product CA05668 needs to be ordered in multiples of 5. We have altered your quantity from 17 to 15".

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.
If the first question is: "Can the user purchase quantity X of Product Y?"
This second question is "Is product Y available for purchase?"

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants