Bookings and appointment slots
Paid add-on. A form can offer appointment slots: times, capacity, a waiting list and cancellation through a signed link.
This document describes what the module guarantees and what it refuses. The user guide gives the short version, under “Taking appointments”.
Everything is decided in the site’s time zone
A nine o’clock slot is at nine o’clock for whoever is receiving. Deciding it in the visitor’s zone would make opening hours appear to differ depending on the country you look from, and an appointment taken “at nine” from Montreal would land at two in the afternoon on site.
Instants are stored in UTC and configured in local time. Storing local time would mean a slot booked before a clock change and read back afterwards designated another moment; configuring in UTC would force the administrator to recompute their hours every season.
The spring forward
In spring, a local hour does not exist: you go from two o’clock to three. PHP folds the missing hour onto the next one, and two neighbouring slots then designate the same instant. Slots are therefore deduplicated by instant — failing which the form would offer the same appointment twice that day, with two separate capacities for a single real window.
In autumn, a local hour exists twice. PHP keeps one, and the other is not offered: one hour fewer that day is better than a slot that nobody — neither the client nor the person receiving — could say which of the two passes it belongs to.
Capacity is carried by an index
A confirmed booking occupies a numbered seat, and
UNIQUE (form_id, slot_start, seat) forbids two bookings from occupying the
same. Taking a place is therefore an insert: it succeeds, or it fails because the
place is taken.
The alternative — count the bookings, then insert if the count allows — leaves a window between the two in which anything can happen. Two simultaneous clicks on the last place both count “one place free” and both insert. No amount of re-reading rescues that; only a constraint makes it impossible.
Why no explicit transaction: it would bring nothing, a single write being at stake. It would even be misleading, since MyISAM — which some hosts still impose — accepts one without honouring it. Better a guarantee the engine really applies.
The seat can be null
MySQL allows several null values in a unique index. A cancelled or waitlisted booking therefore carries a null seat: it occupies nothing, and several can coexist on the same slot.
That is what makes a cancellation genuinely free the place without erasing the row: the history stays, the seat goes back. Keeping the seat on a cancelled booking would have tied up a place nobody occupies any more.
entry_id is unique: one submission books once, and a replayed treatment does
not take a second place.
Displayed availability is not a promise
It is read at render time, and the slot can fill up between the display and the send. The display is there to choose from, not to guarantee — and that is why the place is taken by an insert, not from the number shown.
A full slot disappears from the list, unless the waiting list is open: offering a place you know is taken makes someone fill in a form for nothing.
The number of places left is announced only beyond one: “one place left” on a slot with a capacity of one teaches nothing, and repeated down the whole list it is just noise.
The field
A dropdown grouped by day, and not a clickable calendar. A home-made date picker
means writing keyboard navigation, screen reader announcements, touch behaviour
and focus management — that is, everything a native select already does,
better, and in the system’s language.
The optgroup elements give the structure a calendar would bring, without taking
on its problems.
The field computes nothing: slots are injected into its configuration at
render time, by the solis_forms_form_pages filter. Giving it access to the
calendar and the repository would have made it impossible to render outside a
complete request, and therefore impossible to test.
Payment first, the place afterwards
Booking happens after the payment guard has refused any submission whose transaction is unconfirmed: a slot is never taken on a pending payment.
It also consults solis_forms_pro_entry_effects_allowed: if the form asks for an
address confirmation, the place waits until the address is confirmed. A place
held by an address nobody has proved they own is a place lost to everyone.
Cancellation
The link is signed — the same mechanism as address confirmation, in a distinct context: presenting a confirmation link to the cancellation route, or the reverse, gives nothing even if the row numbers coincide. The database keeps only a fingerprint.
The link redirects immediately, like the confirmation one: rendering a page at
its address would leave the secret in the history, the address bar, the access
log and the Referer header.
The cancellation window protects whoever is receiving. Past that window the link stops working: a cancellation an hour before an appointment does not free a usable place, and above all leaves somebody facing an empty slot they had set aside in their schedule.
Set it alongside the notice period: a twenty-four-hour cancellation window and no notice period mean the first slots offered are already non-cancellable.
Administrative cancellation, for its part, knows no window — it is the person receiving, or their team, who acts.
The waiting list
Promotion is manual. An automatic promotion would send a confirmation to somebody who may have taken another appointment in the meantime, and nobody would be there to judge — nor to call them.
The address that is notified is designated
Not guessed. Taking “the first email field” would send the confirmation to a third party’s address on a form that carries two — a companion’s, an employer’s.
What the module does not do
No Google or Outlook synchronisation, no multiple resources, no two-way appointments, no complex recurrence, no tax invoicing.
Nor does it hold the place during a payment in progress: two people paying for the last place at the same time will see one of them put on the waiting list, or refused if the list is not open. Holding a place means deciding when to release it, and a place tied up by an abandoned basket is a place lost until that delay runs out.
