Skip to content

gpb-sync-bookings-on-entry-trash-restore.php: Added conflict handling and orphan retry for trash/restore booking sync and refactored to class-based snippet.#1237

Open
SebastianWiz wants to merge 4 commits into
masterfrom
SebastianWiz-patch-9
Open

gpb-sync-bookings-on-entry-trash-restore.php: Added conflict handling and orphan retry for trash/restore booking sync and refactored to class-based snippet.#1237
SebastianWiz wants to merge 4 commits into
masterfrom
SebastianWiz-patch-9

Conversation

@SebastianWiz
Copy link
Copy Markdown
Contributor

Context

⛑️ Ticket(s): https://secure.helpscout.net/conversation/3224123263/97555?viewId=8614371

Summary

This PR improves trash/restore booking sync by handling conflicts and recovering orphaned entries. Previously, restoring a trashed entry whose time slot was already rebooked would silently fail, leaving an entry with no booking and no way to recover.

Changes:

  • Adds an entry note when a restored booking can't be recreated due to a conflict.
  • Flags orphaned entries and automatically retries booking creation when a conflicting slot is freed.
  • Refactored to class based snippet.

…ng and orphan retry for trash/restore booking sync and refactored to class-based snippet.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 18, 2026

Walkthrough

Refactors entry trash/restore handling into a new GPB_Trash_Sync singleton that tracks deleted bookings via a meta flag, retries recreation of orphaned bookings, logs conflicts/notes, and surfaces transient admin notices for retry/results.

Changes

Cohort / File(s) Summary
Trash/Restore Booking Sync
gp-bookings/gpb-sync-bookings-on-entry-trash-restore.php
Replaces inline hook with GPB_Trash_Sync singleton. Adds handle_status_change, handle_trash, handle_restore, retry_orphaned_bookings, flag meta constant (FLAG_KEY), transient-based admin flash notices (queue_notice, ajax_flash_notices, print_flash_script), logging helpers, and instantiates via GPB_Trash_Sync::get_instance().

Sequence Diagram(s)

sequenceDiagram
  participant AdminUI as Admin UI
  participant GFEntry as Gravity Forms Entry
  participant GPB as GPB_Trash_Sync
  participant Booking as Booking System
  participant DB as Database/Meta

  AdminUI->>GFEntry: Trash or Restore entry
  GFEntry->>GPB: trigger handle_status_change(entry_id, new, old)
  GPB->>DB: set/clear FLAG_KEY meta on trash/restore
  GPB->>Booking: attempt delete bookings on trash
  Booking-->>GPB: success / conflict error
  alt conflict or failure
    GPB->>GFEntry: add note (conflict/error)
    GPB->>DB: keep FLAG_KEY for retry
  else success
    GPB->>DB: clear FLAG_KEY
    GPB->>GFEntry: add success note
  end
  GPB->>GPB: retry_orphaned_bookings(form_id, skip_id) (on trash)
  GPB->>Booking: recreate orphaned bookings for flagged entries
  Booking-->>GPB: result
  GPB->>AdminUI: queue transient notices (ajax_flash_notices / print_flash_script)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • saifsultanc
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: conflict handling, orphan retry mechanism, and class-based refactoring of the trash/restore booking sync functionality.
Description check ✅ Passed The description includes the required Context section with a Help Scout ticket link and a Summary section clearly explaining the improvements, changes made, and refactoring.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SebastianWiz-patch-9

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.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 18, 2026

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

Generated by 🚫 dangerJS against d0ddc1e

…ng and orphan retry for trash/restore booking sync and refactored to class-based snippet.
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@gp-bookings/gpb-sync-bookings-on-entry-trash-restore.php`:
- Around line 92-104: GFAPI::get_entries() can return WP_Error, so guard the
$entries result before iterating: after the GFAPI::get_entries(...) call, check
is_wp_error($entries) and handle it (e.g., log the error via error_log or a
logger and return early), and also ensure $entries is an array
(is_array($entries)) before running the foreach that processes entries; update
the code around the GFAPI::get_entries call and the iteration to perform these
checks (reference: GFAPI::get_entries and the $entries variable and the
subsequent foreach).
- Line 100: The inline associative array "( 'key' => 'date_created', 'direction'
=> 'ASC' )" triggers the PHPCS rule; replace that inline array with a multiline
array declaration so each key/value is on its own line and include a trailing
comma, e.g. change array( 'key' => 'date_created', 'direction' => 'ASC' ) to a
multiline array( \n    'key' => 'date_created',\n    'direction' => 'ASC',\n) at
the call site where the array is passed (the array literal in the code that ends
with "), array( 'key' => 'date_created', 'direction' => 'ASC' ) ); ).

Comment thread gp-bookings/gpb-sync-bookings-on-entry-trash-restore.php
Comment thread gp-bookings/gpb-sync-bookings-on-entry-trash-restore.php Outdated
…ng and orphan retry for trash/restore booking sync and refactored to class-based snippet.
…ng and orphan retry for trash/restore booking sync and refactored to class-based snippet.
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@gp-bookings/gpb-sync-bookings-on-entry-trash-restore.php`:
- Around line 157-165: The ajax_flash_notices() endpoint currently only calls
check_ajax_referer() but doesn't check the current user's permissions; update
ajax_flash_notices to verify an appropriate capability (e.g.,
current_user_can('gform_edit_entries') or another Gravity Forms entry access
capability) before reading/deleting the transient (use the same transient_key()
reference) and returning notices; if the capability check fails, respond with
wp_send_json_error() or wp_die() to deny access.
- Around line 85-92: The catch block currently treats every \Throwable as a
"slot occupied" conflict; change the handler in the catch (\Throwable $e) around
the recreate-on-restore logic to distinguish real booking-conflict errors from
other exceptions (e.g. check for a BookingConflictException type or implement an
is_conflict_exception($e) check that inspects exception class/code/message), and
only call GFFormsModel::add_note(..., 'gpb_restore_conflict') and the
conflict-specific queue_notice/log when that check is true; otherwise add a
generic failure note via GFFormsModel::add_note with the actual error message,
emit a non-conflict queue_notice, and log the full exception using $this->log so
non-conflict errors are not mislabeled.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 962af08 and d0ddc1e.

📒 Files selected for processing (1)
  • gp-bookings/gpb-sync-bookings-on-entry-trash-restore.php

Comment on lines 85 to +92
} catch ( \Throwable $e ) {
if ( function_exists( 'gp_bookings' ) ) {
gp_bookings()->log_debug( sprintf( 'Restore recreation failed for entry %d: %s', $entry_id, $e->getMessage() ) );
$note = sprintf(
'Booking could not be recreated on restore — the time slot is occupied by another entry. This entry will be automatically rebooked if the conflicting booking is freed. (Error: %s)',
$e->getMessage()
);
GFFormsModel::add_note( $entry_id, 0, 'GP Bookings', $note, 'gpb_restore_conflict' );
$this->queue_notice( 'error', sprintf( 'Could not recreate booking for entry #%d — slot is occupied. Automatic retry enabled.', $entry_id ) );
$this->log( sprintf( 'Restore recreation failed for entry %d: %s', $entry_id, $e->getMessage() ) );
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid labeling every restore failure as a slot conflict.

On Line 85-Line 92, all \Throwable paths are treated as “slot occupied”. That can misclassify non-conflict failures (DB/runtime/plugin errors) and leave misleading entry notes.

💡 Suggested adjustment
 		} catch ( \Throwable $e ) {
-			$note = sprintf(
-				'Booking could not be recreated on restore — the time slot is occupied by another entry. This entry will be automatically rebooked if the conflicting booking is freed. (Error: %s)',
-				$e->getMessage()
-			);
+			$note = sprintf(
+				'Booking could not be recreated on restore due to an unexpected error. Automatic retry remains enabled. (Error: %s)',
+				$e->getMessage()
+			);
 			GFFormsModel::add_note( $entry_id, 0, 'GP Bookings', $note, 'gpb_restore_conflict' );
-			$this->queue_notice( 'error', sprintf( 'Could not recreate booking for entry #%d — slot is occupied. Automatic retry enabled.', $entry_id ) );
+			$this->queue_notice( 'error', sprintf( 'Could not recreate booking for entry #%d. Automatic retry enabled.', $entry_id ) );
 			$this->log( sprintf( 'Restore recreation failed for entry %d: %s', $entry_id, $e->getMessage() ) );
 		}
📝 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
} catch ( \Throwable $e ) {
if ( function_exists( 'gp_bookings' ) ) {
gp_bookings()->log_debug( sprintf( 'Restore recreation failed for entry %d: %s', $entry_id, $e->getMessage() ) );
$note = sprintf(
'Booking could not be recreated on restore — the time slot is occupied by another entry. This entry will be automatically rebooked if the conflicting booking is freed. (Error: %s)',
$e->getMessage()
);
GFFormsModel::add_note( $entry_id, 0, 'GP Bookings', $note, 'gpb_restore_conflict' );
$this->queue_notice( 'error', sprintf( 'Could not recreate booking for entry #%d — slot is occupied. Automatic retry enabled.', $entry_id ) );
$this->log( sprintf( 'Restore recreation failed for entry %d: %s', $entry_id, $e->getMessage() ) );
} catch ( \Throwable $e ) {
$note = sprintf(
'Booking could not be recreated on restore due to an unexpected error. Automatic retry remains enabled. (Error: %s)',
$e->getMessage()
);
GFFormsModel::add_note( $entry_id, 0, 'GP Bookings', $note, 'gpb_restore_conflict' );
$this->queue_notice( 'error', sprintf( 'Could not recreate booking for entry #%d. Automatic retry enabled.', $entry_id ) );
$this->log( sprintf( 'Restore recreation failed for entry %d: %s', $entry_id, $e->getMessage() ) );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gp-bookings/gpb-sync-bookings-on-entry-trash-restore.php` around lines 85 -
92, The catch block currently treats every \Throwable as a "slot occupied"
conflict; change the handler in the catch (\Throwable $e) around the
recreate-on-restore logic to distinguish real booking-conflict errors from other
exceptions (e.g. check for a BookingConflictException type or implement an
is_conflict_exception($e) check that inspects exception class/code/message), and
only call GFFormsModel::add_note(..., 'gpb_restore_conflict') and the
conflict-specific queue_notice/log when that check is true; otherwise add a
generic failure note via GFFormsModel::add_note with the actual error message,
emit a non-conflict queue_notice, and log the full exception using $this->log so
non-conflict errors are not mislabeled.

Comment on lines +157 to +165
public function ajax_flash_notices() {
check_ajax_referer( 'gpb_flash_notices', 'nonce' );

$notices = get_transient( $this->transient_key() );
delete_transient( $this->transient_key() );

wp_send_json_success( array(
'notices' => is_array( $notices ) ? $notices : array(),
) );
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add authorization check to the AJAX notice endpoint.

On Line 157-Line 165, check_ajax_referer() validates intent but not permissions. Please gate this action with a capability check (e.g., Gravity Forms entry access capability) before returning notices.

🔐 Suggested adjustment
 	public function ajax_flash_notices() {
 		check_ajax_referer( 'gpb_flash_notices', 'nonce' );
+		if ( ! current_user_can( 'gravityforms_view_entries' ) ) {
+			wp_send_json_error( array( 'message' => 'Unauthorized.' ), 403 );
+		}
 
 		$notices = get_transient( $this->transient_key() );
 		delete_transient( $this->transient_key() );
📝 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
public function ajax_flash_notices() {
check_ajax_referer( 'gpb_flash_notices', 'nonce' );
$notices = get_transient( $this->transient_key() );
delete_transient( $this->transient_key() );
wp_send_json_success( array(
'notices' => is_array( $notices ) ? $notices : array(),
) );
public function ajax_flash_notices() {
check_ajax_referer( 'gpb_flash_notices', 'nonce' );
if ( ! current_user_can( 'gravityforms_view_entries' ) ) {
wp_send_json_error( array( 'message' => 'Unauthorized.' ), 403 );
}
$notices = get_transient( $this->transient_key() );
delete_transient( $this->transient_key() );
wp_send_json_success( array(
'notices' => is_array( $notices ) ? $notices : array(),
) );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gp-bookings/gpb-sync-bookings-on-entry-trash-restore.php` around lines 157 -
165, The ajax_flash_notices() endpoint currently only calls check_ajax_referer()
but doesn't check the current user's permissions; update ajax_flash_notices to
verify an appropriate capability (e.g., current_user_can('gform_edit_entries')
or another Gravity Forms entry access capability) before reading/deleting the
transient (use the same transient_key() reference) and returning notices; if the
capability check fails, respond with wp_send_json_error() or wp_die() to deny
access.

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.

1 participant