PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.20
Booktics – Appointment Booking Calendar for Service Businesses v1.0.20
1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.2 1.0.3 All 31 releases
booktics / core / module / google-calendar / Controller / calendar-sync-controller.php

calendar-sync-controller.php in Booktics – Appointment Booking Calendar for Service Businesses 1.0.20, at core/module/google-calendar/Controller/calendar-sync-controller.php

490 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Calendar Sync Controller
4 *
5 * @package BookticsGoogleCalendar\Google\Controller
6 */
7
8 namespace Booktics\Module\Google_Calendar\Controller;
9
10 if ( ! defined( 'ABSPATH' ) ) exit;
11
12 use Booktics\Abstracts\Base_Rest_Controller;
13 use Booktics\Module\Google_Calendar\Service\Calendar;
14 use Booktics\Models\Appointment_Model;
15 use Booktics\Models\Service_Model;
16 use WP_REST_Request;
17 use WP_Error;
18
19 /**
20 * Class Calendar_Sync_Controller
21 */
22 class Calendar_Sync_Controller extends Base_Rest_Controller {
23
24 /**
25 * The namespace of this controller's route.
26 *
27 * @var string
28 */
29 protected $namespace = 'booktics/v1';
30
31 /**
32 * The base of this controller's route.
33 *
34 * @var string
35 */
36 protected $rest_base = 'sync/events';
37
38 /**
39 * Google Calendar service instance.
40 *
41 * @var Calendar
42 */
43 private $calendar;
44
45 /**
46 * Temporary storage for appointment data before deletion.
47 *
48 * @var array
49 */
50 private $deletion_data = array();
51
52 /**
53 * Initialize the class and set up hooks.
54 */
55 public function __construct() {
56 $this->calendar = new Calendar();
57 $this->register();
58 }
59
60 /**
61 * Register the routes for the controller.
62 */
63 public function register_routes(): void {
64 add_filter( 'booktics_appointments_for_calendar_view', array( $this, 'add_google_appointments' ), 10, 2 );
65 add_filter( 'booktics_appointment_scheduled_notification_flow_data', [ $this, 'get_meet_url' ], 10, 2 );
66 add_filter( 'booktics_order_created_notification_flow_data', [ $this, 'get_meet_url_for_order_mail' ], 10, 3 );
67 add_filter( 'booktics_booking_calendar_urls', array( $this, 'get_add_to_calendar_links' ), 10, 2 );
68
69 add_action( 'booktics_after_appointment_created', array( $this, 'handle_appointment_created' ), 10, 1 );
70 add_action( 'booktics_after_appointment_updated', array( $this, 'handle_appointment_updated' ), 10, 1 );
71 add_action( 'before_delete_post', array( $this, 'capture_appointment_data_before_delete' ), 10, 1 );
72 add_action( 'booktics_after_appointment_deleted', array( $this, 'handle_appointment_deleted' ), 10, 1 );
73 }
74
75 /**
76 * Add Google Calendar appointments to the appointments list.
77 *
78 * @param array $appointments The appointments list.
79 * @param array $args The arguments passed to the function.
80 * @return array The modified appointments list.
81 */
82 public function add_google_appointments( $appointments, $args ) {
83 $api_filters = $this->get_google_calendar_api_filters( $args );
84 $google_calendar_appointments = $this->calendar->get_google_calendar_events( get_current_user_id(), $api_filters );
85
86 if ( is_wp_error( $google_calendar_appointments ) ) {
87 return $appointments;
88 }
89
90 // Get all existing Google Calendar event IDs from Booktics appointments to avoid duplicates
91 $existing_event_ids = $this->get_existing_google_event_ids( $appointments );
92
93 foreach ( $google_calendar_appointments as $event ) {
94 // Skip events that already exist as Booktics appointments
95 if ( isset( $event['id'] ) && in_array( $event['id'], $existing_event_ids ) ) {
96 continue;
97 }
98
99 $appointments[] = $this->calendar->prepare_appointment_data_from_event( $event );
100 }
101
102 return $appointments;
103 }
104
105 /**
106 * Get existing Google Calendar event IDs from current appointments to avoid duplicates.
107 *
108 * @param array $appointments The current appointments list.
109 * @return array Array of Google Calendar event IDs.
110 */
111 private function get_existing_google_event_ids( $appointments ) {
112 $existing_event_ids = array();
113
114 // Get event IDs from the passed appointments array
115 foreach ( $appointments as $appointment ) {
116 $event_id = get_post_meta( $appointment->id, '_google_calendar_event_id', true );
117 if ( ! empty( $event_id ) ) {
118 $existing_event_ids[] = $event_id;
119 }
120 }
121
122 return array_unique( $existing_event_ids );
123 }
124
125 /**
126 * On appointment created, create a Google Calendar event for the booking.
127 *
128 * @param \Booktics\Models\Appointment_Model $appointment appointment data
129 */
130 public function handle_appointment_created( $appointment ) {
131 try {
132 $team_member_id = get_post_meta( $appointment->id, 'team_member_id', true );
133
134 // If not in meta, check if it's a property of the appointment object
135 if ( !$team_member_id && isset( $appointment->team_member_id ) ) {
136 $team_member_id = $appointment->team_member_id;
137 }
138
139 $event_id = $this->calendar->create_event( $appointment, $team_member_id );
140
141 if ( $event_id ) {
142 update_post_meta( $appointment->id, '_google_calendar_event_id', $event_id );
143 }
144 } catch ( \Throwable $e ) {
145 if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
146 error_log( '[Booktics Google Calendar] Failed to create Google Calendar event: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
147 }
148 }
149 }
150
151 /**
152 * On appointment updated, update the Google Calendar event for the booking.
153 *
154 * @param \Booktics\Models\Appointment_Model $appointment appointment data
155 */
156 public function handle_appointment_updated( $appointment ) {
157 try {
158 $team_member_id = get_post_meta( $appointment->id, 'team_member_id', true );
159 $event_id = get_post_meta( $appointment->id, '_google_calendar_event_id', true );
160
161 if ( $event_id ) {
162 $this->calendar->update_event( $event_id, $appointment, $team_member_id );
163 }
164 } catch ( \Throwable $e ) {
165 if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
166 error_log( 'Failed to updated Google Calendar event: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
167 }
168 }
169 }
170
171 /**
172 * Capture appointment Google Calendar data before WordPress deletes the post.
173 *
174 * @param int $post_id Post ID being deleted
175 */
176 public function capture_appointment_data_before_delete( $post_id ) {
177 // Only process booktics appointments
178 if ( get_post_type( $post_id ) !== 'booktics-appointment' ) {
179 return;
180 }
181
182 // Capture the data we need before deletion
183 $team_member_id = get_post_meta( $post_id, 'team_member_id', true );
184 $event_id = get_post_meta( $post_id, '_google_calendar_event_id', true );
185
186 if ( $team_member_id && $event_id ) {
187 $this->deletion_data[ $post_id ] = array(
188 'team_member_id' => $team_member_id,
189 'event_id' => $event_id,
190 );
191 }
192 }
193
194 public function handle_appointment_deleted( $appointment ) {
195 try {
196 // Get the data we captured before deletion
197 if ( ! isset( $this->deletion_data[ $appointment->id ] ) ) {
198 return;
199 }
200
201 $data = $this->deletion_data[ $appointment->id ];
202 $team_member_id = $data['team_member_id'];
203 $event_id = $data['event_id'];
204
205 if ( $event_id && $team_member_id ) {
206 $deleted = $this->calendar->delete_event( $event_id, $team_member_id );
207 if ( $deleted ) {
208 // Clean up the stored data
209 unset( $this->deletion_data[ $appointment->id ] );
210 }
211 }
212 } catch ( \Throwable $e ) {
213 if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
214 error_log( '[Booktics Google Calendar] Exception in handle_appointment_deleted: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
215 }
216 }
217 }
218
219 /**
220 * Check if a given request has permission to sync events.
221 *
222 * @param WP_REST_Request $request Full data about the request.
223 * @return bool|WP_Error
224 */
225 public function permission_check( $request ) {
226 if ( ! current_user_can( 'manage_options' ) ) {
227 return new WP_Error(
228 'rest_forbidden',
229 __( 'You do not have sufficient permissions to access this endpoint.', 'booktics' ),
230 array( 'status' => 403 )
231 );
232 }
233
234 return true;
235 }
236
237 /**
238 * Get the query params for collections.
239 *
240 * @return array
241 */
242 public function get_collection_params() {
243 return array(
244 'team_member_id' => array(
245 'description' => __( 'Team member ID to sync calendar for.', 'booktics' ),
246 'type' => 'integer',
247 'required' => true,
248 'validate_callback' => 'absint',
249 'sanitize_callback' => 'absint',
250 ),
251 );
252 }
253
254 /**
255 * Get Google Meet URL for order mail
256 *
257 * @param array $notification_flow_data The notification flow data
258 * @param array $formatted_appointment The formatted appointment data
259 * @param Order_Model $order_model The order model for the notification data
260 *
261 * @return array The updated notification flow data with Google Meet URL
262 */
263 public function get_meet_url_for_order_mail( $notification_flow_data, $formatted_appointment, $order_model ) {
264 try {
265 return $this->get_meet_url( $notification_flow_data, $formatted_appointment );
266 } catch ( \Throwable $e) {
267 return $notification_flow_data;
268 }
269 }
270
271 /**
272 * Get Google Meet URL
273 *
274 * @param array $notification_flow_data The notification flow data
275 * @param array $formatted_appointment The formatted appointment data
276 *
277 * @return array The updated notification flow data with Google Meet URL
278 */
279 public function get_meet_url( $notification_flow_data, $formatted_appointment ) {
280 if ( ! isset ( $formatted_appointment['service']['enable_google_meet'] ) ) {
281 return $notification_flow_data;
282 }
283
284 // If Google Meet is not enabled for the service, return the original notification flow data
285 if ( ! $formatted_appointment['service']['enable_google_meet'] ) {
286 return $notification_flow_data;
287 }
288
289 $user_id = get_current_user_id();
290 $tokens = get_user_meta( $user_id, 'booktics_google_auth_token', true );
291 $access_token = $tokens['access_token'] ?? '';
292
293 if ( empty( $access_token ) ) {
294 return $notification_flow_data;
295 }
296 try {
297 // Get the WordPress timezone
298 $timezone = wp_timezone();
299 $timezone_string = $timezone->getName();
300
301 // Convert timestamps to ISO 8601 format with timezone
302 $start_timestamp = strtotime( $formatted_appointment['start_time'] );
303 $end_timestamp = strtotime( $formatted_appointment['end_time'] );
304
305 // Format dates in ISO 8601 format with timezone using wp_date
306 $start_time = wp_date( 'c', $start_timestamp, $timezone );
307 $end_time = wp_date( 'c', $end_timestamp, $timezone );
308
309 $event_data = array(
310 'summary' => __( 'Booktics Meeting', 'booktics' ),
311 'start' => array(
312 'dateTime' => $start_time,
313 'timeZone' => $timezone_string,
314 ),
315 'end' => array(
316 'dateTime' => $end_time,
317 'timeZone' => $timezone_string,
318 ),
319 'conferenceData' => array(
320 'createRequest' => array(
321 'requestId' => uniqid( 'meet_', true ),
322 'conferenceSolutionKey' => array(
323 'type' => 'hangoutsMeet',
324 ),
325 ),
326 ),
327 );
328
329 $response = wp_remote_post(
330 'https://www.googleapis.com/calendar/v3/calendars/primary/events?conferenceDataVersion=1',
331 array(
332 'headers' => array(
333 'Authorization' => 'Bearer ' . $access_token,
334 'Content-Type' => 'application/json',
335 ),
336 'body' => wp_json_encode( $event_data ),
337 'timeout' => 15,
338 )
339 );
340
341 if ( is_wp_error( $response ) ) {
342 return $notification_flow_data;
343 }
344
345 $body = json_decode( wp_remote_retrieve_body( $response ), true );
346
347 // Get the Meet link from conferenceData
348 if ( isset( $body['hangoutLink'] ) ) {
349 $notification_flow_data['google_meet_url'] = esc_url_raw( $body['hangoutLink'] );
350 return $notification_flow_data;
351 }
352
353 // Fallback or log error
354 return $notification_flow_data;
355 } catch ( \Throwable $e ) {
356 return $notification_flow_data;
357 }
358 }
359
360 /**
361 * Get Google Calendar API filters.
362 *
363 * @param array $args The arguments passed to the function.
364 * @return array The API filters.
365 */
366 private function get_google_calendar_api_filters( $args ) {
367 $api_filters = array();
368
369 // Get start and end dates from args in appropriate format
370 if ( isset( $args['start_date'] ) ) {
371 $api_filters['timeMin'] = rawurlencode( wp_date( DATE_RFC3339, strtotime( $args['start_date'] ) ) );
372 }
373
374 if ( isset( $args['end_date'] ) ) {
375 $api_filters['timeMax'] = rawurlencode( wp_date( DATE_RFC3339, strtotime( $args['end_date'] ) ) );
376 }
377
378 return $api_filters;
379 }
380
381 /**
382 * Adds add event to google calendar link for customers in the thank you page.
383 *
384 * @param array $calendars The calendars list.
385 * @param array $request The request data.
386 * @return array The modified calendars list.
387 */
388 public function get_add_to_calendar_links( $calendars, $request ) {
389 $meeting_id = ! empty( $request['meeting_id'] ) ? intval( $request['meeting_id'] ) : 0;
390 $booking_date = ! empty( $request['booking_date'] ) ? sanitize_text_field( $request['booking_date'] ) : '';
391 $start_time = ! empty( $request['start_time'] ) ? sanitize_text_field( $request['start_time'] ) : '';
392 $end_time = ! empty( $request['end_time'] ) ? sanitize_text_field( $request['end_time'] ) : '';
393
394 $meeting = ( new Appointment_Model( $meeting_id ) )->to_array();
395 $service = ( new Service_Model( $meeting['service_id'] ) )->to_array();
396 // Use the appointment's timezone if available, otherwise fallback to WordPress timezone
397 $appointment_timezone = ! empty( $meeting['timezone'] ) ? $meeting['timezone'] : wp_timezone_string();
398
399 $google_url = $this->get_add_to_google_calendar_url(
400 array(
401 'date' => $booking_date,
402 'start_time' => $start_time,
403 'end_time' => $end_time,
404 'title' => $service['title'],
405 'description' => $meeting['note'],
406 'timezone' => $appointment_timezone,
407 )
408 );
409
410 $calendars = array();
411
412 if ( $google_url ) {
413 $calendars[] = array(
414 'id' => 'google',
415 'name' => esc_html__( 'Google', 'booktics' ),
416 'url' => esc_url_raw( $google_url ),
417 );
418 }
419
420 return $calendars;
421 }
422
423 /**
424 * Get Google Calendar URL that will be used to add event to Google Calendar
425 * From the customer side.
426 *
427 * @param array $args {
428 * @type string $date Booking date in 'Y-m-d' format.
429 * @type string $start_time Start time in 'H:i' format.
430 * @type string $end_time End time in 'H:i' format.
431 * @type string $location Event location.
432 * @type string $title Event title.
433 * @type string $description Event description.
434 * @type string $timezone Event timezone.
435 * }
436 *
437 * @return string Google Calendar URL
438 */
439 private function get_add_to_google_calendar_url( $args = array() ) {
440 $defaults = array(
441 'date' => '',
442 'start_time' => '',
443 'end_time' => '',
444 'location' => '',
445 'title' => '',
446 'description' => '',
447 'timezone' => '',
448 );
449
450 $args = wp_parse_args( $args, $defaults );
451
452 if ( empty( $args['date'] ) || empty( $args['start_time'] ) || empty( $args['end_time'] ) ) {
453 return '';
454 }
455
456 // Create datetime objects with the appointment's timezone and convert to UTC
457 try {
458 // Use the provided timezone for the appointment
459 $appointment_timezone = new \DateTimeZone( $args['timezone'] ?: wp_timezone_string() );
460
461 // Create DateTime objects in the appointment's timezone
462 $start_dt = new \DateTime( $args['date'] . ' ' . $args['start_time'], $appointment_timezone );
463 $end_dt = new \DateTime( $args['date'] . ' ' . $args['end_time'], $appointment_timezone );
464
465 // Convert to UTC for Google Calendar
466 $start_dt->setTimezone( new \DateTimeZone( 'UTC' ) );
467 $end_dt->setTimezone( new \DateTimeZone( 'UTC' ) );
468
469 // Format as Google expects: YYYYMMDDTHHMMSSZ
470 $start_utc = $start_dt->format( 'Ymd\THis\Z' );
471 $end_utc = $end_dt->format( 'Ymd\THis\Z' );
472 } catch ( \Throwable $e ) {
473 return '';
474 }
475
476 $params = array(
477 'action' => 'TEMPLATE',
478 'text' => $args['title'],
479 'dates' => $start_utc . '/' . $end_utc,
480 'details' => $args['description'],
481 'location' => $args['location'],
482 'sf' => 'true',
483 'output' => 'xml',
484 'ctz' => $args['timezone'],
485 );
486
487 return 'https://calendar.google.com/calendar/render?' . http_build_query( $params );
488 }
489 }
490