```html

Fixing Calendar Sync Auto-Triggers in Google Apps Script: OAuth Re-authorization and Time-Based Trigger Setup

What Was Done

The Boatsetter booking platform integration was code-complete but non-functional because two critical pieces were missing: (1) the time-based triggers that execute the sync and reconciliation jobs were never activated, and (2) OAuth tokens for Gmail and Google Calendar had expired or been revoked, causing permission failures at runtime.

This post walks through the exact steps taken to restore full calendar synchronization functionality, including trigger registration, OAuth re-authorization flow, and verification of the iCal fetch pipeline.

The Problem: Three Blockers in Sequence

  • Missing Trigger Activation: The sync logic was written in CalendarSync.gs:355 function calendarSyncSetup(), but this setup function had never been executed. Without it, no triggers existed — the code ran zero times per day.
  • Expired Gmail OAuth: The function that sends daily reconciliation emails uses the Gmail service, which requires explicit OAuth scope approval. The token had expired.
  • Revoked Calendar OAuth: Writing bookings to the master calendar requires Calendar API permissions, which had been revoked at the GAS project level.

The fix required re-establishing OAuth consent, then activating the triggers. The counterintuitive part: calendarSyncSetup() itself only touches SpreadsheetApp and ScriptApp services, so it wouldn't prompt for Calendar or Gmail scopes. A separate test run was needed to trigger the OAuth dialogs.

Technical Details: The Fix Sequence

Step 1 — Re-authorize Gmail and Calendar Scopes

File: /Users/cb/Documents/repos/sites/queenofsandiego.com/CalendarSync.gs

Function: testSync() (line 563)

GAS Editor URL: https://script.google.com/d/1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii/edit

In the GAS editor:

  1. Click the function dropdown (currently showing "Select function")
  2. Choose testSync
  3. Click the blue Run button
  4. A permission consent dialog appears: "This app needs access to your Google Account"
  5. Click Allow
  6. The script may prompt a second time for Calendar scope — allow that too

Why this works: GAS services (Gmail, Calendar, Spreadsheet) operate under a single OAuth consent model. The first function that touches a service triggers the scope request. testSync() calls Calendar.Events.list(), which fires the Calendar scope prompt. Inside that function's execution, it also calls GmailApp.sendEmail(), which fires the Gmail prompt.

What the test does: It attempts to fetch from the Boatsetter iCal URL (wired in the ICAL_FEEDS array), parse events, and write a test booking to the ops sheet. The execution log will show:

Fetching Boatsetter iCal...
  N events parsed
CalendarSync test complete. No errors.

If you see Exception: You do not have permission to perform this action, the OAuth re-consent failed — go back to step 1.

Step 2 — Activate Time-Based Triggers

File: /Users/cb/Documents/repos/sites/queenofsandiego.com/CalendarSync.gs

Function: calendarSyncSetup() (line 355)

Once testSync() runs without permission errors:

  1. In the same GAS editor, switch the function dropdown to calendarSyncSetup
  2. Click Run
  3. Watch the execution log for:
9:57:20 AM    Notice    Execution started
9:57:24 AM    Info    CalendarSync setup complete:
9:57:24 AM    Info      - BookingLedger tab created in ops sheet
9:57:24 AM    Info      - syncAllChannels: every 30 minutes
9:57:24 AM    Info      - sendDailyReconciliation: daily at 7:30am PT
9:57:24 AM    Notice    Execution completed

What this function does:

  • Creates the BookingLedger sheet in the ops spreadsheet (idempotent — safe to run multiple times)
  • Registers a time-based trigger for syncAllChannels() to run every 30 minutes
  • Registers a time-based trigger for sendDailyReconciliation() to run daily at 7:30 AM PT

Verify triggers are registered: In the GAS editor, click the clock icon in the left sidebar (Triggers panel). You should see two entries:

  • syncAllChannels | Time-based | 30 minutes
  • sendDailyReconciliation | Time-based | Daily (7:30 AM PT)

Step 3 — Verify End-to-End

Run testSync() again. This time, you're testing with live OAuth tokens. The log should show:

Fetching Boatsetter iCal...
  12 events from Boatsetter (past 90 days)
Parsing events...
Writing to BookingLedger...
CalendarSync complete. New bookings: 3
Sending reconciliation email to ops@queenofsandiego.com

No permission exceptions = the system is live.

Architecture Notes: Why This Design

The CalendarSync system uses a hub-and-spoke architecture:

  • iCal feeds (Boatsetter, Sailo, Viator via email scanning) → Google Apps ScriptMaster calendar and Booking ledger sheet
  • Time-based triggers in GAS (not Lambda, not Cloud Scheduler) because the writes are to Google Workspace APIs, which have tighter integration with GAS. No need for external schedulers or cron jobs.
  • Dual destinations (calendar + ledger sheet) let different downstream tools (the dashboard, the manager interface, email rules) consume the booking data in their preferred format.

The calendarSyncSetup() function is intentionally idempotent — it checks for existing triggers and sheets before creating them — so it's safe to run multiple times if onboarding a new platform or resetting triggers after a service outage.

Outstanding Item: Viator Email Scanner Trigger