PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.62
Timetics – Appointment Booking Calendar & Scheduling v1.0.62
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 1.0.22 All 64 releases
timetics / core / bookings / booking.php

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

1,309 lines 33.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Booking Class
4 *
5 * @package Timetics
6 */
7
8 namespace Timetics\Core\Bookings;
9
10 defined( 'ABSPATH' ) || exit;
11
12 use Timetics\Core\Appointments\Appointment;
13 use Timetics\Core\Customers\Customer;
14 use Timetics\Core\Integrations\Google\Service\Calendar;
15 use Timetics\Core\Staffs\Staff;
16 use TimeticsEvent\Core\Event_Bookings\Event_Booking;
17 use TimeticsEvent\Core\Events\Event;
18 use WP_Query;
19
20 /**
21 * Class Booking
22 */
23 class Booking {
24 /**
25 * Store booking post type
26 *
27 * @var string
28 */
29 protected $pos_type = 'timetics-booking';
30
31 /**
32 * Store booking meta prefix
33 *
34 * @var string
35 */
36 protected $meta_prefix = '_tt_booking_';
37
38 /**
39 * Store booking id
40 *
41 * @var string
42 */
43 protected $id;
44
45 /**
46 * Store appointment object
47 *
48 * @var Object
49 */
50 protected $appointment;
51
52 /**
53 * Store appointment object
54 *
55 * @var Object
56 */
57 protected $event;
58
59 /**
60 * Store booking data
61 *
62 * @var array
63 */
64 protected $data = [
65
66 'post_status' => '',
67 'customer' => '',
68 'appointment' => '',
69 'appointment_name' => '',
70 'staff' => '',
71 'speaker' => '',
72 'event' => '',
73 'location' => '',
74 'description' => '',
75 'location_type' => '',
76 'payment_method' => '',
77 'payment_status' => '',
78 'payment_details' => '',
79 'order_total' => '',
80 'start_date' => '',
81 'end_date' => '',
82 'start_time' => '',
83 'end_time' => '',
84 'calendar_event' => '',
85 'random_id' => '',
86 'seats' => '',
87 'guests' => '',
88 'zoom' => '',
89 'custom_form_data' => '',
90 'timezone' => '',
91 'recurrence' => [],
92 'parent' => 0,
93 'cancel_reason' => '',
94 'custom_location_url' => '',
95 'security_token' => ''
96 ];
97
98 /**
99 * Booking Constructor
100 *
101 * @return void
102 */
103 public function __construct( $booking = 0 ) {
104 if ( $booking instanceof self ) {
105 $this->set_id( $booking->get_id() );
106 } elseif ( ! empty( $booking->ID ) ) {
107 $this->set_id( $booking->ID );
108 } elseif ( is_numeric( $booking ) && $booking > 0 ) {
109 $this->set_id( $booking );
110 }
111 }
112
113 /**
114 * Get id
115 *
116 * @return integer
117 */
118 public function get_id() {
119 return $this->id;
120 }
121
122 /**
123 * Get customer id
124 *
125 * @return integer
126 */
127 public function get_customer_id() {
128 return $this->get_prop( 'customer' );
129 }
130
131 /**
132 * Get attendee id
133 *
134 * @return integer
135 */
136 public function get_attendees() {
137 $attendees = $this->get_prop( 'attendees' );
138 return $attendees ? $attendees : [];
139 }
140
141 /**
142 * Get custom url
143 *
144 * @return string
145 */
146 public function get_custom_location_url() {
147 $custom_url = $this->get_prop( 'custom_location_url' );
148 return $custom_url ? $custom_url : '';
149 }
150
151 /**
152 * Get staff for the current booking
153 *
154 * @return integer
155 */
156 public function get_staff_id() {
157 return $this->get_prop( 'staff' );
158 }
159
160 /**
161 * Get speaker id
162 *
163 * @return integer
164 */
165 public function get_speaker_id() {
166 return $this->get_prop( 'speaker' );
167 }
168
169 /**
170 * Get booking type
171 *
172 * @return string
173 */
174 public function get_type() {
175 return $this->get_prop( 'type' );
176 }
177
178 /**
179 * Get event id
180 *
181 * @return integer
182 */
183 public function get_event_id() {
184 return $this->get_prop( 'event' );
185 }
186
187 /**
188 * Get customer
189 *
190 * @return User Object
191 */
192 public function get_customer() {
193 return get_userdata( $this->get_customer_id() );
194 }
195
196 /**
197 * Get seats of a booking
198 *
199 * @return array
200 */
201 public function get_seat() {
202 return $this->get_prop( 'seats' );
203 }
204
205 /**
206 * Get guests
207 *
208 * @return array
209 */
210 public function get_guests() {
211 return $this->get_prop( 'guests' );
212 }
213
214 /**
215 * Get custom fields
216 *
217 * @return array
218 */
219 public function get_custom_fields() {
220 return $this->get_prop( 'custom_form_data' );
221 }
222
223 /**
224 * Get billing first name
225 *
226 * @return string
227 */
228 public function get_billing_first_name() {
229 return $this->get_prop( 'first_name' );
230 }
231
232 /**
233 * Get location
234 *
235 * @return string
236 */
237 public function get_location() {
238 return $this->get_prop( 'location' );
239 }
240
241 /**
242 * Get location type
243 *
244 * @return string
245 */
246 public function get_location_type() {
247 return $this->get_prop( 'location_type' );
248 }
249
250 /**
251 * Get booking time
252 *
253 * @return string
254 */
255 public function get_booking_time() {
256 return $this->get_prop( 'booking_time' );
257 }
258
259 /**
260 * Get last name
261 *
262 * @return string
263 */
264 public function get_billing_last_name() {
265 return $this->get_prop( 'last_name' );
266 }
267
268 /**
269 * Get email
270 *
271 * @return string
272 */
273 public function get_billing_email() {
274 return $this->get_prop( 'email' );
275 }
276
277 /**
278 * Get billing phone
279 *
280 * @return string
281 */
282 public function get_billing_phone() {
283 return $this->get_prop( 'phone' );
284 }
285
286 /**
287 * Get timezone
288 *
289 * @return string
290 */
291 public function get_timezone() {
292 return $this->get_prop( 'timezone' );
293 }
294
295 /**
296 * Get description
297 *
298 * @return string
299 */
300 public function get_description() {
301 return $this->get_prop( 'description' );
302 }
303
304 /**
305 * Get billing address
306 *
307 * @param string $adress_type Booking billing address
308 *
309 * @return string
310 */
311 public function get_billing_address( $adress_type = 'address_1' ) {
312 return $this->get_prop( $adress_type );
313 }
314
315 /**
316 * Get billing city
317 *
318 * @return string
319 */
320 public function get_billing_city() {
321 return $this->get_prop( 'city' );
322 }
323
324 /**
325 * Get billing state
326 *
327 * @return string
328 */
329 public function get_billing_state() {
330 return $this->get_prop( 'state' );
331 }
332
333 /**
334 * Get billing post code
335 *
336 * @return string
337 */
338 public function get_billing_post_code() {
339 return $this->get_prop( 'post_code' );
340 }
341
342 /**
343 * Get billing
344 *
345 * @return string
346 */
347 public function get_billing_country() {
348 return $this->get_prop( 'country' );
349 }
350
351 /**
352 * Get payment method
353 *
354 * @return string
355 */
356 public function get_payment_method() {
357 return $this->get_prop( 'payment_method' );
358 }
359
360 /**
361 * Get payment status
362 *
363 * @return string
364 */
365 public function get_payment_status() {
366 return $this->get_prop( 'payment_status' );
367 }
368
369 /**
370 * Get appointment
371 *
372 * @return Appointment
373 */
374 public function get_appointment() {
375 return $this->get_prop( 'appointment' );
376 }
377
378 /**
379 * Get appointment name
380 *
381 * @return string
382 */
383 public function get_appointment_name() {
384 return $this->get_prop( 'appointment_name' );
385 }
386
387 /**
388 * Get calendar event
389 *
390 * @return array
391 */
392 public function get_event() {
393 return $this->get_prop( 'calendar_event' );
394 }
395
396 /**
397 * Get post status
398 *
399 * @return string
400 */
401 public function get_status() {
402 return get_post( $this->id )->post_status;
403 }
404
405 /**
406 * Get total
407 *
408 * @return integer
409 */
410 public function get_total() {
411 return $this->get_prop( 'order_total' );
412 }
413
414 /**
415 * Get start date
416 *
417 * @return string
418 */
419 public function get_start_date() {
420 return $this->get_prop( 'start_date' );
421 }
422 /**
423 * Get start date
424 *
425 * @return string
426 */
427 public function get_date() {
428 return $this->get_prop( 'date' );
429 }
430
431 /**
432 * Get end date
433 *
434 * @return string
435 */
436 public function get_end_date() {
437 return $this->get_prop( 'end_date' );
438 }
439
440 /**
441 * Get start time
442 *
443 * @return string
444 */
445 public function get_start_time() {
446 return $this->get_prop( 'start_time' );
447 }
448
449 /**
450 * Get end time
451 *
452 * @return string
453 */
454 public function get_end_time() {
455 return $this->get_prop( 'end_time' );
456 }
457
458 /**
459 * Get random id for the current booking
460 *
461 * @return string
462 */
463 public function get_random_id() {
464 return $this->get_prop( 'random_id' );
465 }
466
467 /**
468 * Get payment details
469 *
470 * @return array
471 */
472 public function get_payment_details() {
473 return $this->get_prop( 'payment_details' );
474 }
475
476 /**
477 * Get zoom meeting details
478 *
479 * @return array
480 */
481 public function get_zoom() {
482 return $this->get_prop( 'zoom' );
483 }
484
485 /**
486 * Get recurrences
487 *
488 * @return array
489 */
490 public function get_recurrence() {
491 return $this->get_prop( 'recurrence' );
492 }
493
494 /**
495 * Get parent
496 *
497 * @return integer
498 */
499 public function get_parent() {
500 return $this->get_prop( 'parent' );
501 }
502
503 /**
504 * Get booking cancel reason
505 *
506 * @return integer
507 */
508 public function get_cancel_reason() {
509 return $this->get_prop( 'cancel_reason' );
510 }
511
512 /**
513 * Get booking data
514 *
515 * @param string $prop
516 *
517 * @return mixed
518 */
519 private function get_prop( $prop = '' ) {
520 return $this->get_metadata( $prop );
521 }
522
523 /**
524 * Get metadata
525 *
526 * @param string $prop
527 *
528 * @return mixed
529 */
530 private function get_metadata( $prop = '' ) {
531 $meta_key = $this->meta_prefix . $prop;
532
533 return get_post_meta( $this->id, $meta_key, true );
534 }
535
536 /**
537 * Generate random id
538 *
539 * @return string
540 */
541 private function generate_randon_id() {
542 return chr( wp_rand( ord( 'A' ), ord( 'Z' ) ) ) . wp_rand( 575639, 575639 + 400 );
543 }
544
545 // Setter.
546
547 /**
548 * Set booking id
549 *
550 * @param integer $id
551 *
552 * @return void
553 */
554 public function set_id( $id ) {
555 $this->id = $id;
556 }
557
558 /**
559 * Set data
560 *
561 * @param array $args Booking Args
562 *
563 * @return void
564 */
565 public function set_props( $args = [] ) {
566 $this->data = wp_parse_args( $args, $this->data );
567 $this->appointment = new Appointment( $this->data['appointment'] );
568 if ( isset( $this->data['type'] ) && 'timetics-event' == $this->data['type'] ) {
569 $this->event = new Event( $this->data['event'] );
570 }
571
572 }
573
574 /**
575 * Save metada for booking
576 *
577 * @return void
578 */
579 public function save_metadata( $args = [] ) {
580 foreach ( $args as $key => $value ) {
581
582 // If a key not exists on data it will skip to save the data.
583 if ( ! array_key_exists( $key, $this->data ) ) {
584 continue;
585 }
586
587 // Prepare meta key.
588 $meta_key = $this->meta_prefix . $key;
589
590 if ( ! $value ) {
591 $value = $this->get_prop( $key );
592 }
593
594 // Update appointment meta data.
595 update_post_meta( $this->id, $meta_key, $value );
596 }
597 }
598
599 /**
600 * Save appointment
601 *
602 * @return void
603 */
604 public function save( $type = '' ) {
605 $args = [
606 'post_title' => $this->appointment->get_name(),
607 'post_type' => $this->pos_type,
608 'post_status' => $this->data['post_status'],
609 'post_author' => get_current_user_id(),
610 ];
611
612 if ( 'timetics-event' == $type ) {
613 $args = [
614 'post_title' => $this->event->get_name(),
615 'post_type' => $this->pos_type,
616 'post_status' => $this->data['post_status'],
617 'post_author' => get_current_user_id(),
618 ];
619 }
620
621 $updated = false;
622
623 if ( ! empty( $this->id ) ) {
624 $this->send_notification( 'update' );
625 $args['ID'] = $this->id;
626 $updated = true;
627 }
628
629 // Insert or Update appointment.
630 $booking_id = wp_insert_post( $args );
631 $this->data['random_id'] = $this->generate_randon_id();
632 $this->data = apply_filters( 'timetics/event/booking/all_data', $this->data);
633
634 if ( ! is_wp_error( $booking_id ) ) {
635 $this->set_id( $booking_id );
636 $this->save_metadata( $this->data );
637 }
638
639 if ( ! $updated ) {
640 $this->send_notification( 'create' );
641 }
642 }
643
644 /**
645 * Update booking
646 *
647 * @param array $args
648 *
649 * @return bool
650 */
651 public function update( $args = [] ) {
652 $post = get_post( $this->id )->to_array();
653 $args = wp_parse_args( $args, $post );
654
655 $updated = wp_update_post( $args );
656
657 if ( ! is_wp_error( $updated ) ) {
658 $this->save_metadata( $args );
659 }
660
661 return $updated;
662 }
663
664 /**
665 * Find the Booking_Entry (shared per-slot schedule record) this booking occupies.
666 *
667 * @param Appointment|null $meeting Optional pre-built meeting for this booking.
668 * @param string $start_date Slot date. Defaults to the booking's own.
669 * @param string $start_time Slot start. Defaults to the booking's own.
670 * @param string $timezone Timezone of the two above. Defaults to the booking's own.
671 * @return Booking_Entry|null The first matching entry, or null if none.
672 */
673 private function find_slot_entry( $meeting = null, $start_date = '', $start_time = '', $timezone = '' ) {
674 $meeting = $meeting ?: new Appointment( $this->get_appointment() );
675 $booking_entry = new Booking_Entry();
676
677 $start_date = $start_date ?: $this->get_start_date();
678 $start_time = $start_time ?: $this->get_start_time();
679 $timezone = $timezone ?: $this->get_timezone();
680
681 $date_time = timetics_convert_timezone(
682 $start_date . ' ' . $start_time,
683 $timezone,
684 $meeting->get_timezone()
685 );
686
687 $entries = $booking_entry->find(
688 [
689 'staff_id' => $this->get_staff_id(),
690 'meeting_id' => $this->get_appointment(),
691 'date' => $date_time->format( 'Y-m-d' ),
692 'start' => $date_time->format( 'h:i a' ),
693 ]
694 );
695
696 return $entries ? $booking_entry->first() : null;
697 }
698
699 /**
700 * Release the slot held by this booking.
701 *
702 * @return void
703 */
704 public function release_slot() {
705 if ( ! $this->is_booking() ) {
706 return;
707 }
708
709 // Idempotency guard: never release the same booking's slot twice.
710 if ( $this->get_prop( 'slot_released' ) ) {
711 return;
712 }
713
714 $this->release_slot_at( $this->get_start_date(), $this->get_start_time() );
715
716 update_post_meta( $this->id, $this->meta_prefix . 'slot_released', 1 );
717 }
718
719 /**
720 * Release the entry for one named slot of this booking.
721 *
722 * Rescheduling saves the new time first, so the old slot has to be named
723 * rather than read off the booking. No idempotency guard: the booking lives
724 * on and may release more slots as it moves.
725 *
726 * @param string $start_date Slot date, Y-m-d.
727 * @param string $start_time Slot start.
728 * @param string $timezone Timezone of the two above. Defaults to the booking's own.
729 *
730 * @return void
731 */
732 public function release_slot_at( $start_date, $start_time, $timezone = '' ) {
733 if ( ! $this->is_booking() ) {
734 return;
735 }
736
737 $meeting = new Appointment( $this->get_appointment() );
738 $entry = $this->find_slot_entry( $meeting, $start_date, $start_time, $timezone );
739
740 if ( ! $entry ) {
741 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
742 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only diagnostics for a missing booking entry.
743 error_log(
744 sprintf(
745 'Timetics release_slot: no Booking_Entry found for booking #%d ( staff %s, meeting %s, slot %s %s ).',
746 $this->id,
747 $this->get_staff_id(),
748 $this->get_appointment(),
749 $start_date,
750 $start_time
751 )
752 );
753 }
754
755 return;
756 }
757
758 if ( 'one-to-one' === strtolower( $meeting->get_type() ) ) {
759 $entry->delete();
760
761 return;
762 }
763
764 $booked = max( 0, intval( $entry->get_booked() ) - 1 );
765 $booked_seat = ! empty( $this->get_seat() ) ? $this->get_seat() : [];
766 $existing_seat = ! empty( $entry->get_seats() ) ? $entry->get_seats() : [];
767
768 $entry->update(
769 [
770 'booked' => $booked,
771 'seats' => array_values( array_diff( $existing_seat, $booked_seat ) ),
772 ]
773 );
774 }
775
776 /**
777 * Delete booking
778 *
779 * @return bool | WP_Error
780 */
781 public function delete() {
782 // Set action for sending notification.
783 $this->send_notification( 'delete' );
784
785 return wp_delete_post( $this->id, true );
786 }
787
788 /**
789 * Check is a booking or not
790 *
791 * @return bool
792 */
793 public function is_booking() {
794 $post = get_post( $this->id );
795
796 if ( $post && $this->pos_type === $post->post_type ) {
797 return true;
798 }
799
800 return false;
801 }
802
803 /**
804 * Get all bookings
805 *
806 * @param array $args Bookings args
807 *
808 * @return array
809 */
810 public static function all( $args = [] ) {
811
812 $status = [
813 'approved',
814 'pending',
815 'cancel',
816 'completed',
817 ];
818 $defaults = [
819 'post_type' => 'timetics-booking',
820 'posts_per_page' => 20,
821 'post_status' => 'any',
822 'paged' => 1,
823 ];
824
825 $args = wp_parse_args( $args, $defaults );
826
827 $args = apply_filters( 'timetics/admin/bookings/all', $args, $defaults );
828
829 $meta_query = ! empty( $args['meta_query'] ) && is_array( $args['meta_query'] ) ? $args['meta_query'] : [];
830
831 if ( ! empty( $args['start_date'] ) ) {
832 $meta_query[] = [
833 'key' => '_tt_booking_start_date',
834 'value' => $args['start_date'],
835 'compare' => '>=',
836 'type' => 'DATE',
837 ];
838 }
839
840 if ( ! empty( $args['meeting'] ) ) {
841 $meta_query[] = [
842 'key' => '_tt_booking_appointment',
843 'value' => $args['meeting'],
844 'compare' => '=',
845 ];
846 }
847
848 if ( ! empty( $args['staff'] ) ) {
849 $meta_query[] = [
850 'key' => '_tt_booking_staff',
851 'value' => $args['staff'],
852 'compare' => '=',
853 ];
854 }
855
856 if ( ! empty( $meta_query ) ) {
857 if ( count( $meta_query ) > 1 && empty( $meta_query['relation'] ) ) {
858 $meta_query['relation'] = 'AND';
859 }
860 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering bookings
861 $args['meta_query'] = $meta_query;
862 }
863
864 $post = new WP_Query( $args );
865
866 return [
867 'total' => $post->found_posts,
868 'items' => $post->posts,
869 ];
870 }
871
872 /**
873 * Create event after creating a booking
874 *
875 * @return bool
876 */
877 public function create_event() {
878
879 if ( ! timetics_google_setup() ) {
880 return;
881 }
882
883 if ('timetics-event' == $this->get_type()) {
884 Event_Booking::instance()->create_online_event($this->get_id());
885 }else {
886 $this->create_appointment_event();
887 }
888
889 }
890
891 public function create_appointment_event() {
892 // No summary/description passed, so prepare_event() falls back to the
893 // meeting's own name and description. The booking-created email subject
894 // and body used to be reused here, which put the notification copy
895 // ("New meeting scheduled!", greeting, date and time lines) into the
896 // calendar entry instead of the meeting's details.
897 $calendar = new Calendar();
898 $event_data = $this->prepare_event();
899
900 if ( ! $event_data ) {
901 return;
902 }
903
904 $entry = $this->find_slot_entry();
905
906 $event = false;
907
908 if ( $entry ) {
909 $event = $entry->get_google_event();
910
911 if ( ! $event ) {
912 $event = $calendar->create_event( $event_data );
913 $entry->update( [
914 'google_event' => $event,
915 ] );
916 }
917 }
918
919 if ( $event ) {
920 update_post_meta( $this->id, $this->meta_prefix . 'calendar_event', $event );
921
922 // Also record the bare event id. Google_Calendar_Sync uses this meta
923 // to recognise events Timetics itself created, so that they are not
924 // pulled back in as external events and used to block their own slot.
925 if ( ! empty( $event['id'] ) ) {
926 $this->set_google_event_id( $event['id'] );
927 }
928 }
929 }
930
931 /**
932 * Update calendar event
933 *
934 * @return bool
935 */
936 public function update_event() {
937 if ( ! timetics_google_setup() ) {
938 return;
939 }
940
941 // Same as create_appointment_event(): the meeting's name and description
942 // belong in the calendar entry, not the reschedule email copy. Keeping
943 // them consistent also stops a reschedule from rewriting the title.
944 $calendar = new Calendar();
945 $event_data = $this->prepare_event();
946 $calendar_event = $this->get_event();
947
948 if ( ! is_array( $calendar_event ) ) {
949 return;
950 }
951
952 if ( ! empty( $calendar_event['id'] ) ) {
953 // Update calendar event.
954 $event = $calendar->update_event( $calendar_event['id'], $event_data );
955
956 // update calendar event data.
957 update_post_meta( $this->id, $this->meta_prefix . 'calendar_event', $event );
958 }
959 }
960
961 /**
962 * Delete calendar event
963 *
964 * @return
965 */
966 public function delete_event() {
967 if ( ! timetics_google_setup() ) {
968 return;
969 }
970
971 $calendar = new Calendar();
972 $calendar_event = $this->get_event();
973 $access_token = timetics_get_google_access_token( $this->get_staff_id() );
974
975 if ( ! is_array( $calendar_event ) ) {
976 return;
977 }
978
979 if ( ! $access_token ) {
980 return;
981 }
982
983 if ( ! empty( $calendar_event['id'] ) ) {
984 // Update calendar event.
985 $event = $calendar->delete_event( $calendar_event['id'], $access_token );
986
987 // update calendar event data.
988 update_post_meta( $this->id, $this->meta_prefix . 'calendar_event', $event );
989
990 // The event no longer exists in Google, so drop the id used by
991 // Google_Calendar_Sync to skip Timetics-created events.
992 $this->set_google_event_id( '' );
993 }
994 }
995
996 /**
997 * Prepare event data
998 *
999 * @return array
1000 */
1001 private function prepare_event( $data = [] ) {
1002
1003 $meeting_id = $this->get_appointment();
1004 $staff_id = $this->get_staff_id();
1005 $customer_id = $this->get_customer_id();
1006 $location_type = $this->get_location_type();
1007 $access_token = timetics_get_google_access_token( $staff_id );
1008
1009 if ( ! $access_token ) {
1010 return false;
1011 }
1012
1013 $meeting = new Appointment( $meeting_id );
1014 $staff = new Staff( $staff_id );
1015 $customer = new Customer( $customer_id );
1016
1017 $time = gmdate( 'h:i a', strtotime( $this->get_start_time() ) );
1018 $date = gmdate( 'd F Y', strtotime( $this->get_start_date() ) );
1019 $location = $this->get_location();
1020
1021 $placeholders = [
1022 '{%meeting_title%}' => $meeting->get_name(),
1023 '{%meeting_date%}' => $date,
1024 '{%meeting_time%}' => $time,
1025 '{%meeting_location%}' => $location,
1026 '{%meeting_duration%}' => $meeting->get_duration(),
1027 '{%host_name%}' => $staff->get_display_name(),
1028 '{%host_email%}' => $staff->get_email(),
1029 '{%customer_name%}' => $customer->get_display_name(),
1030 '{%customer_email%}' => $customer->get_email(),
1031 ];
1032
1033 $summary = ! empty( $data['summary'] ) ? timetics_replace_placeholder( $data['summary'], $placeholders ) : $meeting->get_name();
1034 $description = ! empty( $data['description'] ) ? timetics_replace_placeholder( $data['description'], $placeholders ) : $meeting->get_description();
1035
1036 $description = apply_filters( 'timetics_booking_event_data', $description, $summary, $this );
1037
1038 $event = [
1039 'summary' => $summary,
1040 'description' => $description,
1041 'start' => [
1042 'date' => $this->get_start_date(),
1043 'time' => $this->get_start_time(),
1044 ],
1045 'end' => [
1046 'date' => $this->get_end_date(),
1047 'time' => $this->get_end_time(),
1048 ],
1049 'attendees' => [
1050 ['email' => $staff->get_email()],
1051 ['email' => $customer->get_email()],
1052 ],
1053 'timezone' => $this->get_timezone(),
1054 'google_meet' => 'google-meet' === $location_type,
1055 'access_token' => $access_token,
1056 ];
1057
1058 return $event;
1059 }
1060
1061 /**
1062 * Get all data of an object
1063 *
1064 * @return array
1065 */
1066 public function to_array() {
1067 $data = [];
1068
1069 foreach ( $this->data as $key => $value ) {
1070
1071 $data[$key] = $this->get_prop( $key );
1072 }
1073
1074 return $data;
1075 }
1076
1077 /**
1078 * Send notification after booking
1079 *
1080 * @return void
1081 */
1082 private function send_notification( $action ) {
1083
1084 do_action( 'timetics_booking_notification', $this, $action );
1085 }
1086
1087 /**
1088 * Get booking ids by appointment id
1089 *
1090 * @param array Collection of appointment ids
1091 *
1092 * @return array Collection of booking ids
1093 */
1094 public static function get_booking_ids_by_appointment( $appointment_ids, $per_page = -1 ) {
1095 if ( empty( $appointment_ids ) ) {
1096 return [];
1097 }
1098
1099 $query = new WP_Query([
1100 'post_type' => 'timetics-booking',
1101 'post_status' => 'any',
1102 'posts_per_page' => $per_page,
1103 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering bookings by appointment
1104 'meta_query' => [
1105 [
1106 'key' => '_tt_booking_appointment',
1107 'value' => $appointment_ids,
1108 'compare' => 'IN',
1109 ],
1110 ],
1111 ]);
1112
1113 return [
1114 'total' => $query->found_posts,
1115 'items' => $query->posts,
1116 ];
1117 }
1118
1119 /**
1120 * Get booking ids visible to a given user.
1121 *
1122 * Union of:
1123 * - bookings where the user is the assigned staff (`_tt_booking_staff`)
1124 * - bookings whose appointment is authored by the user
1125 *
1126 * @param integer $user_id WP user id.
1127 *
1128 * @return int[] Deduped list of booking post ids.
1129 */
1130 public static function get_visible_ids_for_user( $user_id ) {
1131 $user_id = (int) $user_id;
1132
1133 if ( ! $user_id ) {
1134 return [];
1135 }
1136
1137 $staff_ids = get_posts( [
1138 'post_type' => 'timetics-booking',
1139 'post_status' => 'any',
1140 'posts_per_page' => -1,
1141 'fields' => 'ids',
1142 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- needed to scope bookings to current staff user
1143 'meta_query' => [
1144 [
1145 'key' => '_tt_booking_staff',
1146 'value' => $user_id,
1147 ],
1148 ],
1149 ] );
1150
1151 $appointment_ids = Appointment::get_appointment_ids_by_author( $user_id );
1152
1153 $appointment_booking_ids = [];
1154 if ( ! empty( $appointment_ids ) ) {
1155 $appointment_booking_ids = get_posts( [
1156 'post_type' => 'timetics-booking',
1157 'post_status' => 'any',
1158 'posts_per_page' => -1,
1159 'fields' => 'ids',
1160 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- needed to scope bookings to appointments owned by current user
1161 'meta_query' => [
1162 [
1163 'key' => '_tt_booking_appointment',
1164 'value' => $appointment_ids,
1165 'compare' => 'IN',
1166 ],
1167 ],
1168 ] );
1169 }
1170
1171 return array_values( array_unique( array_map( 'intval', array_merge( $staff_ids, $appointment_booking_ids ) ) ) );
1172 }
1173
1174 /**
1175 * Get Google Calendar Event ID for this booking
1176 *
1177 * @return string
1178 */
1179 public function get_google_event_id() {
1180 return $this->get_metadata( 'google_event_id' );
1181 }
1182
1183 /**
1184 * Set Google Calendar Event ID for this booking
1185 *
1186 * @param string $event_id
1187 * @return bool
1188 */
1189 public function set_google_event_id( $event_id ) {
1190 // Written directly rather than through save_metadata(), which takes an
1191 // array and only accepts keys declared in $this->data — neither is true
1192 // here, so it silently discarded every write.
1193 $meta_key = $this->meta_prefix . 'google_event_id';
1194
1195 if ( ! $event_id ) {
1196 return delete_post_meta( $this->id, $meta_key );
1197 }
1198
1199 return update_post_meta( $this->id, $meta_key, $event_id );
1200 }
1201
1202 public function set_sync_status( $status ) {
1203 $meta_key = $this->meta_prefix . 'google_calendar_sync_status';
1204
1205 if ( ! $status ) {
1206 return delete_post_meta( $this->id, $meta_key );
1207 }
1208
1209 return update_post_meta( $this->id, $meta_key, $status );
1210 }
1211
1212 public function get_sync_status() {
1213 return $this->get_metadata( 'google_calendar_sync_status' );
1214 }
1215 /**
1216 * Get all Google Event IDs for all bookings
1217 *
1218 * @return array
1219 */
1220 public function get_all_google_event_ids() {
1221 $meta_key = $this->meta_prefix . 'google_event_id';
1222
1223 $posts = get_posts(
1224 array(
1225 'post_type' => 'any',
1226 'posts_per_page' => -1,
1227 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering posts by meta key
1228 'meta_query' => array(
1229 array(
1230 'key' => $meta_key,
1231 'value' => '',
1232 'compare' => '!=',
1233 ),
1234 ),
1235 'fields' => 'ids',
1236 )
1237 );
1238
1239 if ( empty( $posts ) ) {
1240 return array();
1241 }
1242
1243 $event_ids = array();
1244 foreach ( $posts as $post_id ) {
1245 $event_id = get_post_meta( $post_id, $meta_key, true );
1246 if ( ! empty( $event_id ) ) {
1247 $event_ids[] = $event_id;
1248 }
1249 }
1250
1251 return $event_ids;
1252 }
1253
1254 /**
1255 * Get name.
1256 *
1257 * @since 1.0.0
1258 *
1259 * @return string
1260 */
1261 public function get_security_token() {
1262 return $this->get_prop( 'security_token' );
1263 }
1264
1265 /**
1266 * Generate and store a secure reschedule token for a booking
1267 *
1268 * @param int $booking_id
1269 * @return string The generated token
1270 */
1271 public function generate_security_token() {
1272 $token = bin2hex(random_bytes(4)); // 8 hex chars
1273 return $token;
1274 }
1275
1276 /**
1277 * Rotate the booking's security token so the previously-issued one cannot
1278 * be reused (e.g. after a payment is approved).
1279 *
1280 * @return void
1281 */
1282 public function rotate_security_token() {
1283 $meta_key = $this->meta_prefix . 'security_token';
1284 $new_token = $this->generate_security_token();
1285 update_post_meta( $this->id, $meta_key, $new_token );
1286 }
1287
1288 /**
1289 * Get the Stripe PaymentIntent id bound to this booking, if any.
1290 *
1291 * @return string
1292 */
1293 public function get_stripe_payment_intent_id() {
1294 return (string) get_post_meta( $this->id, '_tt_stripe_payment_intent_id', true );
1295 }
1296
1297 /**
1298 * Bind a Stripe PaymentIntent id to this booking. Used for replay
1299 * protection: a second make_payment call with a different intent will be
1300 * rejected.
1301 *
1302 * @param string $intent_id
1303 * @return void
1304 */
1305 public function set_stripe_payment_intent_id( $intent_id ) {
1306 update_post_meta( $this->id, '_tt_stripe_payment_intent_id', sanitize_text_field( (string) $intent_id ) );
1307 }
1308 }
1309