Skip to content

Conversation

SebastianWiz
Copy link
Contributor

Context

⛑️ Ticket(s): https://secure.helpscout.net/conversation/3084918923/89679?viewId=8172236

Summary

This PR adds a snippet that lets you set a daily booking limit for one or multiple services. The snippet hooks into Gravity Forms validation to block submissions once the combined quantity for that day reaches the limit and shows the “fully booked” validation message on the booking-time field. It also hooks gpb_before_booking_created so the same check runs during booking creation, preventing overbookings if multiple requests land at the same time.

Loom demo: https://www.loom.com/share/6cae41c053a647e48deec68868e23ef4

Copy link

coderabbitai bot commented Sep 26, 2025

Walkthrough

Adds new class GPB_Daily_Service_Limit that enforces a shared per-day booking cap across configured GP Bookings services by registering a pre-create guard and a Gravity Forms submission validator, normalizing booking dates, counting existing bookings per date, caching per-submission totals, flagging field errors, and throwing CapacityException on overages.

Changes

Cohort / File(s) Summary
Daily booking limit feature
gp-bookings/gpb-daily-service-booking-limit.php
New class GPB_Daily_Service_Limit with __construct(array $args), guard_booking_creation(array $booking_data, $bookable), validate_submission($result), and helpers: get_total_for_date(string $date), normalize_booking_date($start, $end, $bookable), get_posted_value(int $field_id), flag_field_error(array &$form, int $field_id). Registers hooks (gpb_before_booking_created, gform_validation), maintains per-submission totals cache, and includes example instantiation at file bottom.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant GF as Gravity Forms
    participant Validator as GPB_Daily_Service_Limit.validate_submission
    participant DB as Booking Store
    participant Guard as GPB_Daily_Service_Limit.guard_booking_creation

    User->>GF: Submit form
    GF->>Validator: Run validation
    loop Each booking field/date in submission
        Validator->>Validator: resolve posted service/time and qty
        Validator->>DB: get_total_for_date(date) (uses cache when available)
        DB-->>Validator: current count
        Validator->>Validator: accumulate submission total
        alt Accumulated + existing > daily_limit
            Validator->>GF: flag field error and set form message
        end
    end
    alt Validation passes
        GF->>Guard: gpb_before_booking_created hook
        Guard->>Guard: normalize_booking_date(start,end,bookable)
        Guard->>DB: get_total_for_date(date)
        DB-->>Guard: current count
        alt Would exceed daily_limit
            Guard-->>GF: throw CapacityException (prevent create)
        else
            Guard-->>GF: allow booking creation
        end
    else Validation fails
        GF-->>User: show validation errors
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title references the file and notes a snippet addition, which aligns with the change but does not summarize the core feature (daily booking limit), making it only partially descriptive yet still related enough to pass.
Description Check ✅ Passed The description adheres to the repository template by including a Context section with the ticket link and a concise Summary that explains the new snippet’s behavior, its integration points, and provides a demonstration link while omitting unused template lines.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SebastianWiz-patch-3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

github-actions bot commented Sep 26, 2025

Fails
🚫

Commit message 'gpb-daily-service-booking-limit.php: Added new snippet' needs to end in a period or exclamation.

🚫

Commit message 'gpb-daily-service-booking-limit.php: Added a new snippet' needs to end in a period or exclamation.

Warnings
⚠️ When ready, don't forget to request reviews on this pull request from your fellow wizards.

Generated by 🚫 dangerJS against 1ea2f3f

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fd3967 and 5ef5330.

📒 Files selected for processing (1)
  • gp-bookings/gpb-daily-service-booking-limit.php (1 hunks)
🧰 Additional context used
🪛 GitHub Check: PHPCS (Files Changed)
gp-bookings/gpb-daily-service-booking-limit.php

[warning] 209-209:
Array double arrow not aligned correctly; expected 5 space(s) between "'form_id'" and double arrow, but found 1.


[warning] 151-151:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 6 spaces


[warning] 150-150:
Equals sign not aligned with surrounding assignments; expected 1 space but found 4 spaces


[failure] 33-33:
Tabs must be used to indent lines; spaces are not allowed


[failure] 32-32:
Tabs must be used to indent lines; spaces are not allowed


[failure] 31-31:
Tabs must be used to indent lines; spaces are not allowed


[failure] 30-30:
Tabs must be used to indent lines; spaces are not allowed


[failure] 29-29:
Tabs must be used to indent lines; spaces are not allowed


[failure] 28-28:
Tabs must be used to indent lines; spaces are not allowed


[failure] 26-26:
Tabs must be used to indent lines; spaces are not allowed


[failure] 25-25:
Tabs must be used to indent lines; spaces are not allowed


[failure] 24-24:
Tabs must be used to indent lines; spaces are not allowed


[failure] 23-23:
Tabs must be used to indent lines; spaces are not allowed

🪛 GitHub Actions: PHP Lint (PR)
gp-bookings/gpb-daily-service-booking-limit.php

[error] 23-23: Tabs must be used to indent lines; spaces are not allowed (Generic.WhiteSpace.DisallowSpaceIndent.SpacesUsed). PHPCS check failed.

🔇 Additional comments (2)
gp-bookings/gpb-daily-service-booking-limit.php (2)

54-69: Good concurrency guard before booking creation.

Validates against the cap with normalized dates and throws a CapacityException to prevent races. Solid safeguard.


112-116: Confirm expected behavior for multi‑day bookings.

Currently, the date normalized from start/end is reduced to the start day only. If multi‑day bookings should decrement capacity for each day in the range, this needs a loop over all dates in the span.

Do you intend to count only the start day, or every day in a multi‑day booking’s range?

Comment on lines 97 to 105
$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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix service resolution for “Select Service” fields (use posted value).

Using only $service->gpbService fails when the Booking field is configured as “Select Service” (no static service). Read the posted service ID as a fallback so validation applies to user-selected services.

Apply this diff:

-            $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 );
+            // Resolve the target service. For "Single Service", gpbService is set.
+            // For "Select Service", read the posted value.
+            $service_id = isset( $service->gpbService ) ? (int) $service->gpbService : 0;
+            if ( ! $service_id ) {
+                $service_id = (int) $this->get_posted_value( (int) $service->id );
+            }
+            if ( ! $service_id || ! in_array( $service_id, $this->service_ids, true ) ) {
+                continue;
+            }
+            $service_model = \GP_Bookings\Service::get( $service_id );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$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;
}
// Resolve the target service. For "Single Service", gpbService is set.
// For "Select Service", read the posted value.
$service_id = isset( $service->gpbService ) ? (int) $service->gpbService : 0;
if ( ! $service_id ) {
$service_id = (int) $this->get_posted_value( (int) $service->id );
}
if ( ! $service_id || ! in_array( $service_id, $this->service_ids, true ) ) {
continue;
}
$service_model = \GP_Bookings\Service::get( $service_id );
if ( ! $service_model ) {
continue;
}
🤖 Prompt for AI Agents
In gp-bookings/gpb-daily-service-booking-limit.php around lines 97 to 105, the
code only reads $service->gpbService which breaks when the field is configured
as “Select Service”; modify the resolution to fall back to the posted service
value: if $service->gpbService is empty, read the posted service ID (cast to
int) from the request payload for that field (e.g. the gpbService form input),
then use that ID for the in_array check and to load \GP_Bookings\Service::get;
ensure you treat missing/invalid posted values as 0 and continue early as
before.

Comment on lines 117 to 119
$quantity = rgpost( 'input_' . (int) $field->id . '_3' );
$quantity = $quantity === null || $quantity === '' ? 1 : max( 1, (int) $quantity );

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t hard‑code the quantity input index; resolve the Quantity child field.

Referencing input_{field->id}_3 is brittle and can miscount quantities, leading to overbooking. Use the Booking field’s Quantity child (with a legacy fallback).

Apply this diff:

-            $quantity = rgpost( 'input_' . (int) $field->id . '_3' );
-            $quantity = $quantity === null || $quantity === '' ? 1 : max( 1, (int) $quantity );
+            $quantity_field = $children['quantity'] ?? null;
+            $quantity_value = $quantity_field ? $this->get_posted_value( (int) $quantity_field->id ) : null;
+            // Fallback for legacy compound input indexing if needed.
+            if ( $quantity_value === null ) {
+                $quantity_value = rgpost( 'input_' . (int) $field->id . '_3' );
+            }
+            $quantity = $quantity_value === null || $quantity_value === '' ? 1 : max( 1, (int) $quantity_value );
🤖 Prompt for AI Agents
In gp-bookings/gpb-daily-service-booking-limit.php around lines 117-119, the
code currently reads the quantity from a hard-coded child input index
("input_{field->id}_3"), which is brittle; instead locate the Booking field's
Quantity child field (use the field object's get_child_by_type or equivalent
helper, falling back to the legacy index if not found), build the input name
from that child's id (e.g. 'input_' . (int)$field->id . '_' .
(int)$quantity_child->id or use the child's input name helper), then read rgpost
from that resolved input, and finally apply the same defaulting logic (treat
null/empty as 1 and coerce to max(1,(int)$value)). Ensure the fallback preserves
existing behavior when a Quantity child cannot be resolved.

@SebastianWiz SebastianWiz changed the title gpb-daily-service-booking-limit.php: Added new snippet gpb-daily-service-booking-limit.php: Added new snippet. Sep 26, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a28114 and 1ea2f3f.

📒 Files selected for processing (1)
  • gp-bookings/gpb-daily-service-booking-limit.php (1 hunks)
🧰 Additional context used
🪛 GitHub Check: PHPCS (Files Changed)
gp-bookings/gpb-daily-service-booking-limit.php

[warning] 209-209:
Array double arrow not aligned correctly; expected 5 space(s) between "'form_id'" and double arrow, but found 1.


[failure] 193-193:
void return type is not present in PHP version 7.0 or earlier


[failure] 173-173:
Nullable return types are not supported in PHP 7.0 or earlier.


[failure] 82-82:
Short array syntax is not allowed

🪛 GitHub Actions: PHP Lint (PR)
gp-bookings/gpb-daily-service-booking-limit.php

[error] 82-82: PHPCS: Short array syntax is not allowed (Generic.Arrays.DisallowShortArraySyntax.Found)

🔇 Additional comments (2)
gp-bookings/gpb-daily-service-booking-limit.php (2)

97-105: Don't skip Select Service bookings when enforcing the cap

When the booking field uses “Select Service,” gpbService is empty, so $service_id stays 0 and we skip validation entirely, effectively disabling the limit for those forms. Please fall back to the posted child value before bailing out.

-			$service_id = isset( $service->gpbService ) ? (int) $service->gpbService : 0;
-			if ( ! $service_id || ! in_array( $service_id, $this->service_ids, true ) ) {
+			$service_id = isset( $service->gpbService ) ? (int) $service->gpbService : 0;
+			if ( ! $service_id ) {
+				$posted_service = $this->get_posted_value( (int) $service->id );
+				$service_id     = $posted_service ? (int) $posted_service : 0;
+			}
+			if ( ! $service_id || ! in_array( $service_id, $this->service_ids, true ) ) {

117-129: Quantity lookup must resolve the actual child input

Hard-coding _3 for the quantity input breaks as soon as the field is reordered or uses the Quantity child. That causes us to undercount bookings and miss the cap. Resolve the quantity child and fall back to the legacy index only if needed.

-			$quantity = rgpost( 'input_' . (int) $field->id . '_3' );
-			$quantity = $quantity === null || $quantity === '' ? 1 : max( 1, (int) $quantity );
+			$quantity_field = $children['quantity'] ?? null;
+			$quantity_value = $quantity_field ? $this->get_posted_value( (int) $quantity_field->id ) : null;
+			if ( $quantity_value === null ) {
+				$quantity_value = rgpost( 'input_' . (int) $field->id . '_3' );
+			}
+			$quantity = $quantity_value === null || $quantity_value === '' ? 1 : max( 1, (int) $quantity_value );

$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 = [];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Replace [] with array() to satisfy PHPCS

Our coding standard forbids short array syntax and the lint job is failing on this line. Switch to array().

-		$daily_totals = [];
+		$daily_totals = array();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$daily_totals = [];
$daily_totals = array();
🧰 Tools
🪛 GitHub Check: PHPCS (Files Changed)

[failure] 82-82:
Short array syntax is not allowed

🪛 GitHub Actions: PHP Lint (PR)

[error] 82-82: PHPCS: Short array syntax is not allowed (Generic.Arrays.DisallowShortArraySyntax.Found)

🤖 Prompt for AI Agents
In gp-bookings/gpb-daily-service-booking-limit.php around line 82 the short
array syntax is used ($daily_totals = [];), which violates PHPCS rules; replace
the short syntax with the long form by assigning $daily_totals = array(); and
save/run the linter to confirm PHPCS passes.

Comment on lines +173 to +201
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;
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove PHP 7.1-only return types

?string and : void aren’t allowed under our PHP 7.0 minimum and PHPCS is failing. Drop those return type declarations so the snippet stays compatible.

-	private function normalize_booking_date( $start, $end, $bookable ): ?string {
+	private function normalize_booking_date( $start, $end, $bookable ) {
@@
-	private function flag_field_error( array &$form, int $field_id ): void {
+	private function flag_field_error( array &$form, int $field_id ) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
}
private function normalize_booking_date( $start, $end, $bookable ) {
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 ) {
foreach ( $form['fields'] as &$field ) {
if ( (int) $field->id === $field_id ) {
$field->failed_validation = true;
$field->validation_message = $this->capacity_message;
break;
}
}
}
🧰 Tools
🪛 GitHub Check: PHPCS (Files Changed)

[failure] 193-193:
void return type is not present in PHP version 7.0 or earlier


[failure] 173-173:
Nullable return types are not supported in PHP 7.0 or earlier.

🤖 Prompt for AI Agents
In gp-bookings/gpb-daily-service-booking-limit.php around lines 173 to 201, the
methods declare PHP 7.1+ return type hints (?string and : void) which break
compatibility with our PHP 7.0 minimum; remove the nullable and void return type
declarations from normalize_booking_date and flag_field_error (and any other
methods in this range using those return types), leaving the functions without
explicit return type hints and keeping their existing behavior and doc/comments
intact.

@spivurno
Copy link
Contributor

@SebastianWiz Strong! How hard/possible would it be to also prevent the day from being selected at all once the limit is reached? I wouldn't sink too much more time into this, just curious if it's an option?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants