Skip to content

Commit c8ed9f2

Browse files
authored
gpb-daily-service-booking-limit.php: Added a new snippet that limits the number of bookings allowed per day.
1 parent 99b672c commit c8ed9f2

File tree

1 file changed

+220
-0
lines changed

1 file changed

+220
-0
lines changed
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
<?php
2+
/**
3+
* Gravity Perks // GP Bookings // Daily Service Booking Limit
4+
* https://gravitywiz.com/documentation/gravity-forms-bookings/
5+
*
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.
9+
*
10+
* Instructions:
11+
*
12+
* 1. Install this snippet by following the steps here:
13+
* https://gravitywiz.com/documentation/how-do-i-install-a-snippet/
14+
*
15+
* 2. Update the configuration at the bottom of the snippet:
16+
* - List the GP Bookings service IDs that should share the daily cap in service_ids.
17+
* - Adjust daily_limit to the maximum combined bookings allowed per day.
18+
*/
19+
class GPB_Daily_Service_Limit {
20+
21+
private $service_ids;
22+
private $daily_limit;
23+
24+
public function __construct( array $args ) {
25+
$args = wp_parse_args( $args, array(
26+
'service_ids' => array(),
27+
'daily_limit' => 10,
28+
));
29+
30+
$this->service_ids = array_map( 'intval', (array) $args['service_ids'] );
31+
$this->daily_limit = (int) $args['daily_limit'];
32+
33+
if ( empty( $this->service_ids ) || $this->daily_limit < 1 ) {
34+
return;
35+
}
36+
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 );
40+
}
41+
42+
public function guard_booking_creation( array $booking_data, $bookable ) {
43+
if ( ! ( $bookable instanceof \GP_Bookings\Service ) || ! $this->is_tracked_service( $bookable->get_id() ) ) {
44+
return;
45+
}
46+
47+
$date = $this->normalize_booking_date(
48+
$booking_data['start_datetime'] ?? '',
49+
$booking_data['end_datetime'] ?? ( $booking_data['start_datetime'] ?? '' ),
50+
$bookable
51+
);
52+
if ( ! $date ) {
53+
return;
54+
}
55+
56+
$quantity = isset( $booking_data['quantity'] ) ? max( 1, (int) $booking_data['quantity'] ) : 1;
57+
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' ) );
61+
}
62+
}
63+
64+
public function filter_rest_availability( $response, $server, $request ) {
65+
if ( ! ( $request instanceof \WP_REST_Request ) || 'GET' !== $request->get_method() ) {
66+
return $response;
67+
}
68+
69+
$route = ltrim( $request->get_route(), '/' );
70+
if ( 'gp-bookings/v1/availability/days' !== $route ) {
71+
return $response;
72+
}
73+
74+
$service_id = (int) $request->get_param( 'serviceId' );
75+
if ( ! $service_id || ! $this->is_tracked_service( $service_id ) ) {
76+
return $response;
77+
}
78+
79+
if ( is_wp_error( $response ) || ! ( $response instanceof \WP_HTTP_Response ) ) {
80+
return $response;
81+
}
82+
83+
$data = $response->get_data();
84+
if ( empty( $data['days'] ) || ! is_array( $data['days'] ) ) {
85+
return $response;
86+
}
87+
88+
$dates = array_keys( $data['days'] );
89+
if ( ! $dates ) {
90+
return $response;
91+
}
92+
93+
$exclude_booking_id = (int) $request->get_param( 'exclude_booking_id' );
94+
$exclude_booking_id = $exclude_booking_id > 0 ? $exclude_booking_id : null;
95+
96+
$totals = $this->get_daily_totals( $dates, $exclude_booking_id );
97+
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;
104+
}
105+
}
106+
unset( $day );
107+
108+
$response->set_data( $data );
109+
return $response;
110+
}
111+
112+
private function exceeds_limit( array $dates, int $incoming_quantity = 0, $exclude_booking_id = null ): bool {
113+
$dates = array_filter( array_unique( $dates ) );
114+
$totals = $dates ? $this->get_daily_totals( $dates, $exclude_booking_id ) : array();
115+
116+
foreach ( $dates as $date ) {
117+
$existing_total = $totals[ $date ] ?? 0;
118+
if ( $existing_total + $incoming_quantity > $this->daily_limit ) {
119+
return true;
120+
}
121+
}
122+
123+
return false;
124+
}
125+
126+
private function get_daily_totals( array $dates, $exclude_booking_id = null ): array {
127+
$dates = array_values( array_filter( array_unique( array_map( 'trim', $dates ) ) ) );
128+
if ( ! $dates ) {
129+
return array();
130+
}
131+
132+
$start_datetime = min( $dates ) . ' 00:00:00';
133+
$end_datetime = max( $dates ) . ' 23:59:59';
134+
135+
return $this->get_totals_for_range( $start_datetime, $end_datetime, $exclude_booking_id );
136+
}
137+
138+
private function get_totals_for_range( string $start_datetime, string $end_datetime, $exclude_booking_id = null ): array {
139+
if ( '' === $start_datetime || '' === $end_datetime ) {
140+
return array();
141+
}
142+
143+
$bookings = \GP_Bookings\Queries\Booking_Query::get_bookings_in_range(
144+
$start_datetime,
145+
$end_datetime,
146+
array(
147+
'object_id' => $this->service_ids,
148+
'object_type' => 'service',
149+
'status' => array( 'pending', 'confirmed' ),
150+
'exclude_service_with_resource' => false,
151+
'exclude_booking_id' => $exclude_booking_id,
152+
)
153+
);
154+
155+
if ( ! $bookings ) {
156+
return array();
157+
}
158+
159+
$totals = array();
160+
161+
foreach ( $bookings as $booking ) {
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();
188+
}
189+
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 );
195+
}
196+
197+
/**
198+
* Normalize booking date.
199+
*
200+
* @return string|null Returns the normalized start date (Y-m-d) or null on failure.
201+
*/
202+
private function normalize_booking_date( $start, $end, $bookable ) {
203+
try {
204+
$normalized = \GP_Bookings\Booking::normalize_datetime_values( $start, $end, $bookable );
205+
} catch ( \Throwable $e ) {
206+
return null;
207+
}
208+
209+
return $normalized['start']->format( 'Y-m-d' );
210+
}
211+
212+
}
213+
214+
# Configuration
215+
new GPB_Daily_Service_Limit(
216+
array(
217+
'service_ids' => array( 123, 456 ), // Enter one or more service IDs
218+
'daily_limit' => 10, // Enter the daily limit
219+
)
220+
);

0 commit comments

Comments
 (0)