2026-07-25

Sprint 9 Closes: Rebuilding the Confirmation Flow, and Finally Fixing the Bug Left Broken on Purpose

Last session ended with development paused and a real bug sitting unfixed on purpose: after logging in, the app never redirects the user anywhere. The fix was one line, but where that line should send the user was exactly the question a UX discovery session needed to answer first. That session produced a full re-scope of the confirmation flow, and this sprint is that re-scope, built: six issues, six pull requests, stacked in dependency order, plus the redirect finally pointed somewhere real.

The same beer, three different credits

The mug club's whole premise is working through roughly 200 beers on the tavern's list. Until this sprint, "confirmed" meant one thing per beer, full stop. But a customer who has the same beer on draft one week and in a can the next is genuinely trying something different, not repeating themselves, and the product should credit that. So BeerConfirmation gained a serving format, draft, can, or bottle, and the row that used to be unique per customer-and-beer is now unique per customer, beer, and format.

That's a breaking schema change, and breaking schema changes need a real answer for the data that already exists. Every confirmation on record predates the concept of format entirely, so the migration backfills all of it to "draft" — not because every one of those beers was actually poured from a tap, but because it's the closest honest default and nothing about the product changes for a customer who never uses the other two formats. The alternative, a fourth "unspecified" value, would have meant every downstream screen had to handle a state that would only ever mean "this row predates the feature," forever. Draft was the simpler lie.

One PIN, a whole round

The old flow meant one PIN entry per beer, which is fine for a single confirm and actively annoying for a flight or a round. So confirming is now a queue: add several beer-and-format pairs, then hand the bartender the phone once for the whole batch.

The interesting design question was what a single PIN entry actually authorizes. A wrong or locked-out PIN fails the entire batch, nothing gets written, because the PIN is the thing standing in for "a real bartender looked at this and approved it" — there's no partial credit for an authorization that didn't happen. But a duplicate confirmation buried inside an otherwise-valid batch, say the same beer queued twice by mistake, isn't an authorization problem at all, it's a data conflict local to that one item. So that gets skipped silently while every other item in the batch still writes. Same endpoint, two different kinds of failure, two different blast radiuses, and the design only holds together because those two things actually are different problems.

Letting customers add the beer that isn't there

The tavern's list won't ever perfectly track what's actually on shelf, so customers can now add a beer themselves when it's missing, just a name, brewery, and style, and it's confirmable that same visit. No admin has to approve it first. The catalog gets a verified flag instead of a moderation queue: an admin can find and clean up self-serve entries afterward, but nothing blocks the customer in the moment. That's the same instinct as everything else in this app, the paper sheet never made someone wait for a manager to say a beer counted, and neither should this.

Catching six bugs before anyone else could

Before any of this merged, I ran an independent review pass over the whole backend diff, five separate reviewers each looking through a different lens, and every finding got checked a second time by trying specifically to prove it wrong before it counted. Three of the six real bugs it caught are worth naming.

One was a concurrency bug in the new batch endpoint: two nearly-simultaneous requests confirming the same beer and format could both pass the app's own duplicate check before either one had written to the database, and the second one would hit the database's own uniqueness constraint and blow up into a raw, unhandled error. That's the exact same category of bug Sprint 8 shipped by accident, a real database enforcing something the in-memory test stand-in doesn't, except this time it was found by deliberately looking for it instead of by luck.

The second was quieter and nastier: the self-serve beer's "who added this" field was getting silently erased the very first time an admin edited that beer, because the edit endpoint replaces the whole record and the edit form never sends that field back. Nothing crashed, nothing logged an error, the attribution just disappeared.

The third lived in the sprint's new anomaly signal, which flags a customer confirming an unusually large number of beers in one day. It counted the day in UTC. A long, busy night that happened to straddle UTC midnight could get split into two ordinary-looking days and never trip the alert at all, exactly the kind of bug that only shows up on the right date and never during a quick manual check.

All six got fixed and folded back into the same pull requests before they went out, along with regression tests aimed at each one specifically.

The PM angle

Testing usually asks "does this do what it's supposed to." A review pass like this asks a different question: "can I prove this is wrong." Those aren't the same exercise, and running the second one deliberately, before anything ships rather than after something breaks, is closer to a real pre-release bug bash than to writing more unit tests. The bugs it found weren't exotic either — a race condition, a silent overwrite, a timezone assumption — they're the ordinary ways real systems fail, which is exactly why it's worth looking for them on purpose instead of waiting for a customer to find them first.

Where the project stands

Six issues, six pull requests (#111 through #116), stacked so each one's diff stays scoped to what it actually changed, plus a seventh for the planning docs. Backend suite: 360 passing. Frontend suite: 215 passing. Every backend piece verified live against a real Postgres database, and once a flaky local Docker build finally cooperated, the two-tab nav and the new dial-pad confirm screen got a live pass too. Next up is the sprint that's been waiting on this one: a real My Beers screen, built on a confirmation model that's finally shaped the way the product actually needs it to be.

Comments

Loading comments...