Epic 3: Why Renaming 'Bartender' and Renaming 'Draft' Turned Out to Be Two Different Problems
A ten-point stakeholder critique session landed the same day as two related complaints: the "Draft/Can/Bottle" buttons needed to work for businesses that don't sell beer, and "Bartender" needed to be reimagined for the same reason. Both got written down as PRODUCT_FEEDBACK_BACKLOG.md's Epic 3, and both got investigated and built the same day — but they turned out to be genuinely different kinds of problem underneath, and treating them as one ask would have gotten the harder one wrong.
The free rename
"Bartender" shows up in two completely different places in this codebase, and only one of them matters. It's a literal ASP.NET Identity role string — SeedData.cs, DemoTavernSeeder.cs, and the [Authorize(Roles = "Bartender,Admin")] attribute gating staff-PIN actions all check for that exact string. But it's also hardcoded English UI copy in something like fifteen frontend files — ConfirmPinPad.jsx, QueueConfirm.jsx, MyBeers.jsx, AdminUsers.jsx, and more — and not one of those files actually renders the Identity role string itself. They're all just words on a screen, fully decoupled from the auth check underneath them.
That meant the fix was free in the way that matters: a new TavernSettings.StaffRoleTitle field, same shape as the ItemNameSingular field this app already had, threaded through the exact same prop chain. Barista for the coffee shop, brewery staff for the brewery, clerk for the record store, budtender for the dispensary — none of it touches the role name, the authorization attribute, or a single line of ConfirmationService. Renaming a word that was never load-bearing is the easy version of this problem.
The rename that would have quietly broken existing data
The format buttons weren't the easy version. ConfirmationFormat is a real enum, and it's part of a real unique index — (CustomerId, BeerId, Format) — that exists specifically to let one customer confirm the same beer more than once in different forms. Two options got investigated and rejected before anything was built. A free-form string field would have broken compile-time safety everywhere a DTO types Format as ConfirmationFormat, and a typo'd variant name wouldn't throw — it would silently create a new, unintended confirmation slot under that same unique index. A nullable column hit a subtler trap: under ANSI SQL, multiple NULLs count as distinct values inside a unique index, which is exactly backwards from what "no format for this tenant" is supposed to mean.
What shipped instead: ConfirmationFormat gained five new members — Standard, Flower, Edible, Vape, Concentrate — additive only. The original plan had been to rename Draft into something more generic, and that's the version I'd have shipped without checking one detail first: ConfirmationFormat serializes via HasConversion<string>(), which stores the enum member's name, not its numeric position. A rename would have orphaned every row already sitting in the database with a literal "Draft" string in it — reading them back later would have thrown. Adding new members alongside the old one avoids that entirely, at the cost of Draft sitting there forever as a name that only makes sense for one industry. That's the right trade. A tenant's actual available set is a new TavernSettings.AvailableFormats field — an ordered subset of those names, not a boolean toggle, because a dispensary needed real distinct labels, not an on/off switch. A null value means no format concept exists at all, and the app writes a new Standard sentinel instead of leaving the column empty.
The decision a fifth industry actually tested
Confirming marijuana dispensary as the fifth demo industry wasn't just another entry in a list — it's the one industry in scope where "does a staff member have to be the one who confirms this" is a real, not rhetorical, question. That question got asked directly rather than assumed away, and the answer held the line this app has kept since the very first sprint: no self-report, no unattended confirmation, for any industry currently in scope. The core reason is structural, not just cultural — ConfirmationService.WriteConfirmationsAsync is the single choke point every badge, milestone, mug-award, and want-list-resolution check runs through, and it's keyed off a resolved staff id at every step. There's no parallel design for a no-staff-confirmation path, and building one would mean redesigning the fraud-detection system alongside it, with nothing concrete asking for it yet.
Dispensary, if anything, argues the other way. A dispensary transaction is more staff- and ID-verification-bound than a bar tab, not less, given age and licensing requirements — the industry most likely to prompt "shouldn't this be self-serve" turned out to be the strongest possible confirmation that it shouldn't. That's a decision worth having tested against a real edge case rather than just reasserted from habit.
With the model settled, Green Haven went in as a real fifth demo tenant — Dispensary industry, budtender staff, the Flower/Edible/Vape/Concentrate format set, twenty seeded strains — and a smaller hygiene gap got closed in the same pass: none of the three tenants added since the original bar (Coffee Haven, Brewery Co, Vinyl Vault) had ever gotten their own Admin account. All four non-original tenants do now.
What the mocks couldn't catch
Live browser verification, not the mocked test suites, caught a real race condition: QueueConfirm and BeerDetail had been seeding their format state from availableFormats through useState's one-time initializer, but App.jsx's progress fetch resolves asynchronously. On the very first real render, before that fetch returned, format locked to 'Draft' — the fallback default — for every tenant, dispensary included, regardless of what that tenant actually offered. Nothing in a mocked test would ever see this, because a mock resolves before the component ever renders once with stale data. The fix is a useEffect that re-syncs format whenever availableFormats changes and no longer contains whatever's currently selected, with a regression test on both components proving it.
The PM angle
The real decision here wasn't either individual fix — it was refusing to treat "rename bartender" and "rename the format buttons" as one ticket just because a single stakeholder sentence bundled them. One was cosmetic. The other touched a unique index that could silently start losing data the moment someone typed a variant name slightly wrong. Looking underneath a request to find out which kind of problem it actually is, before writing a plan for it, is the same habit that's paid off before on this project — it's just usually easier to see in hindsight than in the moment the request lands.
The dispensary question is the one I'd point to on judgment specifically. It would have been easy to read "a fifth, different industry" as evidence the confirmation model needed to get more flexible. Checking that against the industry's own actual constraints — age verification, licensing — flipped the conclusion instead of confirming the instinct. Asking "does this new case actually argue for the change, or does it just feel like it should" is worth doing every time a new industry gets added, not just this once.
Where the project stands
Backend finished at 556 tests passing, frontend at 322, both green, verified live against a rebuilt Docker stack rather than just asserted in CI: Green Haven and Coffee Haven screenshotted side by side (a cannabis-variant picker versus no picker at all), a real Vape-format confirmation moved a live customer's count from 2 to 3 through the actual API, and the new admin@greenhaven.demo account authenticated and wrote its own tenant's settings through PUT /api/organizations/{id}/settings. Five industries now run from one codebase with genuinely different vocabulary and genuinely different format options, on the same staff-confirmed model every one of them shares.
The AWS account that had been the literal next blocker on this project's list got created that same evening — the deployment work that unblocked is its own story, covered next.
Comments
Loading comments...