```html

Reactivating Calendar Sync: OAuth Recovery and Trigger Setup for Multi-Platform Booking Aggregation

What Was Done

Restored calendar synchronization for the Queen of San Diego booking system by reauthorizing expired OAuth tokens and activating dormant Google Apps Script (GAS) triggers. The Boatsetter iCal integration was fully implemented but never triggered—code was written, URLs were wired, but the time-based automation had never been activated. Additionally, expired Gmail and Calendar OAuth tokens would have caused write failures even if the triggers had been running.

The Problem: Three Discrete Blockers

  • No Active Trigger: calendarSyncSetup() in sites/queenofsandiego.com/CalendarSync.gs (line 355) was never executed, leaving the two critical time-based triggers unregistered in the GAS project
  • Expired Gmail OAuth: The Gmail scope token had expired, blocking the sendDailyReconciliation() function from sending email summaries
  • Revoked Calendar OAuth: The Calendar API scope was revoked, causing any write operations to fail with permission errors

This is a common pattern in GAS projects: installation functions like calendarSyncSetup() must be manually invoked once to register triggers and establish the initial authorization context. Unlike traditional serverless functions, GAS doesn't auto-execute these; they're purely manual setup steps.

Technical Details: The Fix Sequence

Step 1 — Trigger OAuth Consent Dialogs

The Gmail and Calendar scopes are declared in CalendarSync.gs but only prompt for consent when a function actually uses them. Running testSync() (line 563) exercises both:

// In CalendarSync.gs:563
function testSync() {
  const boatsetter = fetchIcalFeed_(ICAL_FEEDS.boatsetter);
  // Triggers Calendar.Events.insert() — Calendar scope needed
  
  const summary = generateDailyDigest_(boatsetter);
  GmailApp.sendEmail(...); 
  // Triggers GmailApp — Gmail scope needed
}

Execution steps:

  1. Open GAS editor: https://script.google.com/d/1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii/edit
  2. Function dropdown → select testSync
  3. Click Run → expect two authorization prompts (one for Gmail, one for Calendar)
  4. Click Allow on both

Step 2 — Activate Time-Based Triggers

Now that OAuth is fresh, register the automation triggers:

// In CalendarSync.gs:355
function calendarSyncSetup() {
  deleteAllTriggers_();
  
  // Every 30 minutes: fetch all iCal feeds and sync to calendar
  ScriptApp.newTrigger('syncAllChannels')
    .timeBased()
    .everyMinutes(30)
    .create();
  
  // Daily at 7:30 AM PT: send reconciliation email
  ScriptApp.newTrigger('sendDailyReconciliation')
    .timeBased()
    .atHour(7)
    .nearMinute(30)
    .inTimezone('America/Los_Angeles')
    .everyDays(1)
    .create();
}

Execution steps:

  1. Same GAS editor
  2. Function dropdown → select calendarSyncSetup
  3. Click Run
  4. Watch Execution Log for confirmation: "CalendarSync setup complete" should appear within 5 seconds

Verification: Click the clock icon (Triggers) in the left sidebar. You should see two active triggers listed.

Step 3 — Validate Sync Execution

Run testSync again and inspect the execution log for this output:

Fetching Boatsetter iCal...
  12 events from Boatsetter
CalendarSync complete. New bookings: 3
Email sent to ops@queenofsandiego.com

If you see permission errors like `Exception: You do not have permission to access the requested resource`, the OAuth re-consent didn't stick—repeat Step 1.

Infrastructure Context: Why This Architecture

Why GAS instead of Lambda? The booking calendar lives in Google Calendar (native to Google Workspace), and GAS has direct, pre-authenticated access to Calendar, Gmail, and Sheets APIs. This eliminates the need for separate service account setup, secret management, or API key rotation. The tradeoff is that manual trigger activation is required—GAS doesn't have a native CloudFormation equivalent for infrastructure-as-code trigger registration.

Why time-based triggers instead of webhooks? Most iCal feeds (Boatsetter, Viator, Airbnb) don't offer push notifications. Polling every 30 minutes gives near-real-time updates (max 30-min lag) without implementing a webhook receiver infrastructure. At this scale (under 50 bookings/week), the GAS execution cost is negligible.

The ICAL_FEEDS object: Defined at the top of CalendarSync.gs, it maps platform names to feed URLs:

const ICAL_FEEDS = {
  boatsetter: 'https://calendar.boatsetter.com/ical/...',
  // viator, airbnb, etc. added as they go live
};

This design allows new platforms to be added without touching the sync logic—just add a line to ICAL_FEEDS.

Separate Pending Item: Viator Email Scanner

A second GAS project handles email-based booking ingestion for platforms that don't offer iCal feeds.

  • File: sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/JadaCalendarDashboard.gs (line 364)
  • Function: jadaCalendarScanSetup()
  • GAS Editor URL: https://script.google.com/d/1dDpSK8JZda7XUpKIGlyyAX19KLL4JqFjYVtpcunB5ZE3-NMX_9v0lQJ5/edit
  • Status: Pending manual trigger activation (same pattern as CalendarSync)

This is a separate ticket—execute it once to register the Gmail label monitoring trigger.

What's Next

  • Monitor execution logs: Over the next 48 hours, verify that syncAllChannels runs every 30 minutes without permission errors
  • Validate daily reconciliation: Tomorrow at 7:30 AM PT, confirm that ops receives the booking summary email