Skip to main content

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​

#AskScreenEvidence
1List shows the booking IS a group/reservationsGroup badge renders on member rows
2List shows HOW MANY rooms/reservationsGroup · N rooms via bookingScale
3Tape chart shows a bar is PART of a group/front-desk/calendarGRP marker on the bar
4Tape chart shows HOW MANY rooms/front-desk/calendarGRP 3 / GRP 4 observed live — see below
5Click a group bar → panel gives group infotape-chart panelGroup panel opens with block detail
6Check in ALL roomsgroup Rooms tab4 of 4 checked in, one at a time
7Check in a SELECTIVE subsetgroup Rooms tabin [252,253], out [250,251]
8Check in ONE, others NOT checked ingroup Rooms tab252 in; 253/250/251 untouched
9Manage INDIVIDUAL room foliosgroup folio → per-roomPer-room folio opened and read back
10Charge lands on THAT room onlygroup folio → Add chargeRoom 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:

groupfrontdesk-localcanonicalmatch
170a20c533✅
316fe4ca33✅
f57a638f44✅ (the known 4-room fixture)
3491b20e33✅

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:

  1. 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.
  2. Running pod: 13/13 group bars carry groupRoomCount; 4/4 groups match canonical.
  3. Browser, /front-desk/calendar: markers read GRP 3 and GRP 4. GRP 4 is 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_TODAY scope 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).