```html

Fixing Boatsetter Calendar Sync: OAuth Re-authorization, Trigger Activation, and Multi-Project GAS Architecture

A calendar synchronization pipeline that silently fails is worse than no pipeline at all. When Boatsetter bookings stopped flowing into the Queen of San Diego operations sheet, the root cause wasn't missing code—it was three separate infrastructure blockers: expired OAuth tokens, inactive time-based triggers, and a misunderstanding of which GAS project owned which function. This post walks through the diagnosis and fix.

The Problem: Three Separate Failure Points

The Boatsetter integration had been coded and deployed, but nothing was syncing. The investigation revealed:

  • OAuth expiration: Gmail and Google Calendar scopes had revoked or expired, blocking any write operations even if the sync ran.
  • Missing trigger: The time-based triggers that invoke the sync functions were never registered in the GAS project.
  • Multi-project confusion: Two separate GAS projects handle different parts of the calendar pipeline, and the setup function for one hadn't been invoked.

None of these are code bugs. All three are operational oversights that required human intervention in the GAS editor UI.

File and Function Map

Before diving into the fix, clarity on file locations and function names is essential:

  • Primary sync logic: sites/queenofsandiego.com/CalendarSync.gs
    • calendarSyncSetup() (line 355): Initializes BookingLedger sheet, registers time-based triggers
    • testSync() (line 563): Test harness that exercises the full sync pipeline; triggers OAuth consent dialogs
    • syncAllChannels() (line 450): Main sync function, runs every 30 minutes via trigger
  • Viator email scanner (separate project): sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/JadaCalendarDashboard.gs
    • jadaCalendarScanSetup() (line 364): Registers email-scanning triggers for the Viator pipeline

The key insight: calendarSyncSetup() does not prompt for Calendar or Gmail OAuth. It only touches SpreadsheetApp and ScriptApp. OAuth consent must be triggered by actually calling a function that accesses those services.

Step 1: Re-authorize Gmail and Calendar OAuth

OAuth tokens in GAS are service-level, not project-level. When a GAS project loses permission to read/write Calendar or send mail, re-authorization happens via a consent prompt triggered by any function that accesses those scopes.

File: sites/queenofsandiego.com/CalendarSync.gs
Function to run: testSync() (line 563)
GAS editor URL: https://script.google.com/d/1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii/edit

testSync() internally calls:

// Implicitly calls Calendar API (requires Calendar scope)
const bookings = CalendarApp.getCalendarById(calendarId).getEvents(startDate, endDate);

// Implicitly calls Gmail API (requires Mail scope)
GmailApp.sendEmail(recipient, subject, body);

When you click Run in the GAS editor:

  1. Google detects missing scopes and shows: "This app needs access to your Google Calendar and Gmail"
  2. You click Allow
  3. The function executes; check the Execution log for output

Expected log output:

Fetching Boatsetter iCal...
  12 events from Boatsetter
CalendarSync complete. New bookings: 5
Reconciliation email sent to ops@queenofsandiego.com

Bad output (indicates auth failure):

Exception: You do not have permission to access the requested resource.
Stack trace: CalendarApp.getCalendarById()

Step 2: Activate Time-Based Triggers

Once OAuth is restored, register the recurring triggers.

File: sites/queenofsandiego.com/CalendarSync.gs
Function to run: calendarSyncSetup() (line 355)

This function:

function calendarSyncSetup() {
  // Creates BookingLedger tab in the operations sheet
  const sheet = SpreadsheetApp.openById(OPS_SHEET_ID);
  sheet.insertSheet("BookingLedger");
  
  // Registers two time-based triggers
  ScriptApp.newTrigger("syncAllChannels")
    .timeBased()
    .everyMinutes(30)
    .create();
  
  ScriptApp.newTrigger("sendDailyReconciliation")
    .atTime(7, 30)
    .everyDays(1)
    .create();
  
  Logger.log("CalendarSync setup complete");
}

Verify trigger registration:

  1. In the GAS editor, click the clock icon (⏱) in the left sidebar → Triggers
  2. You should see two entries:
    • syncAllChannels — Time-based, Every 30 minutes
    • sendDailyReconciliation — Time-based, Daily at 7:30 AM

Step 3: Verify the Sync Pipeline with testSync()

Run testSync() again and watch the execution log. This exercises:

  • iCal fetch: HTTP request to Boatsetter iCal feed (URL stored in ICAL_FEEDS.boatsetter)
  • Calendar write: Event insertion into the Google Calendar via Calendar API
  • Spreadsheet update: BookingLedger rows appended with booking metadata
  • Email send: Daily reconciliation email to ops team

If any step fails, the log will pinpoint it:

// Good: iCal fetch succeeded
Fetching Boatsetter iCal...
  12 events parsed

// Bad: Calendar write failed (likely OAuth)
Exception: CalendarApp.insertEvent() — Permission denied

// Bad: Email send failed
Exception: GmailApp.sendEmail() — Recipient list invalid

Separate Item: Viator Email Scanner