# Architecture **Analysis Date:** 2026-05-05 ## Pattern Overview WordPress plugin with layered architecture: CPT-backed data model, REST API surface, Singleton orchestrators, static utility classes. - Entry point bootstraps a single Singleton (`Yacht_Booking`) via `plugins_loaded` - All public-facing state changes go through REST API (`/wp-json/yacht-booking/v1/`) - Admin state changes use WordPress PRG pattern (form POST → redirect) - External calendar sync handled by separate Integration namespaces ## Class Inventory | Class | File | Pattern | Responsibility | |-------|------|---------|---------------| | `Yacht_Booking` | `includes/class-yacht-booking.php` | Singleton | Master orchestrator — wires all subsystems | | `Yacht` | `includes/class-yacht.php` | Static utility | `yacht` CPT registration + meta accessors | | `Booking` | `includes/class-booking.php` | Static utility | `yacht_booking` CPT + `create()` + status | | `Inquiry` | `includes/class-inquiry.php` | Static utility | `yacht_inquiry` CPT + `send_emails()` | | `Availability` | `includes/class-availability.php` | Static utility | All `wp_yacht_availability` table operations | | `Rest_Controller` | `api/class-rest-controller.php` | Extends `WP_REST_Controller` | 7 REST endpoints, booking orchestration | | `Settings` | `includes/class-settings.php` | Static utility | Typed `wp_options` accessors + formatting helpers | | `Installer` | `includes/class-installer.php` | Plain class | DB table creation + default options on activation | | `Email_Templates` | `includes/class-email-templates.php` | Static utility | Template storage, compilation, tag replacement | | `Admin` | `admin/class-admin.php` | Singleton (admin-only) | Menu, form processing, CSV export, customer emails | | `Booking_List_Table` | `admin/class-booking-list-table.php` | Extends `WP_List_Table` | Bookings admin table | | `Yacht_List_Table` | `admin/class-yacht-list-table.php` | Extends `WP_List_Table` | Yachts admin table | | `Inquiry_List_Table` | `admin/class-inquiry-list-table.php` | Extends `WP_List_Table` | Inquiries admin table | | `Sync_Controller` | `integrations/google-calendar/class-sync-controller.php` | Singleton | GCal cron sync orchestration | | `GCal_Service` | `integrations/google-calendar/class-gcal-service.php` | Plain class | Google Calendar API calls | | `OAuth_Handler` | `integrations/google-calendar/class-oauth-handler.php` | Plain class | OAuth 2.0 token storage + refresh | | `ICal_Feed` | `integrations/ical/class-ical-feed.php` | Static/plain | iCal feed generation + rewrite rule | | `ICal_Import` | `integrations/ical/class-ical-import.php` | Static/plain | External iCal URL import + cron | | `Shortcode` | `frontend/class-shortcode.php` | Singleton | `[yacht_calendar]` shortcode | | `Calendar_Widget` | `frontend/class-calendar-widget.php` | Extends Elementor `Widget_Base` | Elementor `yacht-calendar` widget | ## Dependency Graph ``` yacht-booking-system.php └── Yacht_Booking (Singleton) ├── Yacht (static) ├── Booking (static) │ └── fires: yacht_booking_created │ yacht_booking_status_changed ├── Availability (static) ├── Settings (static) ├── Email_Templates (static) ├── Rest_Controller │ ├── Yacht, Booking, Availability, Settings, Inquiry │ ├── Email_Templates │ └── listens: yacht_booking_created → send_booking_notification (admin email) ├── Admin (Singleton, admin-only) │ ├── Booking, Availability, Yacht, Settings, Email_Templates, Inquiry │ ├── Booking_List_Table → Booking, Availability, Settings │ ├── Yacht_List_Table, Inquiry_List_Table │ └── listens: yacht_booking_status_changed → send_customer_notification ├── Sync_Controller (Singleton) │ ├── GCal_Service → OAuth_Handler │ └── writes: Availability::mark_as_blocked ├── ICal_Import → Availability::mark_as_blocked └── ICal_Feed (feed output) ``` ## WP Hook Map | Hook | Type | Registered by | Handler | |------|------|---------------|---------| | `plugins_loaded` (p10) | action | bootstrap | `yacht_booking_init()` → `Yacht_Booking::get_instance()` | | `init` (p10) | action | Yacht_Booking | `register_post_types()` | | `init` (p15) | action | Yacht_Booking | `register_shortcodes()` | | `init` | action | bootstrap | `yacht_booking_load_textdomain()` | | `wp_enqueue_scripts` | action | Yacht_Booking | `enqueue_frontend_assets()` (conditional) | | `admin_enqueue_scripts` | action | Yacht_Booking | `enqueue_admin_assets()` (conditional) | | `rest_api_init` | action | Yacht_Booking | `register_rest_routes()` | | `elementor/widgets/register` | action | Yacht_Booking | `register_elementor_widgets()` | | `admin_init` | action | Yacht_Booking | `add_custom_capabilities()` | | `admin_menu` (p9) | action | Admin | `register_admin_menu()` | | `admin_init` | action | Admin | `process_bulk_actions()`, `process_booking_actions()`, `process_yacht_save()`, `process_settings_save()`, `process_export_download()` | | `admin_notices` | action | Admin | `display_admin_notices()` | | `yacht_booking_created` | custom action | Rest_Controller | `send_booking_notification()` (admin email) | | `yacht_booking_status_changed` | custom action | Admin | `send_customer_notification()` | | `before_delete_post` | action | Sync_Controller | `on_booking_deleted()` (GCal event delete) | | `wp_ajax_yacht_booking_manual_sync` | action | Sync_Controller | `ajax_manual_sync()` | | `yacht_booking_sync_to_gcal` | cron action | Sync_Controller | `sync_booking_to_gcal()` | | `yacht_booking_pull_from_gcal` | cron action (hourly) | Sync_Controller | `pull_from_gcal()` | | `yacht_booking_ical_import` | cron action (hourly) | ICal_Import | `run_import()` | | `init` | action | ICal_Feed | `add_rewrite_rules()` | | `template_redirect` | action | ICal_Feed | `handle_feed_request()` | ## Key Data Flows ### Booking Creation (Frontend → REST → DB) 1. User picks dates on FullCalendar → `GET /availability/{yacht_id}` → `Availability::get_availability_calendar()` 2. User submits form → `POST /bookings` with `X-WP-Nonce` header 3. `Rest_Controller::create_booking()`: nonce check → `is_booking_enabled()` → `Availability::is_available()` 4. Price: `count_days() × get_price_per_day()` 5. `Booking::create()` → inserts CPT post + meta → fires `yacht_booking_created` 6. `Availability::mark_as_booked()` → inserts rows in `wp_yacht_availability` 7. Admin email sent via `Email_Templates`; JSON response to frontend ### Admin Booking Management 1. Bookings list → `Booking_List_Table::prepare_items()` (WP_Query) 2. Row action URLs: nonce-protected GET `?action=approve_booking_{id}` 3. `Admin::process_booking_actions()` → `Booking::update_status()` → fires `yacht_booking_status_changed` 4. `Admin::send_customer_notification()` → customer email 5. On cancel/delete: `Availability::clear_booking_availability($booking_id)` deletes rows by booking_id 6. PRG redirect back to list ### GCal Sync 1. Hourly WP Cron: `pull_from_gcal()` → `GCal_Service` → Google API → `Availability::mark_as_blocked()` 2. On booking create: scheduled single cron `yacht_booking_sync_to_gcal` → push to GCal 3. OAuth token refresh: `OAuth_Handler::get_access_token()` → auto-refresh if expired ## Error Handling Strategy - REST endpoints return `WP_Error` with HTTP status codes - Admin operations: `wp_die()` on capability failure; `?error=1` redirect on soft errors - `Booking::create()` returns `false` on failure — caller checks - GCal API: `is_wp_error()` check only; HTTP 4xx/5xx responses silently ignored - No exceptions used anywhere ## Custom Capabilities Added once to `administrator` role via `Yacht_Booking::add_custom_capabilities()`: - `yacht_booking_manage_yachts` - `yacht_booking_manage_bookings` - `yacht_booking_manage_settings`