The per-worker service allow-list was only enforced on the new-booking form.
On the guest manage-booking page a worker could still be offered for services
they do not perform:
- manage_booking() branched on $firstService, which was never assigned - the
resolve loop above it used $resolvedService - so the guarded branch was dead
and the worker list fell through to getActiveWorkers(), filtered by vertical
only. Assigning it activates both the category filter and the capability
check, and with it the $isOtherCategory checkbox disabling in the view.
- The page's own getAvailableWorkersByServiceCategorySlug() never sent
service_ids, so re-picking services asked for a category-only worker list.
It now mirrors the booking form, including the empty-result message.
- setWorker() clears any time already chosen: changing worker or services left
a stale selection with the submit button still enabled.
manage_booking_process() did re-check capability server-side, but every
rejection there answered with raw JSON, which this non-AJAX form renders as an
unstyled blob in the guest's browser. All eight rejections, plus the
cancellation cutoff, now go through _booking_error(), which takes an optional
back URL so the guest returns to their own manage page rather than an empty
booking form. Adds the cutoff_passed message in all three languages and makes
the error title and button label reflect which flow failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYGSbK1erKv7VG1pvdPEjG