Case 03 · Cactus · Shift & Cash
Closing a shift without reopening it
Closing a register could force staff to reopen one they had already closed. I defined the validation hierarchy with engineering, so every check runs before the action is offered.
Company
Role
Sole product designer
Team
[confirm: engineers on Shift & Cash]
Surface
POS tablet app (Flutter)
Status
Designed · in development
The rule
By the time shift closing starts, there must be nothing left to resolve.
Everything else in this case follows from that one sentence. It was agreed with engineering before any screen was worth drawing.
My contribution
Found the structural issues in the closing flow.
Defined the validation hierarchy and the state and transition model with engineering.
Designed the closing flow: Express Checkout, billing layer, reconciliation and shift confirmation.
Not mine: [confirm] the implementation and the payment and tax integrations, built by engineering.
The problem
It’s one in the morning on a Saturday. The venue is empty, the staff want to leave, and the cashier starts closing the register.
The system stops them: a table has an unpaid order. They leave the closing flow, find the table, charge it, and start again from the beginning. Then shift closing runs the same validation a second time and finds something cash closing didn’t catch.
In a venue with more than one register, the pending item belonged to a register that was already closed. To resolve it, someone had to reopen a closed register, complete the payment, and close it again. A record that was supposed to be final was being reopened as routine, by whoever was still awake.
Diagnosis
Every screen was legible. The order was wrong.
Nobody had defined what state the system had to be in before each transition was allowed. Four things were true at once.
01
Cash closing only checked payments
And its answer to finding one was to send the user somewhere else.
02
Shift closing repeated the checks
Without a shared hierarchy, so the two validations could disagree with each other.
03
Billing validation had gaps
In some paths it was missing entirely. A shift could close with unbilled sales inside it.
04
The system was reactive
It waited for the user to hit a wall and then showed an alert, instead of checking before offering the action.
Validation hierarchy
Each step resolves completely before the next one is offered.
The flow is a consequence of the state model, not the other way around.
01
Pending orders & tables
Unpaid orders and open tables are cleared first.
02
Billing integrity
Unbilled sales and failed invoices are resolved.
03
Reconciliation & cash count
POS and drawer totals matched against recorded sales.
?
Is this the last open register?
No: the register closes on its own. Yes: the shift is verified as a whole.
04
Clean multi-cash finalization
No pending payments, billing confirmed, every register reconciled.
05
Shift closing
Confirmation only. No validation left to run.
The flow
What the cashier sees, in order.
Problems are resolved where they’re found. The last screen is now the least interesting one, and it used to be where everything went wrong.

01 · Express Checkout, inside the closing
A pending table is paid where it was found. Anything outside its scope hands off to the full checkout.

02 · Billing is verified before counting
Failed operations retry silently first. Only what’s still broken reaches the cashier.

03 · Reconciliation against what was recorded
The cashier enters POS and drawer totals. A difference is surfaced and recorded, not blocked.

05 · Shift closed, with a note for tomorrow
A confirmation, not an investigation. Optional notes for whoever opens next.
Decisions
Five calls, and what I said no to.
Most of them were made in conversation with engineering, not in Figma.
01
Where problems get resolved
Chose
Bring the resolution into the closing flow.
Rejected
Keep the redirect, and return the user to the exact point they left.
Why
The cashier isn’t lost. They’re holding an unfinished closing in their head while the system sends them somewhere else.
02
A second payment surface
Chose
Express Checkout: a reduced checkout inside the closing.
Rejected
Only one checkout in the product.
Why
I accepted two places where a payment can be taken, both kept in sync, to keep the closing intact.
03
Failed invoices
Chose
Retry silently first, surface only what’s still broken.
Rejected
Show every billing failure to the cashier.
Why
Most failures are transient. A warning everyone learns to dismiss is worse than none, because it still looks like a control.
04
A drawer that doesn’t balance
Chose
Surface the discrepancy and record it.
Rejected
Block the closing until it matches.
Why
The venue is shutting either way. What the system owes the business is a difference that’s recorded and attributable.
05
When to check
Chose
Verify conditions before offering the action.
Rejected
Alert after the user hits the wall.
Why
Proactive checks change what the interface has to say, and when. That’s a design decision, not an engineering one.
States & edge cases
The closing has to work on the bad nights too.
E1
Pending order at closing
Express Checkout opens in the flow. Nobody restarts.
E2
Invoice rejected by the tax authority
Retried in the background. Only a persistent failure reaches the cashier.
E3
Drawer doesn’t balance
The difference is recorded against the register. The closing continues.
E4
Another register still open
This register closes on its own. The shift waits for the last one.
E5
Last open register
The whole shift is verified: payments, billing, every register reconciled.
E6
Payment outside Express scope
Hands off to the full checkout, then returns to the closing.
With engineering
The most useful thing I produced wasn’t a screen.
It was the state and transition model, written down in one place: which states a register and a shift can be in, which transitions are legal, how background retries behave while a cashier waits, and how registers stay in sync when several are open. The team stopped resolving the same ambiguity three different ways in three implementations.
How I’ll know
Some of it is guaranteed by construction. The rest needs measuring.
It’s worth keeping the two apart.
Guaranteed by construction
Reopening a closed register goes from routine workaround to edge case.
Pending payments and failed invoices are caught before reconciliation, not after.
Every closed register is reconciled. Where there’s a difference, it’s on the record.
Shift closing runs one structured pass instead of reactive alerts.
What I’ll instrument
Reopen events per venue, per month
Should trend to zero. The cleanest test of whether the architecture holds.
Support tickets mentioning shift closure
Before and after.
Time from starting a cash closing to confirming it
Shifts closing with unresolved billing
Zero by design, which is why it’s worth measuring.
[baseline pending] Current reopen rate from production logs. Before rollout: prototype sessions with cashiers on a multi-register closing.
What I took from it
In financial software, architecture is the user experience.
Hierarchy beats coverage.
The old flow validated almost everything it needed to. It just did it in an order that let two checks contradict each other.
A record that can be reopened isn’t a record.
The reopening wasn’t a workaround around the design. It was the design admitting it couldn’t guarantee what it claimed.
Proactive checks are a design decision.
Verifying before offering an action, instead of alerting after, changes what the interface says and when.
Nobody will see the state model.
They’ll feel it as whether the closing works the first time, at one in the morning.
Keep reading
4 pieces · 3 cases, 1 concept
If your product got complicated, let’s talk.
Copy email
All contact details
