The owner could sell from a store that has no till
A sales rule checked that a site had a till but skipped the check for account owners, so a sale could be booked and stock taken from a production store.
Nothing in this system stopped a till sale being booked against a site that has no till. The stock comes out of a production store that never sells anything, and the sale lands in that store’s day book. It only happened when the account owner did it, because the rule was skipped for owners.
We found it by reading the code rather than from a support ticket, which is the only lucky part. The cost of a fault like this is quiet: stock leaves the wrong place, the day book of a store that never sells now has a sale in it, and nobody has reason to look until the figures stop matching the shelves.
What was actually going on
Every sales screen was protected by a check that the site in use can take till sales. The check works. It looks at the site, confirms the ability is on its list, and refuses when it is not.
It also steps aside, with no checks at all, when the person is an account owner. The reason is reasonable and is written in the code: owners are not tied to one site, so demanding that they name one would break every list they open. That holds for reading. It had been applied to every action, including writing.
So for an owner, the check never established which site was meant. The next part of the code then had to get the site from somewhere, and it took it from the details the user’s screen had sent. That value was never checked against the organisation, never checked against the ability the screen claimed to require, and never checked to see whether the site existed. It went straight into the sale and into the lookup that takes stock out.
The check had been doing two jobs at once: deciding whether this person may act, and establishing where they are acting. When it stepped aside, it skipped both, and the second job was silently taken over by whatever the user sent. Nothing downstream marks the difference, because a value the server verified and a value the screen supplied end up in the same place.
The pattern appeared in dozens of places, and on the reading side the fallback was a value in the web address. Each one was written by someone being helpful about a real usability problem, and none was wrong on the day it was added.
What we changed
We split the check in two. One question, may this person act in this organisation, applies to everyone. A second works out the site and, for any action that writes, refuses to take a site from what the user sent. An owner who does not name a site when writing is told to choose one. That is a slightly worse experience on exactly one screen and the correct one everywhere.
The till ability check moved out of the permission layer into the validation of the action itself, alongside the checks that the products exist and the quantities are positive. It is a rule about the site, not about the person, and putting it in the permission layer is what let a rule about people switch it off.
The reading screens kept an equivalent on purpose. An owner filtering a list by site is choosing a filter, and a filter that names a site outside their organisation returns nothing, because the organisation limit is applied first and separately. A filter narrows what is already limited; it never widens it.
What it did not fix
Owners now have one extra step when they record a sale or other write. We accepted that because the alternative was the fault itself.
The pattern, for anyone with an owner account
Seniority is a reason to skip a permission check, never a reason to skip a feasibility check. An owner’s authority does not create a till.
Test it with the most powerful login you have. Try the thing that should be impossible, such as a sale in a store that does not sell, as an owner. If the system allows it, a rule is being skipped for seniority. And ask your supplier to search their own code for every place where the location of a record is taken from what the user’s screen sent.
Where this ends up
The till sale, the production store the stock came out of and the ability that was meant to keep them apart are all part of Sazinga Factory, where a finished batch has to be traceable back to the material it consumed, which only holds if every movement is booked against the site that really made it.