From 113df8f9a79846596071bb21a61061da47f2a1b4 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 13 Mar 2026 09:19:10 +1000 Subject: [PATCH 1/6] Prevent Stripe non-card payments from timing out prematurely Delayed payment methods (boleto, OXXO, etc.) can take days to settle. Previously, the 24-hour timeout would mark these attendees as timed out before payment could complete. Changes: - Mark attendees as pending (not completed) when returning from Stripe with an unpaid delayed payment session - Delay timeout for attendees with pending Stripe payments by resetting their timestamp, with a 7-day maximum safety limit - Include pending attendees in the timeout review process - Delay timeout for open Stripe sessions that have not yet expired Fixes #1532 Co-Authored-By: Claude Opus 4.6 --- .../plugins/camptix/addons/payment-stripe.php | 84 +++++++++++++++++-- .../wp-content/plugins/camptix/camptix.php | 6 +- 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/public_html/wp-content/plugins/camptix/addons/payment-stripe.php b/public_html/wp-content/plugins/camptix/addons/payment-stripe.php index 46285b5907..202a2433fa 100644 --- a/public_html/wp-content/plugins/camptix/addons/payment-stripe.php +++ b/public_html/wp-content/plugins/camptix/addons/payment-stripe.php @@ -425,7 +425,6 @@ public function payment_return() { return $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_FAILED, $payment_data ); } - // Success! (status can only be open, or completed) // Technically there can be multiple charges (ie. partial payments / installments) but we don't have that enabled. $transaction_id = $session['payment_intent']['latest_charge'] ?? ''; @@ -440,6 +439,13 @@ public function payment_return() { ), ); + // Delayed payment methods (boleto, OXXO, etc.) complete the session but payment is still pending. + if ( 'complete' === $session['status'] && 'unpaid' === $session['payment_status'] ) { + $camptix->log( 'Stripe checkout complete, payment pending (delayed payment method).', $order['attendee_id'], $session ); + return $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_PENDING, $payment_data ); + } + + // Success! Payment is confirmed. return $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_COMPLETED, $payment_data ); } @@ -641,14 +647,19 @@ public function send_refund_request( $payment_token ) { } /** - * Check if a stripe session timed out. + * Check if a Stripe session should be timed out, or if the timeout should be delayed. + * + * Handles three scenarios: + * 1. The payment completed successfully -- mark the attendee as paid. + * 2. The payment is still pending (delayed payment methods like boleto) -- delay the timeout. + * 3. The session is still open and not expired -- delay the timeout. */ public function pre_attendee_timeout( $attendee_id ) { /** @var CampTix_Plugin $camptix */ global $camptix; - // precheck the attendee is in draft. - if ( 'draft' !== get_post_field( 'post_status', $attendee_id ) ) { + // Only process attendees that are still awaiting payment. + if ( ! in_array( get_post_field( 'post_status', $attendee_id ), array( 'draft', 'pending' ), true ) ) { return; } @@ -665,7 +676,7 @@ public function pre_attendee_timeout( $attendee_id ) { return; } - // Uh oh, we've hit timeout on a ticket, but the linked checkout session succeeded. + // Scenario 1: The checkout session completed and payment is confirmed. if ( 'complete' === $session['status'] && 'paid' === $session['payment_status'] ) { $camptix->log( 'Stripe checkout timed out, but order succeeded.', $attendee_id, $session ); @@ -678,7 +689,70 @@ public function pre_attendee_timeout( $attendee_id ) { ); $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_COMPLETED, $payment_data, false /* non-interactive */ ); + return; } + + // Scenario 2: The checkout session completed but payment is still pending. + // This happens with delayed payment methods like boleto, OXXO, etc. + if ( 'complete' === $session['status'] && 'unpaid' === $session['payment_status'] ) { + $payment_intent_status = $session['payment_intent']['status'] ?? ''; + + // Only delay timeout if the payment is still in a pending state. + if ( in_array( $payment_intent_status, array( 'requires_action', 'processing' ), true ) ) { + $camptix->log( 'Stripe payment still pending, delaying timeout.', $attendee_id, array( + 'payment_intent_status' => $payment_intent_status, + 'payment_status' => $session['payment_status'], + ) ); + + $this->delay_attendee_timeout( $attendee_id ); + return; + } + } + + // Scenario 3: The checkout session is still open and has not expired per Stripe. + if ( 'open' === $session['status'] ) { + $expires_at = $session['expires_at'] ?? 0; + + if ( $expires_at > time() ) { + $camptix->log( 'Stripe session still open, delaying timeout.', $attendee_id, array( + 'expires_at' => $expires_at, + ) ); + + $this->delay_attendee_timeout( $attendee_id ); + return; + } + } + } + + /** + * Delay an attendee's timeout by resetting its timestamp. + * + * This pushes the attendee's timestamp forward so that the timeout review + * will not pick it up again until the next cycle. A maximum age limit of + * 7 days from the original purchase prevents indefinite delays. + * + * @param int $attendee_id The attendee post ID. + */ + protected function delay_attendee_timeout( $attendee_id ) { + $original_timestamp = get_post_meta( $attendee_id, 'tix_timestamp_original', true ); + + // Store the original timestamp if not already saved. + if ( ! $original_timestamp ) { + $original_timestamp = get_post_meta( $attendee_id, 'tix_timestamp', true ); + update_post_meta( $attendee_id, 'tix_timestamp_original', $original_timestamp ); + } + + // Do not delay beyond 7 days from the original purchase. + $max_age = 7 * DAY_IN_SECONDS; + if ( ( time() - $original_timestamp ) >= $max_age ) { + /** @var CampTix_Plugin $camptix */ + global $camptix; + $camptix->log( 'Stripe payment pending too long, allowing timeout.', $attendee_id ); + return; + } + + // Push the timestamp forward to now, so the 24-hour timeout resets. + update_post_meta( $attendee_id, 'tix_timestamp', time() ); } } diff --git a/public_html/wp-content/plugins/camptix/camptix.php b/public_html/wp-content/plugins/camptix/camptix.php index 35de9724a3..1644d4154a 100644 --- a/public_html/wp-content/plugins/camptix/camptix.php +++ b/public_html/wp-content/plugins/camptix/camptix.php @@ -7297,7 +7297,7 @@ function review_timeout_payments() { while ( $attendees = get_posts( array( 'fields' => 'ids', 'post_type' => 'tix_attendee', - 'post_status' => 'draft', + 'post_status' => array( 'draft', 'pending' ), 'posts_per_page' => 100, 'cache_results' => false, 'meta_query' => array( @@ -7319,8 +7319,8 @@ function review_timeout_payments() { foreach ( $attendees as $attendee_id ) { do_action( 'camptix_pre_attendee_timeout', $attendee_id ); - // Check the post_status again, incase a filter has caused the post to change. - if ( 'draft' !== get_post_field( 'post_status', $attendee_id ) ) { + // Check the post_status again, in case a hook has caused the post to change. + if ( ! in_array( get_post_field( 'post_status', $attendee_id ), array( 'draft', 'pending' ), true ) ) { continue; } From 682008d4c12b14f7424862d88bc5642f947308b7 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 13 Mar 2026 09:22:32 +1000 Subject: [PATCH 2/6] Fix PHPCS multi-line function call formatting Co-Authored-By: Claude Opus 4.6 --- .../plugins/camptix/addons/payment-stripe.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public_html/wp-content/plugins/camptix/addons/payment-stripe.php b/public_html/wp-content/plugins/camptix/addons/payment-stripe.php index 202a2433fa..6eb18a172d 100644 --- a/public_html/wp-content/plugins/camptix/addons/payment-stripe.php +++ b/public_html/wp-content/plugins/camptix/addons/payment-stripe.php @@ -699,10 +699,11 @@ public function pre_attendee_timeout( $attendee_id ) { // Only delay timeout if the payment is still in a pending state. if ( in_array( $payment_intent_status, array( 'requires_action', 'processing' ), true ) ) { - $camptix->log( 'Stripe payment still pending, delaying timeout.', $attendee_id, array( + $log_data = array( 'payment_intent_status' => $payment_intent_status, 'payment_status' => $session['payment_status'], - ) ); + ); + $camptix->log( 'Stripe payment still pending, delaying timeout.', $attendee_id, $log_data ); $this->delay_attendee_timeout( $attendee_id ); return; @@ -714,9 +715,8 @@ public function pre_attendee_timeout( $attendee_id ) { $expires_at = $session['expires_at'] ?? 0; if ( $expires_at > time() ) { - $camptix->log( 'Stripe session still open, delaying timeout.', $attendee_id, array( - 'expires_at' => $expires_at, - ) ); + $log_data = array( 'expires_at' => $expires_at ); + $camptix->log( 'Stripe session still open, delaying timeout.', $attendee_id, $log_data ); $this->delay_attendee_timeout( $attendee_id ); return; From fa68b88676a6ca1646b472fe42d272631a577481 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 28 Apr 2026 13:14:01 +1000 Subject: [PATCH 3/6] Handle completed Stripe sessions on cancel return --- .../plugins/camptix/addons/payment-stripe.php | 75 +++++++++++++------ 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/public_html/wp-content/plugins/camptix/addons/payment-stripe.php b/public_html/wp-content/plugins/camptix/addons/payment-stripe.php index 6eb18a172d..95fec2d3df 100644 --- a/public_html/wp-content/plugins/camptix/addons/payment-stripe.php +++ b/public_html/wp-content/plugins/camptix/addons/payment-stripe.php @@ -364,12 +364,39 @@ public function payment_cancel() { $camptix->log( sprintf( 'Running payment_cancel. Request data attached.' ), null, $_REQUEST ); - $payment_token = $_REQUEST['tix_payment_token'] ?? ''; + $payment_token = wp_unslash( $_REQUEST['tix_payment_token'] ?? '' ); + $stripe_session = wp_unslash( $_REQUEST['tix_stripe_session'] ?? '' ); if ( ! $payment_token ) { wp_die( 'empty token' ); } + $order = $this->get_order( $payment_token ); + if ( $order ) { + if ( ! $stripe_session || '{CHECKOUT_SESSION_ID}' === $stripe_session ) { + $stripe_session = get_post_meta( $order['attendee_id'], '_stripe_checkout_session_id', true ); + } + + if ( $stripe_session ) { + $stripe = new CampTix_Stripe_API_Client( $payment_token, $this->get_api_credentials()['api_secret_key'] ); + $session = $stripe->get_session( $stripe_session ); + + if ( ! is_wp_error( $session ) && ! empty( $session['status'] ) && 'complete' === $session['status'] ) { + $payment_data = $this->get_payment_data_for_session( $session ); + + if ( 'unpaid' === $session['payment_status'] ) { + $camptix->log( 'False alarm on Stripe payment_cancel. Payment is pending.', $order['attendee_id'], $session ); + return $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_PENDING, $payment_data ); + } + + if ( 'paid' === $session['payment_status'] ) { + $camptix->log( 'False alarm on Stripe payment_cancel. Payment is complete.', $order['attendee_id'], $session ); + return $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_COMPLETED, $payment_data ); + } + } + } + } + // Set the associated attendees to cancelled. return $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_CANCELLED ); } @@ -425,19 +452,7 @@ public function payment_return() { return $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_FAILED, $payment_data ); } - // Technically there can be multiple charges (ie. partial payments / installments) but we don't have that enabled. - $transaction_id = $session['payment_intent']['latest_charge'] ?? ''; - - /** - * Note that when returning a successful payment, CampTix will be - * expecting the transaction_id and transaction_details array keys. - */ - $payment_data = array( - 'transaction_id' => $transaction_id, - 'transaction_details' => array( - 'raw' => $session, - ), - ); + $payment_data = $this->get_payment_data_for_session( $session ); // Delayed payment methods (boleto, OXXO, etc.) complete the session but payment is still pending. if ( 'complete' === $session['status'] && 'unpaid' === $session['payment_status'] ) { @@ -514,6 +529,7 @@ public function payment_checkout( $payment_token ) { 'tix_action' => 'payment_cancel', 'tix_payment_token' => $payment_token, 'tix_payment_method' => 'stripe', + 'tix_stripe_session' => '{CHECKOUT_SESSION_ID}', ), $camptix->get_tickets_url() ); @@ -680,13 +696,7 @@ public function pre_attendee_timeout( $attendee_id ) { if ( 'complete' === $session['status'] && 'paid' === $session['payment_status'] ) { $camptix->log( 'Stripe checkout timed out, but order succeeded.', $attendee_id, $session ); - $transaction_id = $session['payment_intent']['latest_charge'] ?? ''; - $payment_data = array( - 'transaction_id' => $transaction_id, - 'transaction_details' => array( - 'raw' => $session, - ), - ); + $payment_data = $this->get_payment_data_for_session( $session ); $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_COMPLETED, $payment_data, false /* non-interactive */ ); return; @@ -754,6 +764,29 @@ protected function delay_attendee_timeout( $attendee_id ) { // Push the timestamp forward to now, so the 24-hour timeout resets. update_post_meta( $attendee_id, 'tix_timestamp', time() ); } + + /** + * Get the payment data CampTix stores for a Stripe checkout session. + * + * @param array $session The Stripe checkout session. + * + * @return array + */ + protected function get_payment_data_for_session( $session ) { + // Technically there can be multiple charges (ie. partial payments / installments) but we don't have that enabled. + $transaction_id = $session['payment_intent']['latest_charge'] ?? ''; + + /** + * Note that when returning a successful payment, CampTix will be + * expecting the transaction_id and transaction_details array keys. + */ + return array( + 'transaction_id' => $transaction_id, + 'transaction_details' => array( + 'raw' => $session, + ), + ); + } } camptix_register_addon( 'CampTix_Payment_Method_Stripe' ); From d000c0139a2cc99516748c5e9850a84f20c32de6 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 28 Apr 2026 13:19:42 +1000 Subject: [PATCH 4/6] Handle Stripe session lookup errors --- .../plugins/camptix/addons/payment-stripe.php | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/public_html/wp-content/plugins/camptix/addons/payment-stripe.php b/public_html/wp-content/plugins/camptix/addons/payment-stripe.php index 95fec2d3df..0879eff51d 100644 --- a/public_html/wp-content/plugins/camptix/addons/payment-stripe.php +++ b/public_html/wp-content/plugins/camptix/addons/payment-stripe.php @@ -379,17 +379,28 @@ public function payment_cancel() { if ( $stripe_session ) { $stripe = new CampTix_Stripe_API_Client( $payment_token, $this->get_api_credentials()['api_secret_key'] ); - $session = $stripe->get_session( $stripe_session ); + $session = $this->get_session_with_retry( $stripe, $stripe_session ); - if ( ! is_wp_error( $session ) && ! empty( $session['status'] ) && 'complete' === $session['status'] ) { + if ( is_wp_error( $session ) ) { + $camptix->log( 'Error during Stripe payment_cancel, failed to fetch session twice.', $order['attendee_id'], $session ); + wp_die( 'A temporary issue has occurred with the payment gateway. The order was not cancelled; please try again in a few minutes.' ); + } + + if ( empty( $session['status'] ) ) { + $camptix->log( "Dying because couldn't get Payment status during Stripe payment_cancel", $order['attendee_id'], compact( 'payment_token', 'stripe_session' ) ); + wp_die( 'could not find payment details' ); + } + + if ( 'complete' === $session['status'] ) { $payment_data = $this->get_payment_data_for_session( $session ); + $payment_status = $session['payment_status'] ?? ''; - if ( 'unpaid' === $session['payment_status'] ) { + if ( 'unpaid' === $payment_status ) { $camptix->log( 'False alarm on Stripe payment_cancel. Payment is pending.', $order['attendee_id'], $session ); return $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_PENDING, $payment_data ); } - if ( 'paid' === $session['payment_status'] ) { + if ( 'paid' === $payment_status ) { $camptix->log( 'False alarm on Stripe payment_cancel. Payment is complete.', $order['attendee_id'], $session ); return $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_COMPLETED, $payment_data ); } @@ -435,7 +446,12 @@ public function payment_return() { // Fetch the Payment details. $stripe = new CampTix_Stripe_API_Client( $payment_token, $this->get_api_credentials()['api_secret_key'] ); - $session = $stripe->get_session( $stripe_session ); + $session = $this->get_session_with_retry( $stripe, $stripe_session ); + + if ( is_wp_error( $session ) ) { + $camptix->log( 'Error during post-stripe return, failed to fetch session twice.', $order['attendee_id'], $session ); + wp_die( 'A temporary issue has occurred with the payment gateway. Your purchase may have been processed; please try again in a few minutes.' ); + } if ( empty( $session['status'] ) ) { $camptix->log( "Dying because couldn't get Payment status", $order['attendee_id'], compact( 'payment_token', 'payment_session' ) ); @@ -453,9 +469,10 @@ public function payment_return() { } $payment_data = $this->get_payment_data_for_session( $session ); + $payment_status = $session['payment_status'] ?? ''; // Delayed payment methods (boleto, OXXO, etc.) complete the session but payment is still pending. - if ( 'complete' === $session['status'] && 'unpaid' === $session['payment_status'] ) { + if ( 'complete' === $session['status'] && 'unpaid' === $payment_status ) { $camptix->log( 'Stripe checkout complete, payment pending (delayed payment method).', $order['attendee_id'], $session ); return $camptix->payment_result( $payment_token, CampTix_Plugin::PAYMENT_STATUS_PENDING, $payment_data ); } @@ -547,7 +564,7 @@ public function payment_checkout( $payment_token ) { 'Got Stripe checkout session.', $order['attendee_id'], array( - 'stripe_payment_logs' => esc_url( 'https://dashboard.stripe.com/payments/' . urlencode( $session['payment_intent'] ) ), + 'stripe_payment_logs' => esc_url( 'https://dashboard.stripe.com/payments/' . urlencode( $session['payment_intent'] ?? '' ) ), 'camptix_payment_token' => $payment_token, 'request_payload' => compact( 'order_items', 'receipt_email' ), 'response' => $session, @@ -686,9 +703,15 @@ public function pre_attendee_timeout( $attendee_id ) { } $stripe = new CampTix_Stripe_API_Client( $payment_token, $this->get_api_credentials()['api_secret_key'] ); - $session = $stripe->get_session( $stripe_session_id ); + $session = $this->get_session_with_retry( $stripe, $stripe_session_id ); + + if ( is_wp_error( $session ) ) { + $camptix->log( 'Stripe session lookup failed during timeout review, delaying timeout.', $attendee_id, $session ); + $this->delay_attendee_timeout( $attendee_id ); + return; + } - if ( is_wp_error( $session ) || empty( $session['status'] ) ) { + if ( empty( $session['status'] ) ) { return; } @@ -765,6 +788,24 @@ protected function delay_attendee_timeout( $attendee_id ) { update_post_meta( $attendee_id, 'tix_timestamp', time() ); } + /** + * Retrieve a Stripe checkout session, retrying once on transient API errors. + * + * @param CampTix_Stripe_API_Client $stripe The Stripe API client. + * @param string $stripe_session The Stripe checkout session ID. + * + * @return array|WP_Error + */ + protected function get_session_with_retry( $stripe, $stripe_session ) { + $session = $stripe->get_session( $stripe_session ); + + if ( is_wp_error( $session ) ) { + $session = $stripe->get_session( $stripe_session ); + } + + return $session; + } + /** * Get the payment data CampTix stores for a Stripe checkout session. * @@ -774,7 +815,8 @@ protected function delay_attendee_timeout( $attendee_id ) { */ protected function get_payment_data_for_session( $session ) { // Technically there can be multiple charges (ie. partial payments / installments) but we don't have that enabled. - $transaction_id = $session['payment_intent']['latest_charge'] ?? ''; + $payment_intent = $session['payment_intent'] ?? array(); + $transaction_id = is_array( $payment_intent ) ? ( $payment_intent['latest_charge'] ?? '' ) : ''; /** * Note that when returning a successful payment, CampTix will be From 04b91449142ca2915b84eb6ca1bd4178e16b045f Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 28 Apr 2026 13:41:40 +1000 Subject: [PATCH 5/6] Address Stripe timeout review feedback --- .../plugins/camptix/addons/payment-stripe.php | 17 ++- .../wp-content/plugins/camptix/camptix.php | 103 ++++++++++++------ .../camptix/tests/test-camptix-admin.php | 49 +++++++++ 3 files changed, 128 insertions(+), 41 deletions(-) diff --git a/public_html/wp-content/plugins/camptix/addons/payment-stripe.php b/public_html/wp-content/plugins/camptix/addons/payment-stripe.php index 0879eff51d..aa5c1dec62 100644 --- a/public_html/wp-content/plugins/camptix/addons/payment-stripe.php +++ b/public_html/wp-content/plugins/camptix/addons/payment-stripe.php @@ -712,11 +712,15 @@ public function pre_attendee_timeout( $attendee_id ) { } if ( empty( $session['status'] ) ) { + $camptix->log( 'Stripe session lookup did not return a status during timeout review, delaying timeout.', $attendee_id, $session ); + $this->delay_attendee_timeout( $attendee_id ); return; } + $payment_status = $session['payment_status'] ?? ''; + // Scenario 1: The checkout session completed and payment is confirmed. - if ( 'complete' === $session['status'] && 'paid' === $session['payment_status'] ) { + if ( 'complete' === $session['status'] && 'paid' === $payment_status ) { $camptix->log( 'Stripe checkout timed out, but order succeeded.', $attendee_id, $session ); $payment_data = $this->get_payment_data_for_session( $session ); @@ -727,14 +731,15 @@ public function pre_attendee_timeout( $attendee_id ) { // Scenario 2: The checkout session completed but payment is still pending. // This happens with delayed payment methods like boleto, OXXO, etc. - if ( 'complete' === $session['status'] && 'unpaid' === $session['payment_status'] ) { - $payment_intent_status = $session['payment_intent']['status'] ?? ''; + if ( 'complete' === $session['status'] && 'unpaid' === $payment_status ) { + $payment_intent = $session['payment_intent'] ?? array(); + $payment_intent_status = is_array( $payment_intent ) ? ( $payment_intent['status'] ?? '' ) : ''; // Only delay timeout if the payment is still in a pending state. if ( in_array( $payment_intent_status, array( 'requires_action', 'processing' ), true ) ) { $log_data = array( 'payment_intent_status' => $payment_intent_status, - 'payment_status' => $session['payment_status'], + 'payment_status' => $payment_status, ); $camptix->log( 'Stripe payment still pending, delaying timeout.', $attendee_id, $log_data ); @@ -767,11 +772,11 @@ public function pre_attendee_timeout( $attendee_id ) { * @param int $attendee_id The attendee post ID. */ protected function delay_attendee_timeout( $attendee_id ) { - $original_timestamp = get_post_meta( $attendee_id, 'tix_timestamp_original', true ); + $original_timestamp = absint( get_post_meta( $attendee_id, 'tix_timestamp_original', true ) ); // Store the original timestamp if not already saved. if ( ! $original_timestamp ) { - $original_timestamp = get_post_meta( $attendee_id, 'tix_timestamp', true ); + $original_timestamp = absint( get_post_meta( $attendee_id, 'tix_timestamp', true ) ); update_post_meta( $attendee_id, 'tix_timestamp_original', $original_timestamp ); } diff --git a/public_html/wp-content/plugins/camptix/camptix.php b/public_html/wp-content/plugins/camptix/camptix.php index 1644d4154a..fc990a1510 100644 --- a/public_html/wp-content/plugins/camptix/camptix.php +++ b/public_html/wp-content/plugins/camptix/camptix.php @@ -7293,50 +7293,83 @@ function review_timeout_payments() { $processed = 0; $current_loop = 1; $max_loops = 500; - - while ( $attendees = get_posts( array( - 'fields' => 'ids', - 'post_type' => 'tix_attendee', - 'post_status' => array( 'draft', 'pending' ), - 'posts_per_page' => 100, - 'cache_results' => false, - 'meta_query' => array( - array( - 'key' => 'tix_timestamp', - 'compare' => '<', - 'value' => time() - 60 * 60 * 24, // 24 hours ago - 'type' => 'NUMERIC', + $timeout_timestamp = time() - 60 * 60 * 24; // 24 hours ago. + $timestamp_meta_query = array( + array( + 'key' => 'tix_timestamp', + 'compare' => '<', + 'value' => $timeout_timestamp, + 'type' => 'NUMERIC', + ), + array( + 'key' => 'tix_timestamp', + 'compare' => '>', + 'value' => 0, + 'type' => 'NUMERIC', + ), + ); + $timeout_queries = array( + array( + 'post_status' => 'draft', + 'meta_query' => $timestamp_meta_query, + ), + array( + 'post_status' => 'pending', + 'meta_query' => array_merge( + $timestamp_meta_query, + array( + array( + 'key' => 'tix_payment_method', + 'compare' => '=', + 'value' => 'stripe', + 'type' => 'CHAR', + ), + ) ), + ), + ); + + foreach ( $timeout_queries as $timeout_query ) { + $query_args = array_merge( array( - 'key' => 'tix_timestamp', - 'compare' => '>', - 'value' => 0, - 'type' => 'NUMERIC', + 'fields' => 'ids', + 'post_type' => 'tix_attendee', + 'posts_per_page' => 100, + 'cache_results' => false, ), - ), - ) ) ) { + $timeout_query + ); - foreach ( $attendees as $attendee_id ) { - do_action( 'camptix_pre_attendee_timeout', $attendee_id ); + while ( $attendees = get_posts( $query_args ) ) { - // Check the post_status again, in case a hook has caused the post to change. - if ( ! in_array( get_post_field( 'post_status', $attendee_id ), array( 'draft', 'pending' ), true ) ) { - continue; - } + foreach ( $attendees as $attendee_id ) { + do_action( 'camptix_pre_attendee_timeout', $attendee_id ); - wp_update_post( [ - 'ID' => $attendee_id, - 'post_status' => 'timeout', - ] ); + // Check the post_status again, in case a hook has caused the post to change. + if ( ! in_array( get_post_field( 'post_status', $attendee_id ), array( 'draft', 'pending' ), true ) ) { + continue; + } - $this->log( 'Attendee timeout', $attendee_id ); + // Check the timestamp again, in case a hook has delayed the timeout. + $attendee_timestamp = absint( get_post_meta( $attendee_id, 'tix_timestamp', true ) ); + if ( ! $attendee_timestamp || $attendee_timestamp >= $timeout_timestamp ) { + continue; + } - $processed++; - } + wp_update_post( [ + 'ID' => $attendee_id, + 'post_status' => 'timeout', + ] ); - // Just in case we get stuck in here - if ( $current_loop++ >= $max_loops ) - break; + $this->log( 'Attendee timeout', $attendee_id ); + + $processed++; + } + + // Just in case we get stuck in here + if ( $current_loop++ >= $max_loops ) + break 2; + } } // Only log action message if we did something. if ( $processed > 0 ) { diff --git a/public_html/wp-content/plugins/camptix/tests/test-camptix-admin.php b/public_html/wp-content/plugins/camptix/tests/test-camptix-admin.php index 12ea0872d1..bea0336e15 100644 --- a/public_html/wp-content/plugins/camptix/tests/test-camptix-admin.php +++ b/public_html/wp-content/plugins/camptix/tests/test-camptix-admin.php @@ -1057,6 +1057,55 @@ public function test_get_beta_features_returns_expected_keys() { $this->assertContains( 'archived', $features ); } + /** + * Verify timeout review respects hooks that delay an attendee timestamp. + */ + public function test_review_timeout_payments_respects_delayed_timestamp_after_hook() { + $ticket_id = $this->create_ticket(); + $attendee_id = $this->create_attendee( $ticket_id, array( + 'status' => 'draft', + 'payment_method' => 'stripe', + ) ); + + update_post_meta( $attendee_id, 'tix_timestamp', time() - 2 * DAY_IN_SECONDS ); + + $delay_timeout = static function( $timeout_attendee_id ) use ( $attendee_id ) { + if ( $attendee_id === $timeout_attendee_id ) { + update_post_meta( $attendee_id, 'tix_timestamp', time() ); + } + }; + + add_action( 'camptix_pre_attendee_timeout', $delay_timeout ); + self::$camptix->review_timeout_payments(); + remove_action( 'camptix_pre_attendee_timeout', $delay_timeout ); + + $this->assertSame( 'draft', get_post_status( $attendee_id ) ); + } + + /** + * Verify timeout review only includes Stripe attendees when sweeping pending payments. + */ + public function test_review_timeout_payments_only_sweeps_pending_stripe_attendees() { + $ticket_id = $this->create_ticket(); + + $paypal_attendee_id = $this->create_attendee( $ticket_id, array( + 'status' => 'pending', + 'payment_method' => 'paypal', + ) ); + $stripe_attendee_id = $this->create_attendee( $ticket_id, array( + 'status' => 'pending', + 'payment_method' => 'stripe', + ) ); + + update_post_meta( $paypal_attendee_id, 'tix_timestamp', time() - 2 * DAY_IN_SECONDS ); + update_post_meta( $stripe_attendee_id, 'tix_timestamp', time() - 2 * DAY_IN_SECONDS ); + + self::$camptix->review_timeout_payments(); + + $this->assertSame( 'pending', get_post_status( $paypal_attendee_id ) ); + $this->assertSame( 'timeout', get_post_status( $stripe_attendee_id ) ); + } + /** * Verify is_wordcamp_closed returns false when no WordCamp post exists. */ From 2e66ba71214bf5d1ede56da74586537363594cf1 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 28 Apr 2026 13:46:09 +1000 Subject: [PATCH 6/6] Fix timeout test formatting --- .../camptix/tests/test-camptix-admin.php | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/public_html/wp-content/plugins/camptix/tests/test-camptix-admin.php b/public_html/wp-content/plugins/camptix/tests/test-camptix-admin.php index bea0336e15..cb3541a037 100644 --- a/public_html/wp-content/plugins/camptix/tests/test-camptix-admin.php +++ b/public_html/wp-content/plugins/camptix/tests/test-camptix-admin.php @@ -1062,14 +1062,17 @@ public function test_get_beta_features_returns_expected_keys() { */ public function test_review_timeout_payments_respects_delayed_timestamp_after_hook() { $ticket_id = $this->create_ticket(); - $attendee_id = $this->create_attendee( $ticket_id, array( - 'status' => 'draft', - 'payment_method' => 'stripe', - ) ); + $attendee_id = $this->create_attendee( + $ticket_id, + array( + 'status' => 'draft', + 'payment_method' => 'stripe', + ) + ); update_post_meta( $attendee_id, 'tix_timestamp', time() - 2 * DAY_IN_SECONDS ); - $delay_timeout = static function( $timeout_attendee_id ) use ( $attendee_id ) { + $delay_timeout = static function ( $timeout_attendee_id ) use ( $attendee_id ) { if ( $attendee_id === $timeout_attendee_id ) { update_post_meta( $attendee_id, 'tix_timestamp', time() ); } @@ -1088,14 +1091,20 @@ public function test_review_timeout_payments_respects_delayed_timestamp_after_ho public function test_review_timeout_payments_only_sweeps_pending_stripe_attendees() { $ticket_id = $this->create_ticket(); - $paypal_attendee_id = $this->create_attendee( $ticket_id, array( - 'status' => 'pending', - 'payment_method' => 'paypal', - ) ); - $stripe_attendee_id = $this->create_attendee( $ticket_id, array( - 'status' => 'pending', - 'payment_method' => 'stripe', - ) ); + $paypal_attendee_id = $this->create_attendee( + $ticket_id, + array( + 'status' => 'pending', + 'payment_method' => 'paypal', + ) + ); + $stripe_attendee_id = $this->create_attendee( + $ticket_id, + array( + 'status' => 'pending', + 'payment_method' => 'stripe', + ) + ); update_post_meta( $paypal_attendee_id, 'tix_timestamp', time() - 2 * DAY_IN_SECONDS ); update_post_meta( $stripe_attendee_id, 'tix_timestamp', time() - 2 * DAY_IN_SECONDS );