From 5ef53305bd4770192aa636a17296ca45ce6b6460 Mon Sep 17 00:00:00 2001 From: SebastianWiz <165194375+SebastianWiz@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:14:26 -0700 Subject: [PATCH 1/7] `gpb-daily-service-booking-limit.php`: Added new snippet --- .../gpb-daily-service-booking-limit.php | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 gp-bookings/gpb-daily-service-booking-limit.php diff --git a/gp-bookings/gpb-daily-service-booking-limit.php b/gp-bookings/gpb-daily-service-booking-limit.php new file mode 100644 index 000000000..a89615c9b --- /dev/null +++ b/gp-bookings/gpb-daily-service-booking-limit.php @@ -0,0 +1,213 @@ + false, + 'service_ids' => array(), + 'daily_limit' => 10, + 'capacity_message' => __( 'We are fully booked for that day. Please choose another date.', 'gp-bookings' ), + ) ); + + $this->form_id = $args['form_id']; + $this->service_ids = array_map( 'intval', (array) $args['service_ids'] ); + $this->daily_limit = (int) $args['daily_limit']; + $this->capacity_message = $args['capacity_message']; + + if ( empty( $this->service_ids ) ) { + return; + } + + add_action( 'gpb_before_booking_created', array( $this, 'guard_booking_creation' ), 10, 2 ); + add_filter( 'gform_validation', array( $this, 'validate_submission' ) ); + } + + public function guard_booking_creation( array $booking_data, $bookable ) { + if ( ! $bookable instanceof \GP_Bookings\Service || ! in_array( $bookable->get_id(), $this->service_ids, true ) ) { + return; + } + + // Normalize to the start date in the site timezone so nightly/range bookings count correctly. + $date = $this->normalize_booking_date( + $booking_data['start_datetime'] ?? '', + $booking_data['end_datetime'] ?? ( $booking_data['start_datetime'] ?? '' ), + $bookable + ); + if ( ! $date ) { + return; + } + $quantity = isset( $booking_data['quantity'] ) ? max( 1, (int) $booking_data['quantity'] ) : 1; + + // Guard again at save time so last-second bookings can't slip past the form validation. + if ( $this->get_total_for_date( $date ) + $quantity > $this->daily_limit ) { + throw new \GP_Bookings\Exceptions\CapacityException( $this->capacity_message ); + } + } + + public function validate_submission( $result ) { + $is_object = is_object( $result ); + $form = $is_object ? $result->form : $result['form']; + + if ( $this->form_id && (int) $form['id'] !== (int) $this->form_id ) { + return $result; + } + + $is_valid = $is_object ? $result->is_valid : $result['is_valid']; + + // Track per-day totals so multiple booking fields in one submission don't exceed the cap. + $daily_totals = []; + + foreach ( $form['fields'] as &$field ) { + if ( ! isset( $field->inputType ) || $field->inputType !== 'gpb_booking' ) { + continue; + } + + $children = $field->get_child_fields( $form ); + $service = $children['service'] ?? null; + $time = $children['booking_time'] ?? null; + + if ( ! $service || ! $time ) { + continue; + } + + $service_id = isset( $service->gpbService ) ? (int) $service->gpbService : 0; + if ( ! $service_id || ! in_array( $service_id, $this->service_ids, true ) ) { + continue; + } + + $service_model = \GP_Bookings\Service::get( $service_id ); + if ( ! $service_model ) { + continue; + } + + $datetime = $this->get_posted_value( (int) $time->id ); + if ( ! $datetime ) { + continue; + } + + $date = $this->normalize_booking_date( $datetime, $datetime, $service_model ); + if ( ! $date ) { + continue; + } + + $quantity = rgpost( 'input_' . (int) $field->id . '_3' ); + $quantity = $quantity === null || $quantity === '' ? 1 : max( 1, (int) $quantity ); + + // Reuse the current total for this date so we only hit the database once per day per submission. + $current_total = $daily_totals[ $date ] ?? $this->get_total_for_date( $date ); + + if ( $current_total + $quantity > $this->daily_limit ) { + $this->flag_field_error( $form, (int) $time->id ); + $is_valid = false; + continue; + } + + $daily_totals[ $date ] = $current_total + $quantity; + } + + unset( $field ); + + if ( ! $is_valid ) { + $form['validation_message'] = $this->capacity_message; + } + + if ( $is_object ) { + $result->form = $form; + $result->is_valid = $is_valid; + return $result; + } + + $result['form'] = $form; + $result['is_valid'] = $is_valid; + return $result; + } + + private function get_total_for_date( string $date ): int { + $start = $date . ' 00:00:00'; + $end = $date . ' 23:59:59'; + // Count both pending and confirmed bookings to reflect in-progress reservations. + $bookings = \GP_Bookings\Queries\Booking_Query::get_bookings_in_range( + $start, + $end, + array( + 'object_id' => $this->service_ids, + 'object_type' => 'service', + 'status' => array( 'pending', 'confirmed' ), + 'exclude_service_with_resource' => false, + ) + ); + + $total = 0; + + foreach ( $bookings as $booking ) { + $total += (int) $booking->get_quantity(); + } + + return $total; + } + + private function normalize_booking_date( $start, $end, $bookable ): ?string { + try { + $normalized = \GP_Bookings\Booking::normalize_datetime_values( $start, $end, $bookable ); + } catch ( \Throwable $e ) { + return null; + } + + return $normalized['start']->format( 'Y-m-d' ); + } + + private function get_posted_value( int $field_id ) { + $value = rgpost( 'input_' . $field_id ); + + if ( is_array( $value ) ) { + $value = reset( $value ); + } + + return $value === null || $value === '' ? null : $value; + } + + private function flag_field_error( array &$form, int $field_id ): void { + foreach ( $form['fields'] as &$field ) { + if ( (int) $field->id === $field_id ) { + $field->failed_validation = true; + $field->validation_message = $this->capacity_message; + break; + } + } + + unset( $field ); + } + +} + +# Configuration +new GPB_Daily_Service_Limit( array( + 'form_id' => 123, + 'service_ids' => array( 45, 67 ), + 'daily_limit' => 10, + // 'capacity_message' => '', +) ); From 81915ecfce40104a5f426d274e557a157cc3e601 Mon Sep 17 00:00:00 2001 From: SebastianWiz <165194375+SebastianWiz@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:28:41 -0700 Subject: [PATCH 2/7] `gpb-daily-service-booking-limit.php`: Added a new snippet --- .../gpb-daily-service-booking-limit.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/gp-bookings/gpb-daily-service-booking-limit.php b/gp-bookings/gpb-daily-service-booking-limit.php index a89615c9b..600f4bfaa 100644 --- a/gp-bookings/gpb-daily-service-booking-limit.php +++ b/gp-bookings/gpb-daily-service-booking-limit.php @@ -20,18 +20,18 @@ */ class GPB_Daily_Service_Limit { - private $form_id; - private $service_ids; - private $daily_limit; - private $capacity_message; - - public function __construct( array $args ) { - $args = wp_parse_args( $args, array( - 'form_id' => false, - 'service_ids' => array(), - 'daily_limit' => 10, - 'capacity_message' => __( 'We are fully booked for that day. Please choose another date.', 'gp-bookings' ), - ) ); + private $form_id; + private $service_ids; + private $daily_limit; + private $capacity_message; + + public function __construct( array $args ) { + $args = wp_parse_args( $args, array( + 'form_id' => false, + 'service_ids' => array(), + 'daily_limit' => 10, + 'capacity_message' => __( 'We are fully booked for that day. Please choose another date.', 'gp-bookings' ), + )); $this->form_id = $args['form_id']; $this->service_ids = array_map( 'intval', (array) $args['service_ids'] ); @@ -147,8 +147,8 @@ public function validate_submission( $result ) { } private function get_total_for_date( string $date ): int { - $start = $date . ' 00:00:00'; - $end = $date . ' 23:59:59'; + $start = $date . ' 00:00:00'; + $end = $date . ' 23:59:59'; // Count both pending and confirmed bookings to reflect in-progress reservations. $bookings = \GP_Bookings\Queries\Booking_Query::get_bookings_in_range( $start, From 4a2811470027a1cee0931eac9f877d0c89b5bf11 Mon Sep 17 00:00:00 2001 From: SebastianWiz <165194375+SebastianWiz@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:30:15 -0700 Subject: [PATCH 3/7] `gpb-daily-service-booking-limit.php`: Added a new snippet. --- gp-bookings/gpb-daily-service-booking-limit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gp-bookings/gpb-daily-service-booking-limit.php b/gp-bookings/gpb-daily-service-booking-limit.php index 600f4bfaa..4e830cb55 100644 --- a/gp-bookings/gpb-daily-service-booking-limit.php +++ b/gp-bookings/gpb-daily-service-booking-limit.php @@ -206,7 +206,7 @@ private function flag_field_error( array &$form, int $field_id ): void { # Configuration new GPB_Daily_Service_Limit( array( - 'form_id' => 123, + 'form_id' => 123, 'service_ids' => array( 45, 67 ), 'daily_limit' => 10, // 'capacity_message' => '', From 1ea2f3f7a5b09132a04e96b7e89fd108be64613a Mon Sep 17 00:00:00 2001 From: SebastianWiz <165194375+SebastianWiz@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:35:36 -0700 Subject: [PATCH 4/7] `gpb-daily-service-booking-limit.php`: Added a new snippet. --- .../gpb-daily-service-booking-limit.php | 282 +++++++++--------- 1 file changed, 141 insertions(+), 141 deletions(-) diff --git a/gp-bookings/gpb-daily-service-booking-limit.php b/gp-bookings/gpb-daily-service-booking-limit.php index 4e830cb55..14fb4fbc4 100644 --- a/gp-bookings/gpb-daily-service-booking-limit.php +++ b/gp-bookings/gpb-daily-service-booking-limit.php @@ -33,181 +33,181 @@ public function __construct( array $args ) { 'capacity_message' => __( 'We are fully booked for that day. Please choose another date.', 'gp-bookings' ), )); - $this->form_id = $args['form_id']; - $this->service_ids = array_map( 'intval', (array) $args['service_ids'] ); - $this->daily_limit = (int) $args['daily_limit']; - $this->capacity_message = $args['capacity_message']; + $this->form_id = $args['form_id']; + $this->service_ids = array_map( 'intval', (array) $args['service_ids'] ); + $this->daily_limit = (int) $args['daily_limit']; + $this->capacity_message = $args['capacity_message']; - if ( empty( $this->service_ids ) ) { - return; - } + if ( empty( $this->service_ids ) ) { + return; + } - add_action( 'gpb_before_booking_created', array( $this, 'guard_booking_creation' ), 10, 2 ); - add_filter( 'gform_validation', array( $this, 'validate_submission' ) ); - } + add_action( 'gpb_before_booking_created', array( $this, 'guard_booking_creation' ), 10, 2 ); + add_filter( 'gform_validation', array( $this, 'validate_submission' ) ); + } - public function guard_booking_creation( array $booking_data, $bookable ) { - if ( ! $bookable instanceof \GP_Bookings\Service || ! in_array( $bookable->get_id(), $this->service_ids, true ) ) { - return; - } + public function guard_booking_creation( array $booking_data, $bookable ) { + if ( ! $bookable instanceof \GP_Bookings\Service || ! in_array( $bookable->get_id(), $this->service_ids, true ) ) { + return; + } // Normalize to the start date in the site timezone so nightly/range bookings count correctly. - $date = $this->normalize_booking_date( - $booking_data['start_datetime'] ?? '', - $booking_data['end_datetime'] ?? ( $booking_data['start_datetime'] ?? '' ), - $bookable - ); - if ( ! $date ) { - return; - } - $quantity = isset( $booking_data['quantity'] ) ? max( 1, (int) $booking_data['quantity'] ) : 1; + $date = $this->normalize_booking_date( + $booking_data['start_datetime'] ?? '', + $booking_data['end_datetime'] ?? ( $booking_data['start_datetime'] ?? '' ), + $bookable + ); + if ( ! $date ) { + return; + } + $quantity = isset( $booking_data['quantity'] ) ? max( 1, (int) $booking_data['quantity'] ) : 1; // Guard again at save time so last-second bookings can't slip past the form validation. - if ( $this->get_total_for_date( $date ) + $quantity > $this->daily_limit ) { - throw new \GP_Bookings\Exceptions\CapacityException( $this->capacity_message ); - } - } + if ( $this->get_total_for_date( $date ) + $quantity > $this->daily_limit ) { + throw new \GP_Bookings\Exceptions\CapacityException( $this->capacity_message ); + } + } - public function validate_submission( $result ) { - $is_object = is_object( $result ); - $form = $is_object ? $result->form : $result['form']; + public function validate_submission( $result ) { + $is_object = is_object( $result ); + $form = $is_object ? $result->form : $result['form']; - if ( $this->form_id && (int) $form['id'] !== (int) $this->form_id ) { - return $result; - } + if ( $this->form_id && (int) $form['id'] !== (int) $this->form_id ) { + return $result; + } - $is_valid = $is_object ? $result->is_valid : $result['is_valid']; + $is_valid = $is_object ? $result->is_valid : $result['is_valid']; // Track per-day totals so multiple booking fields in one submission don't exceed the cap. - $daily_totals = []; + $daily_totals = []; - foreach ( $form['fields'] as &$field ) { - if ( ! isset( $field->inputType ) || $field->inputType !== 'gpb_booking' ) { - continue; - } + foreach ( $form['fields'] as &$field ) { + if ( ! isset( $field->inputType ) || $field->inputType !== 'gpb_booking' ) { + continue; + } - $children = $field->get_child_fields( $form ); - $service = $children['service'] ?? null; - $time = $children['booking_time'] ?? null; + $children = $field->get_child_fields( $form ); + $service = $children['service'] ?? null; + $time = $children['booking_time'] ?? null; - if ( ! $service || ! $time ) { - continue; - } + if ( ! $service || ! $time ) { + continue; + } - $service_id = isset( $service->gpbService ) ? (int) $service->gpbService : 0; - if ( ! $service_id || ! in_array( $service_id, $this->service_ids, true ) ) { - continue; - } + $service_id = isset( $service->gpbService ) ? (int) $service->gpbService : 0; + if ( ! $service_id || ! in_array( $service_id, $this->service_ids, true ) ) { + continue; + } - $service_model = \GP_Bookings\Service::get( $service_id ); - if ( ! $service_model ) { - continue; - } + $service_model = \GP_Bookings\Service::get( $service_id ); + if ( ! $service_model ) { + continue; + } - $datetime = $this->get_posted_value( (int) $time->id ); - if ( ! $datetime ) { - continue; - } + $datetime = $this->get_posted_value( (int) $time->id ); + if ( ! $datetime ) { + continue; + } - $date = $this->normalize_booking_date( $datetime, $datetime, $service_model ); - if ( ! $date ) { - continue; - } + $date = $this->normalize_booking_date( $datetime, $datetime, $service_model ); + if ( ! $date ) { + continue; + } - $quantity = rgpost( 'input_' . (int) $field->id . '_3' ); - $quantity = $quantity === null || $quantity === '' ? 1 : max( 1, (int) $quantity ); + $quantity = rgpost( 'input_' . (int) $field->id . '_3' ); + $quantity = $quantity === null || $quantity === '' ? 1 : max( 1, (int) $quantity ); // Reuse the current total for this date so we only hit the database once per day per submission. - $current_total = $daily_totals[ $date ] ?? $this->get_total_for_date( $date ); + $current_total = $daily_totals[ $date ] ?? $this->get_total_for_date( $date ); - if ( $current_total + $quantity > $this->daily_limit ) { - $this->flag_field_error( $form, (int) $time->id ); - $is_valid = false; - continue; - } + if ( $current_total + $quantity > $this->daily_limit ) { + $this->flag_field_error( $form, (int) $time->id ); + $is_valid = false; + continue; + } - $daily_totals[ $date ] = $current_total + $quantity; - } + $daily_totals[ $date ] = $current_total + $quantity; + } - unset( $field ); + unset( $field ); - if ( ! $is_valid ) { - $form['validation_message'] = $this->capacity_message; - } + if ( ! $is_valid ) { + $form['validation_message'] = $this->capacity_message; + } - if ( $is_object ) { - $result->form = $form; - $result->is_valid = $is_valid; - return $result; - } + if ( $is_object ) { + $result->form = $form; + $result->is_valid = $is_valid; + return $result; + } - $result['form'] = $form; - $result['is_valid'] = $is_valid; - return $result; - } + $result['form'] = $form; + $result['is_valid'] = $is_valid; + return $result; + } - private function get_total_for_date( string $date ): int { - $start = $date . ' 00:00:00'; - $end = $date . ' 23:59:59'; + private function get_total_for_date( string $date ): int { + $start = $date . ' 00:00:00'; + $end = $date . ' 23:59:59'; // Count both pending and confirmed bookings to reflect in-progress reservations. - $bookings = \GP_Bookings\Queries\Booking_Query::get_bookings_in_range( - $start, - $end, - array( - 'object_id' => $this->service_ids, - 'object_type' => 'service', - 'status' => array( 'pending', 'confirmed' ), - 'exclude_service_with_resource' => false, - ) - ); - - $total = 0; - - foreach ( $bookings as $booking ) { - $total += (int) $booking->get_quantity(); - } - - return $total; - } - - private function normalize_booking_date( $start, $end, $bookable ): ?string { - try { - $normalized = \GP_Bookings\Booking::normalize_datetime_values( $start, $end, $bookable ); - } catch ( \Throwable $e ) { - return null; - } - - return $normalized['start']->format( 'Y-m-d' ); - } - - private function get_posted_value( int $field_id ) { - $value = rgpost( 'input_' . $field_id ); - - if ( is_array( $value ) ) { - $value = reset( $value ); - } - - return $value === null || $value === '' ? null : $value; - } - - private function flag_field_error( array &$form, int $field_id ): void { - foreach ( $form['fields'] as &$field ) { - if ( (int) $field->id === $field_id ) { - $field->failed_validation = true; - $field->validation_message = $this->capacity_message; - break; - } - } - - unset( $field ); - } + $bookings = \GP_Bookings\Queries\Booking_Query::get_bookings_in_range( + $start, + $end, + array( + 'object_id' => $this->service_ids, + 'object_type' => 'service', + 'status' => array( 'pending', 'confirmed' ), + 'exclude_service_with_resource' => false, + ) + ); + + $total = 0; + + foreach ( $bookings as $booking ) { + $total += (int) $booking->get_quantity(); + } + + return $total; + } + + private function normalize_booking_date( $start, $end, $bookable ): ?string { + try { + $normalized = \GP_Bookings\Booking::normalize_datetime_values( $start, $end, $bookable ); + } catch ( \Throwable $e ) { + return null; + } + + return $normalized['start']->format( 'Y-m-d' ); + } + + private function get_posted_value( int $field_id ) { + $value = rgpost( 'input_' . $field_id ); + + if ( is_array( $value ) ) { + $value = reset( $value ); + } + + return $value === null || $value === '' ? null : $value; + } + + private function flag_field_error( array &$form, int $field_id ): void { + foreach ( $form['fields'] as &$field ) { + if ( (int) $field->id === $field_id ) { + $field->failed_validation = true; + $field->validation_message = $this->capacity_message; + break; + } + } + + unset( $field ); + } } # Configuration new GPB_Daily_Service_Limit( array( 'form_id' => 123, - 'service_ids' => array( 45, 67 ), - 'daily_limit' => 10, - // 'capacity_message' => '', + 'service_ids' => array( 45, 67 ), + 'daily_limit' => 10, + // 'capacity_message' => '', ) ); From 11b6517775c89b7bbfbc6ad0a3f7ab7836a7f033 Mon Sep 17 00:00:00 2001 From: SebastianWiz <165194375+SebastianWiz@users.noreply.github.com> Date: Wed, 8 Oct 2025 23:46:32 -0700 Subject: [PATCH 5/7] `gpb-daily-service-booking-limit.php`: Added new snippet --- .../gpb-daily-service-booking-limit.php | 238 +++++++++--------- 1 file changed, 120 insertions(+), 118 deletions(-) diff --git a/gp-bookings/gpb-daily-service-booking-limit.php b/gp-bookings/gpb-daily-service-booking-limit.php index 14fb4fbc4..40d1a0684 100644 --- a/gp-bookings/gpb-daily-service-booking-limit.php +++ b/gp-bookings/gpb-daily-service-booking-limit.php @@ -3,9 +3,9 @@ * Gravity Perks // GP Bookings // Daily Service Booking Limit * https://gravitywiz.com/documentation/gravity-forms-bookings/ * - * Enforce a daily capacity for one or more GP Bookings services. When the selected services - * meet the limit, new submissions are blocked and the booking time field displays a - * "fully booked" message for that day. List multiple service IDs to share the cap between them. + * Enforce a daily capacity for one or more booking services. When the selected services + * meet the limit, dates are marked as unavailable in the calendar and submissions are blocked. + * List multiple service IDs to share the cap between them. * * Instructions: * @@ -13,45 +13,37 @@ * https://gravitywiz.com/documentation/how-do-i-install-a-snippet/ * * 2. Update the configuration at the bottom of the snippet: - * - Set form_id to the Gravity Form that hosts your booking field (or leave false to run on every form). * - List the GP Bookings service IDs that should share the daily cap in service_ids. * - Adjust daily_limit to the maximum combined bookings allowed per day. - * - Optionally customize capacity_message to change the validation text shown to users. */ class GPB_Daily_Service_Limit { - private $form_id; private $service_ids; private $daily_limit; - private $capacity_message; public function __construct( array $args ) { $args = wp_parse_args( $args, array( - 'form_id' => false, - 'service_ids' => array(), - 'daily_limit' => 10, - 'capacity_message' => __( 'We are fully booked for that day. Please choose another date.', 'gp-bookings' ), + 'service_ids' => array(), + 'daily_limit' => 10, )); - $this->form_id = $args['form_id']; - $this->service_ids = array_map( 'intval', (array) $args['service_ids'] ); - $this->daily_limit = (int) $args['daily_limit']; - $this->capacity_message = $args['capacity_message']; + $this->service_ids = array_map( 'intval', (array) $args['service_ids'] ); + $this->daily_limit = (int) $args['daily_limit']; - if ( empty( $this->service_ids ) ) { - return; - } + if ( empty( $this->service_ids ) || $this->daily_limit < 1 ) { + return; + } - add_action( 'gpb_before_booking_created', array( $this, 'guard_booking_creation' ), 10, 2 ); - add_filter( 'gform_validation', array( $this, 'validate_submission' ) ); + // Guard creation and REST availability so the cap is enforced everywhere. + add_action( 'gpb_before_booking_created', array( $this, 'guard_booking_creation' ), 10, 2 ); + add_filter( 'rest_post_dispatch', array( $this, 'filter_rest_availability' ), 10, 3 ); } public function guard_booking_creation( array $booking_data, $bookable ) { - if ( ! $bookable instanceof \GP_Bookings\Service || ! in_array( $bookable->get_id(), $this->service_ids, true ) ) { + if ( ! ( $bookable instanceof \GP_Bookings\Service ) || ! $this->is_tracked_service( $bookable->get_id() ) ) { return; } - // Normalize to the start date in the site timezone so nightly/range bookings count correctly. $date = $this->normalize_booking_date( $booking_data['start_datetime'] ?? '', $booking_data['end_datetime'] ?? ( $booking_data['start_datetime'] ?? '' ), @@ -60,114 +52,146 @@ public function guard_booking_creation( array $booking_data, $bookable ) { if ( ! $date ) { return; } + $quantity = isset( $booking_data['quantity'] ) ? max( 1, (int) $booking_data['quantity'] ) : 1; - // Guard again at save time so last-second bookings can't slip past the form validation. - if ( $this->get_total_for_date( $date ) + $quantity > $this->daily_limit ) { - throw new \GP_Bookings\Exceptions\CapacityException( $this->capacity_message ); + if ( $this->exceeds_limit( array( $date ), $quantity ) ) { + // Stop the submission when the shared limit would be exceeded. + throw new \GP_Bookings\Exceptions\CapacityException( __( 'We are fully booked for that day. Please choose another date.', 'gp-bookings' ) ); } } - public function validate_submission( $result ) { - $is_object = is_object( $result ); - $form = $is_object ? $result->form : $result['form']; - - if ( $this->form_id && (int) $form['id'] !== (int) $this->form_id ) { - return $result; + public function filter_rest_availability( $response, $server, $request ) { + if ( ! ( $request instanceof \WP_REST_Request ) || 'GET' !== $request->get_method() ) { + return $response; } - $is_valid = $is_object ? $result->is_valid : $result['is_valid']; - - // Track per-day totals so multiple booking fields in one submission don't exceed the cap. - $daily_totals = []; + $route = ltrim( $request->get_route(), '/' ); + if ( 'gp-bookings/v1/availability/days' !== $route ) { + return $response; + } - foreach ( $form['fields'] as &$field ) { - if ( ! isset( $field->inputType ) || $field->inputType !== 'gpb_booking' ) { - continue; - } + $service_id = (int) $request->get_param( 'serviceId' ); + if ( ! $service_id || ! $this->is_tracked_service( $service_id ) ) { + return $response; + } - $children = $field->get_child_fields( $form ); - $service = $children['service'] ?? null; - $time = $children['booking_time'] ?? null; + if ( is_wp_error( $response ) || ! ( $response instanceof \WP_HTTP_Response ) ) { + return $response; + } - if ( ! $service || ! $time ) { - continue; - } + $data = $response->get_data(); + if ( empty( $data['days'] ) || ! is_array( $data['days'] ) ) { + return $response; + } - $service_id = isset( $service->gpbService ) ? (int) $service->gpbService : 0; - if ( ! $service_id || ! in_array( $service_id, $this->service_ids, true ) ) { - continue; - } + $dates = array_keys( $data['days'] ); + if ( ! $dates ) { + return $response; + } - $service_model = \GP_Bookings\Service::get( $service_id ); - if ( ! $service_model ) { - continue; - } + $exclude_booking_id = (int) $request->get_param( 'exclude_booking_id' ); + $exclude_booking_id = $exclude_booking_id > 0 ? $exclude_booking_id : null; - $datetime = $this->get_posted_value( (int) $time->id ); - if ( ! $datetime ) { - continue; - } + $totals = $this->get_daily_totals( $dates, $exclude_booking_id ); - $date = $this->normalize_booking_date( $datetime, $datetime, $service_model ); - if ( ! $date ) { - continue; + foreach ( $data['days'] as $date => &$day ) { + if ( ( $totals[ $date ] ?? 0 ) >= $this->daily_limit ) { + // Flag the day as unavailable in the REST response. + $day['available'] = false; + $day['status'] = 'booked'; + $day['remainingSlots'] = 0; } + } + unset( $day ); - $quantity = rgpost( 'input_' . (int) $field->id . '_3' ); - $quantity = $quantity === null || $quantity === '' ? 1 : max( 1, (int) $quantity ); + $response->set_data( $data ); + return $response; + } - // Reuse the current total for this date so we only hit the database once per day per submission. - $current_total = $daily_totals[ $date ] ?? $this->get_total_for_date( $date ); + private function exceeds_limit( array $dates, int $incoming_quantity = 0, ?int $exclude_booking_id = null ): bool { + $dates = array_filter( array_unique( $dates ) ); + $totals = $dates ? $this->get_daily_totals( $dates, $exclude_booking_id ) : array(); - if ( $current_total + $quantity > $this->daily_limit ) { - $this->flag_field_error( $form, (int) $time->id ); - $is_valid = false; - continue; + foreach ( $dates as $date ) { + $existing_total = $totals[ $date ] ?? 0; + if ( $existing_total + $incoming_quantity > $this->daily_limit ) { + return true; } - - $daily_totals[ $date ] = $current_total + $quantity; } - unset( $field ); + return false; + } - if ( ! $is_valid ) { - $form['validation_message'] = $this->capacity_message; + private function get_daily_totals( array $dates, ?int $exclude_booking_id = null ): array { + $dates = array_values( array_filter( array_unique( array_map( 'trim', $dates ) ) ) ); + if ( ! $dates ) { + return array(); } - if ( $is_object ) { - $result->form = $form; - $result->is_valid = $is_valid; - return $result; - } + $start_datetime = min( $dates ) . ' 00:00:00'; + $end_datetime = max( $dates ) . ' 23:59:59'; - $result['form'] = $form; - $result['is_valid'] = $is_valid; - return $result; + return $this->get_totals_for_range( $start_datetime, $end_datetime, $exclude_booking_id ); } - private function get_total_for_date( string $date ): int { - $start = $date . ' 00:00:00'; - $end = $date . ' 23:59:59'; - // Count both pending and confirmed bookings to reflect in-progress reservations. + private function get_totals_for_range( string $start_datetime, string $end_datetime, ?int $exclude_booking_id = null ): array { + if ( '' === $start_datetime || '' === $end_datetime ) { + return array(); + } + $bookings = \GP_Bookings\Queries\Booking_Query::get_bookings_in_range( - $start, - $end, + $start_datetime, + $end_datetime, array( 'object_id' => $this->service_ids, 'object_type' => 'service', 'status' => array( 'pending', 'confirmed' ), 'exclude_service_with_resource' => false, + 'exclude_booking_id' => $exclude_booking_id, ) ); - $total = 0; + if ( ! $bookings ) { + return array(); + } + + $totals = array(); foreach ( $bookings as $booking ) { - $total += (int) $booking->get_quantity(); + try { + $service_id = (int) $booking->get_service_id(); + } catch ( \Throwable $e ) { + continue; + } + + if ( ! $this->is_tracked_service( $service_id ) ) { + continue; + } + + $service = \GP_Bookings\Service::get( $service_id ); + if ( ! $service ) { + continue; + } + + $date = $this->normalize_booking_date( + $booking->get_start_datetime(), + $booking->get_end_datetime(), + $service + ); + + if ( ! $date ) { + continue; + } + + $totals[ $date ] = ( $totals[ $date ] ?? 0 ) + (int) $booking->get_quantity(); } - return $total; + return $totals; + } + + private function is_tracked_service( int $service_id ): bool { + return in_array( $service_id, $this->service_ids, true ); } private function normalize_booking_date( $start, $end, $bookable ): ?string { @@ -180,34 +204,12 @@ private function normalize_booking_date( $start, $end, $bookable ): ?string { return $normalized['start']->format( 'Y-m-d' ); } - private function get_posted_value( int $field_id ) { - $value = rgpost( 'input_' . $field_id ); - - if ( is_array( $value ) ) { - $value = reset( $value ); - } - - return $value === null || $value === '' ? null : $value; - } - - private function flag_field_error( array &$form, int $field_id ): void { - foreach ( $form['fields'] as &$field ) { - if ( (int) $field->id === $field_id ) { - $field->failed_validation = true; - $field->validation_message = $this->capacity_message; - break; - } - } - - unset( $field ); - } - } # Configuration -new GPB_Daily_Service_Limit( array( - 'form_id' => 123, - 'service_ids' => array( 45, 67 ), - 'daily_limit' => 10, - // 'capacity_message' => '', -) ); +new GPB_Daily_Service_Limit( + array( + 'service_ids' => array( 123, 456 ), // Enter one or more service IDs + 'daily_limit' => 10, // Enter the daily limit + ) +); From 8fb788fd652dd5369e7310d6830b81f0605b2601 Mon Sep 17 00:00:00 2001 From: SebastianWiz <165194375+SebastianWiz@users.noreply.github.com> Date: Wed, 8 Oct 2025 23:53:44 -0700 Subject: [PATCH 6/7] `gpb-daily-service-booking-limit.php`: Added new snippet --- gp-bookings/gpb-daily-service-booking-limit.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gp-bookings/gpb-daily-service-booking-limit.php b/gp-bookings/gpb-daily-service-booking-limit.php index 40d1a0684..1c9c5d17c 100644 --- a/gp-bookings/gpb-daily-service-booking-limit.php +++ b/gp-bookings/gpb-daily-service-booking-limit.php @@ -31,11 +31,11 @@ public function __construct( array $args ) { $this->daily_limit = (int) $args['daily_limit']; if ( empty( $this->service_ids ) || $this->daily_limit < 1 ) { - return; - } + return; + } // Guard creation and REST availability so the cap is enforced everywhere. - add_action( 'gpb_before_booking_created', array( $this, 'guard_booking_creation' ), 10, 2 ); + add_action( 'gpb_before_booking_created', array( $this, 'guard_booking_creation' ), 10, 2 ); add_filter( 'rest_post_dispatch', array( $this, 'filter_rest_availability' ), 10, 3 ); } @@ -109,7 +109,7 @@ public function filter_rest_availability( $response, $server, $request ) { return $response; } - private function exceeds_limit( array $dates, int $incoming_quantity = 0, ?int $exclude_booking_id = null ): bool { + private function exceeds_limit( array $dates, int $incoming_quantity = 0, $exclude_booking_id = null ): bool { $dates = array_filter( array_unique( $dates ) ); $totals = $dates ? $this->get_daily_totals( $dates, $exclude_booking_id ) : array(); @@ -123,7 +123,7 @@ private function exceeds_limit( array $dates, int $incoming_quantity = 0, ?int $ return false; } - private function get_daily_totals( array $dates, ?int $exclude_booking_id = null ): array { + private function get_daily_totals( array $dates, $exclude_booking_id = null ): array { $dates = array_values( array_filter( array_unique( array_map( 'trim', $dates ) ) ) ); if ( ! $dates ) { return array(); @@ -135,7 +135,7 @@ private function get_daily_totals( array $dates, ?int $exclude_booking_id = null return $this->get_totals_for_range( $start_datetime, $end_datetime, $exclude_booking_id ); } - private function get_totals_for_range( string $start_datetime, string $end_datetime, ?int $exclude_booking_id = null ): array { + private function get_totals_for_range( string $start_datetime, string $end_datetime, $exclude_booking_id = null ): array { if ( '' === $start_datetime || '' === $end_datetime ) { return array(); } From 97b1e8346204575c661ff40d54cc9018cf79bfff Mon Sep 17 00:00:00 2001 From: saifsultanc Date: Fri, 24 Oct 2025 15:42:39 +0530 Subject: [PATCH 7/7] `gpb-daily-service-booking-limit.php`: Added new snippet. --- gp-bookings/gpb-daily-service-booking-limit.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gp-bookings/gpb-daily-service-booking-limit.php b/gp-bookings/gpb-daily-service-booking-limit.php index 1c9c5d17c..ff3cb1566 100644 --- a/gp-bookings/gpb-daily-service-booking-limit.php +++ b/gp-bookings/gpb-daily-service-booking-limit.php @@ -194,7 +194,12 @@ private function is_tracked_service( int $service_id ): bool { return in_array( $service_id, $this->service_ids, true ); } - private function normalize_booking_date( $start, $end, $bookable ): ?string { + /** + * Normalize booking date. + * + * @return string|null Returns the normalized start date (Y-m-d) or null on failure. + */ + private function normalize_booking_date( $start, $end, $bookable ) { try { $normalized = \GP_Bookings\Booking::normalize_datetime_values( $start, $end, $bookable ); } catch ( \Throwable $e ) {