PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.56
Timetics – Appointment Booking Calendar & Scheduling v1.0.56
1.0.64 1.0.62 1.0.63 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 All 65 releases
timetics / core / bookings / api-booking.php

api-booking.php in Timetics – Appointment Booking Calendar & Scheduling 1.0.56, at core/bookings/api-booking.php

1,420 lines 51.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Booking api
4 *
5 * @package Timetics
6 */
7 namespace Timetics\Core\Bookings;
8
9 use Error;
10 use Timetics\Base\Api;
11 use Timetics\Core\Appointments\Api_Appointment;
12 use Timetics\Core\Appointments\Appointment;
13 use Timetics\Core\Customers\Customer;
14 use Timetics\Core\Emails\Cancel_Event_Customer_Email;
15 use Timetics\Core\Emails\Cancel_Event_Email;
16 use Timetics\Core\Emails\New_Event_Customer_Email;
17 use Timetics\Core\Emails\New_Event_Email;
18 use Timetics\Core\Emails\Update_Event_Customer_Email;
19 use Timetics\Core\Emails\Update_Event_Email;
20 use Timetics\Core\Staffs\Staff;
21 use Timetics\Utils\Singleton;
22 use TimeticsPro\Core\SeatPlan\SeatPlan;
23 use WP_Error;
24 use WP_HTTP_Response;
25 use WP_Query;
26
27 class Api_Booking extends Api {
28 use Singleton;
29
30 /**
31 * Store api namespace
32 *
33 * @var string
34 */
35 protected $namespace = 'timetics/v1';
36
37 /**
38 * Store rest base
39 *
40 * @var string
41 */
42 protected $rest_base = 'bookings';
43
44 /**
45 * Booking Type
46 *
47 * @var string
48 */
49 protected $type = '';
50
51 /**
52 * Register rest routes
53 *
54 * @return void
55 */
56 public function register_routes() {
57 /**
58 * Register route
59 *
60 * @var void
61 */
62 register_rest_route(
63 $this->namespace, $this->rest_base, [
64 [
65 'methods' => \WP_REST_Server::READABLE,
66 'callback' => [$this, 'get_items'],
67 'permission_callback' => function () {
68 return current_user_can( 'manage_timetics' );
69 },
70 ],
71 [
72 'methods' => \WP_REST_Server::CREATABLE,
73 'callback' => [$this, 'create_item'],
74 'permission_callback' => function () {
75 return true;
76 },
77 ],
78 [
79 'methods' => \WP_REST_Server::DELETABLE,
80 'callback' => [$this, 'bulk_delete'],
81 'permission_callback' => function () {
82 return current_user_can( 'edit_booking' );
83 },
84 ],
85 ]
86 );
87
88 /**
89 * Register route
90 *
91 * @var void
92 */
93 register_rest_route(
94 $this->namespace, '/' . $this->rest_base . '/(?P<booking_id>[\d]+)', [
95 [
96 'methods' => \WP_REST_Server::READABLE,
97 'callback' => [$this, 'get_item'],
98 'permission_callback' => [$this, 'get_item_permission_callback'],
99 ],
100 [
101 'methods' => \WP_REST_Server::EDITABLE,
102 'callback' => [$this, 'update_item'],
103 'permission_callback' => [$this, 'update_item_permission_callback'],
104 ],
105 [
106 'methods' => \WP_REST_Server::DELETABLE,
107 'callback' => [$this, 'delete_item'],
108 'permission_callback' => function () {
109 return current_user_can( 'edit_booking' );
110 },
111 ],
112 ]
113 );
114
115 register_rest_route(
116 $this->namespace, '/' . $this->rest_base . '/(?P<booking_id>[\d]+)/payment', [
117 [
118 'methods' => \WP_REST_Server::EDITABLE,
119 'callback' => [$this, 'make_payment'],
120 'permission_callback' => [$this, 'make_payment_permission_callback'],
121 ],
122 ]
123 );
124
125 register_rest_route(
126 $this->namespace, $this->rest_base . '/search', [
127 [
128 'methods' => \WP_REST_Server::READABLE,
129 'callback' => [$this, 'search_items'],
130 'permission_callback' => function () {
131 return current_user_can( 'edit_posts' );
132 },
133 ],
134 ]
135 );
136
137 register_rest_route(
138 $this->namespace, $this->rest_base . '/entries', [
139 [
140 'methods' => \WP_REST_Server::READABLE,
141 'callback' => [$this, 'get_entries'],
142 'permission_callback' => function () {
143 return true;
144 },
145 ],
146 ]
147 );
148
149 register_rest_route(
150 $this->namespace, $this->rest_base . '/payment_methods', [
151 [
152 'methods' => \WP_REST_Server::READABLE,
153 'callback' => [$this, 'get_payment_methods'],
154 'permission_callback' => function () {
155 return true;
156 },
157 ],
158 ]
159 );
160 }
161
162 /**
163 * Get all bookings
164 *
165 * @param WP_Rest_Request $request
166 *
167 * @return JSON
168 */
169 public function get_items( $request ) {
170 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
171 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
172 $meeting_id = ! empty( $request['meeting_id'] ) ? intval( $request['meeting_id'] ) : 0;
173 $start_date = ! empty( $request['start_date'] ) ? $request['start_date'] : '';
174
175 $args = [
176 'posts_per_page' => $per_page,
177 'paged' => $paged,
178 'meeting' => $meeting_id,
179 ];
180
181 $args = apply_filters( 'timetics/add/item/data', $args, $request );
182
183 if ( $start_date ) {
184 $args['start_date'] = $start_date;
185 }
186
187 if ( ! current_user_can( 'manage_options' ) ) {
188 $allowed_ids = Booking::get_visible_ids_for_user( get_current_user_id() );
189 $args['post__in'] = ! empty( $allowed_ids ) ? $allowed_ids : [ 0 ];
190 }
191
192 $bookings = Booking::all( $args );
193 $items = [];
194
195 foreach ( $bookings['items'] as $item ) {
196 $items[] = $this->prepare_item( $item->ID );
197 }
198
199 /**
200 * Added temporary for leagacy sass. It will remove in future.
201 */
202 $items = apply_filters( 'timetics/admin/booking/get_items', $items );
203
204 $data = [
205 'success' => 1,
206 'status_code' => 200,
207 'data' => [
208 'total' => $bookings['total'],
209 'items' => $items,
210 ],
211 ];
212
213 return rest_ensure_response( $data );
214 }
215
216 /**
217 * Get single booking
218 *
219 * @param WP_Rest_Request $request
220 *
221 * @return JSON
222 */
223 public function get_item( $request ) {
224 $booking_id = (int) $request['booking_id'];
225 $booking = new Booking( $booking_id );
226
227 if ( ! $booking->is_booking() ) {
228 return [
229 'success' => 0,
230 'status_code' => 404,
231 'message' => esc_html__( 'Invalid booking id.', 'timetics' ),
232 'data' => [],
233 ];
234 }
235
236 /**
237 * Added temporary for leagacy sass. It will remove in future.
238 */
239 do_action( 'timetics/admin/booking/get_item', $this->prepare_item( $booking ) );
240
241 $data = [
242 'success' => 1,
243 'status_code' => 200,
244 'data' => $this->prepare_item( $booking ),
245 ];
246
247 return rest_ensure_response( $data );
248 }
249
250 /**
251 * Create booking
252 *
253 * @param WP_Rest_Request $request
254 *
255 * @return JSON
256 */
257 public function create_item( $request ) {
258
259 $bookings_count = Booking::all();
260
261 $response = [
262 'success' => 0,
263 'status_code' => 502,
264 'message' => esc_html__( 'Something went wrong', 'timetics' ),
265 'data' => [],
266 ];
267
268 if ( apply_filters( 'timetics/staff/booking/count_check', false, $bookings_count ) == true ) {
269 return new WP_HTTP_Response( apply_filters( 'timetics/admin/booking/error_data', $response, 'count_check' ), 403 );
270 }
271
272 $data = json_decode( $request->get_body(), true );
273
274 if ( apply_filters( 'timetics/booking/appointment/type_check', false, $request ) == true ) {
275 return new WP_HTTP_Response( apply_filters( 'timetics/admin/booking/error_data', $response, 'type_check' ), 403 );
276 }
277
278 $recurring_booking = ! empty( $data['recurring_dates'] ) ? $data['recurring_dates'] : [];
279
280 if ( $recurring_booking && apply_filters( 'timetics/booking/appointment/recurring_check', false, $recurring_booking ) == true ) {
281 $response = [
282 'status_code' => 403,
283 'success' => 0,
284 'message' => esc_html__( 'Recurring booking limit exit', 'timetics' ),
285 ];
286
287 return new WP_HTTP_Response( $response, 403 );
288 } // End.
289
290 return $this->save_bookings( $request );
291 }
292
293 /**
294 * Update booking
295 *
296 * @param WP_Rest_Request $request
297 *
298 * @return JSON
299 */
300 public function update_item( $request ) {
301
302 $booking_id = (int) $request['booking_id'];
303 $booking = new Booking( $booking_id );
304
305 if ( ! $booking->is_booking() ) {
306 return [
307 'status_code' => 404,
308 'message' => esc_html__( 'Invalid booking id.', 'timetics' ),
309 'data' => [],
310 ];
311 }
312
313 if ( apply_filters( 'timetics/booking/appointment/custom_form_data', false, $request ) == true ) {
314 $response = [
315 'status_code' => 409,
316 'success' => 0,
317 'message' => esc_html__( 'Custom Field Booking Restricted ', 'timetics' ),
318 ];
319
320 return new WP_HTTP_Response( $response, 403 );
321 }
322
323 return $this->save_bookings( $request, $booking_id );
324 }
325
326 /**
327 * Delete booking
328 *
329 * @param WP_Rest_Request $request
330 *
331 * @return JSON
332 */
333 public function delete_item( $request ) {
334
335 $booking_id = (int) $request['booking_id'];
336
337 $delete = $this->delete( $booking_id );
338
339 if ( ! $delete ) {
340 $data = [
341 'success' => 1,
342 'status_code' => 409,
343 'message' => esc_html__( 'Something went wrong, Please try again.', 'timetics' ),
344 'data' => [],
345 ];
346
347 return new WP_HTTP_Response( $data, 409 );
348 }
349
350 $data = [
351 'success' => 1,
352 'status_code' => 200,
353 'message' => esc_html__( 'Successfully deleted booking', 'timetics' ),
354 'data' => [],
355 ];
356
357 return rest_ensure_response( $data );
358 }
359
360 /**
361 * Delete multiples
362 *
363 * @param WP_Rest_Request $request
364 *
365 * @return JSON
366 */
367 public function bulk_delete( $request ) {
368
369 $bookings = json_decode( $request->get_body(), true );
370
371 foreach ( $bookings as $booking ) {
372 $delete = $this->delete( $booking );
373
374 if ( ! $delete ) {
375 return [
376 'success' => 0,
377 'status_code' => 404,
378 'message' => esc_html__( 'Invalid booking id.', 'timetics' ),
379 'data' => [],
380 ];
381 }
382 }
383
384 /**
385 * Added temporary for leagacy sass. It will remove in future.
386 */
387 do_action( 'timetics/admin/booking/bulk_delete', $bookings );
388
389 return [
390 'success' => 1,
391 'status_code' => 200,
392 'message' => esc_html__( 'Successfully deleted booking', 'timetics' ),
393 ];
394 }
395
396 /**
397 * Get payment methods
398 *
399 * @return array
400 */
401 public function get_payment_methods() {
402
403 $payment_methods = timetics_get_payment_methods();
404
405 return [
406 'success' => 1,
407 'status_code' => 200,
408 'data' => $payment_methods,
409 ];
410 }
411
412 /**
413 * Search bookings
414 *
415 * @param WP_Rest_Request $request
416 *
417 * @return JSON
418 */
419 public function search_items( $request ) {
420
421 // Prepare search args.
422 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
423 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
424 $search = ! empty( $request['search'] ) ? sanitize_text_field( $request['search'] ) : '';
425
426 // Get search.
427 $booking = new WP_Query(
428 array(
429 'post_type' => 'timetics-booking',
430 'posts_per_page' => $per_page,
431 'paged' => $paged,
432 'post_status' => 'any',
433
434 // @codingStandardsIgnoreStart
435 'meta_query' => array(
436 'relation' => 'OR',
437 array(
438 'key' => '_tt_booking_customer_fname',
439 'value' => $search,
440 'compare' => 'LIKE',
441 ),
442 array(
443 'key' => '_tt_booking_customer_lname',
444 'value' => $search,
445 'compare' => 'LIKE',
446 ),
447 array(
448 'key' => '_tt_booking_customer_email',
449 'value' => $search,
450 'compare' => 'LIKE',
451 ),
452 array(
453 'key' => '_tt_booking_customer_phone',
454 'value' => $search,
455 'compare' => 'LIKE',
456 ),
457 array(
458 'key' => '_tt_booking_staff_fname',
459 'value' => $search,
460 'compare' => 'LIKE',
461 ),
462 array(
463 'key' => '_tt_booking_staff_lname',
464 'value' => $search,
465 'compare' => 'LIKE',
466 ),
467 array(
468 'key' => '_tt_booking_staff_email',
469 'value' => $search,
470 'compare' => 'LIKE',
471 ),
472 array(
473 'key' => '_tt_booking_meeting_name',
474 'value' => $search,
475 'compare' => 'LIKE',
476 ),
477 array(
478 'key' => '_tt_booking_meeting_description',
479 'value' => $search,
480 'compare' => 'LIKE',
481 ),
482 array(
483 'key' => '_tt_booking_meeting_type',
484 'value' => $search,
485 'compare' => 'LIKE',
486 ),
487 ),
488 // @codingStandardsIgnoreEnd
489 )
490 );
491
492 // Prepare items for response.
493 $items = [];
494
495 foreach ( $booking->posts as $item ) {
496 $items[] = $this->prepare_item( $item->ID );
497 }
498
499 /**
500 * Added temporary for leagacy sass. It will remove in future.
501 */
502 $items = apply_filters( 'timetics/admin/booking/search_items', $items );
503
504 $data = [
505 'success' => 1,
506 'status' => 200,
507 'data' => [
508 'total' => $booking->found_posts,
509 'items' => $items,
510 ],
511 ];
512
513 return rest_ensure_response( $data );
514 }
515
516 /**
517 * Get all booking entries
518 *
519 * @param WP_Rest_Request $request
520 *
521 * @return JSON
522 */
523 public function get_entries( $request ) {
524 $staff_id = ! empty( $request['staff_id'] ) ? intval( $request['staff_id'] ) : 0;
525 $meeting_id = ! empty( $request['meeting_id'] ) ? intval( $request['meeting_id'] ) : 0;
526 $start_date = ! empty( $request['start_date'] ) ? sanitize_text_field( $request['start_date'] ) : 0;
527 $timezone = ! empty( $request['timezone'] ) ? sanitize_text_field( $request['timezone'] ) : 0;
528 $end_date = ! empty( $request['end_date'] ) ? sanitize_text_field( $request['end_date'] ) : 0;
529
530 $meeting = new Appointment( $meeting_id );
531
532 // Validate timezone.
533 if ( ! timetics_is_valid_timezone( $timezone ) ) {
534 return new WP_Error( 'timezone_error', __( 'Your booking timezone is invalid', 'timetics' ) );
535 }
536
537 // Validate meeting timezone.
538 if ( ! timetics_is_valid_timezone( $meeting->get_timezone() ) ) {
539 return new WP_Error( 'timezone_error', __( 'Your meeting timezone is invalid. Please update your meeting timezone with proper timezone.', 'timetics' ) );
540 }
541
542 $days = $meeting->prepare_schedule( $start_date, $end_date, $staff_id, $timezone );
543 $days = apply_filters( 'timetics_schedule_data_for_selected_date', $days, $staff_id, $meeting_id, $timezone );
544
545 $data = [
546 'today' => gmdate( 'Y-m-d' ),
547 'availability_timezone' => $meeting->get_timezone(),
548 'days' => $days,
549 ];
550
551 /**
552 * Added temporary for leagacy sass. It will remove in future.
553 */
554 $data = apply_filters( 'timetics/admin/booking/get_entries', $data );
555
556 return [
557 'success' => true,
558 'status_code' => 200,
559 'message' => esc_html__( 'Get all entries', 'timetics' ),
560 'data' => $data,
561 ];
562 }
563
564 /**
565 * Make payment transaction for the current booking
566 *
567 * @param WP_Rest_Request $request
568 *
569 * @return JSON
570 */
571 public function make_payment( $request ) {
572 $booking_id = intval( $request['booking_id'] );
573 $booking = new Booking( $booking_id );
574 $data = json_decode( $request->get_body(), true );
575 $status = ! empty( $data['status'] ) ? sanitize_text_field( $data['status'] ) : '';
576 $default_booking_status = timetics_get_option( 'default_booking_status', 'approved' );
577 $post_status = 'succeeded' === $status ? $default_booking_status : ( 'failed' === $status ? 'failed' : 'pending' );
578 $payment_method = ! empty( $data['payment_method'] ) ? sanitize_text_field( $data['payment_method'] ) : '';
579 $payment_details = ! empty( $data['payment_details'] ) ? $data['payment_details'] : '';
580 $type = $booking->get_type();
581
582 if ( ! $booking->is_booking() ) {
583 return [
584 'status_code' => 404,
585 'message' => esc_html__( 'Invalid booking id.', 'timetics' ),
586 'data' => [],
587 ];
588 }
589
590 $update = $booking->update(
591 [
592 'post_status' => $post_status,
593 'payment_status' => $status,
594 'payment_details' => $payment_details,
595 'payment_method' => $payment_method,
596 ]
597 );
598
599 if ( is_wp_error( $update ) ) {
600 $data = [
601 'success' => 0,
602 'status_code' => 409,
603 /* translators: Action */
604 'message' => $update->get_error_message(),
605 ];
606
607 return new WP_HTTP_Response( $data, 409 );
608 }
609
610 if ( $default_booking_status === $post_status ) {
611 $booking->create_event();
612
613 if( 'timetics-event' == $type ){
614 return;
615 }
616
617 $is_email_to_customer = timetics_get_option( 'booking_created_customer');
618 $is_email_to_host = timetics_get_option( 'booking_created_host');
619
620 if ( $is_email_to_host ) {
621 $new_event_email = new New_Event_Email( $booking );
622 $new_event_email->send();
623 }
624
625 if ( $is_email_to_customer ) {
626 $new_event_customer_email = new New_Event_Customer_Email( $booking );
627 $new_event_customer_email->send();
628 }
629
630 do_action( 'timetics_booking_payment', $booking );
631
632 }
633
634 /**
635 * Added temporary for leagacy sass. It will remove in future.
636 */
637 do_action( 'timetics/admin/booking/make_payment', $post_status );
638
639 $data = [
640 'success' => 1,
641 'status_code' => 200,
642 /* translators: Action */
643 'message' => sprintf( esc_html__( 'Payment %s', 'timetics' ), $post_status ),
644 ];
645
646 return new WP_HTTP_Response( $data, 200 );
647 }
648
649 /**
650 * Save booking
651 *
652 * @param WP_Rest_Request $request
653 * @param integer $id Booking id
654 *
655 * @return JSON
656 */
657 public function save_bookings( $request, $id = 0 ) {
658 $data = json_decode( $request->get_body(), true );
659
660 if( isset( $data['type'] ) && 'timetics-event' == $data['type'] ) {
661 $this->type = $data['type'];
662 return apply_filters('timetics_booking_event', $data, $id );
663 }else {
664 return $this->booking_appointment($data, $id);
665 }
666 }
667
668 /**
669 * Booking Appointment
670 *
671 * @param array $data All the data of booking
672 * @param integer $id Booking id
673 *
674 * @return JSON
675 */
676 protected function booking_appointment ($data, $id) {
677 $first_name = ! empty( $data['first_name'] ) ? sanitize_text_field( $data['first_name'] ) : '';
678 $last_name = ! empty( $data['last_name'] ) ? sanitize_text_field( $data['last_name'] ) : '';
679 $email = ! empty( $data['email'] ) ? sanitize_text_field( $data['email'] ) : '';
680 $phone = ! empty( $data['phone'] ) ? sanitize_text_field( $data['phone'] ) : '';
681 $city = ! empty( $data['city'] ) ? sanitize_text_field( $data['city'] ) : '';
682 $state = ! empty( $data['state'] ) ? sanitize_text_field( $data['state'] ) : '';
683 $post_code = ! empty( $data['post_code'] ) ? sanitize_text_field( $data['post_code'] ) : '';
684 $country = ! empty( $data['country'] ) ? sanitize_text_field( $data['country'] ) : '';
685 $payment_method = ! empty( $data['payment_method'] ) ? sanitize_text_field( $data['payment_method'] ) : '';
686 $address_1 = ! empty( $data['address_1'] ) ? sanitize_text_field( $data['address_1'] ) : '';
687 $address_2 = ! empty( $data['address_2'] ) ? sanitize_text_field( $data['address_2'] ) : '';
688 $appointment = ! empty( $data['appointment'] ) ? intval( $data['appointment'] ) : 0;
689 $staff_id = ! empty( $data['staff'] ) ? intval( $data['staff'] ) : 0;
690 $start_date = ! empty( $data['start_date'] ) ? sanitize_text_field( $data['start_date'] ) : '';
691 $date = ! empty( $data['date'] ) ? sanitize_text_field( $data['date'] ) : '';
692 $end_date = ! empty( $data['end_date'] ) ? sanitize_text_field( $data['end_date'] ) : $start_date;
693 $start_time = ! empty( $data['start_time'] ) ? sanitize_text_field( $data['start_time'] ) : '';
694 $end_time = ! empty( $data['end_time'] ) ? sanitize_text_field( $data['end_time'] ) : '';
695 $order_total = ! empty( $data['order_total'] ) ? intval( $data['order_total'] ) : 0;
696 $status = ! empty( $data['status'] ) ? sanitize_text_field( $data['status'] ) : timetics_get_option( 'default_booking_status', 'approved' );
697 $location = ! empty( $data['location'] ) ? sanitize_text_field( $data['location'] ) : '';
698 $location_type = ! empty( $data['location_type'] ) ? sanitize_text_field( $data['location_type'] ) : '';
699 $description = ! empty( $data['description'] ) ? sanitize_text_field( $data['description'] ) : '';
700 $timezone = ! empty( $data['timezone'] ) ? sanitize_text_field( $data['timezone'] ) : '';
701 $recurring_dates = ! empty( $data['recurring_dates'] ) ? $data['recurring_dates'] : [];
702 $seats = ! empty( $data['seats'] ) ? $data['seats'] : [];
703 $cancel_reason = ! empty( $data['cancel_reason'] ) ? $data['cancel_reason'] : [];
704 $booking_time = ! empty( $data['booking_createAt'] ) ? $data['booking_createAt'] : '';
705 $action = $id ? 'updated' : 'created';
706
707 // For WooCommerce payments, create booking in failed status until payment is confirmed
708 if ( 'woocommerce' === $payment_method && 'created' === $action && $order_total != 0 ) {
709 $status = 'failed';
710 }
711
712 // For Stripe payments, create booking in pending status until payment is confirmed
713 if ( 'stripe' === strtolower( $payment_method ) && 'created' === $action && $order_total != 0 ) {
714 $status = 'pending';
715 }
716 $appointment_token = ! empty( $data['appointment_token'] ) ? sanitize_text_field( $data['appointment_token'] ) : '';
717
718 if ( $id ) {
719 $email_validation = $this->validate_email_change_permission( $id, $email );
720
721 if ( is_wp_error( $email_validation ) ) {
722 $error_code = $email_validation->get_error_code();
723 $error_response = [
724 'success' => 0,
725 'status_code' => $error_code,
726 'message' => $email_validation->get_error_message(),
727 ];
728 return new WP_HTTP_Response( $error_response, $error_code );
729 }
730
731 // Use the validated email from the security check
732 $email = $email_validation;
733 }
734
735 $validate = $this->validate(
736 $data, [
737 'first_name',
738 'email',
739 'payment_method',
740 'appointment',
741 'start_date',
742 'start_time',
743 'end_time',
744 ]
745 );
746
747 if ( is_wp_error( $validate ) ) {
748 $data = [
749 'status_code' => 403,
750 'success' => 0,
751 'message' => $validate->get_error_messages(),
752 ];
753 return new WP_HTTP_Response( $data, 403 );
754 }
755
756 $customer = new Customer();
757 $meeting = new Appointment( $appointment );
758 $staff = new Staff( $staff_id );
759 $booking = new Booking( $id );
760 $booking_entry = new Booking_Entry();
761
762 // Validate booking
763
764 $validation = $this->validate_booking( $appointment, $data );
765 if(is_wp_error($validation)){
766 return $validation;
767 }
768
769
770
771 if ( 'created' === $action && ! $this->is_available_slot( $meeting, [
772 'staff_id' => $staff->get_id(),
773 'start_date' => $start_date,
774 'start_time' => $start_time,
775 'timezone' => $timezone,
776 ] ) ) {
777 /* translators: %s: Time slot */
778 return new WP_Error( 'time_slot_error', sprintf( __( '%s time slot is not available', 'timetics' ), $start_time ) );
779 }
780
781 if ( $meeting->is_recurring() ) {
782 $valid_recurrence = apply_filters( 'timetics_validate_recurring_booking', $recurring_dates, $start_time, $staff->get_id(), $meeting->get_id() );
783
784 if ( ! $valid_recurrence ) {
785 $recurring_error = [
786 'status_code' => 403,
787 'success' => 0,
788 'message' => __( 'Couldn\'t possible to book. Plese try another time.', 'timetics' ),
789 ];
790
791 return new WP_HTTP_Response( $recurring_error, 403 );
792 }
793 }
794
795 $customer->make(
796 [
797 'first_name' => $first_name,
798 'last_name' => $last_name,
799 'email' => $email,
800 'phone' => $phone,
801 ]
802 );
803
804 // Update booking schedule.
805 if ( $id ) {
806
807 $entries = $booking_entry->find(
808 [
809 'staff_id' => $booking->get_staff_id(),
810 'meeting_id' => $booking->get_appointment(),
811 'date' => $booking->get_start_date(),
812 'start' => $booking->get_start_time(),
813 ]
814
815 );
816
817 if ( $entries ) {
818 $entry = $booking_entry->first();
819
820 if ( 'one-to-one' == strtolower( $meeting->get_type() ) ) {
821 $entry->delete();
822 } else {
823 $booked = intval( $entry->get_booked() ) - 1;
824 $booked_data = apply_filters( 'timetics_booking_update_schedule', $entry, ['booked' => $booked], $data, $booking );
825 $entry->update( $booked_data );
826 }
827 }
828 }
829
830 if ( $id && $booking->get_status() == 'cancel' && $status == 'cancel' ) {
831 return new WP_Error( 'booking_cancel_error', __( 'This booking alreay canceled', 'timetics' ) );
832 }
833
834 $booking_props = [
835 'customer' => $customer->get_id(),
836 'appointment' => $meeting->get_id(),
837 'appointment_name' => $meeting->get_name(),
838 'staff' => $staff->get_id(),
839 'customer_fname' => $customer->get_first_name(),
840 'customer_lname' => $customer->get_last_name(),
841 'customer_email' => $customer->get_email(),
842 'customer_phone' => $customer->get_phone(),
843 'staff_fname' => $staff->get_first_name(),
844 'staff_lname' => $staff->get_last_name(),
845 'staff_email' => $staff->get_email(),
846 'meeting_name' => $meeting->get_name(),
847 'meeting_description' => $meeting->get_description(),
848 'meeting_type' => $meeting->get_type(),
849 'booking_time' => $booking_time,
850 'description' => $description,
851 'start_date' => $start_date,
852 'date' => $date,
853 'end_date' => $end_date,
854 'start_time' => $start_time,
855 'end_time' => $end_time,
856 'order_total' => $this->calculate_order_total( $data ),
857 'post_status' => $status,
858 'location' => $location,
859 'location_type' => $location_type,
860 'timezone' => $timezone,
861 'cancel_reason' => $cancel_reason,
862 ];
863
864 if ( $id ) {
865 $old_start_date = $booking->get_start_date();
866 $old_start_time = $booking->get_start_time();
867 $old_end_time = $booking->get_end_time();
868 }
869
870 if( 'created' == $action ){
871 $booking_props['security_token'] = $booking->generate_security_token();
872 }
873
874 $booking->set_props( $booking_props );
875
876
877 $booking = apply_filters( 'timetics/bookings/booking/set', $booking );
878
879 $booking->save();
880
881 // Fire when booking is completed.
882 do_action( 'timetics_after_booking_create', $booking->get_id(), $customer->get_id(), $meeting->get_id(), $data );
883
884 // Create or update calendar event.
885 if ( $id ) {
886 if ( 'cancel' === $status ) {
887 $booking->delete_event();
888 $is_email_to_customer = timetics_get_option( 'booking_canceled_customer');
889 $is_email_to_host = timetics_get_option( 'booking_canceled_host');
890
891 if ( $is_email_to_host ) {
892 $cancel_event_email = new Cancel_Event_Email( $booking );
893 $cancel_event_email->send();
894 }
895
896 if ( $is_email_to_customer ) {
897 $customer_cancel_event_email = new Cancel_Event_Customer_Email( $booking );
898 $customer_cancel_event_email->send();
899 }
900
901 /**
902 * Added temporary for leagacy sass. It will remove in future.
903 */
904 do_action( 'timetics/admin/booking/after_delete_item', $booking );
905 } else {
906 // Check if the booking date/time was actually changed
907 $date_time_changed = (
908 $old_start_date !== $start_date ||
909 $old_start_time !== $start_time ||
910 $old_end_time !== $end_time
911 );
912
913 $booking->update_event();
914
915 if ( $date_time_changed ) {
916 $is_email_to_reschedule_customer = timetics_get_option( 'booking_rescheduled_customer');
917 $is_email_to_reschedule_host = timetics_get_option( 'booking_rescheduled_host');
918
919 if ( $is_email_to_reschedule_host ) {
920 $update_event_email = new Update_Event_Email( $booking );
921 $update_event_email->send();
922 }
923
924 if ( $is_email_to_reschedule_customer ) {
925 $update_event_customer_email = new Update_Event_Customer_Email( $booking );
926 $update_event_customer_email->send();
927 }
928 }
929 }
930 }
931
932 // Convert booking time to staff/meeting time.
933 $date_time = timetics_convert_timezone( $start_date . ' ' . $start_time, $timezone, $meeting->get_timezone() );
934 $end_time = timetics_convert_timezone( $start_date . ' ' . $end_time, $timezone, $meeting->get_timezone() );
935
936 // Create booking schedule.
937 $entries = $booking_entry->find(
938 [
939 'staff_id' => $staff->get_id(),
940 'meeting_id' => $meeting->get_id(),
941 'date' => $date_time->format( 'Y-m-d' ),
942 'start' => $date_time->format( 'h:i a' ),
943 ]
944 );
945
946 if ( $entries ) {
947 $entry = $booking_entry->first();
948
949 if ( 'cancel' === $status ) {
950 $booked = intval( $entry->get_booked() ) - 1;
951 } else {
952 $booked = intval( $entry->get_booked() ) + 1;
953 }
954
955 $booked_data = apply_filters( 'timetics_booking_update_schedule', $entry, ['booked' => $booked], $data, $booking );
956
957 if ( 'cancel' === $status && 'one-to-one' == strtolower( $meeting->get_type() ) ) {
958 $entry->delete();
959 } else {
960 $entry->update( $booked_data );
961 }
962
963 } else {
964 $book_entry_data = [
965 'meeting_id' => $meeting->get_id(),
966 'staff_id' => $staff->get_id(),
967 'customer_id' => $customer->get_id(),
968 'booking_id' => $booking->get_id(),
969 'booked' => 1,
970 'date' => $date_time->format( 'Y-m-d' ),
971 'start' => $date_time->format( 'h:i a' ),
972 'end' => $end_time->format( 'h:i a' ),
973 ];
974
975 $book_entry_data = apply_filters( 'timetics_booking_schedule', $book_entry_data, $data );
976 $booking_entry->create( $book_entry_data );
977 }
978
979 // Fire after booking schedule create.
980 do_action( 'timetics_after_booking_schedule', $booking->get_id(), $customer->get_id(), $meeting->get_id(), $data );
981
982 $data = [
983 'success' => 1,
984 'status_code' => 200,
985 /* translators: Action */
986 'message' => sprintf( esc_html__( 'Successfully %s booking', 'timetics' ), $action ),
987 'data' => $this->prepare_item( $booking ),
988 ];
989
990 return new WP_HTTP_Response( $data, 200 );
991 }
992
993 /**
994 * Prepare item for response
995 *
996 * @param integer $booking_id
997 *
998 * @return array
999 */
1000 public function prepare_item( $booking_id ) {
1001 $booking = new Booking( $booking_id );
1002 $appointment = new Appointment( $booking->get_appointment() );
1003 $staff = new Staff( $booking->get_staff_id() );
1004 $customer = new Customer( $booking->get_customer_id() );
1005 $meeting_timezone = $appointment->get_timezone();
1006 $booking_timezone = $booking->get_timezone();
1007
1008 $start_date_time = timetics_convert_timezone( $booking->get_start_date() . ' ' . $booking->get_start_time(), $booking_timezone, $meeting_timezone );
1009 $end_date_time = timetics_convert_timezone( $booking->get_end_date() . ' ' . $booking->get_end_time(), $booking_timezone, $meeting_timezone );
1010 $date = timetics_datetime( 'Y-m-d', $booking->get_date(), $meeting_timezone );
1011
1012 $event = $booking->get_event();
1013 $join_link = 'google-meet' === $booking->get_location_type() && ! empty( $event['hangoutLink'] ) ? $event['hangoutLink'] : '';
1014
1015 $booking_title = $appointment->is_appointment() ? $appointment->get_name() : $booking->get_appointment_name();
1016
1017 $response = [
1018 'id' => $booking->get_id(),
1019 'random_id' => $booking->get_random_id(),
1020 'status' => $booking->get_status(),
1021 'order_total' => $booking->get_total(),
1022 'start_date' => $start_date_time->format( 'Y-m-d' ),
1023 'end_date' => $end_date_time->format( 'Y-m-d' ),
1024 'date' => $date,
1025 'start_time' => $start_date_time->format( 'h:i a' ),
1026 'end_time' => $end_date_time->format( 'h:i a' ),
1027 'booking_time' => $booking->get_booking_time(),
1028 'location' => $booking->get_location(),
1029 'location_type' => $booking->get_location_type(),
1030 'description' => $booking->get_description(),
1031 'cancel_reason' => $booking->get_cancel_reason(),
1032 'security_token'=> $booking->get_security_token(),
1033 'customer' => [
1034 'id' => $customer->get_id(),
1035 'full_name' => $customer->get_display_name(),
1036 'first_name' => $customer->get_first_name(),
1037 'last_name' => $customer->get_last_name(),
1038 'email' => $customer->get_email(),
1039 'phone' => $customer->get_phone(),
1040 ],
1041 'appointment' => [
1042 'id' => $appointment->get_id(),
1043 'name' => $booking_title,
1044 'duration' => $appointment->get_duration(),
1045 'type' => $appointment->get_type(),
1046 'price' => $appointment->get_price(),
1047 'locations' => $appointment->get_locations(),
1048 'timezone' => $appointment->get_timezone(),
1049 'permalink' => $appointment->get_appointment_permalink(),
1050 ],
1051 'staff' => [
1052 'id' => $staff->get_id(),
1053 'full_name' => $staff->get_display_name(),
1054 'first_name' => $staff->get_first_name(),
1055 'last_name' => $staff->get_last_name(),
1056 'email_name' => $staff->get_email(),
1057 'phone' => $staff->get_phone(),
1058 'image' => $staff->get_image(),
1059 ],
1060 ];
1061
1062 if ( $join_link ) {
1063 $response['meeting_link'] = $join_link;
1064 }
1065
1066 return apply_filters( 'timetics_booking_json_data', $response, $booking );
1067 }
1068
1069 /**
1070 * Delete booking
1071 *
1072 * @param integer $booking_id
1073 *
1074 * @return bool
1075 */
1076 private function delete( $booking_id ) {
1077 $booking = new Booking( $booking_id );
1078 $meeting = new Appointment( $booking->get_appointment() );
1079
1080 if ( ! $booking->is_booking() ) {
1081 return false;
1082 }
1083
1084 $current_user_id = get_current_user_id();
1085
1086 if (
1087 $meeting->is_appointment()
1088 && ! user_can( $current_user_id, 'manage_options' )
1089 && $meeting->get_author() != $current_user_id
1090 ) {
1091 $data = [
1092 'success' => 0,
1093 'message' => __( 'You are not allowed to delete this booking.', 'timetics' ),
1094 ];
1095
1096 return new WP_HTTP_Response( $data, 403 );
1097 }
1098
1099 $booking_entry = new Booking_Entry();
1100
1101 $date_time = timetics_convert_timezone( $booking->get_start_date() . ' ' . $booking->get_start_time(), $booking->get_timezone(), $meeting->get_timezone() );
1102
1103 $entries = $booking_entry->find(
1104 [
1105 'staff_id' => $booking->get_staff_id(),
1106 'meeting_id' => $booking->get_appointment(),
1107 'date' => $date_time->format( 'Y-m-d' ),
1108 'start' => $date_time->format( 'h:i a' ),
1109 ]
1110 );
1111
1112 if ( $entries ) {
1113 $entry = $booking_entry->first();
1114
1115 if ( 'one-to-one' == strtolower( $meeting->get_type() ) ) {
1116 $entry->delete();
1117 } else {
1118 $booked = intval( $entry->get_booked() ) - 1;
1119 $booked_seat = ! empty( $booking->get_seat() ) ? $booking->get_seat() : [];
1120 $existing_seat = ! empty( $entry->get_seats() ) ? $entry->get_seats() : [];
1121
1122 $entry->update( [
1123 'booked' => $booked,
1124 'seats' => array_values( array_diff( $existing_seat, $booked_seat ) ),
1125 ] );
1126 }
1127 }
1128
1129 $recurrences = $booking->get_recurrence();
1130 $booking->delete_event();
1131 $booking->delete();
1132
1133 $is_email_to_customer = timetics_get_option( 'booking_canceled_customer');
1134 $is_email_to_host = timetics_get_option( 'booking_canceled_host');
1135
1136 if ( $is_email_to_host ) {
1137 $cancel_event_email = new Cancel_Event_Email( $booking );
1138 $cancel_event_email->send();
1139 }
1140
1141 if ( $is_email_to_customer ) {
1142
1143 $customer_cancel_event_email = new Cancel_Event_Customer_Email( $booking );
1144 $customer_cancel_event_email->send();
1145 }
1146
1147
1148
1149 do_action( 'timetics_after_booking_delete', $recurrences );
1150
1151 return true;
1152 }
1153
1154 public function is_available_slot( $meeting, $booking_data = [] ) {
1155 $start_date = $booking_data['start_date'];
1156 $start_time = $booking_data['start_time'];
1157 $booking_timezone = $booking_data['timezone'];
1158 $booking_entry = new Booking_Entry();
1159 $meeting_id = $meeting->get_id();
1160 $staff_id = $booking_data['staff_id'];
1161
1162 $time = is_string( $start_time ) ? strtotime( $start_time ) : $start_time;
1163 $time = gmdate( 'H:i', $time );
1164 $booking_entries = new Booking_Entry();
1165 $meeting = new Appointment( $meeting_id );
1166
1167 $entries = $booking_entries->find( [
1168 'meeting_id' => $meeting_id,
1169 'staff_id' => $staff_id,
1170 'date' => $start_date,
1171 ] );
1172
1173 $booked = false;
1174
1175 foreach ( $entries as $entry ) {
1176 $booking = new Booking( $entry->get_booking_id() );
1177 $booking_time = timetics_convert_timezone( $booking->get_start_date() . ' ' . $entry->get_start(), $booking->get_timezone(), $booking_timezone )->format( 'H:i' );
1178
1179 if ( $booking_time == $time ) {
1180 $booked = $entry;
1181 break;
1182 }
1183 }
1184
1185 if ( $booked && $booked->get_booked() >= $meeting->get_capacity() ) {
1186 return false;
1187 }
1188
1189 return true;
1190 }
1191
1192 /**
1193 * Validates a booking.
1194 *
1195 * @param int $appointment_id The ID of the appointment.
1196 * @param array $data The data for the booking.
1197 * @throws None
1198 * @return mixed Returns an error response if the validation fails, otherwise returns nothing.
1199 */
1200 public function validate_booking($appointment_id, $data) {
1201 $meeting = new Appointment($appointment_id);
1202 $all_seats = (array) $meeting->get_seats();
1203 $meeting_price = $meeting->get_price();
1204 $meeting_locations = (array) $meeting->get_locations();
1205 $total_price = 0;
1206
1207 $staff_id = ! empty( $data['staff'] ) ? intval( $data['staff'] ) : 0;
1208 $order_total = ! empty( $data['order_total'] ) ? floatval( $data['order_total'] ) : 0;
1209 $location_type = ! empty( $data['location_type'] ) ? sanitize_text_field( $data['location_type'] ) : '';
1210 $start_date = ! empty( $data['start_date'] ) ? sanitize_text_field( $data['start_date'] ) : '';
1211 $timezone = ! empty( $data['timezone'] ) ? sanitize_text_field( $data['timezone'] ) : '';
1212 $start_time = ! empty( $data['start_time'] ) ? sanitize_text_field( $data['start_time'] ) : '';
1213 $status = ! empty( $data['status'] ) ? sanitize_text_field( $data['status'] ) : '';
1214 $seats = ! empty( $data['seats'] ) ? $data['seats'] : [];
1215 $timeslots = $meeting->get_avilable_timeslots( $start_date, $staff_id, $timezone );
1216 $meeting_has_buffer_time = $meeting->get_buffer_time_after_in_seconds() > 0 || $meeting->get_buffer_time_before_in_seconds() > 0;
1217
1218 if ( ! $meeting->is_appointment() ) {
1219 return $this->create_error_response( __( 'Invalid meeting.', 'timetics' ), 422 );
1220 }
1221
1222 if ( 'cancel' !== $status ) {
1223 if ( ! $meeting_has_buffer_time && ! in_array( gmdate( 'g:ia', strtotime( $start_time ) ), $timeslots ) ) {
1224 return $this->create_error_response( __( 'Invalid timeslot.', 'timetics' ), 422 );
1225 }
1226
1227 // Check if the staff is matched
1228 if ( ! in_array( $staff_id, $meeting->get_staff_ids() ) ) {
1229 return $this->create_error_response(__('Team member not matched', 'timetics'), 403);
1230
1231 }
1232 // Check if the location type is matched
1233 if ( ! in_array( $location_type, array_column( $meeting_locations, 'location_type' ) ) ) {
1234 return $this->create_error_response(__('Location type not matched', 'timetics'), 403);
1235 }
1236 }
1237 }
1238
1239 /**
1240 * Creates an error response with the given message and status code.
1241 *
1242 * @param string $message The error message.
1243 * @param int $status_code The HTTP status code.
1244 * @return WP_HTTP_Response The error response.
1245 */
1246 public function create_error_response($message, $status_code) {
1247 return new WP_Error( 'timezone_error', $message, ['status' => $status_code] );
1248 }
1249
1250 /**
1251 * Calculate order total
1252 *
1253 * @param array $data Request data
1254 *
1255 * @return integer
1256 */
1257 private function calculate_order_total($data) {
1258 $seats = ! empty( $data['seats'] ) ? $data['seats'] : [];
1259 $meeting_id = ! empty( $data['appointment'] ) ? $data['appointment'] : 0;
1260 $total_price = 0;
1261
1262 if ( class_exists( SeatPlan::class ) && $seats ) {
1263 foreach( $seats as $seat ) {
1264 $seat_object = SeatPlan::find( $seat );
1265 $total_price += $seat_object->price;
1266 }
1267
1268 return $total_price;
1269 }
1270
1271 $meeting = new Appointment( $meeting_id );
1272
1273 $prices = $meeting->get_price();
1274
1275 if ( $prices && is_array( $prices ) ) {
1276 return $prices[0]['ticket_price'];
1277 }
1278
1279 return 0;
1280 }
1281
1282 /**
1283 * Update item permission callback
1284 * @param WP_REST_Request $request
1285 * @return bool
1286 */
1287 public function update_item_permission_callback($request){
1288 $nonce = $request->get_header('X-WP-Nonce');
1289
1290 $booking_id = (int) $request->get_param('booking_id');
1291 $appointment_token = $request->get_param('appointment_token');
1292
1293 $booking = new Booking($booking_id);
1294
1295 if (!$booking->is_booking()) {
1296 return false;
1297 }
1298
1299 // Guests: must provide a valid token
1300 if ( ! empty( $appointment_token ) ) {
1301 $stored_token = $booking->get_security_token();
1302 if ($appointment_token === $stored_token && !empty($stored_token)) {
1303 return true;
1304 }
1305 }
1306
1307 if (empty($booking_id) || ! wp_verify_nonce($nonce, 'wp_rest')) {
1308 return false;
1309 }
1310
1311 // Allow booking owner or admins/managers.
1312 if ( (int) $booking->get_customer_id() === get_current_user_id() || current_user_can( 'manage_timetics' )) {
1313 return true;
1314 }
1315
1316 return false;
1317 }
1318
1319 /**
1320 * Get item permission callback
1321 * @param WP_Rest_Request $request
1322 * @return bool
1323 */
1324 public function get_item_permission_callback($request){
1325 $nonce = $request->get_header('X-WP-Nonce');
1326 $booking_id = (int) $request->get_param('booking_id');
1327 $appointment_token = $request->get_param('appointment_token');
1328
1329 $booking = new Booking($booking_id);
1330
1331 if (!$booking->is_booking()) {
1332 return false;
1333 }
1334
1335 // Guests: must provide a valid token
1336 if ( ! empty( $appointment_token ) ) {
1337 $stored_token = $booking->get_security_token();
1338 if ($appointment_token === $stored_token && !empty($stored_token)) {
1339 return true;
1340 }
1341 }
1342
1343 if (wp_verify_nonce($nonce, 'wp_rest') && current_user_can( 'manage_timetics' ) ) {
1344 return true;
1345 }
1346 return false;
1347 }
1348
1349 /**
1350 * Validate email change permission during booking update.
1351 *
1352 * Prevents non-admin users from reassigning bookings to other users
1353 * by changing the email address. Follows the principle of least privilege.
1354 *
1355 * @param int $booking_id The ID of the booking being updated.
1356 * @param string $new_email The new email address from the request.
1357 *
1358 * @return string|WP_Error Returns the validated email on success, WP_Error on failure.
1359 */
1360 private function validate_email_change_permission( $booking_id, $new_email ) {
1361 // Admin users have full permission to change email addresses
1362 if ( current_user_can( 'manage_timetics' ) ) {
1363 return $new_email;
1364 }
1365
1366 $existing_booking = new Booking( $booking_id );
1367
1368 if ( ! $existing_booking->is_booking() ) {
1369 return new WP_Error( 404, __( 'Booking not found.', 'timetics' ) );
1370 }
1371
1372 // Get original customer email
1373 $existing_customer = new Customer( $existing_booking->get_customer_id() );
1374 $original_email = $existing_customer->get_email();
1375
1376 if ( empty( $original_email ) ) {
1377 return new WP_Error( 500, __( 'Unable to verify booking ownership.', 'timetics' ) );
1378 }
1379
1380 // Check if email is being changed (case-insensitive comparison)
1381 $is_email_changed = ! empty( $new_email ) && strtolower( trim( $new_email ) ) !== strtolower( trim( $original_email ) );
1382
1383 if ( $is_email_changed ) {
1384 return new WP_Error( 403, __( 'You are not allowed to change the email address for this booking.', 'timetics' ) );
1385 }
1386
1387 return $original_email;
1388 }
1389
1390 public function make_payment_permission_callback( $request ) {
1391
1392 $booking_id = (int) $request->get_param('booking_id');
1393 $appointment_token = sanitize_text_field( $request->get_param('appointment_token') );
1394
1395 if ( empty( $booking_id ) || empty( $appointment_token ) ) {
1396 return false;
1397 }
1398
1399 $booking = new Booking( $booking_id );
1400
1401 if ( ! $booking->is_booking() ) {
1402 return false;
1403 }
1404
1405 $stored_token = $booking->get_security_token();
1406
1407 if ( empty( $stored_token ) ) {
1408 return false;
1409 }
1410
1411 // constant-time comparison
1412 if ( hash_equals( $stored_token, $appointment_token ) ) {
1413 return true;
1414 }
1415
1416 return false;
1417 }
1418
1419 }
1420