PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
← All changes | includes/page-appointment-services/appointment_services__booking.php +161 -2 11.6.1 → 11.9 View file →
@@ -221,8 +221,13 @@
221 221
222 222 /**
223 223 * Persist an immutable Service snapshot after the core booking save succeeds.
224 224 *
225 + * The core save path invokes this handler before releasing its availability
226 + * guard, and the released public hook invokes it again for compatibility. A
227 + * request-local content key makes that duplicate call harmless while still
228 + * allowing a later save with changed Service data in the same request.
229 + *
225 230 * @param int $booking_id Saved booking ID.
226 231 * @param array $create_params Normalized booking creation parameters.
227 232 * @param string $where_to_save_booking Booking save context supplied by core.
228 233 *
@@ -228,14 +233,32 @@
228 233 *
229 234 * @return void
230 235 */
231 236 function wpbc_appointment_services_after_booking_save( $booking_id, $create_params, $where_to_save_booking ) {
237 + static $persisted_snapshot_keys = array();
238 +
232 239 if ( empty( $create_params['appointment_service'] ) || empty( $create_params['resource_id'] ) ) {
233 240 return;
234 241 }
242 +
243 + $booking_id = absint( $booking_id );
244 + $snapshot_key = $booking_id . ':' . md5(
245 + wp_json_encode(
246 + array(
247 + 'resource_id' => absint( $create_params['resource_id'] ),
248 + 'appointment_service' => $create_params['appointment_service'],
249 + )
250 + )
251 + );
252 + if ( isset( $persisted_snapshot_keys[ $snapshot_key ] ) ) {
253 + return;
254 + }
255 +
235 256 $saved = wpbc_appointment_services_repository()->save_appointment_snapshot( $booking_id, $create_params['resource_id'], $create_params['appointment_service'] );
236 - if ( ! $saved ) {
237 - do_action( 'wpbc_appointment_snapshot_save_failed', absint( $booking_id ), $create_params, $where_to_save_booking );
257 + if ( $saved ) {
258 + $persisted_snapshot_keys[ $snapshot_key ] = true;
259 + } else {
260 + do_action( 'wpbc_appointment_snapshot_save_failed', $booking_id, $create_params, $where_to_save_booking );
238 261 }
239 262 }
240 263
241 264 add_action( 'wpbc_booking_after_save', 'wpbc_appointment_services_after_booking_save', 10, 3 );
@@ -856,8 +879,144 @@
856 879 return new WP_Error( 'appointment_service_duration_invalid', __( 'The selected Service does not fit in the chosen day. Please select an earlier start time.', 'booking' ) );
857 880 }
858 881
859 882 return $end_seconds;
883 +}
884 +
885 +/**
886 + * Normalize Working Time intervals into continuous scheduling ranges.
887 + *
888 + * Setup Wizard can define several intervals for one weekday. Gaps remain
889 + * scheduling boundaries, while touching or overlapping legacy intervals are
890 + * merged so one continuous range is evaluated consistently.
891 + *
892 + * @param array<int,array<string,int>> $working_intervals Working Time intervals.
893 + *
894 + * @return array<int,array{start_second:int,end_second:int}> Normalized continuous intervals.
895 + */
896 +function wpbc_appointment_services_normalize_working_intervals( $working_intervals ) {
897 + $normalized_intervals = array();
898 +
899 + foreach ( (array) $working_intervals as $working_interval ) {
900 + if ( ! is_array( $working_interval ) || ! isset( $working_interval['start_second'], $working_interval['end_second'] ) ) {
901 + continue;
902 + }
903 +
904 + $start_second = max( 0, min( DAY_IN_SECONDS, absint( $working_interval['start_second'] ) ) );
905 + $end_second = max( 0, min( DAY_IN_SECONDS, absint( $working_interval['end_second'] ) ) );
906 + if ( $start_second >= $end_second ) {
907 + continue;
908 + }
909 +
910 + $normalized_intervals[] = array(
911 + 'start_second' => $start_second,
912 + 'end_second' => $end_second,
913 + );
914 + }
915 +
916 + usort(
917 + $normalized_intervals,
918 + static function ( $left_interval, $right_interval ) {
919 + return $left_interval['start_second'] - $right_interval['start_second'];
920 + }
921 + );
922 +
923 + $continuous_intervals = array();
924 + foreach ( $normalized_intervals as $normalized_interval ) {
925 + $last_interval_index = count( $continuous_intervals ) - 1;
926 + if ( $last_interval_index < 0 || $normalized_interval['start_second'] > $continuous_intervals[ $last_interval_index ]['end_second'] ) {
927 + $continuous_intervals[] = $normalized_interval;
928 + continue;
929 + }
930 +
931 + $continuous_intervals[ $last_interval_index ]['end_second'] = max(
932 + $continuous_intervals[ $last_interval_index ]['end_second'],
933 + $normalized_interval['end_second']
934 + );
935 + }
936 +
937 + return $continuous_intervals;
938 +}
939 +
940 +/**
941 + * Check one Service interval against Working Time intervals for a single day.
942 + *
943 + * The complete Provider-reserved interval must fit inside one continuous
944 + * Working Time interval. This includes the Service's before and after buffers;
945 + * a gap between two daily intervals cannot be crossed.
946 + *
947 + * @param array<string,mixed> $service Effective Service values.
948 + * @param int[] $time_seconds Exact appointment start and end seconds.
949 + * @param array<int,array<string,int>> $working_intervals Working Time intervals for the date.
950 + *
951 + * @return true|WP_Error True when the reserved interval fits, otherwise an availability error.
952 + */
953 +function wpbc_appointment_services_check_working_intervals( $service, $time_seconds, $working_intervals ) {
954 + if ( empty( $service ) || ! is_array( $time_seconds ) || count( $time_seconds ) < 2 ) {
955 + return true;
956 + }
957 +
958 + $appointment_start = (int) $time_seconds[0];
959 + $appointment_end = (int) $time_seconds[1];
960 + if ( $appointment_start < 0 || $appointment_end <= $appointment_start || $appointment_end > DAY_IN_SECONDS ) {
961 + return new WP_Error( 'appointment_service_duration_invalid', __( 'The selected Service duration is invalid. Please contact the website administrator.', 'booking' ) );
962 + }
963 +
964 + $reserved_start = $appointment_start - ( ( isset( $service['buffer_before_minutes'] ) ? absint( $service['buffer_before_minutes'] ) : 0 ) * MINUTE_IN_SECONDS );
965 + $reserved_end = $appointment_end + ( ( isset( $service['buffer_after_minutes'] ) ? absint( $service['buffer_after_minutes'] ) : 0 ) * MINUTE_IN_SECONDS );
966 + foreach ( wpbc_appointment_services_normalize_working_intervals( $working_intervals ) as $working_interval ) {
967 + if ( $reserved_start >= $working_interval['start_second'] && $reserved_end <= $working_interval['end_second'] ) {
968 + return true;
969 + }
970 + }
971 +
972 + return new WP_Error(
973 + 'appointment_service_outside_working_time',
974 + __( "This start time is unavailable because the Service duration and required buffers do not fit within the Provider's Working Time. Please choose another time.", 'booking' )
975 + );
976 +}
977 +
978 +/**
979 + * Check a Service interval against the Provider's effective Working Time.
980 + *
981 + * Global Working Time disabled state and a Provider-specific disabled mode are
982 + * deliberate opt-outs. Inherited and custom schedules use the canonical
983 + * Working Time resolver, including every interval configured for a weekday.
984 + *
985 + * @param array<string,mixed> $service Effective Service values.
986 + * @param int $resource_id Provider resource ID.
987 + * @param string[] $dates Selected SQL dates.
988 + * @param int[] $time_seconds Exact appointment start and end seconds.
989 + *
990 + * @return true|WP_Error True when Working Time is disabled or every date fits.
991 + */
992 +function wpbc_appointment_services_check_working_time( $service, $resource_id, $dates, $time_seconds ) {
993 + if ( empty( $service ) || ! absint( $resource_id ) || empty( $dates ) || ! function_exists( 'wpbc_working_time__get_effective_rule' ) ) {
994 + return true;
995 + }
996 +
997 + if ( false === wpbc_working_time__get_effective_rule( $resource_id ) ) {
998 + return true;
999 + }
1000 +
1001 + foreach ( (array) $dates as $date_value ) {
1002 + $date_value = sanitize_text_field( $date_value );
1003 + $date_parts = array_map( 'absint', explode( '-', $date_value ) );
1004 + if ( $date_value !== wpbc_sanitize_date( $date_value ) || 3 !== count( $date_parts ) || ! checkdate( $date_parts[1], $date_parts[2], $date_parts[0] ) ) {
1005 + return new WP_Error( 'appointment_dates_invalid', __( 'Select a valid appointment date and try again.', 'booking' ) );
1006 + }
1007 +
1008 + $working_time_check = wpbc_appointment_services_check_working_intervals(
1009 + $service,
1010 + $time_seconds,
1011 + wpbc_working_time__get_working_intervals_for_date( $resource_id, $date_value )
1012 + );
1013 + if ( is_wp_error( $working_time_check ) ) {
1014 + return $working_time_check;
1015 + }
1016 + }
1017 +
1018 + return true;
860 1019 }
861 1020
862 1021 /**
863 1022 * Convert Booking Calendar's stored boundary markers to exact interval times.