```html

Debugging Calendar Sync Failures Across Multiple Booking Platforms: Why Boatsetter, Sailo, and Viator Aren't Speaking to JADA's iCal

Over the past week, we discovered a critical gap in our multi-platform calendar synchronization strategy: despite having the infrastructure in place, bookings from Boatsetter, Sailo, and Viator are not automatically syncing to our JADA Internal calendar. A manual booking came through Boatsetter on May 30, but the operator had to update the calendar by hand. This post walks through the root causes we identified, the architectural decisions that led us here, and the specific fixes required to unblock this workflow.

The Root Problem: Disabled Sync Triggers and Expired Credentials

Our calendar synchronization relies on a Google Apps Script (GAS) orchestration layer deployed to clasp projects. The primary sync engine lives in CalendarSync.gs, which reads iCal feeds from Boatsetter, Sailo, and Viator, transforms event data, and writes normalized entries into the JADA Internal Google Calendar.

The issue breaks into two parts:

  • Trigger never activated: The sync function calendarDashboardSetup() was written but never executed in the GAS editor. This function registers the timed trigger that runs syncs every 30 minutes. Without this one-time manual execution, the sync automation never started.
  • OAuth credentials expired: Two separate OAuth tokens have lapsed:
    • Gmail OAuth (ticket t-8d86d5ba): Required to read incoming booking confirmations and parse metadata from Boatsetter/Sailo notification emails.
    • Google Calendar OAuth: Revoked, preventing write operations to the JADA Internal calendar even if sync data is successfully fetched.

Technical Architecture: How the Sync Was Supposed to Work

The design leverages Google Apps Script as a lightweight middleware because it has native access to Google Calendar and Gmail APIs without requiring external servers:

  • Data source: Boatsetter (iCal URL in CalendarSync.gs as BOATSETTER_ICAL_URL), Sailo (similar iCal endpoint), and Viator (API-based, more on this below).
  • Fetch layer: fetchICalEvents() uses UrlFetchApp to retrieve raw iCal data. The function parses VEVENT blocks and extracts SUMMARY, DTSTART, DTEND, DESCRIPTION, and custom X-properties.
  • Transform layer: normalizeEventData() maps external field names to our canonical schema. For example, Boatsetter includes X-BOATSETTER-BOOKING-ID in the iCal; we extract this and store it in our event's description or a custom property.
  • Write layer: createOrUpdateCalendarEvent() uses CalendarApp.getCalendarById() to write to the JADA Internal calendar. It checks for duplicates via booking ID to avoid creating multiple entries for the same reservation.
  • Trigger: A time-based trigger (created by calendarDashboardSetup()) invokes syncAllPlatforms() on a 30-minute interval.

This architecture was chosen because:

  • No external server required: GAS runs on Google's infrastructure. No EC2 instances, no Lambda cold starts, no separate auth to manage.
  • Direct calendar access: GAS has first-class Calendar API support, avoiding API gateways and reducing latency.
  • Low operational overhead: Deployment is via clasp push; no CI/CD pipeline needed for this component.

Why Viator Is Different (and Blocking)

Viator doesn't expose a public iCal feed. Instead, they require OAuth-based API calls to GET /bookings. Ticket t-ad4b92d7 indicates Viator replied to our API integration emails — you need to read their response in jadasailing@gmail.com immediately. Until that's resolved, May 30 Viator bookings must be manually blocked in supplier.viator.com.

Once Viator provides API credentials, we'll add a new function fetchViatorBookings() in CalendarSync.gs` that:

function fetchViatorBookings() {
  const viatorApiKey = PropertiesService.getUserProperties().getProperty('VIATOR_API_KEY');
  const viatorPartnerUid = PropertiesService.getUserProperties().getProperty('VIATOR_PARTNER_UID');
  
  const url = `https://api.viator.com/partner/bookings?partner_uid=${viatorPartnerUid}&modified_since=`;
  const options = {
    method: 'get',
    headers: { 'Accept': 'application/json', 'exp-api-version': '1.1' },
    muteHttpExceptions: true
  };
  
  const response = UrlFetchApp.fetch(url, options);
  return parseViatorBookings_(JSON.parse(response.getContentText()));
}

Credentials would be stored in GAS Properties Service (encrypted at rest by Google).

The Fix: Three Immediate Steps

1. Activate the GAS Trigger (5 minutes)

Open the clasp project for JADA-CalendarSync in the Google Apps Script editor:

clasp open

Navigate to CalendarSync.gs, find the calendarDashboardSetup() function, and click Run in the editor UI. This will:

  • Prompt you to authorize GAS to access your Google Calendar.
  • Register the 30-minute timer trigger in the GAS backend.
  • Create a corresponding entry in the Triggers panel (viewable via the clock icon in the left sidebar).

2. Refresh OAuth Tokens (10 minutes)

For Gmail OAuth (ticket t-8d86d5ba):

  • In the GAS editor, click Project Settings (gear icon).
  • Note the project number (e.g., 123456789).
  • Go to myaccount.google.com/permissions, find the GAS project, and revoke.
  • In CalendarSync.gs, add a simple function that calls GmailApp.getInboxThreads(0, 1) and run it. This re-prompts authorization and refreshes the token.

For Google Calendar OAuth: