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 -351 1.0.63 → 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,190 +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 - * Take back the slot this booking released.
721 - *
722 - * A declined payment marks the booking failed and releases its slot, so an
723 - * attempt the customer walks away from does not block the time forever. When
724 - * the customer retries on that same booking the slot has to be taken back
725 - * before any money moves, otherwise the booking finalizes while the slot still
726 - * reads as free and the next customer can book straight over it.
727 - *
728 - * @return bool True when the slot is held, false when it is no longer free.
729 - */
730 - public function reserve_slot() {
731 - if ( ! $this->is_booking() ) {
732 - return false;
733 - }
734 -
735 - // Nothing was released, so the slot is still held from booking creation.
736 - if ( ! $this->get_prop( 'slot_released' ) ) {
737 - return true;
738 - }
739 -
740 - $meeting = new Appointment( $this->get_appointment() );
741 - $entry = $this->find_slot_entry( $meeting );
742 - $seats = ! empty( $this->get_seat() ) ? (array) $this->get_seat() : [];
743 - $taking = max( 1, count( $seats ) );
744 -
745 - if ( $entry ) {
746 - $booked = intval( $entry->get_booked() ) + $taking;
747 -
748 - // Someone else took the time while this booking was in the failed state.
749 - if ( $booked > $meeting->get_effective_capacity() ) {
750 - return false;
751 - }
752 -
753 - $existing = ! empty( $entry->get_seats() ) ? (array) $entry->get_seats() : [];
754 -
755 - $entry->update(
756 - [
757 - 'booked' => $booked,
758 - 'seats' => array_values( array_unique( array_merge( $existing, $seats ) ) ),
759 - ]
760 - );
761 - } else {
762 - // A one-to-one release deletes the entry outright, so recreate it. Entries
763 - // are stored in the meeting's timezone, the booking's in the customer's.
764 - $start = timetics_convert_timezone( $this->get_start_date() . ' ' . $this->get_start_time(), $this->get_timezone(), $meeting->get_timezone() );
765 - $end = timetics_convert_timezone( $this->get_start_date() . ' ' . $this->get_end_time(), $this->get_timezone(), $meeting->get_timezone() );
766 -
767 - $booking_entry = new Booking_Entry();
768 -
769 - $booking_entry->create(
770 - [
771 - 'meeting_id' => $meeting->get_id(),
772 - 'staff_id' => $this->get_staff_id(),
773 - 'customer_id' => $this->get_customer(),
774 - 'booking_id' => $this->id,
775 - 'booked' => $taking,
776 - 'date' => $start->format( 'Y-m-d' ),
777 - 'start' => $start->format( 'h:i a' ),
778 - 'end' => $end->format( 'h:i a' ),
779 - 'seats' => $seats,
780 - ]
781 - );
782 - }
783 -
784 - delete_post_meta( $this->id, $this->meta_prefix . 'slot_released' );
785 -
786 - return true;
787 - }
788 -
789 - /**
790 - * Release the entry for one named slot of this booking.
791 - *
792 - * Rescheduling saves the new time first, so the old slot has to be named
793 - * rather than read off the booking. No idempotency guard: the booking lives
794 - * on and may release more slots as it moves.
795 - *
796 - * @param string $start_date Slot date, Y-m-d.
797 - * @param string $start_time Slot start.
798 - * @param string $timezone Timezone of the two above. Defaults to the booking's own.
799 - *
800 - * @return void
801 - */
802 - public function release_slot_at( $start_date, $start_time, $timezone = '' ) {
803 - if ( ! $this->is_booking() ) {
804 - return;
805 - }
806 -
807 - $meeting = new Appointment( $this->get_appointment() );
808 - $entry = $this->find_slot_entry( $meeting, $start_date, $start_time, $timezone );
809 -
810 - if ( ! $entry ) {
811 - if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
812 - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only diagnostics for a missing booking entry.
813 - error_log(
814 - sprintf(
815 - 'Timetics release_slot: no Booking_Entry found for booking #%d ( staff %s, meeting %s, slot %s %s ).',
816 - $this->id,
817 - $this->get_staff_id(),
818 - $this->get_appointment(),
819 - $start_date,
820 - $start_time
821 - )
822 - );
823 - }
824 -
825 - return;
826 - }
827 -
828 - if ( 'one-to-one' === strtolower( $meeting->get_type() ) ) {
829 - $entry->delete();
830 -
831 - return;
832 - }
833 -
834 - $booked = max( 0, intval( $entry->get_booked() ) - 1 );
835 - $booked_seat = ! empty( $this->get_seat() ) ? $this->get_seat() : [];
836 - $existing_seat = ! empty( $entry->get_seats() ) ? $entry->get_seats() : [];
837 -
838 - $entry->update(
839 - [
840 - 'booked' => $booked,
841 - 'seats' => array_values( array_diff( $existing_seat, $booked_seat ) ),
842 - ]
843 - );
844 - }
845 -
846 - /**
847 654 * Delete booking
848 655 *
849 656 * @return bool | WP_Error
850 657 */
@@ -893,45 +700,46 @@
893 700 ];
894 701
895 702 $args = wp_parse_args( $args, $defaults );
896 703
897 - $args = apply_filters( 'timetics/admin/bookings/all', $args, $defaults );
704 + $args = apply_filters( 'timetics/admin/bookings/all', $defaults, $args);
898 705
899 - $meta_query = ! empty( $args['meta_query'] ) && is_array( $args['meta_query'] ) ? $args['meta_query'] : [];
900 -
901 706 if ( ! empty( $args['start_date'] ) ) {
902 - $meta_query[] = [
903 - 'key' => '_tt_booking_start_date',
904 - 'value' => $args['start_date'],
905 - 'compare' => '>=',
906 - '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 + ],
907 715 ];
908 716 }
909 717
910 718 if ( ! empty( $args['meeting'] ) ) {
911 - $meta_query[] = [
912 - 'key' => '_tt_booking_appointment',
913 - 'value' => $args['meeting'],
914 - 'compare' => '=',
719 + // @codingStandardsIgnoreStart
720 + $args['meta_query'] = [
721 + [
722 + 'key' => '_tt_booking_appointment',
723 + 'value' => $args['meeting'],
724 + 'compare' => '=',
725 + ],
915 726 ];
727 + // @codingStandardsIgnoreEnd
916 728 }
917 729
918 730 if ( ! empty( $args['staff'] ) ) {
919 - $meta_query[] = [
920 - 'key' => '_tt_booking_staff',
921 - 'value' => $args['staff'],
922 - 'compare' => '=',
731 + // @codingStandardsIgnoreStart
732 + $args['meta_query'] = [
733 + [
734 + 'key' => '_tt_booking_staff',
735 + 'value' => $args['staff'],
736 + 'compare' => '=',
737 + ],
923 738 ];
739 + // @codingStandardsIgnoreEnd
924 740 }
925 741
926 - if ( ! empty( $meta_query ) ) {
927 - if ( count( $meta_query ) > 1 && empty( $meta_query['relation'] ) ) {
928 - $meta_query['relation'] = 'AND';
929 - }
930 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering bookings
931 - $args['meta_query'] = $meta_query;
932 - }
933 -
934 742 $post = new WP_Query( $args );
935 743
936 744 return [
937 745 'total' => $post->found_posts,
@@ -958,25 +766,37 @@
958 766
959 767 }
960 768
961 769 public function create_appointment_event() {
962 - // No summary/description passed, so prepare_event() falls back to the
963 - // meeting's own name and description. The booking-created email subject
964 - // and body used to be reused here, which put the notification copy
965 - // ("New meeting scheduled!", greeting, date and time lines) into the
966 - // 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 +
967 775 $calendar = new Calendar();
968 - $event_data = $this->prepare_event();
776 + $event_data = $this->prepare_event( $data );
969 777
970 778 if ( ! $event_data ) {
971 779 return;
972 780 }
973 781
974 - $entry = $this->find_slot_entry();
782 + $booking_entry = new Booking_Entry();
783 + $meeting = new Appointment( $this->get_appointment() );
975 784
976 - $event = false;
785 + $date_time = timetics_convert_timezone( $this->get_start_date() .' '. $this->get_start_time(), $this->get_timezone(), $meeting->get_timezone() );
977 786
978 - 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 +
979 799 $event = $entry->get_google_event();
980 800
981 801 if ( ! $event ) {
982 802 $event = $calendar->create_event( $event_data );
@@ -987,15 +807,8 @@
987 807 }
988 808
989 809 if ( $event ) {
990 810 update_post_meta( $this->id, $this->meta_prefix . 'calendar_event', $event );
991 -
992 - // Also record the bare event id. Google_Calendar_Sync uses this meta
993 - // to recognise events Timetics itself created, so that they are not
994 - // pulled back in as external events and used to block their own slot.
995 - if ( ! empty( $event['id'] ) ) {
996 - $this->set_google_event_id( $event['id'] );
997 - }
998 811 }
999 812 }
1000 813
1001 814 /**
@@ -1007,13 +820,15 @@
1007 820 if ( ! timetics_google_setup() ) {
1008 821 return;
1009 822 }
1010 823
1011 - // Same as create_appointment_event(): the meeting's name and description
1012 - // belong in the calendar entry, not the reschedule email copy. Keeping
1013 - // 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 +
1014 829 $calendar = new Calendar();
1015 - $event_data = $this->prepare_event();
830 + $event_data = $this->prepare_event( $data );
1016 831 $calendar_event = $this->get_event();
1017 832
1018 833 if ( ! is_array( $calendar_event ) ) {
1019 834 return;
@@ -1055,12 +870,8 @@
1055 870 $event = $calendar->delete_event( $calendar_event['id'], $access_token );
1056 871
1057 872 // update calendar event data.
1058 873 update_post_meta( $this->id, $this->meta_prefix . 'calendar_event', $event );
1059 -
1060 - // The event no longer exists in Google, so drop the id used by
1061 - // Google_Calendar_Sync to skip Timetics-created events.
1062 - $this->set_google_event_id( '' );
1063 874 }
1064 875 }
1065 876
1066 877 /**
@@ -1169,9 +980,8 @@
1169 980 $query = new WP_Query([
1170 981 'post_type' => 'timetics-booking',
1171 982 'post_status' => 'any',
1172 983 'posts_per_page' => $per_page,
1173 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering bookings by appointment
1174 984 'meta_query' => [
1175 985 [
1176 986 'key' => '_tt_booking_appointment',
1177 987 'value' => $appointment_ids,
@@ -1186,63 +996,8 @@
1186 996 ];
1187 997 }
1188 998
1189 999 /**
1190 - * Get booking ids visible to a given user.
1191 - *
1192 - * Union of:
1193 - * - bookings where the user is the assigned staff (`_tt_booking_staff`)
1194 - * - bookings whose appointment is authored by the user
1195 - *
1196 - * @param integer $user_id WP user id.
1197 - *
1198 - * @return int[] Deduped list of booking post ids.
1199 - */
1200 - public static function get_visible_ids_for_user( $user_id ) {
1201 - $user_id = (int) $user_id;
1202 -
1203 - if ( ! $user_id ) {
1204 - return [];
1205 - }
1206 -
1207 - $staff_ids = get_posts( [
1208 - 'post_type' => 'timetics-booking',
1209 - 'post_status' => 'any',
1210 - 'posts_per_page' => -1,
1211 - 'fields' => 'ids',
1212 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- needed to scope bookings to current staff user
1213 - 'meta_query' => [
1214 - [
1215 - 'key' => '_tt_booking_staff',
1216 - 'value' => $user_id,
1217 - ],
1218 - ],
1219 - ] );
1220 -
1221 - $appointment_ids = Appointment::get_appointment_ids_by_author( $user_id );
1222 -
1223 - $appointment_booking_ids = [];
1224 - if ( ! empty( $appointment_ids ) ) {
1225 - $appointment_booking_ids = get_posts( [
1226 - 'post_type' => 'timetics-booking',
1227 - 'post_status' => 'any',
1228 - 'posts_per_page' => -1,
1229 - 'fields' => 'ids',
1230 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- needed to scope bookings to appointments owned by current user
1231 - 'meta_query' => [
1232 - [
1233 - 'key' => '_tt_booking_appointment',
1234 - 'value' => $appointment_ids,
1235 - 'compare' => 'IN',
1236 - ],
1237 - ],
1238 - ] );
1239 - }
1240 -
1241 - return array_values( array_unique( array_map( 'intval', array_merge( $staff_ids, $appointment_booking_ids ) ) ) );
1242 - }
1243 -
1244 - /**
1245 1000 * Get Google Calendar Event ID for this booking
1246 1001 *
1247 1002 * @return string
1248 1003 */
@@ -1256,28 +1011,13 @@
1256 1011 * @param string $event_id
1257 1012 * @return bool
1258 1013 */
1259 1014 public function set_google_event_id( $event_id ) {
1260 - // Written directly rather than through save_metadata(), which takes an
1261 - // array and only accepts keys declared in $this->data — neither is true
1262 - // here, so it silently discarded every write.
1263 - $meta_key = $this->meta_prefix . 'google_event_id';
1264 -
1265 - if ( ! $event_id ) {
1266 - return delete_post_meta( $this->id, $meta_key );
1267 - }
1268 -
1269 - return update_post_meta( $this->id, $meta_key, $event_id );
1015 + return $this->save_metadata( 'google_event_id', $event_id );
1270 1016 }
1271 1017
1272 1018 public function set_sync_status( $status ) {
1273 - $meta_key = $this->meta_prefix . 'google_calendar_sync_status';
1274 -
1275 - if ( ! $status ) {
1276 - return delete_post_meta( $this->id, $meta_key );
1277 - }
1278 -
1279 - return update_post_meta( $this->id, $meta_key, $status );
1019 + return $this->save_metadata( 'google_calendar_sync_status', $status );
1280 1020 }
1281 1021
1282 1022 public function get_sync_status() {
1283 1023 return $this->get_metadata( 'google_calendar_sync_status' );
@@ -1288,14 +1028,13 @@
1288 1028 * @return array
1289 1029 */
1290 1030 public function get_all_google_event_ids() {
1291 1031 $meta_key = $this->meta_prefix . 'google_event_id';
1292 -
1032 +
1293 1033 $posts = get_posts(
1294 1034 array(
1295 1035 'post_type' => 'any',
1296 1036 'posts_per_page' => -1,
1297 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering posts by meta key
1298 1037 'meta_query' => array(
1299 1038 array(
1300 1039 'key' => $meta_key,
1301 1040 'value' => '',
@@ -1329,9 +1068,9 @@
1329 1068 * @return string
1330 1069 */
1331 1070 public function get_security_token() {
1332 1071 return $this->get_prop( 'security_token' );
1333 - }
1072 + }
1334 1073
1335 1074 /**
1336 1075 * Generate and store a secure reschedule token for a booking
1337 1076 *
@@ -1340,39 +1079,6 @@
1340 1079 */
1341 1080 public function generate_security_token() {
1342 1081 $token = bin2hex(random_bytes(4)); // 8 hex chars
1343 1082 return $token;
1344 - }
1345 -
1346 - /**
1347 - * Rotate the booking's security token so the previously-issued one cannot
1348 - * be reused (e.g. after a payment is approved).
1349 - *
1350 - * @return void
1351 - */
1352 - public function rotate_security_token() {
1353 - $meta_key = $this->meta_prefix . 'security_token';
1354 - $new_token = $this->generate_security_token();
1355 - update_post_meta( $this->id, $meta_key, $new_token );
1356 - }
1357 -
1358 - /**
1359 - * Get the Stripe PaymentIntent id bound to this booking, if any.
1360 - *
1361 - * @return string
1362 - */
1363 - public function get_stripe_payment_intent_id() {
1364 - return (string) get_post_meta( $this->id, '_tt_stripe_payment_intent_id', true );
1365 - }
1366 -
1367 - /**
1368 - * Bind a Stripe PaymentIntent id to this booking. Used for replay
1369 - * protection: a second make_payment call with a different intent will be
1370 - * rejected.
1371 - *
1372 - * @param string $intent_id
1373 - * @return void
1374 - */
1375 - public function set_stripe_payment_intent_id( $intent_id ) {
1376 - update_post_meta( $this->id, '_tt_stripe_payment_intent_id', sanitize_text_field( (string) $intent_id ) );
1377 1083 }
1378 1084 }