Covers all questions related to inventory management, order fulfillment, and shipping.
Let's say we have a SKU named SPROCKET that is housed at a fulfillment service using fetch_stock.json.
The fulfillment service has zero units in their warehouse:
{ "SPROCKET": 0 }
So we start out with a Shopify product like this:
Fulfillment Service fetch_stock.json | Shopify Committed | Shopify Available | Shopify On Hand |
0 | 0 | 0 | 0 |
Now, let's say in the backend of Shopify, a client manually enters an order, knowing that they don't have the product now, but they will in a few weeks. Let's say they enter the order for 10 units, and they click "Mark as paid" to turn it into a real order. They leave the order as "Unfulfilled", so the fulfillment service doesn't know about it yet.
Immediately in Shopify we see this state:
Fulfillment Service fetch_stock.json | Shopify Committed | Shopify Available | Shopify On Hand |
(not called) | 10 | -10 | 0 |
This makes sense. So far, so good.
Within the next hour, Shopify calls fetch_stock.json on the fulfillment service. Remember, the fulfillment service does not know that this order exists because it is "Unfulfilled". And the fulfillment service does not have any units in their warehouse, so they continue to return zero.
{ "SPROCKET": 0 }
And Shopify takes that zero and does this:
Fulfillment Service fetch_stock.json | Shopify Committed | Shopify Available | Shopify On Hand |
0 | 10 | 0 | 10 |
OK, that's a little weird. There aren't 10 on hand and no one said there were. Technically, this is harmless because "Available" still works out to <= 0, so customers are prevented from placing orders for it during checkout.
Note that the fulfillment service can't send "-10" because the order is "Unfulfilled" and so the fulfillment service does not know about the order yet.
Internally, the logic of the fulfillment service is to return "(quantity physically on hand in the warehouse) - (quantity committed in all orders, including orders outside of Shopify) + (quantity committed in Shopify orders that we know about) = (the quantity to return in fetch_stock.json)". We add back the quantity committed in Shopify orders that we know about because we are aware that Shopify seems to subtract its own internally-calculated committed value, which include orders that we don't know about, back off.
Let's say that the fulfillment service physically receives 5 units into their warehouse. Because the order is unfulfilled, the fulfillment service doesn't know about the 10 unit order yet, and so on the next hourly call to fetch_stock.json it returns the 5 units:
{ "SPROCKET": 5 }
And we end up with this:
Fulfillment Service fetch_stock.json | Shopify Committed | Shopify Available | Shopify On Hand |
5 | 10 | 0 | 10 |
Interesting. While we're all glad that Shopify still ended up with <= 0 available so customers can't buy this, we would have expected to see 5 on hand - 10 committed = -5 available. I'm not really sure what Shopify did there but it's like they ignored it.
OK, let's forget the 5 units and back up to the state where the fulfillment service actually had zero on hand and Shopify had messed with their on hand value:
Fulfillment Service fetch_stock.json | Shopify Committed | Shopify Available | Shopify On Hand |
0 | 10 | 0 | 10 |
Now let's say the preorder is canceled, so the client cancels it out of Shopify. Then Shopify does this:
Fulfillment Service fetch_stock.json | Shopify Committed | Shopify Available | Shopify On Hand |
(not called) | 0 | 10 | 10 |
OK, this is a problem too. Because Shopify messed up the "on hand" quantity during the last fetch_stock.json update, it suddenly shows as 10 units available as soon as we cancel the order that was allocating the "Committed" units. Even worse, the client can't quickly edit the quantity because it's "managed by the app".
There seems to be no button to force Shopify to call fetch_stock.json. (It used to be that you could trick Shopify by editing the SKU to something else, saving, and then quickly editing it back, and saving, which would cause Shopify to call fetch_stock.json for a specific SKU, but it doesn't seem to do that anymore. If no other orders are committing inventory, though, the client can click "Edit locations" and temporarily turn the location off and back on which still seems to trigger the SKU-specific call.)
So usually we have to wait until the next hourly sync when Shopify calls fetch_stock.json again:
Fulfillment Service fetch_stock.json | Shopify Committed | Shopify Available | Shopify On Hand |
0 | 0 | 0 | 0 |
We eventually got back to the right state, but during that window, we incorrectly advertised that 10 units are available.
The situation gets even stranger, though.
Let's recreate a preorder for 10 units, so we're back in the state where Shopify allowed "Available" to go negative:
Fulfillment Service fetch_stock.json | Shopify Committed | Shopify Available | Shopify On Hand |
(not called) | 10 | -10 | 0 |
And let's wait for the next hourly call to fetch_stock.json where Shopify takes the "0" returned from the fulfillment service and messes up "on hand":
Fulfillment Service fetch_stock.json | Shopify Committed | Shopify Available | Shopify On Hand |
0 | 10 | 0 | 10 |
We're still kind of OK because we ended up at zero available, even though the on hand number is weird. Now let's say the client goes into Draft orders and enters a draft order for 15 units. But instead of converting it into a real order, they leave it as a draft order and they click the "Reserve items" button. We end up with this:
Fulfillment Service fetch_stock.json | Shopify Unavailable | Shopify Committed | Shopify Available | Shopify On Hand |
(not called) | 15 | 10 | -15 | 10 |
OK, we still have the weird 10 on hand, but at least available is still <= 0 so customers can't buy the item.
Now let's wait for the next hourly sync from fetch_stock.json. Remember, the fulfillment service doesn't know about the draft order reserving 15 units and doesn't know about the unfulfilled order reserving 10 units. So the fulfillment service returns 0 and we end up with this:
Fulfillment Service fetch_stock.json | Shopify Unavailable | Shopify Committed | Shopify Available | Shopify On Hand |
0 | 15 | 10 | 0 | 25 |
The client would expect to see
0 on hand at the fulfillment service - 10 committed - 15 reserved = -25 available
but instead somehow we ended up with 25 on hand. It still works out that available is <= 0 so customers can't buy it, but it's all very weird.
Can someone at Shopify explain the logic that is being applied to the numbers from fetch_stock.json and how we can avoid these situations? I can't find the logic documented anywhere.
I think if Shopify just always took the number from fetch_stock.json as "on hand" and did "on hand (from fetch_stock.json) - committed - reserved - unavailable = available, even if negative" this all would be way less confusing for everyone.
ouch, that does seem to be a rather big bug. Especially considering that customers could then technically order products that were not in stock when the pre-order was cancelled.
perhaps the rest is semantics, but I also would be definitely interested in hearing a technical explanation for this logic.
Cheers,
Gary
I think if Shopify just always took the number from fetch_stock.json as "on hand" and did "on hand (from fetch_stock.json) - committed - reserved - unavailable = available, even if negative" this all would be way less confusing for everyone.
Great observations and I totally agree that available should be negative in your example scenarios after the fetch_stock.json returns. In short, I think it is sane to expect that the response to fetch_stock.json should be equivalent to reporting the same number with an API call such as inventorySetOnHandQuantities.
Otherwise we need a way to disable fetch_stock.json if it doesn't respond in a predictable way without disabling inventory management entirely. What happens if you return {}? Does it have no effect or does it assume 0 for every SKU?
Hi Nicholas_P,
Thanks for the great summary of what is happening. We have identified the problem and will be rolling the appropriate changes in the following days.
The corrected approach will set the retrieved quantity from the fetch_stock call as an on-hand quantity.
Richard
Hi Richard,
Thanks for getting back to us on this issue. I have a slightly different issue but in the end it does present some challenges. Here is an image of adjustment history for a particular product.
1) order was created comitted set to 1, available se to -1
2) our scheduled inventory adjustment process was run which ensures that the product count in shopify matches that in the warehouse. Sets the available quantity to 0 (since there are zero products in the warehouse) using the rest api /admin/api/2023-10/inventory_levels/set.json. Shopify then increases the on hand quantity from 0 to 1 which is not correct.
3) at this point the merchant adjusted the inventory on hand back to zero which changed the available to -1
4) the scheduled process ran again setting the available to 0 and shopify then changed on-hand back to 1, again this is wrong.
Finally, we instructed the merchant to remove the product from the location (long story short it shouldnt have been added to our location since its not stocked at the warehouse and subsequently why we were always setting the available quantity to 0).
So apparently the rest api inventory_levels/set endpoint adjusts the inventory available and not the on-hand quantity, and there does not seem to be a way with the rest api to correctly set the actual inventory according to this link: https://shopify.dev/docs/apps/fulfillment/inventory-management-apps/quantities-states#set-inventory-...
"The on_hand state represents the total number of units that are physically stocked at a location."
With graphql there does, however, appear to be a method using the mutation inventorySetOnHandQuantities which allows us to correctly set the physical count.
This obviously is an oversight on the side of Shopify that with one API we can set the onhand quantity with absolute quantities and with the other api we can only set the available quantity, which does not actually reflect the total number of units physically present at the location.
So with that in mind, it would be super if while the developers are fixing the fetch stock api endpoint we can correct the oversight in the inventory_levels/set.json endpoint or more importantly add inventory_levels/set_onhand.json endpoint 🙂
Cheers,
Gary