Let's talk
data-modelling

The low-stock screen was empty while the shelves ran down

Stock was visibly running out and the low-stock screen showed nothing. The opposite failure, an alert on every order, comes from treating blank as zero.

·

A laptop on an office desk by a window showing the stock levels screen of the Sazinga Field portal, where low stock should be flagged.

You open the low-stock screen and it is empty. Stock levels are there, items are plainly running down, and the screen lists nothing. The obvious conclusion is that the screen is broken. The opposite complaint comes from the same place: a low-stock alert on every single order, until somebody turns alerts off, and then the one that mattered is off too.

Both cost the same thing. A screen nobody believes stops being read, and the reorder that should have happened last week is discovered when the depot runs out. An alert that fires on every order is switched off within a day, which is the same outcome by a different route.

What was actually going on

The screen was not broken. It was proved right by setting a reorder level above the current quantity on one item — the item appeared — and then clearing it — the item disappeared. The screen was empty because not one item had a reorder level set, and an item with no reorder level is not low. It cannot be. Low is measured against a threshold, and there was no threshold.

That is the whole subject, and it is a smaller idea than it sounds: a field left blank is not a field set to zero, and every layer of a system will try to merge the two for you.

A reorder level answers “tell me when this drops to here”. Two quite different settings exist and they are not neighbours on a number line. No reorder level means this item is not watched; it will never appear as low and never raise an alert, however far it falls, and most items in most catalogues are like this. A reorder level of zero means this item is watched, and I want to be told the moment it runs out. An item at zero on hand with no level set is silent by design. An item at zero on hand with a level of zero raises an alert. The stock figure is identical; the behaviour is opposite; and which one you get depends on the setting, not on the stock.

The merge happened in the form, for a reason that looked like tidiness. An empty box, read as a number, becomes zero. So the straightforward implementation turned “I have not typed anything” into “alert me at zero”, and it did so for every item where somebody opened the dialog and closed it again.

What we changed

The form now keeps what was typed as typed, all the way to the moment the request is built, and treats an empty box and the digit “0” as two different answers. That looks like an oversight in review — why is this a number stored as text? — and it is the only shape that can hold both states.

The same merge was waiting at three other places, and each got its own decision. The table shows “Not set” rather than 0 for an item with no level, because a value nobody chose should not be displayed as a value. The “below reorder level” filter has three positions — below, not below, and not filtering on this at all — because a tick-box that only knows on and off cannot express the third, and the third is the default. And on the phone, clearing a reorder level is its own action rather than emptying the box, because emptying a box might mean “unset this” and might mean “I have not finished typing”, and an ambiguous gesture should not be the only way to reach a distinct state.

The alert fires when stock crosses the threshold, not whenever it is seen below it. The naive version alerts on every read or every decrement, which for a fast-moving item is an alert per order. Detecting a crossing needs the value before and after the change, and that was only cheap because every stock reduction in the system already goes through one place. That place knows the old value and the new one, so the alert costs nothing extra and is written in the same step as the reduction — if the reduction is undone, so is the alert.

Each of those was verified against the running system rather than reasoned about: set a level above the current quantity and the item appears; clear it and the item disappears; type “0” and the stored value comes back as zero rather than as nothing. Three checks, and they are the only three that separate this implementation from the broken one.

The screen that says nothing is wrong

The empty state on the low-stock screen no longer says “no results”. Nothing being low is the good outcome; it is the state the whole feature exists to produce. Drawing it as a sad empty table teaches the user that the screen is broken, which is precisely the conclusion drawn the first time it was opened. A screen that lists problems should say so when there are none.

What it did not fix

Had stock reductions been scattered across the order, dispatch and demonstration modules, the crossing detection would have meant either a periodic scan or the same logic copied three times with two of them eventually wrong. A single point through which a quantity changes is not only about keeping the change correct; it is the only place from which the change can be observed cheaply. That was a decision made much earlier, for a different reason (avoiding oversell), and this was its second payoff. If your system has more than one such place, fix that first; the alert problem is a symptom.

The mechanism

The reorder level is a nullable column and nothing downstream may coerce it. The input stays a string until the request is built, and "" and "0" are distinguished explicitly. The list renders null as “Not set”. The filter is sent as true, false or omitted. The mobile editor exposes a “remove reorder level” action. Every decrement passes through one guarded statement in one service, which emits the threshold-crossing event inside the same transaction as the decrement.

Where this ends up

Reorder levels in Sazinga Field can be left unset for this reason: depot stock with no threshold against it is not low stock, and the low-stock screen has to be able to say so.

This came out of building Sazinga Field

Orders, stock, dispatch and the people on the road, in one place. The problem above is one we met while building it, and what we did about it is in the product.

If you run something like this, there is one thing you can do without a call: send one day's order sheet.