A Hold That Does Not Report as One
Ten stock statuses are declared. One counts as sellable and five count as a hold. The other four are neither — so stock put into one of them cannot be sold and is reported as not being on hold, which is the worst of both descriptions.
The position, stated first
This is a defect we found by auditing our own code for this post, and it was not previously recorded anywhere. Use the five hold statuses and nothing else. The other four are reachable, they make stock unsellable, and they will not appear in anything that counts held stock.
A set of statuses is not a list. It is a partition, and a partition only works if every value belongs to exactly one meaningful group. Ours has ten values and two groups covering six of them.
The three lists
Stock held at a location carries a status. Ten are declared, each with a label. Two further lists sort them.
One list says which statuses mean the stock can be sold. It has a single entry: available. That is a defensible and conservative choice.
The other says which statuses count as the stock being on hold. It has five: quarantined, awaiting inspection, damaged, expired, and returned. Also reasonable.
Five plus one is six. There are ten.
Every declared status, against both lists
| Status | Sellable | Counts as held | Anything writes it |
|---|---|---|---|
| Available | Yes | No | Yes |
| Quarantined | No | Yes | Yes |
| Awaiting inspection | No | Yes | No |
| Damaged | No | Yes | Yes |
| Expired | No | Yes | Yes |
| Returned | No | Yes | Yes |
| Reserved | No | No | Yes |
| Allocated | No | No | No |
| In transit | No | No | Yes |
| Consigned | No | No | No |
Built and maintained Configurable by you, not maintained by us Not built
The last four rows are the finding. Two "no" answers in the first two columns means stock that is out of circulation and invisible to anything asking what is out of circulation.
Unsellable and not on hold is not a state anybody designed. It is the space between two lists that were each written correctly.
And it is reachable
The obvious hope is that those four are unreachable — declared for future use and impossible to set. They are not.
The service that places stock on hold validates the requested status against the list of all ten, rather than against the five that mean a hold. The routes behind it, on the web and on the API alike, validate only that the value is a string of reasonable length before handing it over.
So a user with permission to hold stock can put a quantity into "consigned" or "in transit" from the hold action, and the system will accept it, remove it from sale, and then report it as not being held.
What happens to 40 units placed into "consigned"
The symptom a user reports is "the system says we have forty and it will not let me sell them", which is the hardest kind of ticket because both halves are true.
Three of the ten have never been written
Awaiting inspection, allocated and consigned are set by nothing anywhere in the application. They are labels in a map, offered by an action, and no automatic path produces them.
Awaiting inspection is the interesting one, because it means the vocabulary for a receiving quality gate exists while the gate does not. Nothing puts arriving stock into inspection; a person can, by hand, one quantity at a time. It also means an internal note recording that this product has no inspection concept was searching for the wrong thing — it looked for columns and the concept is a value.
Consigned has the same shape and the same consequence, for ownership rather than for quality. It marks a quantity as somebody else's without recording whose, which is closer than a blank and further than a field.
What AWRA OpsHub does today
- A per-location, per-quantity status with a reason, notes, the person who held it, when, and a disposition.
- A hold action that splits a row so part of a quantity can be held without the rest, under a database lock.
- Five statuses that behave correctly end to end: quarantined, awaiting inspection, damaged, expired and returned.
- A single sellable status, so nothing ambiguous can be sold by accident.
- Web and API status screens that report sellable and held per status.
What it does not do
- Any restriction preventing the four unclassified statuses from being set through the hold action.
- Any classification for reserved, allocated, in transit or consigned — they are neither sellable nor held.
- Anything that writes awaiting-inspection, allocated or consigned automatically.
- A receiving inspection gate, despite the status existing.
- A reservation or allocation engine, despite two statuses existing for it.
- An owner on consigned stock — the status marks a quantity and names no counterparty.
Not ours, by choice
- This was found by auditing our own code while writing this post, and it is filed in our public gap file as a priority item with its file and line references. It had not been recorded before.
- The fix is small — validate the hold action against the five rather than the ten — and the interesting part is why it was not caught: every individual list is correct, and the defect lives in the gap between them.
- Nothing here is market-specific. This post makes no claim about any country's statute.
The small fix, and the two features the vocabulary is waiting for
One of these is a correction we should make regardless. The other two are real features whose names are already in the code, which is exactly the situation that makes them look built.
Validate the hold action against the hold statuses
Five values instead of ten, on both the web and API routes. This is the correction, it is small, and it removes the ability to create the state described above at all.
A receiving inspection gate
Arriving stock placed into awaiting-inspection automatically, released by an inspection with a result and an inspector. The status already classifies correctly as a hold; what is missing is anything that puts stock there without a person doing it by hand.
Reservation against a demand
Reserved and allocated exist as words with no engine behind them. A real one reserves against a specific order or job, releases on fulfilment, and expires — which is a different and larger piece of work than a status, and worth naming as such.
How it works: you describe the requirement, we return a written scope, timeline and cost, and once agreed it is built into your environment and maintained as part of the product.
Talk to us about stock statusFour questions about status vocabularies anywhere
How many statuses exist, and how many are classified?
A good answer sounds like
The same number twice.
What it actually means
The question that found this. Ours was ten and six.
Which statuses can a user set directly?
A good answer sounds like
A subset, validated.
What it actually means
If the answer is all of them, the classification lists are advisory rather than enforced.
What writes each status automatically?
A good answer sounds like
A named path per status.
What it actually means
Three of ours have none, which means three features exist as vocabulary only.
Show me stock that is neither sellable nor held.
A good answer sounds like
An empty result.
What it actually means
A non-empty one is this defect, in whoever's product you are asking.
Count the statuses, then count the classifications
Two numbers, and they should match. It is the cheapest audit in this post and it found a priority defect in a module that has been in production for years.
Talk about inventory statusFrequently asked questions
Is any of my stock in one of the four right now?
Only if somebody set it deliberately, since nothing writes three of the four automatically and the fourth is written by the transfer path. It is worth checking, and the check is a filter on status.
How do I get stock out of one of those statuses?
The release action returns held stock to available and works on any status, so the recovery path is not affected. It is the reporting of the intermediate state that is wrong, not the ability to leave it.
Why publish a defect you have not fixed yet?
Because a prospect can read it either way, and finding it in our own gap file is a better experience than finding it in your own data. The fix is small and the honest interim answer is to use the five statuses that work.