DESIGN NOTES / SALES SCRIBE
When a bulk action meets a closed month
Why bulk issue refuses locked-period invoices, even for the owner, and reports the outcome for each document.
Accounting software has a feature every user expects: lock the books once a month is finished, so nobody can quietly change figures you've already signed off. It also has a feature every user expects: tick a load of draft invoices and issue them in one click.
Putting those two together turned out to be more interesting than either of them alone.
The situation
Say it's 20 September. August was closed last week, so the books are locked up to the 31st. There are five draft invoices waiting:
| Invoice | Date | Situation |
|---|---|---|
| INV-1041 | 5 September | Fine |
| INV-1042 | 8 September | Fine |
| INV-1043 | 28 August | Inside the locked month |
| INV-1044 | 29 August | Inside the locked month |
| INV-1045 | 12 September | Has no line items |
Tick all five. Hit Issue. What should happen?
What actually happened
Two things, and which one you got depended on who you were.
As a bookkeeper, you got the message "3 failed" and nothing else. Whether the two good invoices had been issued depended on the order the system happened to process them in. If it reached one of the August invoices first, all five stayed as drafts — including the two that were perfectly fine. If it reached them last, those two went through.
Same five invoices, same click, different outcome. That is the class of bug users report as "it sometimes doesn't work", and it is close to impossible to describe from a screen.
As the business owner, you got "5 issued". Both August invoices had posted into the month you'd closed. Nobody asked. Nobody warned you. August's figures had changed, and the only record was a row in an audit table you'd have to go looking for.
Neither behaviour had ever been seen by a user — this was found during a structured code review, before launch — but both had been in the code for some time.
The two problems underneath
The first was mechanical. When one invoice was refused, the system didn't fully clean up after itself before moving to the next one, so the refusal left traces that affected whatever came afterwards. Hence the order-dependence.
The second was a design question wearing a bug's clothing. The owner held permission to post into a locked period — a legitimate power that exists so an accountant can make a late adjustment. The bulk action simply used it, because the user had it. Technically correct. But the owner never decided anything. They clicked a button on a list of five documents and a closed month quietly moved.
That matters more than it first appears. Every other place in the app where you override the lock, you get a warning, you make a choice, and the system records that you made it. If one path writes those same records without anyone having chosen, the records stop meaning what they say. A reader looking at the audit trail six months later can no longer tell a deliberate decision from a side effect.
The decision
A bulk action never makes a judgement call on your behalf.
Concretely: bulk issue refuses any document dated inside a locked period, regardless of the user's permissions — including the owner's. Those documents are reported back, and the user opens them individually, where the warning and the deliberate choice already exist.
The reasoning is that you cannot sensibly decide to reopen a closed month for invoices you haven't looked at. The button doesn't know which August invoices were meant to go into August and which ones simply have the wrong date on them. From a list view, neither do you.
Alongside that, two supporting rules:
Every document is processed independently. The outcome for any one invoice doesn't depend on any other invoice in the batch, or on the order they happen to be processed in. One problem document no longer takes healthy ones down with it.
"Refused" is a different thing from "failed". A refusal is a control doing its job, and it has a known cause and a specific remedy. A failure is something going wrong. Telling a user to retry something that can never succeed is its own kind of bug.
What the user sees now
The same thing, whoever they are, and it doesn't depend on order:
2 of 5 invoices issued.
2 not issued — the date falls in a locked period INV-1043 (28 August), INV-1044 (29 August) Open each invoice to issue it on its own.
1 not issued — nothing to invoice INV-1045 has no line items.
The two good invoices are issued. The other three are still drafts, and the user knows exactly why each one didn't go.
Three things are doing work in that message. The healthy documents always go through. The two reasons are kept apart, because "locked period" and "no line items" need completely different responses. And it says what to do next.
Open INV-1043 and the invoice screen warns, before you press anything, that 28 August falls in a locked period. If you're the bookkeeper, you change the date or ask someone who can post it. If you're the owner, you see the warning, decide to override it, and post — the same outcome as before, but chosen, on one document, having looked at it.
The trade-off we accepted
For the owner, something that used to work now doesn't. Tick five, click Issue, and two come back refused even though you have the authority to post them.
That's a real cost and it was accepted deliberately. The owner loses no power — they lose the ability to exercise it fifty at a time without noticing. Bulk handles the straightforward cases; anything needing judgement goes to the screen where judgement can be exercised.
The principle that generalises
The specific fix matters less than the shape of it. Three things came out of this that apply well beyond one button:
A permission is not an instruction. Holding the authority to do something is not the same as having decided to do it. Software that conflates the two produces audit trails that record intent nobody had.
Order-dependence is a design failure, not a bug to patch. The fix wasn't "clean up properly after a refusal" — it was to make the outcome of each document independent of every other, and to prove it by running the same set in several different orders. That's a property you can test. "Cleans up properly" isn't.
Batch operations should report per item, not per batch. "3 failed" is a count. What a user needs is which ones, why, and what to do — and those three answers are often different for different items in the same batch.
Found during a pre-launch code review rather than by a user. The review's method was deliberately biased towards running things rather than reading them: every significant finding across six review sessions came from executing a path, not from inspecting it.