PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.14
Booktics – Appointment Booking Calendar for Service Businesses v1.0.14
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.14, at core/module/google-calendar/Controller/calendar-sync-controller.php

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