PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
← All changes | stripe/controllers/FrmStrpLiteEventsController.php +181 -40 6.5 → trunk View file →
@@ -9,11 +9,26 @@
9 9 * @var string
10 10 */
11 11 public static $events_to_skip_option_name = 'frm_strp_events_to_skip';
12 12
13 + /**
14 + * @var object|null
15 + */
13 16 private $event;
17 +
18 + /**
19 + * @var object|null
20 + */
14 21 private $invoice;
22 +
23 + /**
24 + * @var string|null
25 + */
15 26 private $charge;
27 +
28 + /**
29 + * @var string|null
30 + */
16 31 private $status;
17 32
18 33 /**
19 34 * @return void
@@ -68,9 +83,11 @@
68 83 }
69 84
70 85 FrmTransLiteAppHelper::add_note_to_payment( $payment_values, $note );
71 86
72 - $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 );
73 90
74 91 echo json_encode(
75 92 array(
76 93 'response' => 'Payment ' . $payment->id . ' was updated',
@@ -76,12 +93,13 @@
76 93 'response' => 'Payment ' . $payment->id . ' was updated',
77 94 'success' => true,
78 95 )
79 96 );
80 - if ( ! $is_partial_refund ) {
97 +
98 + if ( ! $is_partial_refund && $payment_status_still_does_not_match && $updated ) {
81 99 $run_triggers = true;
82 100 }
83 - }
101 + }//end if
84 102
85 103 if ( $run_triggers && $payment && $payment->action_id ) {
86 104 FrmTransLiteActionsController::trigger_payment_status_change(
87 105 array(
@@ -92,8 +110,26 @@
92 110 }
93 111 }
94 112
95 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 + /**
96 132 * Skip updating the payment object for the first recurring payment.
97 133 * This is to prevent double notifications because the first recurring payment creates an invoice and that invoice triggers the payment events.
98 134 *
99 135 * @since 6.5, introduced in v2.07 of the Stripe add on.
@@ -98,8 +134,9 @@
98 134 *
99 135 * @since 6.5, introduced in v2.07 of the Stripe add on.
100 136 *
101 137 * @param stdClass $payment
138 + *
102 139 * @return bool
103 140 */
104 141 private function should_skip_status_update_for_first_recurring_payment( $payment ) {
105 142 if ( ! in_array( $this->event->type, array( 'payment_intent.succeeded', 'payment_intent.payment_failed' ), true ) ) {
@@ -146,14 +183,16 @@
146 183 /**
147 184 * When a customer is deleted in Stripe, remove the link to a user.
148 185 *
149 186 * @since 6.5, introduced in v2.01 of the Stripe add on.
187 + *
150 188 * @return void
151 189 */
152 190 private function reset_customer() {
153 191 global $wpdb;
154 192 $customer_id = $this->invoice->id;
155 - if ( empty( $customer_id ) ) {
193 +
194 + if ( ! $customer_id ) {
156 195 return;
157 196 }
158 197 $wpdb->query(
159 198 $wpdb->prepare(
@@ -167,8 +206,9 @@
167 206 /**
168 207 * @return void
169 208 */
170 209 private function maybe_subscription_canceled() {
210 + // phpcs:ignore Universal.Operators.StrictComparisons
171 211 if ( $this->invoice->cancel_at_period_end == true ) {
172 212 $this->subscription_canceled( 'future_cancel' );
173 213 }
174 214 }
@@ -174,12 +214,14 @@
174 214 }
175 215
176 216 /**
177 217 * @param string $status
218 + *
178 219 * @return bool
179 220 */
180 221 private function subscription_canceled( $status = 'canceled' ) {
181 222 $sub = $this->get_subscription( $this->invoice->id );
223 +
182 224 if ( ! $sub ) {
183 225 return false;
184 226 }
185 227
@@ -202,12 +244,14 @@
202 244 );
203 245 return true;
204 246 }
205 247
248 + /**
249 + * @return false|object
250 + */
206 251 private function prepare_from_invoice() {
207 252 if ( empty( $this->invoice->subscription ) ) {
208 253 // This isn't a subscription.
209 - FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken since this is not a subscription.' );
210 254 echo json_encode(
211 255 array(
212 256 'response' => 'Invoice missing',
213 257 'success' => false,
@@ -216,8 +260,9 @@
216 260 return false;
217 261 }
218 262
219 263 $sub = $this->get_subscription( $this->invoice->subscription );
264 +
220 265 if ( ! $sub ) {
221 266 return false;
222 267 }
223 268
@@ -231,33 +276,117 @@
231 276 // The first payment for the subscription needs to be updated with the receipt id.
232 277 $frm_payment->update( $payment->id, $payment_values );
233 278 $payment_id = $payment->id;
234 279 } else {
280 + $payment_values['test'] = $this->event->livemode ? 0 : 1;
281 +
235 282 // If this isn't the first, create a new payment.
236 283 $payment_id = $frm_payment->create( $payment_values );
237 284 }
238 285
286 + $this->maybe_cancel_subscription( $sub );
239 287 $this->update_next_bill_date( $sub, $payment_values );
288 + return $frm_payment->get_one( $payment_id );
289 + }
240 290
241 - $payment = $frm_payment->get_one( $payment_id );
242 - return $payment;
291 + /**
292 + * Check if a subscription has reached its payment limit.
293 + * If it has, the subscription will be cancelled by period end.
294 + *
295 + * @since 6.11
296 + *
297 + * @param object $sub
298 + *
299 + * @return void
300 + */
301 + private function maybe_cancel_subscription( $sub ) {
302 + $action = FrmFormAction::get_single_action_type( $sub->action_id, 'payment' );
303 +
304 + // @phpstan-ignore-next-line
305 + if ( ! is_object( $action ) || empty( $action->post_content['payment_limit'] ) ) {
306 + return;
307 + }
308 +
309 + $payment_limit = FrmStrpLiteSubscriptionHelper::prepare_payment_limit(
310 + $action->post_content['payment_limit'],
311 + // Form ID.
312 + (int) $action->menu_order,
313 + (int) $sub->item_id
314 + );
315 +
316 + if ( is_wp_error( $payment_limit ) ) {
317 + FrmTransLiteLog::log_message( 'Invalid payment limit value', $payment_limit->get_error_message() );
318 + return;
319 + }
320 +
321 + if ( $this->get_payments_count( $sub->id ) < $payment_limit ) {
322 + return;
323 + }
324 +
325 + // Flag to cancel subscription at period end.
326 + // In this case, we do not want to cancel immediately.
327 + $hook = 'frm_stripe_cancel_subscription_at_period_end';
328 + $filter = function () {
329 + return true;
330 + };
331 +
332 + add_filter( $hook, $filter, 99 );
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 +
337 + if ( $cancelled ) {
338 + FrmTransLiteSubscriptionsController::change_subscription_status(
339 + array(
340 + 'status' => 'future_cancel',
341 + 'sub' => $sub,
342 + )
343 + );
344 + } else {
345 + FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'Unable to cancel subscription ' . $sub->sub_id . ' after it reached its payment limit.' );
346 + }
347 +
348 + remove_filter( $hook, $filter, 99 );
243 349 }
244 350
245 351 /**
352 + * Get the count of completed payments.
353 + *
354 + * @since 6.11
355 + *
356 + * @param string $sub_id Stripe subscriptino id prefixed with 'sub_'.
357 + *
358 + * @return int
359 + */
360 + private function get_payments_count( $sub_id ) {
361 + $frm_payment = new FrmTransLitePayment();
362 + $all_payments = $frm_payment->get_all_by( $sub_id, 'sub_id' );
363 +
364 + return FrmTransLiteAppHelper::count_completed_payments( $all_payments );
365 + }
366 +
367 + /**
246 368 * @since 6.5, introduced in v2.07 of the Stripe add on.
247 369 *
248 370 * @param stdClass $payment
371 + *
249 372 * @return bool
250 373 */
251 374 private function is_first_payment( $payment ) {
252 - return ! $payment->receipt_id || 0 === strpos( $payment->receipt_id, 'pi_' );
375 + return ! $payment->receipt_id || str_starts_with( $payment->receipt_id, 'pi_' );
253 376 }
254 377
378 + /**
379 + * @param string $sub_id
380 + *
381 + * @return object|null
382 + */
255 383 private function get_subscription( $sub_id ) {
256 384 $frm_sub = new FrmTransLiteSubscription();
257 385 $sub = $frm_sub->get_one_by( $sub_id, 'sub_id' );
386 +
258 387 if ( ! $sub ) {
259 - // If this isn't an existing subscription, it must be a charge for another site/plugin
388 + // If this isn't an existing subscription, it must be a charge for another site/plugin.
260 389 FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken since there is not a matching subscription for ' . $sub_id );
261 390 echo json_encode(
262 391 array(
263 392 'response' => 'Invoice missing',
@@ -268,8 +397,13 @@
268 397
269 398 return $sub;
270 399 }
271 400
401 + /**
402 + * @param string $sub_id
403 + *
404 + * @return object|null
405 + */
272 406 private function get_payment_for_sub( $sub_id ) {
273 407 $frm_payment = new FrmTransLitePayment();
274 408 return $frm_payment->get_one_by( $sub_id, 'sub_id' );
275 409 }
@@ -275,8 +409,9 @@
275 409 }
276 410
277 411 /**
278 412 * @param array $payment_values
413 + *
279 414 * @return void
280 415 */
281 416 private function set_payment_values( &$payment_values ) {
282 417 $payment_values['begin_date'] = gmdate( 'Y-m-d' );
@@ -282,17 +417,17 @@
282 417 $payment_values['begin_date'] = gmdate( 'Y-m-d' );
283 418 $payment_values['expire_date'] = '0000-00-00';
284 419
285 420 foreach ( $this->invoice->lines->data as $line ) {
286 - $payment_values['amount'] = number_format( ( $line->amount / 100 ), 2, '.', '' );
421 + $payment_values['amount'] = number_format( $line->amount / 100, 2, '.', '' );
287 422 $payment_values['begin_date'] = gmdate( 'Y-m-d', $line->period->start );
288 423 $payment_values['expire_date'] = gmdate( 'Y-m-d', $line->period->end );
289 424 }
290 425
291 - $payment_values['receipt_id'] = $this->charge ? $this->charge : __( 'None', 'formidable' );
292 - $payment_values['status'] = $this->status;
293 - $payment_values['meta_value'] = array();
294 - $payment_values['created_at'] = current_time( 'mysql', 1 );
426 + $payment_values['receipt_id'] = $this->charge ? $this->charge : __( 'None', 'formidable' );
427 + $payment_values['status'] = $this->status;
428 + $payment_values['meta_value'] = array();
429 + $payment_values['created_at'] = current_time( 'mysql', 1 );
295 430
296 431 FrmTransLiteAppHelper::add_note_to_payment( $payment_values );
297 432 }
298 433
@@ -298,12 +433,14 @@
298 433
299 434 /**
300 435 * @param object $sub
301 436 * @param array $payment
437 + *
302 438 * @return void
303 439 */
304 440 private function update_next_bill_date( $sub, $payment ) {
305 441 $frm_sub = new FrmTransLiteSubscription();
442 +
306 443 if ( $payment['status'] === 'complete' ) {
307 444 $frm_sub->update( $sub->id, array( 'next_bill_date' => $payment['expire_date'] ) );
308 445 } elseif ( $payment['status'] === 'refunded' ) {
309 446 $frm_sub->update( $sub->id, array( 'next_bill_date' => $payment['begin_date'] ) );
@@ -313,19 +450,20 @@
313 450 /**
314 451 * @return bool
315 452 */
316 453 private function is_partial_refund() {
317 - $partial = false;
318 - if ( $this->status === 'refunded' ) {
319 - $amount = $this->invoice->amount;
320 - $amount_refunded = $this->invoice->amount_refunded;
321 - $partial = $amount != $amount_refunded;
454 + if ( $this->status !== 'refunded' ) {
455 + return false;
322 456 }
323 - return $partial;
457 +
458 + $amount = $this->invoice->amount;
459 + $amount_refunded = $this->invoice->amount_refunded;
460 + return $amount !== $amount_refunded;
324 461 }
325 462
326 463 /**
327 464 * @param array $payment_values
465 + *
328 466 * @return void
329 467 */
330 468 private function set_partial_refund( &$payment_values ) {
331 469 $payment_values['amount'] = $this->invoice->amount - $this->invoice->amount_refunded;
@@ -338,11 +476,13 @@
338 476 public function process_connect_events() {
339 477 $this->flush_response();
340 478
341 479 $unprocessed_event_ids = FrmStrpLiteConnectHelper::get_unprocessed_event_ids();
480 +
342 481 if ( $unprocessed_event_ids ) {
343 482 $this->process_event_ids( $unprocessed_event_ids );
344 483 }
484 +
345 485 wp_send_json_success();
346 486 }
347 487
348 488 /**
@@ -348,8 +488,9 @@
348 488 /**
349 489 * @since 6.5, introduced in v2.07 of the Stripe add on.
350 490 *
351 491 * @param array<string> $event_ids
492 + *
352 493 * @return void
353 494 */
354 495 private function process_event_ids( $event_ids ) {
355 496 foreach ( $event_ids as $event_id ) {
@@ -359,15 +500,17 @@
359 500
360 501 set_transient( 'frm_last_process_' . $event_id, time(), 60 );
361 502
362 503 $this->event = FrmStrpLiteConnectHelper::get_event( $event_id );
363 - if ( is_object( $this->event ) ) {
364 - $this->handle_event();
365 - $this->track_handled_event( $event_id );
366 - FrmStrpLiteConnectHelper::process_event( $event_id );
367 - } else {
504 +
505 + if ( ! is_object( $this->event ) ) {
368 506 $this->count_failed_event( $event_id );
507 + continue;
369 508 }
509 +
510 + $this->handle_event();
511 + $this->track_handled_event( $event_id );
512 + FrmStrpLiteConnectHelper::process_event( $event_id );
370 513 }
371 514 }
372 515
373 516 /**
@@ -373,8 +516,9 @@
373 516 /**
374 517 * @since 6.5, introduced in v2.07 of the Stripe add on.
375 518 *
376 519 * @param string $event_id
520 + *
377 521 * @return bool True if the event should be skipped.
378 522 */
379 523 private function should_skip_event( $event_id ) {
380 524 if ( $this->last_attempt_to_process_event_is_too_recent( $event_id ) ) {
@@ -381,22 +525,20 @@
381 525 return true;
382 526 }
383 527
384 528 $option = get_option( self::$events_to_skip_option_name );
385 - if ( ! is_array( $option ) ) {
386 - return false;
387 - }
388 529
389 - return in_array( $event_id, $option, true );
530 + return is_array( $option ) && in_array( $event_id, $option, true );
390 531 }
391 532
392 533 /**
393 534 * @param string $event_id
535 + *
394 536 * @return bool
395 537 */
396 538 private function last_attempt_to_process_event_is_too_recent( $event_id ) {
397 539 $last_process_attempt = get_transient( 'frm_last_process_' . $event_id );
398 - return is_numeric( $last_process_attempt ) && $last_process_attempt > ( time() - 60 );
540 + return is_numeric( $last_process_attempt ) && $last_process_attempt > time() - 60;
399 541 }
400 542
401 543 /**
402 544 * @since 6.5, introduced in v2.07 of the Stripe add on.
@@ -401,24 +543,21 @@
401 543 /**
402 544 * @since 6.5, introduced in v2.07 of the Stripe add on.
403 545 *
404 546 * @param string $event_id
547 + *
405 548 * @return void
406 549 */
407 550 private function count_failed_event( $event_id ) {
408 - $transient_name = 'frm_failed_event_' . $event_id;
409 - $transient = get_transient( $transient_name );
410 - if ( is_int( $transient ) ) {
411 - $failed_count = $transient + 1;
412 - } else {
413 - $failed_count = 1;
414 - }
551 + $transient_name = 'frm_failed_event_' . $event_id;
552 + $transient = get_transient( $transient_name );
553 + $failed_count = is_int( $transient ) ? $transient + 1 : 1;
554 + $maximum_retries = 3;
415 555
416 - $maximum_retries = 3;
417 556 if ( $failed_count >= $maximum_retries ) {
418 557 $this->track_handled_event( $event_id );
419 558 } else {
420 - set_transient( $transient_name, $failed_count );
559 + set_transient( $transient_name, $failed_count, 4 * DAY_IN_SECONDS );
421 560 }
422 561 }
423 562
424 563 /**
@@ -427,8 +566,9 @@
427 566 *
428 567 * @since 6.5, introduced in v2.07 of the Stripe add on.
429 568 *
430 569 * @param string $event_id
570 + *
431 571 * @return void
432 572 */
433 573 private function track_handled_event( $event_id ) {
434 574 $option = get_option( self::$events_to_skip_option_name );
@@ -450,9 +590,10 @@
450 590 * @return void
451 591 */
452 592 private function handle_event() {
453 593 $this->invoice = $this->event->data->object;
454 - $this->charge = isset( $this->invoice->charge ) ? $this->invoice->charge : false;
594 + $this->charge = $this->invoice->charge ?? false;
595 +
455 596 if ( ! $this->charge && $this->invoice->object === 'payment_intent' ) {
456 597 $this->charge = $this->invoice->id;
457 598 }
458 599