# External Integrations **Analysis Date:** 2026-05-05 ## Google Calendar API v3 **Purpose:** Bidirectional sync — bookings push to Google Calendar as events; Google Calendar events pull back as availability blocks. **Implementation:** Native PHP, no SDK. Uses WordPress HTTP API (`wp_remote_post`, `wp_remote_get`, `wp_remote_request`). **Files:** - `integrations/google-calendar/class-gcal-service.php` — API calls - `integrations/google-calendar/class-oauth-handler.php` — OAuth 2.0 token management - `integrations/google-calendar/class-sync-controller.php` — Cron orchestration (Singleton) **Authentication:** OAuth 2.0 with `offline` access (refresh token). - Auth endpoint: `https://accounts.google.com/o/oauth2/v2/auth` - Token endpoint: `https://oauth2.googleapis.com/token` - Scope: `https://www.googleapis.com/auth/calendar` - Credentials stored in `wp_options` key `yacht_booking_gcal_credentials` - Tokens stored in `wp_options` key `yacht_booking_gcal_tokens` - Calendar ID stored in `yacht_booking_gcal_calendar_id` (defaults to `'primary'`) - **OAuth redirect URI hardcoded:** `https://jachty.pagedev.pl/wp-admin/admin.php?page=yacht-bookings-settings&tab=google-calendar&gcal_callback=1` **Outbound API calls:** | Method | URL | Purpose | |--------|-----|---------| | POST | `.../calendars/{id}/events` | Create event | | PATCH | `.../calendars/{id}/events/{eventId}` | Update event | | DELETE | `.../calendars/{id}/events/{eventId}` | Delete event | | GET | `.../calendars/{id}/events` | List events (pull sync) | | GET | `.../users/me/calendarList` | List calendars | | POST | `https://oauth2.googleapis.com/token` | Token exchange/refresh | | GET | `https://www.googleapis.com/oauth2/v2/userinfo` | Get connected email | **Sync model:** Pull-only for external → local (hourly WP Cron `yacht_booking_pull_from_gcal`). Push for local → GCal on `yacht_booking_created` / `yacht_booking_status_changed` (scheduled single cron events). **Limitations:** - Single shared calendar for all yachts — GCal events apply to every yacht, not per-yacht - No incoming webhooks — polling model only - No retry logic on API failure ## iCal Integration ### Export (Feed) **Purpose:** Generate `.ics` file per yacht for subscription by external clients (other booking systems, personal calendars). **File:** `integrations/ical/class-ical-feed.php` **URL pattern:** `{home_url}/yacht-ical/{yacht_id}/{token}.ics` - Token stored in `_yacht_ical_token` post meta (24-char alphanumeric, auto-generated) - Verified with `hash_equals()` — timing-safe - Returns `text/calendar` with all non-cancelled bookings for that yacht - Regenerate token: `ICal_Feed::regenerate_token($yacht_id)` ### Import (Subscribe) **Purpose:** Subscribe to external iCal URLs per yacht; import events as availability blocks (hourly cron). **File:** `integrations/ical/class-ical-import.php` **Config:** Per-yacht `_yacht_ical_import_url` post meta (set via `ICal_Import::set_import_url()`) **Behavior:** Fetches URL via `wp_remote_get()` (30-second timeout, SSL verification), parses VEVENT components, creates `yacht_booking` posts with `_booking_source = 'ical_import'`, marks dates as blocked via `Availability::mark_as_blocked()`. **Limitation:** Does not check VEVENT `STATUS` property — cancelled events in the feed are imported as blocks. ## jsDelivr CDN **Purpose:** FullCalendar library loaded from CDN (no local copy). | Asset | URL | |-------|-----| | FullCalendar JS | `https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js` | | FullCalendar CSS | `https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.css` | | Polish locale | `https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.10/locales/pl.global.min.js` | Enqueued in `Yacht_Booking::enqueue_frontend_assets()`, conditional on page content. ## Email **Provider:** WordPress `wp_mail()` only — no Mailgun, SendGrid, or other transactional service. **Templates:** `includes/class-email-templates.php` — templates stored in `wp_options` as tagged strings, compiled with `get_booking_template_data()`. **When sent:** - On booking creation: admin notification via `Rest_Controller::send_booking_notification()` (hooked on `yacht_booking_created`) - On status change: customer notification via `Admin::send_customer_notification()` (hooked on `yacht_booking_status_changed`) - On inquiry create: admin + customer emails via `Inquiry::send_emails()` (built inline, not using Email_Templates) **Headers:** `From:` pulled from `yacht_booking_email_from` option; `Reply-To:` set explicitly. ## REST API (Internal) Namespace: `yacht-booking/v1` — base: `/wp-json/yacht-booking/v1/` | Method | Endpoint | Auth | Handler | |--------|----------|------|---------| | GET | `/yachts` | Public | `get_yachts()` | | GET | `/yachts/{id}` | Public | `get_yacht()` | | GET | `/availability/{yacht_id}?start=Y-m-d&end=Y-m-d` | Public | `get_availability()` | | POST | `/bookings` | Public + `X-WP-Nonce` | `create_booking()` | | POST | `/inquiries` | Public + `X-WP-Nonce` | `create_inquiry()` | | GET | `/bookings` | Admin cap | `get_bookings()` | | PUT | `/bookings/{id}/status` | Admin cap | `update_booking_status()` | All registered in `api/class-rest-controller.php`. ## Monitoring & Observability - No error tracking service (no Sentry, Bugsnag) - `error_log()` used with prefixes: `[Yacht Booking - GCal]`, `[Yacht Booking - GCal Sync]`, `[Yacht Booking - iCal]` - Logging only fires when `WP_DEBUG === true` — silent in production ## Deployment - **Method:** FTP via VS Code ftp-kr extension (`.vscode/ftp-kr.json`) - **CI/CD:** None — fully manual - **Production URL:** `https://jachty.pagedev.pl` (hardcoded in GCal OAuth redirect)