Group Booking / Group Folio — the ten asks, delivered
Date: 2026-08-04 · Environment: TEST · Status: 10 of 10 delivered, each proven in the browser.
Why this document exists. The operator's constraint was explicit: "Staff is not going to use API to do their tasks." An API pass is not acceptance evidence. Every row below names the screen, and every claim is backed by something observed in a browser against the deployed build — not by a curl transcript.
Two of the ten were reported delivered on an API pass earlier in the programme and were not. That is the reason for the standard, and it is recorded here rather than quietly corrected.
The scoreboard
| # | Ask | Screen | Evidence |
|---|---|---|---|
| 1 | List shows the booking IS a group | /reservations | Group badge renders on member rows |
| 2 | List shows HOW MANY rooms | /reservations | Group · N rooms via bookingScale |
| 3 | Tape chart shows a bar is PART of a group | /front-desk/calendar | GRP marker on the bar |
| 4 | Tape chart shows HOW MANY rooms | /front-desk/calendar | GRP 3 / GRP 4 observed live — see below |
| 5 | Click a group bar → panel gives group info | tape-chart panel | Group panel opens with block detail |
| 6 | Check in ALL rooms | group Rooms tab | 4 of 4 checked in, one at a time |
| 7 | Check in a SELECTIVE subset | group Rooms tab | in [252,253], out [250,251] |
| 8 | Check in ONE, others NOT checked in | group Rooms tab | 252 in; 253/250/251 untouched |
| 9 | Manage INDIVIDUAL room folios | group folio → per-room | Per-room folio opened and read back |
| 10 | Charge lands on THAT room only | group folio → Add charge | Room 254 charged; siblings unchanged |
Ask 4 — the last one, and why it was stuck
The dashboard had rendered GRP <n> since AB#11158. The calendar payload never
sent groupRoomCount, so the bar fell back to a bare GRP for weeks. Both
halves were individually correct and the capability was absent. No unit test on
either side of that seam can see this.
The blocker was recorded as a decision with three costed options, all expensive:
promote the count to a shared @hmp package (pipeline-only publish + lockfile
regen in two repos), call getReservationsByGroup once per group on every render
of the slowest screen in the product, or add a bulk endpoint plus a typed client
method (another publish).
A fourth option was never costed, because nobody had checked whether frontdesk could answer from its own read-model. It can. Measured against the canonical rooming payload at the room grain:
| group | frontdesk-local | canonical | match |
|---|---|---|---|
170a20c5 | 3 | 3 | ✅ |
316fe4ca | 3 | 3 | ✅ |
f57a638f | 4 | 4 | ✅ (the known 4-room fixture) |
3491b20e | 3 | 3 | ✅ |
Non-vacuous (four groups compared) and discriminating (3s and a 4, not one
constant). Reservation.groupId is populated on this read-model — 13 of 13 group
bars carried it — which is also why isGroup has always worked.
Shipped: frontdesk f85d33c (build 40951) · dashboard ead8e1cc (40952) ·
L2 spec 58f5df7a · catalogue registration hmp-infrastructure 30c94b14 ·
docs 40a4553 + 771ce7c4.
Verified, strongest evidence first:
- The L2 spec was RED on the pre-deploy build — it cleared both positive-data guards, resolved the canonical counts, found rendered markers, and reported 0 carrying a count — and went GREEN unaided on 40951.
- Running pod: 13/13 group bars carry
groupRoomCount; 4/4 groups match canonical. - Browser,
/front-desk/calendar: markers readGRP 3andGRP 4.GRP 4is the CheckinModes group, canonical count 4.
Design decisions worth not re-litigating
The count is computed from its OWN unwindowed query. Reusing the rows the calendar already fetched would be free and wrong: they are filtered to the visible date range, so a group with rooms outside the window would report fewer rooms than it has. A confidently wrong number is worse than the bare badge it replaces, because staff act on it. A group's size does not depend on which dates are on screen.
An all-excluded group is omitted, not mapped to 0. The field arrives absent
and the bar renders the bare marker. Emitting 0 would render GRP 0.
The wording differs per surface; the decision does not. A bar is a few pixels
wide (GRP 4); a list row has space (Group · 4 rooms). Unifying the wording
would be wrong. What was duplicated — and is now shared as
hasShowableRoomCount — is the safety guard: a count we do not have must not be
invented as 0 or 1. Two copies of a safety guard is one copy too many.
A second copy of the excluded-status list now exists in frontdesk, and it is gated. This is the duplication the decision existed to avoid, so it is not hidden: the list is a pinned mirror, and its spec reads reservation-service's canonical file, strips comments before parsing, asserts the parse is non-empty so an empty parse cannot "agree" with anything, and fails the build naming the status if the two diverge. Upgradeable to the shared-package option later without changing the payload contract.
Traps found while verifying — recorded so they are not re-learned
The tape chart virtualises. The first browser read of ask 4 found zero markers, which looks exactly like "the feature does not work". It was a viewport artefact: ~27 of 139 rows render, and the group rooms sat below the fold. Scrolling the grid container surfaced them. On a virtualised board, "no marker visible" means nothing until you scroll.
A pod restart expires the browser session. Immediately after a deploy the
dashboard redirects to /login?error=SessionExpired. Re-authenticate before
calling a page broken.
sentinel/flows.mjs is not registration. It records findings;
catalogue.mjs is what certify.mjs runs. Registering only in the former left
the new spec an orphan. Doing this surfaced tape-chart-position-band in the
same state — a GREEN finding history since 2026-07-30 on a spec the catalogue
runner never executed. Measured rather than extrapolated: of 20 dashboard
orphans exactly one was in that state, so this is a single instance, not a
systemic twenty. Both are now registered; 19 pre-existing orphans remain and
are untouched — that is its own scoped work, not something to fold in silently.
A check-in needs a government photo ID. The property enforces
requireAadhaarAtCheckIn. With no ID the button fires no POST and toasts "A
government photo ID (type and number) is required" — catchable only with an
observer armed before the click. A TEST-DL-… value is rejected as an
invalid Indian DL format; a format-valid synthetic works.
Fixture discipline
Every probe row was cleaned up with DELETE /api/reservations/:id carrying a
{"reason": …} body. Never PATCH /status CANCELLED — that verb is a
cross-service sync route that flips the status without releasing the room hold,
and the state is terminal, so the inventory is unrecoverable through any API.
The check-in fixture was restored exactly: four undo calls, then a read-back
confirming all four rooms returned to CONFIRMED/RESERVED with the group
aggregate unchanged. The ask-10 charge was voided and the folio totals read back
to their exact baseline.
What is NOT delivered, stated plainly
- No BULK group check-in control. The backend has had an
ALL_TODAYscope since AB#9742; no screen drives it. Asks 6/7/8 are satisfied room-by-room, which is why this is an efficiency gap rather than a missing capability — but it is a gap. - 19 dashboard specs remain unregistered in the certification catalogue (pre-existing).