| @@ -83,9 +83,11 @@ | ||
| 83 | 83 | } |
| 84 | 84 | |
| 85 | 85 | FrmTransLiteAppHelper::add_note_to_payment( $payment_values, $note ); |
| 86 | 86 | |
| 87 | - $u = $frm_payment->update( $payment->id, $payment_values ); | |
| 87 | + // Read the status again right before the update, in case another request has already changed it. | |
| 88 | + $payment_status_still_does_not_match = $this->payment_status_still_does_not_match( $payment->id ); | |
| 89 | + $updated = $frm_payment->update( $payment->id, $payment_values ); | |
| 88 | 90 | |
| 89 | 91 | echo json_encode( |
| 90 | 92 | array( |
| 91 | 93 | 'response' => 'Payment ' . $payment->id . ' was updated', |
| @@ -92,9 +94,9 @@ | ||
| 92 | 94 | 'success' => true, |
| 93 | 95 | ) |
| 94 | 96 | ); |
| 95 | 97 | |
| 96 | - if ( ! $is_partial_refund ) { | |
| 98 | + if ( ! $is_partial_refund && $payment_status_still_does_not_match && $updated ) { | |
| 97 | 99 | $run_triggers = true; |
| 98 | 100 | } |
| 99 | 101 | }//end if |
| 100 | 102 | |
| @@ -108,8 +110,26 @@ | ||
| 108 | 110 | } |
| 109 | 111 | } |
| 110 | 112 | |
| 111 | 113 | /** |
| 114 | + * Double check that the payment status has not changed. | |
| 115 | + * This is to avoid running actions twice by mistake, since a Stripe Link | |
| 116 | + * return URL and a webhook event can both process the same payment. | |
| 117 | + * | |
| 118 | + * @since 6.35 | |
| 119 | + * | |
| 120 | + * @param int $payment_id The id of the payment to check. | |
| 121 | + * | |
| 122 | + * @return bool | |
| 123 | + */ | |
| 124 | + private function payment_status_still_does_not_match( $payment_id ) { | |
| 125 | + $frm_payment = new FrmTransLitePayment(); | |
| 126 | + $payment = $frm_payment->get_one( $payment_id ); | |
| 127 | + | |
| 128 | + return $payment && $payment->status !== $this->status; | |
| 129 | + } | |
| 130 | + | |
| 131 | + /** | |
| 112 | 132 | * Skip updating the payment object for the first recurring payment. |
| 113 | 133 | * This is to prevent double notifications because the first recurring payment creates an invoice and that invoice triggers the payment events. |
| 114 | 134 | * |
| 115 | 135 | * @since 6.5, introduced in v2.07 of the Stripe add on. |
| @@ -170,9 +190,9 @@ | ||
| 170 | 190 | private function reset_customer() { |
| 171 | 191 | global $wpdb; |
| 172 | 192 | $customer_id = $this->invoice->id; |
| 173 | 193 | |
| 174 | - if ( empty( $customer_id ) ) { | |
| 194 | + if ( ! $customer_id ) { | |
| 175 | 195 | return; |
| 176 | 196 | } |
| 177 | 197 | $wpdb->query( |
| 178 | 198 | $wpdb->prepare( |
| @@ -186,8 +206,9 @@ | ||
| 186 | 206 | /** |
| 187 | 207 | * @return void |
| 188 | 208 | */ |
| 189 | 209 | private function maybe_subscription_canceled() { |
| 210 | + // phpcs:ignore Universal.Operators.StrictComparisons | |
| 190 | 211 | if ( $this->invoice->cancel_at_period_end == true ) { |
| 191 | 212 | $this->subscription_canceled( 'future_cancel' ); |
| 192 | 213 | } |
| 193 | 214 | } |
| @@ -263,11 +284,9 @@ | ||
| 263 | 284 | } |
| 264 | 285 | |
| 265 | 286 | $this->maybe_cancel_subscription( $sub ); |
| 266 | 287 | $this->update_next_bill_date( $sub, $payment_values ); |
| 267 | - | |
| 268 | - $payment = $frm_payment->get_one( $payment_id ); | |
| 269 | - return $payment; | |
| 288 | + return $frm_payment->get_one( $payment_id ); | |
| 270 | 289 | } |
| 271 | 290 | |
| 272 | 291 | /** |
| 273 | 292 | * Check if a subscription has reached its payment limit. |
| @@ -310,10 +329,12 @@ | ||
| 310 | 329 | return true; |
| 311 | 330 | }; |
| 312 | 331 | |
| 313 | 332 | add_filter( $hook, $filter, 99 ); |
| 314 | - $cancelled = FrmStrpLiteApiHelper::cancel_subscription( $sub->sub_id ); | |
| 315 | 333 | |
| 334 | + // There is no logged in user when a webhook event is processed, so the customer check has to be skipped here. | |
| 335 | + $cancelled = FrmStrpLiteAppHelper::call_stripe_helper_class( 'cancel_subscription_without_customer_check', $sub->sub_id ); | |
| 336 | + | |
| 316 | 337 | if ( $cancelled ) { |
| 317 | 338 | FrmTransLiteSubscriptionsController::change_subscription_status( |
| 318 | 339 | array( |
| 319 | 340 | 'status' => 'future_cancel', |
| @@ -319,9 +340,12 @@ | ||
| 319 | 340 | 'status' => 'future_cancel', |
| 320 | 341 | 'sub' => $sub, |
| 321 | 342 | ) |
| 322 | 343 | ); |
| 344 | + } else { | |
| 345 | + FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'Unable to cancel subscription ' . $sub->sub_id . ' after it reached its payment limit.' ); | |
| 323 | 346 | } |
| 347 | + | |
| 324 | 348 | remove_filter( $hook, $filter, 99 ); |
| 325 | 349 | } |
| 326 | 350 | |
| 327 | 351 | /** |
| @@ -335,11 +359,10 @@ | ||
| 335 | 359 | */ |
| 336 | 360 | private function get_payments_count( $sub_id ) { |
| 337 | 361 | $frm_payment = new FrmTransLitePayment(); |
| 338 | 362 | $all_payments = $frm_payment->get_all_by( $sub_id, 'sub_id' ); |
| 339 | - $count = FrmTransLiteAppHelper::count_completed_payments( $all_payments ); | |
| 340 | 363 | |
| 341 | - return $count; | |
| 364 | + return FrmTransLiteAppHelper::count_completed_payments( $all_payments ); | |
| 342 | 365 | } |
| 343 | 366 | |
| 344 | 367 | /** |
| 345 | 368 | * @since 6.5, introduced in v2.07 of the Stripe add on. |
| @@ -348,9 +371,9 @@ | ||
| 348 | 371 | * |
| 349 | 372 | * @return bool |
| 350 | 373 | */ |
| 351 | 374 | private function is_first_payment( $payment ) { |
| 352 | - return ! $payment->receipt_id || 0 === strpos( $payment->receipt_id, 'pi_' ); | |
| 375 | + return ! $payment->receipt_id || str_starts_with( $payment->receipt_id, 'pi_' ); | |
| 353 | 376 | } |
| 354 | 377 | |
| 355 | 378 | /** |
| 356 | 379 | * @param string $sub_id |
| @@ -427,16 +450,15 @@ | ||
| 427 | 450 | /** |
| 428 | 451 | * @return bool |
| 429 | 452 | */ |
| 430 | 453 | private function is_partial_refund() { |
| 431 | - $partial = false; | |
| 454 | + if ( $this->status !== 'refunded' ) { | |
| 455 | + return false; | |
| 456 | + } | |
| 432 | 457 | |
| 433 | - if ( $this->status === 'refunded' ) { | |
| 434 | - $amount = $this->invoice->amount; | |
| 435 | - $amount_refunded = $this->invoice->amount_refunded; | |
| 436 | - $partial = $amount != $amount_refunded; | |
| 437 | - } | |
| 438 | - return $partial; | |
| 458 | + $amount = $this->invoice->amount; | |
| 459 | + $amount_refunded = $this->invoice->amount_refunded; | |
| 460 | + return $amount !== $amount_refunded; | |
| 439 | 461 | } |
| 440 | 462 | |
| 441 | 463 | /** |
| 442 | 464 | * @param array $payment_values |
| @@ -458,8 +480,9 @@ | ||
| 458 | 480 | |
| 459 | 481 | if ( $unprocessed_event_ids ) { |
| 460 | 482 | $this->process_event_ids( $unprocessed_event_ids ); |
| 461 | 483 | } |
| 484 | + | |
| 462 | 485 | wp_send_json_success(); |
| 463 | 486 | } |
| 464 | 487 | |
| 465 | 488 | /** |
| @@ -478,15 +501,16 @@ | ||
| 478 | 501 | set_transient( 'frm_last_process_' . $event_id, time(), 60 ); |
| 479 | 502 | |
| 480 | 503 | $this->event = FrmStrpLiteConnectHelper::get_event( $event_id ); |
| 481 | 504 | |
| 482 | - if ( is_object( $this->event ) ) { | |
| 483 | - $this->handle_event(); | |
| 484 | - $this->track_handled_event( $event_id ); | |
| 485 | - FrmStrpLiteConnectHelper::process_event( $event_id ); | |
| 486 | - } else { | |
| 505 | + if ( ! is_object( $this->event ) ) { | |
| 487 | 506 | $this->count_failed_event( $event_id ); |
| 507 | + continue; | |
| 488 | 508 | } |
| 509 | + | |
| 510 | + $this->handle_event(); | |
| 511 | + $this->track_handled_event( $event_id ); | |
| 512 | + FrmStrpLiteConnectHelper::process_event( $event_id ); | |
| 489 | 513 | } |
| 490 | 514 | } |
| 491 | 515 | |
| 492 | 516 | /** |
| @@ -502,13 +526,9 @@ | ||
| 502 | 526 | } |
| 503 | 527 | |
| 504 | 528 | $option = get_option( self::$events_to_skip_option_name ); |
| 505 | 529 | |
| 506 | - if ( ! is_array( $option ) ) { | |
| 507 | - return false; | |
| 508 | - } | |
| 509 | - | |
| 510 | - return in_array( $event_id, $option, true ); | |
| 530 | + return is_array( $option ) && in_array( $event_id, $option, true ); | |
| 511 | 531 | } |
| 512 | 532 | |
| 513 | 533 | /** |
| 514 | 534 | * @param string $event_id |
| @@ -527,17 +547,11 @@ | ||
| 527 | 547 | * |
| 528 | 548 | * @return void |
| 529 | 549 | */ |
| 530 | 550 | private function count_failed_event( $event_id ) { |
| 531 | - $transient_name = 'frm_failed_event_' . $event_id; | |
| 532 | - $transient = get_transient( $transient_name ); | |
| 533 | - | |
| 534 | - if ( is_int( $transient ) ) { | |
| 535 | - $failed_count = $transient + 1; | |
| 536 | - } else { | |
| 537 | - $failed_count = 1; | |
| 538 | - } | |
| 539 | - | |
| 551 | + $transient_name = 'frm_failed_event_' . $event_id; | |
| 552 | + $transient = get_transient( $transient_name ); | |
| 553 | + $failed_count = is_int( $transient ) ? $transient + 1 : 1; | |
| 540 | 554 | $maximum_retries = 3; |
| 541 | 555 | |
| 542 | 556 | if ( $failed_count >= $maximum_retries ) { |
| 543 | 557 | $this->track_handled_event( $event_id ); |