PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.58
Timetics – Appointment Booking Calendar & Scheduling v1.0.58
1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 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.19 1.0.2 1.0.20 1.0.21 1.0.22 1.0.23 1.0.24 All 62 releases
timetics / core / integrations / google / service / google-calendar-sync.php

google-calendar-sync.php in Timetics – Appointment Booking Calendar & Scheduling 1.0.58, at core/integrations/google/service/google-calendar-sync.php

321 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Google Calendar Sync Class
4 *
5 * Handles two-way synchronization between Google Calendar and Timetics appointments.
6 *
7 * @package Timetics
8 */
9
10 namespace Timetics\Core\Integrations\Google\Service;
11
12 defined( 'ABSPATH' ) || exit;
13
14 use Timetics\Core\Appointments\Api_Appointment;
15 use Timetics\Core\Bookings\Booking;
16 use Timetics\Core\Customers\Customer;
17 use Timetics\Core\Appointments\Appointment;
18 use WP_Error;
19 use Timetics\Utils\Singleton;
20 use DateTime;
21 use DateTimeZone;
22
23 /**
24 * Class Google_Calendar_Sync
25 */
26 class Google_Calendar_Sync {
27 use Singleton;
28
29 /**
30 * Google Calendar service instance
31 *
32 * @var Calendar
33 */
34 private $calendar;
35
36 /**
37 * Appointment API instance
38 *
39 * @var Api_Appointment
40 */
41 private $appointment_api;
42
43 /**
44 * Meta key for storing Google Event ID
45 */
46 const EVENT_ID_META_KEY = 'tt_google_calendar_event_id';
47
48 /**
49 * Meta key for storing sync status
50 */
51 const SYNC_STATUS_META_KEY = 'tt_google_calendar_sync_status';
52
53 /**
54 * Meta key for storing ETag
55 */
56 const ETAG_META_KEY = 'tt_google_calendar_etag';
57
58 /**
59 * Constructor
60 */
61 public function __construct() {
62 try {
63 $this->calendar = new Calendar();
64 $this->appointment_api = new Api_Appointment();
65
66 // Add hooks
67 add_action( 'timetics_after_booking_schedule', array( $this, 'sync_booking_to_google_calendar' ), 10, 4 );
68 add_filter( 'timetics/admin/booking/get_items', array( $this, 'get_events_from_google' ) );
69 add_filter( 'timetics_schedule_data_for_selected_date', array( $this, 'block_timeslots_by_google_events' ), 10, 5 );
70 } catch ( \Throwable $e ) {
71 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
72 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging is properly guarded by WP_DEBUG checks
73 error_log( 'Timetics Google Calendar Sync: ' . $e->getMessage() );
74 }
75 }
76 }
77
78 /**
79 * Get all Google Event IDs that were created by Timetics
80 *
81 * @return array
82 */
83 private function get_timetics_google_event_ids() {
84 $booking = new Booking();
85 return $booking->get_all_google_event_ids();
86 }
87
88 /**
89 * Sync booking to Google Calendar
90 *
91 * @param int $booking_id Booking ID
92 * @param int $customer_id Customer ID
93 * @param int $meeting_id Meeting ID
94 * @param array $data Booking data
95 *
96 * @return void|WP_Error
97 */
98 public function sync_booking_to_google_calendar( $booking_id, $customer_id, $meeting_id, $data ) {
99 try {
100 if ( ! is_numeric( $booking_id ) || ! is_numeric( $customer_id ) || ! is_numeric( $meeting_id ) ) {
101 return new WP_Error( 'invalid_parameters', 'Invalid parameters provided for Google Calendar sync.' );
102 }
103
104 $booking = new Booking( $booking_id );
105 $meeting = new Appointment( $meeting_id );
106 $customer = new Customer( $customer_id );
107
108 // Get the current user's access token
109 $user_id = get_current_user_id();
110 $access_token = timetics_get_google_access_token( $user_id );
111
112 if ( empty( $access_token ) ) {
113 return new WP_Error( 'no_access_token', 'No Google Calendar access token found. Please reconnect your Google account.' );
114 }
115
116 // Prepare event data
117 $event_data = array(
118 'access_token' => sanitize_text_field( $access_token ),
119 'summary' => sanitize_text_field( $meeting->get_name() ),
120 'description' => sanitize_text_field( $meeting->get_description() ),
121 'start' => array(
122 'date' => $booking->get_start_date(),
123 'time' => $booking->get_start_time(),
124 ),
125 'end' => array(
126 'date' => $booking->get_end_date(),
127 'time' => $booking->get_end_time(),
128 ),
129 'attendees' => array(
130 array( 'email' => $customer->get_email() ),
131 ),
132 'reminders' => array(
133 'useDefault' => true,
134 ),
135 'guestsCanInviteOthers' => false,
136 'guestsCanModify' => false,
137 'guestsCanSeeOtherGuests' => false,
138 'timezone' => $booking->get_timezone(),
139 );
140
141 // Check if this booking already has a Google Event ID
142 $event_id = $booking->get_google_event_id();
143
144 if ( $event_id ) {
145 // Update existing event
146 $result = $this->calendar->update_event( $event_id, $event_data );
147 } else {
148 // Create new event
149 $result = $this->calendar->create_event( $event_data );
150
151 // Save the event ID for future updates
152 if ( ! empty( $result['id'] ) ) {
153 $booking->set_google_event_id( $result['id'] );
154 }
155 }
156
157 if ( is_wp_error( $result ) ) {
158 $booking->set_sync_status( 'error' );
159 return $result;
160 }
161
162 $booking->set_sync_status( 'synced' );
163 } catch ( \Throwable $e ) {
164 // If sync fails silenty exits to ensure no other process gets hampered
165 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
166 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging is properly guarded by WP_DEBUG checks
167 error_log( 'Timetics Google Calendar Sync: ' . $e->getMessage() );
168 }
169 }
170 }
171
172 /**
173 * Get events from Google Calendar
174 * Only returns events that were not created by Timetics
175 *
176 * @param array $bookings Existing bookings array
177 *
178 * @return array Modified bookings array with Google Calendar events
179 */
180 public function get_events_from_google( $bookings ) {
181 try {
182 // Get Google Calendar events
183 $events = $this->calendar->get_events( get_current_user_id() );
184
185 if ( is_wp_error( $events ) || empty( $events ) ) {
186 return $bookings;
187 }
188
189 // Get all Timetics-created Google Event IDs
190 $timetics_event_ids = $this->get_timetics_google_event_ids();
191
192 $google_events = array();
193
194 foreach ( $events as $event ) {
195 // Skip events that were created by Timetics
196 if ( in_array( $event['id'] ?? null, $timetics_event_ids, true ) ) {
197 continue;
198 }
199
200 // mapping google events data to timetics booking format
201 $google_events[] = array(
202 'id' => $event['id'] ?? '',
203 'order_total' => '',
204 'title' => $event['summary'] ?? '',
205 'description' => $event['description'] ?? '',
206 'date' => $event['start_date'] ?? '',
207 'start_date' => $event['start_date'] ?? '',
208 'end_date' => $event['end_date'] ?? '',
209 'start_time' => $event['start_time'] ?? '',
210 'end_time' => $event['end_time'] ?? '',
211 'source' => 'google',
212 'status' => 'approved',
213 'random_id' => 'I' . ( $event['id'] ?? '' ),
214 'appointment' => array(
215 'id' => $event['id'] ?? '',
216 'name' => $event['summary'] ?? '',
217 'timezone' => $event['timezone'] ?? '',
218 ),
219 );
220 }
221
222 // Merge with existing bookings
223 return array_merge( $bookings, $google_events );
224 } catch ( \Throwable $e ) {
225 return $bookings;
226 }
227 }
228
229 /**
230 * Block timeslots by Google events
231 *
232 * @param array $data
233 * @param int $staff_id
234 * @param int $meeting_id
235 * @param string $timezone
236 *
237 * @return array Modified bookings array with Google Calendar events
238 */
239 public function block_timeslots_by_google_events( $data, $staff_id, $meeting_id, $timezone ) {
240 try {
241 if ( ! timetics_get_option( 'google_calendar_overlap', false ) ) {
242 return $data;
243 }
244
245 if ( empty( $data ) ) {
246 return $data;
247 }
248
249 // Fetch Google events for the full date range in one request
250 $first_day = reset( $data );
251 $last_day = end( $data );
252
253 $date_obj_start = new DateTime( $first_day['date'] . ' 00:00:00', new DateTimeZone( $timezone ) );
254 $date_obj_end = new DateTime( $last_day['date'] . ' 23:59:59', new DateTimeZone( $timezone ) );
255
256 $google_events = $this->calendar->get_events( $staff_id, [
257 'timeMin' => rawurlencode( $date_obj_start->format( DateTime::RFC3339 ) ),
258 'timeMax' => rawurlencode( $date_obj_end->format( DateTime::RFC3339 ) ),
259 'orderBy' => 'startTime',
260 'singleEvents' => 'true',
261 'timeZone' => $timezone,
262 ]);
263
264 // if theres no google calendar event for the staff member, return the data as it is
265 if ( is_wp_error( $google_events ) || empty( $google_events ) ) {
266 return $data;
267 }
268
269 // Index events by date for efficient per-day lookup
270 $events_by_date = [];
271 foreach ( $google_events as $event ) {
272 $event_date = $event['start_date'] ?? '';
273 if ( $event_date ) {
274 $events_by_date[ $event_date ][] = $event;
275 }
276 }
277
278 // Process each day in the range
279 foreach ( $data as $index => $time_slot_data ) {
280 $date = $time_slot_data['date'];
281
282 if ( empty( $events_by_date[ $date ] ) ) {
283 continue;
284 }
285
286 $day_events = $events_by_date[ $date ];
287 $updated_slots = [];
288
289 foreach ( $time_slot_data['slots'] as $slot ) {
290 $slot_start_time = strtotime( $slot['start_time'] );
291
292 // Check against each Google Calendar event for this day
293 foreach ( $day_events as $event ) {
294 $event_start = strtotime( $event['start_time'] );
295 $event_end = strtotime( $event['end_time'] );
296
297 // If the slot overlaps with the event, mark it as unavailable
298 if ( $slot_start_time >= $event_start && $slot_start_time < $event_end ) {
299 $slot['status'] = 'unavailable';
300 break;
301 }
302 }
303
304 if ( $slot['status'] === 'unavailable' ) {
305 continue;
306 }
307
308 $updated_slots[] = $slot;
309 }
310
311 $data[ $index ]['slots'] = $updated_slots;
312 }
313
314 return $data;
315 } catch (\Throwable $e) {
316 // Silenty reverts to normal behavior incase of error
317 return $data;
318 }
319 }
320 }
321