CU-04: Microsoft / Outlook calendar integration

Priority: P2 Accounts/sessions: Outlook host ravikant0909@outlook.com (logged into Calendo via "Sign in with Microsoft", Outlook calendar connected); access to outlook.com calendar + outlook.com mail in the same browser profile. Invitee identity uses P1's Gmail plus-alias ravikantguptaofficial+inv-<RUNID>@gmail.com so the confirmation email lands in the single searchable P1 Gmail inbox (mail.google.com). Parallel-safe: Yes — does not rewrite global host availability; only creates/removes throwaway calendar events and one throwaway event type + one booking, all scoped by RUNID. Exclusive (rewrites global host availability?): No. Estimated time: 35 minutes. L3 reality checks: Outlook calendar event create (booking) + delete (cancellation), and an Outlook BUSY event suppressing a booking-page slot. Email confirmation L3 in P1 Gmail.

Goal

This suite proves the Outlook/Microsoft mirror of the Google Calendar integration: a host who signed into Calendo with Microsoft and connected their Outlook calendar gets true two-way calendar sync. Specifically, (1) a BUSY event sitting in the host's real Outlook calendar during their available hours removes that slot from the public booking page (conflict detection via Microsoft Graph calendarView, filtering busy/tentative/oof), and (2) when an invitee books a free slot, Calendo writes the meeting into the real Outlook calendar, and (3) cancelling the Calendo booking removes the event from the real Outlook calendar. This matters because Outlook/Microsoft 365 users are a large share of professional schedulers; if conflict detection or event write-back silently fails, the host gets double-booked or their Outlook calendar drifts from Calendo. It also documents the Microsoft consent screen (including the unverified-publisher warning) and the offline_access refresh-token behavior so the human reviewer knows the OAuth posture.

Preconditions

Test data

Steps

Order matters: connect/confirm Outlook FIRST (Step 1), then create the event type (Step 2), then create the BUSY blocker in Outlook BEFORE checking the booking page (Steps 5–6) so the conflict test is valid. Only AFTER confirming the busy slot is suppressed do you remove the blocker and book a free slot (Steps 7–10), then cancel (Step 11). Doing the BUSY test before the booking test avoids the two interfering.

  1. Action Go to https://calendo.dev/dashboard/ and open the Availability tab (sidebar link .sidebar-nav a[data-tab="availability"]), then open the calendar-settings sub-panel (#avail-panel-calendar-settings, heading "Calendars to check for conflicts"). Read the connections list (#calSettingsConnections). → Expect A connection card (.cal-connection-card) labeled "Outlook Calendar" (.cal-connection-name) with a disconnect "×" button (.avail-btn-remove, onclick="disconnectCalendar(<id>)"). If present, Outlook is already connected — skip to Step 2. Capture screenshot: cu04-01-outlook-connected.png. → [L2]
  1. Action (only if Step 1 shows NO Outlook card) Click "+ Connect calendar account" (#calSettingsConnectBtn) which points to /api/calendars/connect/outlook (or use the onboarding/welcome dialog "Connect Outlook Calendar" link, <a href="/api/calendars/connect/outlook">). Observe the Microsoft consent screen at login.microsoftonline.com/common/oauth2/v2.0/authorize. → Expect Microsoft sign-in/consent for ravikant0909@outlook.com. The consent requests scopes openid, email, profile, offline_access, Calendars.ReadWrite, OnlineMeetings.ReadWrite. If a blue "unverified app / publisher not verified" warning appears, RECORD it verbatim in the report (this is expected for the indie Calendo app and is a documentation item, not a failure). If Microsoft demands a password, STOP and FLAG precondition failure. After accepting, the browser returns to Calendo. → Expect Redirect lands on https://calendo.dev/dashboard/?calendar=connected (success). On the Availability tab the Outlook card now appears in #calSettingsConnections. Capture screenshot: cu04-02-outlook-consent.png (consent screen) and cu04-02b-connected-redirect.png. → [L1]
  1. Action Reload https://calendo.dev/dashboard/, go to Availability → calendar settings, and confirm the Outlook connection persists; also check the primary-calendar dropdown (#calSettingsPrimarySelect) lists an "Outlook" option. → Expect Outlook still shown after reload; dropdown contains an Outlook entry. (This proves the connection is stored, not just a transient redirect.) → [L2]
  1. Action Go to the Overview tab (.sidebar-nav a[data-tab="overview"]). Click "+ New" to create an event type (#overviewCreateEtBtn); in the modal/panel (#editEtModal / #editEtPanel) set Name = CU04 Outlook <RUNID> (#editEtName), Duration = 30 (#editEtDuration), then Save/Create (#editEtSaveBtn). → Expect Event type appears in the overview list (#overviewEventTypes contains CU04 Outlook <RUNID>). Open its booking link and record the exact slug; build <BOOKING_URL> = https://calendo.dev/booking/?user=<HOST_SLUG>&event=cu04-outlook-<RUNID>. Capture screenshot: cu04-04-eventtype-created.png. → [L1]/[L2]
  1. Action (BUSY conflict setup — do BEFORE checking slots) Open https://outlook.com/calendar as ravikant0909@outlook.com. Pick the first weekday at least 2 days out that has open daytime slots on the Calendo booking page (you will cross-check in Step 6); call it <TARGET_DAY>. Create a new event: title CU04 BUSY <RUNID>, on <TARGET_DAY> from 10:00–11:00 in the host's Outlook timezone, and set Show as = Busy (the default; do NOT set Free). Save. → Expect The event CU04 BUSY <RUNID> is visible on <TARGET_DAY> 10:00–11:00 in outlook.com, marked Busy. Capture screenshot: cu04-05-outlook-busy-created.png. → [L3] (real Outlook event created)
  1. Action In a fresh tab open <BOOKING_URL>. Navigate the booking calendar (.calendar-nav-btn) to <TARGET_DAY> and click that day cell (.calendar-day.available). Inspect the time slots panel (#time-slots-panel / #time-slots-list, slot buttons .time-slot). → Expect There is no bookable 10:00 / 10:30 slot for <TARGET_DAY> (the 30-min slots overlapping 10:00–11:00 are absent). Slots outside 10:00–11:00 (e.g. 09:30 or 11:00) should still be present if within host hours. If the day shows "No available times" (.time-slots-empty) entirely, pick a different <TARGET_DAY> with more hours and repeat Steps 5–6 so the test isolates the 10:00 block. Capture screenshot: cu04-06-slot-suppressed.png. → [L3→L1] (real Outlook busy suppresses a real booking-page slot)
  1. Action Return to https://outlook.com/calendar, open the CU04 BUSY <RUNID> event, and delete it. → Expect The blocker event no longer appears on <TARGET_DAY> in outlook.com. Capture screenshot: cu04-07-outlook-busy-deleted.png. → [L3]
  1. Action Reload <BOOKING_URL>, navigate again to <TARGET_DAY>, click the day, and confirm the previously-suppressed slot has returned. → Expect A bookable slot at/around 10:00 (e.g. the 10:00 .time-slot) is now present again — proving conflict detection is live, not cached. (If Outlook busy results are briefly cached, allow up to ~1 minute and reload once.) Capture screenshot: cu04-08-slot-restored.png. → [L3→L1]
  1. Action (book a free slot) Still on <BOOKING_URL> at <TARGET_DAY>, click an available slot (.time-slot, choose the restored 10:00 slot or any free slot). On the confirm form fill Name = CU04 Invitee <RUNID> (#input-name), Email = ravikantguptaofficial+inv-<RUNID>@gmail.com (#input-email), then click Confirm (#btn-confirm). → Expect Confirmation screen shows title "Booking Confirmed" (.confirmation-title) and the manage links (#confirmation-manage with Cancel / Reschedule anchors). Record the booking time <BOOK_START> (the exact start datetime shown) and the cancel link href. Capture screenshot: cu04-09-booking-confirmed.png. → [L1]
  1. Action Go to the dashboard Bookings tab (.sidebar-nav a[data-tab="bookings"]) and confirm the new booking is listed for CU04 Invitee <RUNID> on <TARGET_DAY> at <BOOK_START>. → Expect Booking row present with the invitee name and event CU04 Outlook <RUNID>. → [L2]
  1. Action (cancel) Open the cancel link recorded in Step 9 (or https://calendo.dev/booking/cancel.html reached via the confirmation page / confirmation email), and complete the cancellation. → Expect The page confirms the booking is cancelled; in the dashboard Bookings tab the row shows cancelled/removed. Capture screenshot: cu04-11-cancel-confirmed.png. → [L1]/[L2]
  1. Action (document token-refresh behavior — read-only note) No in-browser action forces a token refresh, but record the expected posture for the reviewer: Calendo requested offline_access, so it stores a refresh token; getValidOutlookToken refreshes when the access token is within 5 minutes of expiry OR when no expiry is recorded; if Microsoft returns AADSTS700082 / AADSTS50173 (refresh token expired after ~90 days inactivity), the host must re-connect Outlook. If during ANY step above a calendar action silently no-ops (slot not suppressed, or event not written), check the Availability calendar-settings panel for a disconnected/error state and a "Calendar disconnected" indicator — that points to an expired refresh token and is a real failure to report. → Expect Documented note + no observed disconnect/error state during the run. → [L2]

L3 reality checks

A. Outlook busy suppresses a slot (Steps 5–8).

B. Booking writes a real Outlook event (Step 9).

C. Cancellation removes the Outlook event (Step 11).

D. Email confirmation in P1 Gmail (Step 9).

Cleanup

Leave both host accounts clean. Perform regardless of pass/fail:

  1. Ensure the booking from Step 9 is cancelled (Step 11). If you skipped it, cancel it now via https://calendo.dev/booking/cancel.html or the dashboard Bookings tab.
  2. In https://outlook.com/calendar, delete any remaining CU04 BUSY <RUNID> event AND any CU04 Outlook <RUNID> with CU04 Invitee <RUNID> event (the cancellation should have removed the latter; if it lingers, delete manually and FLAG it as a write-back/cancel-sync bug).
  3. In the Calendo dashboard, delete the throwaway event type CU04 Outlook <RUNID> (Overview/Event Types → its delete/×).
  4. Do NOT disconnect the Outlook calendar connection — it is a shared precondition for reruns. Only disconnect if you connected it fresh in Step 2 AND the reviewer wants the account left exactly as found; otherwise leave it connected and note it.
  5. In P1 Gmail, the confirmation/cancellation emails can be left (they are RUNID-scoped and searchable); optionally archive/trash anything matching inv-<RUNID> to keep the inbox tidy.

Pass/Fail criteria

PASSES only if ALL are met:

FAILS if: any slot is NOT suppressed by the busy event; the event is NOT written to Outlook; the cancellation leaves a stale Outlook event; the connection does not persist; or a token/disconnect error surfaces.

Evidence to capture

Manual residue / cannot-verify