Skip to content

Commit 11b6517

Browse files
authored
gpb-daily-service-booking-limit.php: Added new snippet
1 parent 1ea2f3f commit 11b6517

File tree

1 file changed

+120
-118
lines changed

1 file changed

+120
-118
lines changed

gp-bookings/gpb-daily-service-booking-limit.php

Lines changed: 120 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,47 @@
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
*/
2119
class 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

Comments
 (0)