Skip to content

Google Calendar and Outlook

Paid add-on. A confirmed booking places an appointment in Google Calendar or Outlook; cancelling it takes the appointment back out.

This module depends on the appointment-slots one: it writes nothing without a booking, and the panel only appears on a form that takes them. It is the only one in the series in that position.

A clock change does not move an appointment

That is the plan’s first test, and the decision carrying everything else.

An appointment can be described in two ways: “29 March at 2:30 am, Paris time”, or “at this precise instant”. The two are equivalent the rest of the year; they diverge on the two nights when the clocks change.

29 March 2026 at 2:30 am, Paris time, does not exist: that hour is skipped. And 25 October at 2:30 am exists twice. A booking taken in January for a summer date, described in local time, shifts by an hour when the day comes — or falls into a hole.

The slot therefore goes out as an absolute instant, in universal time; the timezone accompanies the display only. The calendar has nothing left to interpret.

The timezone kept is the booking’s, not the site’s at the moment of placing it: a site that changes timezone in between must not move an appointment already announced to somebody. A timezone PHP does not know — a fixed offset like UTC+2, which neither service accepts — falls back on universal time: the appointment will show at the wrong hour, but it will take place at the right moment, and it is the instant that triggers the alarm.

A slot date that cannot be read is not approximated: nothing is placed, and the reason says so.

The two services do not describe a date the same way

Google accepts the universal instant — a trailing Z — with a display timezone alongside. Microsoft Graph refuses that Z and wants a bare time plus the name of its timezone; it therefore receives universal time and the UTC zone.

That is the divergence CalendarProviderInterface does not hide. Smoothing it behind a single shape would have forced a choice of which is “the real one” — and that is precisely the question where you get the hour wrong twice a year.

Only a confirmed place enters a calendar

A place on the waiting list is not an appointment: putting an event there would block a slot nobody obtained, and have a real request refused.

Three moments, and not one more: a confirmed place goes in, a place promoted from the waiting list goes in too — it has become a real appointment — and a cancelled place comes out.

Payment is already settled upstream

The plan asks that the event only be created “after confirmed payment where required”. This module has nothing to check for that: a submission whose payment does not suit is refused before any recording, by the payment module’s guard. No submission, therefore no booking, therefore no appointment.

Redoing that check here would have given a second place to get it wrong, and a false impression of a guarantee if the two came to diverge.

Nobody is invited without being asked for

Adding the visitor’s address as a guest does not merely put them on the event: Google and Microsoft then send them an invitation from the client’s account. That is a second message, which nobody wrote, arriving after the one the site has just sent — and sometimes in another language.

The setting is therefore off by default, and the screen says what it triggers before you switch it on. At Google, sendUpdates=none is placed on every call, including with no guest: a default that changed at their end must not translate into emails going out from a site that did not ask for them.

What the calendar displays

A calendar is shared, sometimes with a whole team, and an appointment stays visible there for a long time. That is what sets this output apart from the others.

The description therefore carries only the fields designated one by one — not “every value”. Sensitive types are set aside whatever happens, and the whole is bounded: a ten-thousand-character description is not read in a calendar box.

The title accepts two markers, {form} and {field:id}, because a calendar is read at a glance and “Appointment” on thirty lines informs nobody. An unknown marker is removed rather than left as it is, and a marker designating an excluded field returns nothing.

The calendar is never guessed

It would have been convenient to take the default calendar when none is chosen. That would mean putting appointments in the personal calendar of the person who authorised, when they may have been aiming at another — and a personal calendar is sometimes shared with a family.

As long as no calendar is chosen, nothing is placed, and the screen says so. The choice happens after the authorisation, since you cannot list somebody’s calendars before. Only those the account can write to are offered; a calendar shared read-only would refuse the first appointment. And an id typed by hand is refused: it would designate at best nothing, at worst somebody else’s calendar.

Cancellation, and the failure that really counts

An appointment that was not placed gets noticed: the person calls, and nobody has anything in their calendar.

A cancelled appointment that was not removed does not get noticed at all: somebody keeps a slot reserved for a visitor who will never come, and finds out on the day. The screen therefore distinguishes the two, and the second is written as what it is — something to go and settle by hand in the calendar.

An event already absent is a success: Google answers 410, Microsoft 404, and both mean “it is no longer there”, which was the request. Treating that as a failure would have made a second cancellation fail forever on what the first had succeeded at.

There is no resend button, and that is deliberate

Replaying a placement would create a second appointment if the first had in fact succeeded — and a duplicate in a calendar blocks a slot and has a real request refused. The automatic retries are enough; what they do not obtain is settled in the calendar, where you can see what is actually there.

Erasing a submission cancels nobody’s appointment

The tracking row goes, the event stays.

Deleting a submission is an act of data erasure; cancelling an appointment is an act of administration, and it concerns the person waiting for it. Conflating the two would make an appointment somebody intended to honour disappear from a calendar, without anybody having decided it.

It is the booking’s cancellation that removes the event, and it alone.

Two extension points added to the slots module

solis_forms_pro_booking_cancelled and solis_forms_pro_booking_promoted, emitted from the repository — where the state actually changes — and not at their callers’, who are already two and would be three tomorrow. A guarantee depending on each caller remembering it is not one: the first you forgot would leave a cancelled appointment in somebody’s calendar.

They join solis_forms_pro_booking_reserved, which already existed.

One connection per provider, and not the storage one

The storage module already authorises Google and Microsoft. But an authorisation carries scopes: storage’s opens files, this one opens a calendar. Merging them would have meant asking everybody for both — and making somebody who never wanted a calendar reauthorise their storage, interrupting their uploads until they noticed.

The scopes requested are the narrowest that work: calendar.events at Google’s — and not calendar, which would also give the right to delete whole calendars — and Calendars.ReadWrite at Microsoft’s.

Schema

slf_calendar_events: one row per booking carried to a calendar — and not per submission, because it is the booking that has a life in a calendar. With the provider, the calendar, the remote id, the state, the reason for the last problem and the number of attempts.

Five states instead of four: the fifth, removed, exists because an appointment has an end. Conflating it with “failed” would have made every successful cancellation appear in the list of problems.

The remote id is kept only in order to be able to cancel. Without it, a cancellation would not know what to remove.

The connections live in an option: the client id, the calendar and the account’s name in clear — they are not secrets, and the calendar is the first thing to check when appointments arrive in the wrong place — the tokens and the client secret encrypted.

The delivery log carries neither the request nor the response — they contain the title and description composed from the submission — nor the calendar’s id, which at Google is most often the account’s email address.

What V1 does not do

No reading of availability from the calendars, no two-way moving, no recurring event, no reconciliation of a change made by hand.

That last point deserves saying plainly: if somebody moves the appointment in their calendar, SolisForms will not know, and the booking will keep the original time. The site remains the source of the slot; the calendar is its reflection.