Five places where the data existed and the screen did not use it.
Booking a whole course had no button because it needs a doctor and the course
does not carry one — each session can be with a different doctor. The page now
asks for the doctor the same way the resource booking page does, and the button
explains that it is all-or-nothing before it is pressed.
A course whose package does not cover the remaining sessions is still valid —
the rest is simply charged normally — but nobody was told. The course response
carries package_balance and the shortfall, and the page warns. Before session
six, not during it.
The credit ledger already returned who recorded a row and which appointment it
belonged to, and showed neither. An adjustable ledger without the name of the
person who adjusted it is half an audit trail.
Version history printed a JSON blob of each version's effects, which does not
answer the question anyone actually has: what changed? It now diffs each
version against the previous one, field by field, and says so plainly when a
version changed nothing meaningful.
A resource with no calendar showed "—" for utilization. Null means undefined,
not zero, and the next step is always the same: set up the calendar. It is a
link now. The report range also accepts a custom from/to, kept in the URL like
the rest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Writing the query-count test that task 14 owed showed the growth was real: one
resource cost 10 queries, six cost 33 — about five per resource, because the
available-minutes figure walked each resource's calendar on its own.
Holidays, tenant overrides and branch hours are identical for every resource in
a report, so they now load once outside the loop; shifts and exceptions load for
all resources in one query each. The batched path is a new method rather than a
change to rawAvailability, which the booking engine also calls. The test pins
the shape of the growth, not an exact count.
Also landed:
- app:segment:seed-templates with beauty, dental and physio presets. Building
four segments and their requirements by hand is the first thing a new clinic
must do and the most tedious; this gives them something to edit instead of an
empty page. It refuses to touch a service that already has segments unless
--force, and it will not invent resource types the tenant never defined.
- book-all is all-or-nothing, proven rather than asserted: with a calendar open
one day a week and a 1-2 day protocol gap, session one finds a slot and
session two cannot, and every session must come back planned.
- credit_refundable: false takes the credit back with a negative adjustment and
deletes nothing — the ledger stays append-only.
- the segments editor has frontend tests, including that it sends back what the
user sees and renders read-only without the permission.
useBranches now returns [] for a non-array payload instead of throwing
"branches.map is not a function" and taking the page down with it.
BookingLocationsScanTest built a Clinic around a Doctor loaded from a different
manager, which Doctrine treats as a new entity; it flushed fine most runs and
failed on cascade in others. It now loads the doctor from the same manager.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tasks 07 through 13 each changed something the rest of the system might want
to know about, with no contract for saying so. And task 05 shipped a powerful
segment editor with no feedback on whether a clinic defined its segments right.
Events
- A closed list of names, because a consumer branches on the string and a
one-letter typo would produce an event nobody hears and no error either
- Payloads carry uuids and scalars only; non-scalars are dropped, not
serialised, so a consumer always fetches fresh rather than reading a stale
detached entity
- record() deliberately does not flush: the event row commits with the change
it describes, so a rolled-back transaction leaves no event behind. A test
pins exactly that
- app:events:publish drains the outbox; five failed attempts park a row with
its error rather than deleting it, because a silently dropped event is a
loss with no trace. app:events:prune only ever removes published rows
Reports
- Resource utilisation separates available, occupied and active minutes.
The gap between occupied and active is what exposes a bad segment
definition, and available is multiplied by capacity so a three-chair room
does not read as permanently over 100%
- A resource with no calendar reports utilization: null, not zero — dividing
by zero means something different from being idle
- Plan accuracy compares planned against actual duration per service and
flags both directions: running short wastes capacity that could have been
sold. Its row links straight to editing that service's segments, because a
report with no route to a fix does not get read
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>