33 * Gravity Perks // GP Bookings // Daily Service Booking Limit
44 * https://gravitywiz.com/documentation/gravity-forms-bookings/
55 *
6- * Enforce a daily capacity for one or more GP Bookings services. When the selected services
7- * meet the limit, new submissions are blocked and the booking time field displays a
8- * "fully booked" message for that day. List multiple service IDs to share the cap between them.
6+ * Enforce a daily capacity for one or more booking services. When the selected services
7+ * meet the limit, dates are marked as unavailable in the calendar and submissions are blocked.
8+ * List multiple service IDs to share the cap between them.
99 *
1010 * Instructions:
1111 *
1212 * 1. Install this snippet by following the steps here:
1313 * https://gravitywiz.com/documentation/how-do-i-install-a-snippet/
1414 *
1515 * 2. Update the configuration at the bottom of the snippet:
16- * - Set form_id to the Gravity Form that hosts your booking field (or leave false to run on every form).
1716 * - List the GP Bookings service IDs that should share the daily cap in service_ids.
1817 * - Adjust daily_limit to the maximum combined bookings allowed per day.
19- * - Optionally customize capacity_message to change the validation text shown to users.
2018 */
2119class GPB_Daily_Service_Limit {
2220
23- private $ form_id ;
2421 private $ service_ids ;
2522 private $ daily_limit ;
26- private $ capacity_message ;
2723
2824 public function __construct ( array $ args ) {
2925 $ args = wp_parse_args ( $ args , array (
30- 'form_id ' => false ,
31- 'service_ids ' => array (),
32- 'daily_limit ' => 10 ,
33- 'capacity_message ' => __ ( 'We are fully booked for that day. Please choose another date. ' , 'gp-bookings ' ),
26+ 'service_ids ' => array (),
27+ 'daily_limit ' => 10 ,
3428 ));
3529
36- $ this ->form_id = $ args ['form_id ' ];
37- $ this ->service_ids = array_map ( 'intval ' , (array ) $ args ['service_ids ' ] );
38- $ this ->daily_limit = (int ) $ args ['daily_limit ' ];
39- $ this ->capacity_message = $ args ['capacity_message ' ];
30+ $ this ->service_ids = array_map ( 'intval ' , (array ) $ args ['service_ids ' ] );
31+ $ this ->daily_limit = (int ) $ args ['daily_limit ' ];
4032
41- if ( empty ( $ this ->service_ids ) ) {
42- return ;
43- }
33+ if ( empty ( $ this ->service_ids ) || $ this -> daily_limit < 1 ) {
34+ return ;
35+ }
4436
45- add_action ( 'gpb_before_booking_created ' , array ( $ this , 'guard_booking_creation ' ), 10 , 2 );
46- add_filter ( 'gform_validation ' , array ( $ this , 'validate_submission ' ) );
37+ // Guard creation and REST availability so the cap is enforced everywhere.
38+ add_action ( 'gpb_before_booking_created ' , array ( $ this , 'guard_booking_creation ' ), 10 , 2 );
39+ add_filter ( 'rest_post_dispatch ' , array ( $ this , 'filter_rest_availability ' ), 10 , 3 );
4740 }
4841
4942 public function guard_booking_creation ( array $ booking_data , $ bookable ) {
50- if ( ! $ bookable instanceof \GP_Bookings \Service || ! in_array ( $ bookable ->get_id (), $ this -> service_ids , true ) ) {
43+ if ( ! ( $ bookable instanceof \GP_Bookings \Service ) || ! $ this -> is_tracked_service ( $ bookable ->get_id () ) ) {
5144 return ;
5245 }
5346
54- // Normalize to the start date in the site timezone so nightly/range bookings count correctly.
5547 $ date = $ this ->normalize_booking_date (
5648 $ booking_data ['start_datetime ' ] ?? '' ,
5749 $ booking_data ['end_datetime ' ] ?? ( $ booking_data ['start_datetime ' ] ?? '' ),
@@ -60,114 +52,146 @@ public function guard_booking_creation( array $booking_data, $bookable ) {
6052 if ( ! $ date ) {
6153 return ;
6254 }
55+
6356 $ quantity = isset ( $ booking_data ['quantity ' ] ) ? max ( 1 , (int ) $ booking_data ['quantity ' ] ) : 1 ;
6457
65- // Guard again at save time so last-second bookings can't slip past the form validation.
66- if ( $ this -> get_total_for_date ( $ date ) + $ quantity > $ this -> daily_limit ) {
67- throw new \GP_Bookings \Exceptions \CapacityException ( $ this -> capacity_message );
58+ if ( $ this -> exceeds_limit ( array ( $ date ), $ quantity ) ) {
59+ // Stop the submission when the shared limit would be exceeded.
60+ throw new \GP_Bookings \Exceptions \CapacityException ( __ ( ' We are fully booked for that day. Please choose another date. ' , ' gp-bookings ' ) );
6861 }
6962 }
7063
71- public function validate_submission ( $ result ) {
72- $ is_object = is_object ( $ result );
73- $ form = $ is_object ? $ result ->form : $ result ['form ' ];
74-
75- if ( $ this ->form_id && (int ) $ form ['id ' ] !== (int ) $ this ->form_id ) {
76- return $ result ;
64+ public function filter_rest_availability ( $ response , $ server , $ request ) {
65+ if ( ! ( $ request instanceof \WP_REST_Request ) || 'GET ' !== $ request ->get_method () ) {
66+ return $ response ;
7767 }
7868
79- $ is_valid = $ is_object ? $ result -> is_valid : $ result [ ' is_valid ' ] ;
80-
81- // Track per-day totals so multiple booking fields in one submission don't exceed the cap.
82- $ daily_totals = [];
69+ $ route = ltrim ( $ request -> get_route (), ' / ' ) ;
70+ if ( ' gp-bookings/v1/availability/days ' !== $ route ) {
71+ return $ response ;
72+ }
8373
84- foreach ( $ form [ ' fields ' ] as & $ field ) {
85- if ( ! isset ( $ field -> inputType ) || $ field -> inputType !== ' gpb_booking ' ) {
86- continue ;
87- }
74+ $ service_id = ( int ) $ request -> get_param ( ' serviceId ' );
75+ if ( ! $ service_id || ! $ this -> is_tracked_service ( $ service_id ) ) {
76+ return $ response ;
77+ }
8878
89- $ children = $ field -> get_child_fields ( $ form );
90- $ service = $ children [ ' service ' ] ?? null ;
91- $ time = $ children [ ' booking_time ' ] ?? null ;
79+ if ( is_wp_error ( $ response ) || ! ( $ response instanceof \WP_HTTP_Response ) ) {
80+ return $ response ;
81+ }
9282
93- if ( ! $ service || ! $ time ) {
94- continue ;
95- }
83+ $ data = $ response ->get_data ();
84+ if ( empty ( $ data ['days ' ] ) || ! is_array ( $ data ['days ' ] ) ) {
85+ return $ response ;
86+ }
9687
97- $ service_id = isset ( $ service -> gpbService ) ? ( int ) $ service -> gpbService : 0 ;
98- if ( ! $ service_id || ! in_array ( $ service_id , $ this -> service_ids , true ) ) {
99- continue ;
100- }
88+ $ dates = array_keys ( $ data [ ' days ' ] ) ;
89+ if ( ! $ dates ) {
90+ return $ response ;
91+ }
10192
102- $ service_model = \GP_Bookings \Service::get ( $ service_id );
103- if ( ! $ service_model ) {
104- continue ;
105- }
93+ $ exclude_booking_id = (int ) $ request ->get_param ( 'exclude_booking_id ' );
94+ $ exclude_booking_id = $ exclude_booking_id > 0 ? $ exclude_booking_id : null ;
10695
107- $ datetime = $ this ->get_posted_value ( (int ) $ time ->id );
108- if ( ! $ datetime ) {
109- continue ;
110- }
96+ $ totals = $ this ->get_daily_totals ( $ dates , $ exclude_booking_id );
11197
112- $ date = $ this ->normalize_booking_date ( $ datetime , $ datetime , $ service_model );
113- if ( ! $ date ) {
114- continue ;
98+ foreach ( $ data ['days ' ] as $ date => &$ day ) {
99+ if ( ( $ totals [ $ date ] ?? 0 ) >= $ this ->daily_limit ) {
100+ // Flag the day as unavailable in the REST response.
101+ $ day ['available ' ] = false ;
102+ $ day ['status ' ] = 'booked ' ;
103+ $ day ['remainingSlots ' ] = 0 ;
115104 }
105+ }
106+ unset( $ day );
116107
117- $ quantity = rgpost ( 'input_ ' . (int ) $ field ->id . '_3 ' );
118- $ quantity = $ quantity === null || $ quantity === '' ? 1 : max ( 1 , (int ) $ quantity );
108+ $ response ->set_data ( $ data );
109+ return $ response ;
110+ }
119111
120- // Reuse the current total for this date so we only hit the database once per day per submission.
121- $ current_total = $ daily_totals [ $ date ] ?? $ this ->get_total_for_date ( $ date );
112+ private function exceeds_limit ( array $ dates , int $ incoming_quantity = 0 , ?int $ exclude_booking_id = null ): bool {
113+ $ dates = array_filter ( array_unique ( $ dates ) );
114+ $ totals = $ dates ? $ this ->get_daily_totals ( $ dates , $ exclude_booking_id ) : array ();
122115
123- if ( $ current_total + $ quantity > $ this -> daily_limit ) {
124- $ this -> flag_field_error ( $ form , ( int ) $ time -> id ) ;
125- $ is_valid = false ;
126- continue ;
116+ foreach ( $ dates as $ date ) {
117+ $ existing_total = $ totals [ $ date ] ?? 0 ;
118+ if ( $ existing_total + $ incoming_quantity > $ this -> daily_limit ) {
119+ return true ;
127120 }
128-
129- $ daily_totals [ $ date ] = $ current_total + $ quantity ;
130121 }
131122
132- unset( $ field );
123+ return false ;
124+ }
133125
134- if ( ! $ is_valid ) {
135- $ form ['validation_message ' ] = $ this ->capacity_message ;
126+ private function get_daily_totals ( array $ dates , ?int $ exclude_booking_id = null ): array {
127+ $ dates = array_values ( array_filter ( array_unique ( array_map ( 'trim ' , $ dates ) ) ) );
128+ if ( ! $ dates ) {
129+ return array ();
136130 }
137131
138- if ( $ is_object ) {
139- $ result ->form = $ form ;
140- $ result ->is_valid = $ is_valid ;
141- return $ result ;
142- }
132+ $ start_datetime = min ( $ dates ) . ' 00:00:00 ' ;
133+ $ end_datetime = max ( $ dates ) . ' 23:59:59 ' ;
143134
144- $ result ['form ' ] = $ form ;
145- $ result ['is_valid ' ] = $ is_valid ;
146- return $ result ;
135+ return $ this ->get_totals_for_range ( $ start_datetime , $ end_datetime , $ exclude_booking_id );
147136 }
148137
149- private function get_total_for_date ( string $ date ): int {
150- $ start = $ date . ' 00:00:00 ' ;
151- $ end = $ date . ' 23:59:59 ' ;
152- // Count both pending and confirmed bookings to reflect in-progress reservations.
138+ private function get_totals_for_range ( string $ start_datetime , string $ end_datetime , ?int $ exclude_booking_id = null ): array {
139+ if ( '' === $ start_datetime || '' === $ end_datetime ) {
140+ return array ();
141+ }
142+
153143 $ bookings = \GP_Bookings \Queries \Booking_Query::get_bookings_in_range (
154- $ start ,
155- $ end ,
144+ $ start_datetime ,
145+ $ end_datetime ,
156146 array (
157147 'object_id ' => $ this ->service_ids ,
158148 'object_type ' => 'service ' ,
159149 'status ' => array ( 'pending ' , 'confirmed ' ),
160150 'exclude_service_with_resource ' => false ,
151+ 'exclude_booking_id ' => $ exclude_booking_id ,
161152 )
162153 );
163154
164- $ total = 0 ;
155+ if ( ! $ bookings ) {
156+ return array ();
157+ }
158+
159+ $ totals = array ();
165160
166161 foreach ( $ bookings as $ booking ) {
167- $ total += (int ) $ booking ->get_quantity ();
162+ try {
163+ $ service_id = (int ) $ booking ->get_service_id ();
164+ } catch ( \Throwable $ e ) {
165+ continue ;
166+ }
167+
168+ if ( ! $ this ->is_tracked_service ( $ service_id ) ) {
169+ continue ;
170+ }
171+
172+ $ service = \GP_Bookings \Service::get ( $ service_id );
173+ if ( ! $ service ) {
174+ continue ;
175+ }
176+
177+ $ date = $ this ->normalize_booking_date (
178+ $ booking ->get_start_datetime (),
179+ $ booking ->get_end_datetime (),
180+ $ service
181+ );
182+
183+ if ( ! $ date ) {
184+ continue ;
185+ }
186+
187+ $ totals [ $ date ] = ( $ totals [ $ date ] ?? 0 ) + (int ) $ booking ->get_quantity ();
168188 }
169189
170- return $ total ;
190+ return $ totals ;
191+ }
192+
193+ private function is_tracked_service ( int $ service_id ): bool {
194+ return in_array ( $ service_id , $ this ->service_ids , true );
171195 }
172196
173197 private function normalize_booking_date ( $ start , $ end , $ bookable ): ?string {
@@ -180,34 +204,12 @@ private function normalize_booking_date( $start, $end, $bookable ): ?string {
180204 return $ normalized ['start ' ]->format ( 'Y-m-d ' );
181205 }
182206
183- private function get_posted_value ( int $ field_id ) {
184- $ value = rgpost ( 'input_ ' . $ field_id );
185-
186- if ( is_array ( $ value ) ) {
187- $ value = reset ( $ value );
188- }
189-
190- return $ value === null || $ value === '' ? null : $ value ;
191- }
192-
193- private function flag_field_error ( array &$ form , int $ field_id ): void {
194- foreach ( $ form ['fields ' ] as &$ field ) {
195- if ( (int ) $ field ->id === $ field_id ) {
196- $ field ->failed_validation = true ;
197- $ field ->validation_message = $ this ->capacity_message ;
198- break ;
199- }
200- }
201-
202- unset( $ field );
203- }
204-
205207}
206208
207209# Configuration
208- new GPB_Daily_Service_Limit ( array (
209- ' form_id ' => 123 ,
210- 'service_ids ' => array ( 45 , 67 ),
211- 'daily_limit ' => 10 ,
212- // 'capacity_message' => '',
213- ) ) ;
210+ new GPB_Daily_Service_Limit (
211+ array (
212+ 'service_ids ' => array ( 123 , 456 ), // Enter one or more service IDs
213+ 'daily_limit ' => 10 , // Enter the daily limit
214+ )
215+ );
0 commit comments