Is your feature request related to a problem? Please describe.
When you track inventory and the stock level gets to 0, it is still possible to order this products. This is not suitable for every case. Most of the time you want to prevent customers from ordering products which you don't have in stock.
Describe the solution you'd like
I think it would be best if the store owner can decide if he wants to allow back orders or not. So this should be a global setting for the store.
But also the store owner should be able to make exceptions directly on the product variant level. So for example you could disable back orders globally but allow them for one specific product variant.
Allowing or disallowing should just be boolean value with a checkbox for the admin UI.
Describe alternatives you've considered
None.
Additional context
It is critical to discuss what should happen when you try to order more than the available stock. What should be the response from the API when you try to add an order with e.g. 10 pieces but there are only 5 left in stock?
I agree with the idea of:
This design is exactly the same as the trackInventory setting, so it makes sense to replicate the pattern here.
Scenario: There are 3 of "Widget A" in stock. A customer invokes addItemToOrder with Widget A and a quantity of 5. Some ideas:
allowBackorders is true, then add all 5 to the order. If trackInventory is true, set the stockOnHand to -2.allowBackorders is false we can either:2.i. may be confusing to the customer - there is a good chance they will not notice that a lower quantity has been added to the order. Of course, the storefront logic can always check the requested quantity against the added quantity and display some warning, even if no error is thrown.
2.ii. is more explicit but has the downside of requiring more interactions from the customer, depending on the storefront implementation (the storefront may catch the error, and display a dialog saying "we only have 3 Widget As in stock, do you want to add 3?"
Regarding point 2 I would suggest doing both of 2.i. and 2.ii.
So my suggestion would be:
allowBackorders is falseWith this it would be easy for the storefront to show a message in the cart. You won't have to do some checks yourself in the storefront. Just check if there is this warning in the response.
What also must be considered is the case, when you add the last 3 WidgetAs to your cart and while you need some time to
place your order, another person was faster and ordered 2 WidgetAs. So when you finally place your order, the shop system has to check again, if the order can be placed or if there is not enough stock available and fail with an error if it is so.
This is exactly the way we are doing it at the moment: After callingaddPaymentToOrder mutation in frontend, the backend payment handler checks in createPayment callback the order quantity and the available amount. Then we can easily return anerrorMessage in the JSON, optionally providing some metadata, i.e. {reason: "OUT_OF_STOCK"}.
The problem that @Zwergal describes happens a lot in my ecommerce today (I am not using vendure in production yet). Would be great to cover these problems in Vendure! 😁
Just to add a prior art about this issue:
https://docs.magento.com/user-guide/catalog/inventory-backorders.html
So my suggestion would be:
- If the local or global allowBackorders is false
Add 3 WidgetA to the order and also give a warning/info hint to the client
This makes sense. However, the current API design is not set up well to handle this. Currently the addItemToOrder mutation can either return the Order object on success, or the server can throw in which case the GraphQL errors array will contain the error. We cannot support _both_ success _and_ an error/warning.
This limitation makes a good case for implementing #437, which will not only make it simple to support this use case, but also make the very possibility of this outcome (i.e. the "insufficient stock" warning result) discoverable via the GraphQL API introspection mechanism.
Therefore I'm inclined to first work on #437 and then come back to this so that we can implement a satisfactory API for this issue.
Some more thoughts after studying the Magento docs linked above and some other research:
Stock on hand Currently we use the stockOnHand property of a ProductVariant to signify the available stock, and if we allow back orders then this would go negative. However, it would be more correct to have "stock on hand" mean the _number of units physically available_. In this case, it does not make sense to allow a negative value. I think we need to introduce a couple of other concepts:
Allocated means how many units have been assigned to Orders. This would occur when the customer checks out. An Allocation stock movement would be created for each OrderItem. The stockOnHand value would not be affected by the creation of an Allocation. Only when a Fulfillment is created, we would create a Sale stock movement and then reduce the stockOnHand value.
Saleable means how many units are available to be sold to customers. This is what matters in the storefront. Typically this would equal stockOnHand - allocated, but if back-orders are enabled, we could allow units to be sold even if there are more allocated than on hand. Magento does this via a "out-of-stock threshold" which means "the number of available units below which we consider this SKU to be out of stock".
Some examples:
Stock on hand | Allocated | Out-of-stock threshold | Saleable
----------------|-----------|----------------------------|----------
10 | 0 | 0 | 10
10 | 0 | 3 | 7
10 | 5 | 0 | 5
10 | 5 | 3 | 2
10 | 10 | 0 | 0
10 | 10 | -5 | 5
As you can see in the last line in the table, setting a negative value on the out-of-stock threshold effectively enables back-orders to be placed.
So it seems that rather than a simple allowBackorders boolean at the global/variant level, we could instead just have a outOfStockThreshold value, which defaults to 0, meaning no backorders allowed. It could then be adjusted globally or per variant to a negative value to enable back-orders.
This solution gives much more control over stock management than a simple boolean "on/off" switch.
The ProductVariant entity would be extended thus:
class ProductVariant {
useGlobalOutOfStockThreshold: boolean;
outOfStockThreshold: number;
stockAllocated: number;
}
When a customer checks out, we create an Allocation stock movement and increment the stockAllocated value. The stockAllocated, stockOnHand & outOfStockThreshold can then be used to derive the number of saleable units available. Note that right now we will not implement a "saleable" field on the Shop API ProductVariant type, since this is covered by #442.
When a fulfillment is created, we create a Sale stock movement, reduce the stockAllocated and stockOnHand values.
@michaelbromley Awesome work. I like the concept as you wrote it. Gives very much control on how back-orders should behave.
Am I correct, that by default the stock information will not be exposed via the public store API? So if someone wants stock information he has to implement an own resolver to add the information he needs/wants?
Thanks for the feedback!
Am I correct, that by default the stock information will not be exposed via the public store API? So if someone wants stock information he has to implement an own resolver to add the information he needs/wants?
Correct, though I plan to eventually make this a bit more convenient: #442
Most helpful comment
This makes sense. However, the current API design is not set up well to handle this. Currently the addItemToOrder mutation can either return the
Orderobject on success, or the server can throw in which case the GraphQLerrorsarray will contain the error. We cannot support _both_ success _and_ an error/warning.This limitation makes a good case for implementing #437, which will not only make it simple to support this use case, but also make the very possibility of this outcome (i.e. the "insufficient stock" warning result) discoverable via the GraphQL API introspection mechanism.
Therefore I'm inclined to first work on #437 and then come back to this so that we can implement a satisfactory API for this issue.