PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.38
Timetics – Appointment Booking Calendar & Scheduling v1.0.38
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
← All changes | core/bookings/booking.php +57 -281 1.0.62 → 1.0.38 View file →
@@ -6,10 +6,8 @@
6 6 */
7 7
8 8 namespace Timetics\Core\Bookings;
9 9
10 -defined( 'ABSPATH' ) || exit;
11 -
12 10 use Timetics\Core\Appointments\Appointment;
13 11 use Timetics\Core\Customers\Customer;
14 12 use Timetics\Core\Integrations\Google\Service\Calendar;
15 13 use Timetics\Core\Staffs\Staff;
@@ -357,17 +355,8 @@
357 355 return $this->get_prop( 'payment_method' );
358 356 }
359 357
360 358 /**
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 359 * Get appointment
371 360 *
372 361 * @return Appointment
373 362 */
@@ -538,9 +527,9 @@
538 527 *
539 528 * @return string
540 529 */
541 530 private function generate_randon_id() {
542 - return chr( wp_rand( ord( 'A' ), ord( 'Z' ) ) ) . wp_rand( 575639, 575639 + 400 );
531 + return chr( rand( ord( 'A' ), ord( 'Z' ) ) ) . rand( 575639, 575639 + 400 );
543 532 }
544 533
545 534 // Setter.
546 535
@@ -661,120 +650,8 @@
661 650 return $updated;
662 651 }
663 652
664 653 /**
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 654 * Delete booking
778 655 *
779 656 * @return bool | WP_Error
780 657 */
@@ -823,45 +700,46 @@
823 700 ];
824 701
825 702 $args = wp_parse_args( $args, $defaults );
826 703
827 - $args = apply_filters( 'timetics/admin/bookings/all', $args, $defaults );
704 + $args = apply_filters( 'timetics/admin/bookings/all', $defaults, $args);
828 705
829 - $meta_query = ! empty( $args['meta_query'] ) && is_array( $args['meta_query'] ) ? $args['meta_query'] : [];
830 -
831 706 if ( ! empty( $args['start_date'] ) ) {
832 - $meta_query[] = [
833 - 'key' => '_tt_booking_start_date',
834 - 'value' => $args['start_date'],
835 - 'compare' => '>=',
836 - 'type' => 'DATE',
707 + //_tt_booking_start_date
708 + $args['meta_query'] = [
709 + [
710 + 'key' => '_tt_booking_start_date',
711 + 'value' => $args['start_date'],
712 + 'compare' => '>=',
713 + 'type' => 'DATE',
714 + ],
837 715 ];
838 716 }
839 717
840 718 if ( ! empty( $args['meeting'] ) ) {
841 - $meta_query[] = [
842 - 'key' => '_tt_booking_appointment',
843 - 'value' => $args['meeting'],
844 - 'compare' => '=',
719 + // @codingStandardsIgnoreStart
720 + $args['meta_query'] = [
721 + [
722 + 'key' => '_tt_booking_appointment',
723 + 'value' => $args['meeting'],
724 + 'compare' => '=',
725 + ],
845 726 ];
727 + // @codingStandardsIgnoreEnd
846 728 }
847 729
848 730 if ( ! empty( $args['staff'] ) ) {
849 - $meta_query[] = [
850 - 'key' => '_tt_booking_staff',
851 - 'value' => $args['staff'],
852 - 'compare' => '=',
731 + // @codingStandardsIgnoreStart
732 + $args['meta_query'] = [
733 + [
734 + 'key' => '_tt_booking_staff',
735 + 'value' => $args['staff'],
736 + 'compare' => '=',
737 + ],
853 738 ];
739 + // @codingStandardsIgnoreEnd
854 740 }
855 741
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 742 $post = new WP_Query( $args );
865 743
866 744 return [
867 745 'total' => $post->found_posts,
@@ -888,25 +766,37 @@
888 766
889 767 }
890 768
891 769 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.
770 + $data = [
771 + 'summary' => timetics_get_option( 'booking_created_customer_email_title' ),
772 + 'description' => timetics_get_option( 'booking_created_customer_email_body' ),
773 + ];
774 +
897 775 $calendar = new Calendar();
898 - $event_data = $this->prepare_event();
776 + $event_data = $this->prepare_event( $data );
899 777
900 778 if ( ! $event_data ) {
901 779 return;
902 780 }
903 781
904 - $entry = $this->find_slot_entry();
782 + $booking_entry = new Booking_Entry();
783 + $meeting = new Appointment( $this->get_appointment() );
905 784
906 - $event = false;
785 + $date_time = timetics_convert_timezone( $this->get_start_date() .' '. $this->get_start_time(), $this->get_timezone(), $meeting->get_timezone() );
907 786
908 - if ( $entry ) {
787 + $entries = $booking_entry->find(
788 + [
789 + 'staff_id' => $this->get_staff_id(),
790 + 'meeting_id' => $this->get_appointment(),
791 + 'date' => $date_time->format('Y-m-d'),
792 + 'start' => $date_time->format('h:i a'),
793 + ]
794 + );
795 +
796 + if ( $entries ) {
797 + $entry = $booking_entry->first();
798 +
909 799 $event = $entry->get_google_event();
910 800
911 801 if ( ! $event ) {
912 802 $event = $calendar->create_event( $event_data );
@@ -917,15 +807,8 @@
917 807 }
918 808
919 809 if ( $event ) {
920 810 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 811 }
929 812 }
930 813
931 814 /**
@@ -937,13 +820,15 @@
937 820 if ( ! timetics_google_setup() ) {
938 821 return;
939 822 }
940 823
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.
824 + $data = [
825 + 'summary' => timetics_get_option( 'booking_rescheduled_customer_email_title' ),
826 + 'description' => timetics_get_option( 'booking_rescheduled_customer_email_body' ),
827 + ];
828 +
944 829 $calendar = new Calendar();
945 - $event_data = $this->prepare_event();
830 + $event_data = $this->prepare_event( $data );
946 831 $calendar_event = $this->get_event();
947 832
948 833 if ( ! is_array( $calendar_event ) ) {
949 834 return;
@@ -985,12 +870,8 @@
985 870 $event = $calendar->delete_event( $calendar_event['id'], $access_token );
986 871
987 872 // update calendar event data.
988 873 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 874 }
994 875 }
995 876
996 877 /**
@@ -1099,9 +980,8 @@
1099 980 $query = new WP_Query([
1100 981 'post_type' => 'timetics-booking',
1101 982 'post_status' => 'any',
1102 983 '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 984 'meta_query' => [
1105 985 [
1106 986 'key' => '_tt_booking_appointment',
1107 987 'value' => $appointment_ids,
@@ -1116,63 +996,8 @@
1116 996 ];
1117 997 }
1118 998
1119 999 /**
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 1000 * Get Google Calendar Event ID for this booking
1176 1001 *
1177 1002 * @return string
1178 1003 */
@@ -1186,28 +1011,13 @@
1186 1011 * @param string $event_id
1187 1012 * @return bool
1188 1013 */
1189 1014 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 );
1015 + return $this->save_metadata( 'google_event_id', $event_id );
1200 1016 }
1201 1017
1202 1018 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 );
1019 + return $this->save_metadata( 'google_calendar_sync_status', $status );
1210 1020 }
1211 1021
1212 1022 public function get_sync_status() {
1213 1023 return $this->get_metadata( 'google_calendar_sync_status' );
@@ -1218,14 +1028,13 @@
1218 1028 * @return array
1219 1029 */
1220 1030 public function get_all_google_event_ids() {
1221 1031 $meta_key = $this->meta_prefix . 'google_event_id';
1222 -
1032 +
1223 1033 $posts = get_posts(
1224 1034 array(
1225 1035 'post_type' => 'any',
1226 1036 '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 1037 'meta_query' => array(
1229 1038 array(
1230 1039 'key' => $meta_key,
1231 1040 'value' => '',
@@ -1259,9 +1068,9 @@
1259 1068 * @return string
1260 1069 */
1261 1070 public function get_security_token() {
1262 1071 return $this->get_prop( 'security_token' );
1263 - }
1072 + }
1264 1073
1265 1074 /**
1266 1075 * Generate and store a secure reschedule token for a booking
1267 1076 *
@@ -1270,39 +1079,6 @@
1270 1079 */
1271 1080 public function generate_security_token() {
1272 1081 $token = bin2hex(random_bytes(4)); // 8 hex chars
1273 1082 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 1083 }
1308 1084 }