PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.26
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.26
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
formidable / stripe / controllers / FrmStrpLiteEventsController.php

FrmStrpLiteEventsController.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.26, at stripe/controllers/FrmStrpLiteEventsController.php

606 lines 15.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmStrpLiteEventsController {
7
8 /**
9 * @var string
10 */
11 public static $events_to_skip_option_name = 'frm_strp_events_to_skip';
12
13 /**
14 * @var object|null
15 */
16 private $event;
17
18 /**
19 * @var object|null
20 */
21 private $invoice;
22
23 /**
24 * @var string|null
25 */
26 private $charge;
27
28 /**
29 * @var string|null
30 */
31 private $status;
32
33 /**
34 * @return void
35 */
36 private function set_payment_status() {
37 if ( $this->status === 'refunded' ) {
38 $this->charge = $this->invoice->id;
39 }
40
41 $frm_payment = new FrmTransLitePayment();
42 $payment = false;
43
44 if ( $this->charge ) {
45 $payment = $frm_payment->get_one_by( $this->charge, 'receipt_id' );
46 }
47
48 if ( ! $payment && $this->status === 'refunded' ) {
49 // If the refunded payment doesn't exist, stop here.
50 FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken. The refunded payment does not exist' );
51 echo json_encode(
52 array(
53 'response' => 'no payment exists',
54 'success' => false,
55 )
56 );
57 return;
58 }
59
60 $run_triggers = false;
61
62 if ( ! $payment ) {
63 $payment = $this->prepare_from_invoice();
64 $run_triggers = true;
65 } elseif ( $payment->status !== $this->status ) {
66 if ( $this->should_skip_status_update_for_first_recurring_payment( $payment ) ) {
67 return;
68 }
69
70 $payment_values = (array) $payment;
71 $is_partial_refund = $this->is_partial_refund();
72
73 if ( $is_partial_refund ) {
74 $this->set_partial_refund( $payment_values );
75 $amount_refunded = number_format( $this->invoice->amount_refunded / 100, 2 );
76 // translators: %s: The amount of money that was refunded.
77 $note = sprintf( __( 'Payment partially refunded %s', 'formidable' ), $amount_refunded );
78 } else {
79 $payment_values['status'] = $this->status;
80 $payment->status = $this->status;
81 // translators: %s: The status of the payment.
82 $note = sprintf( __( 'Payment %s', 'formidable' ), $payment_values['status'] );
83 }
84
85 FrmTransLiteAppHelper::add_note_to_payment( $payment_values, $note );
86
87 $u = $frm_payment->update( $payment->id, $payment_values );
88
89 echo json_encode(
90 array(
91 'response' => 'Payment ' . $payment->id . ' was updated',
92 'success' => true,
93 )
94 );
95
96 if ( ! $is_partial_refund ) {
97 $run_triggers = true;
98 }
99 }//end if
100
101 if ( $run_triggers && $payment && $payment->action_id ) {
102 FrmTransLiteActionsController::trigger_payment_status_change(
103 array(
104 'status' => $this->status,
105 'payment' => $payment,
106 )
107 );
108 }
109 }
110
111 /**
112 * Skip updating the payment object for the first recurring payment.
113 * This is to prevent double notifications because the first recurring payment creates an invoice and that invoice triggers the payment events.
114 *
115 * @since 6.5, introduced in v2.07 of the Stripe add on.
116 *
117 * @param stdClass $payment
118 *
119 * @return bool
120 */
121 private function should_skip_status_update_for_first_recurring_payment( $payment ) {
122 if ( ! in_array( $this->event->type, array( 'payment_intent.succeeded', 'payment_intent.payment_failed' ), true ) ) {
123 return false;
124 }
125
126 if ( empty( $payment->sub_id ) ) {
127 // Only skip for subscriptions. This is because subscription events create an invoice, and the status change events trigger for the invoice as well.
128 return false;
129 }
130
131 return $this->is_first_payment( $payment );
132 }
133
134 /**
135 * Tell Stripe Connect API that the request came through by flushing early before processing.
136 * Flushing early allows the API to end the request earlier.
137 *
138 * @since 6.5, introduced in v2.07 of the Stripe add on.
139 *
140 * @return void
141 */
142 private function flush_response() {
143 ob_start();
144
145 // Get the size of the output.
146 $size = ob_get_length();
147
148 // Disable compression (in case content length is compressed).
149 header( 'Content-Encoding: none' );
150
151 // Set the content length of the response.
152 header( 'Content-Length: ' . $size );
153
154 // Close the connection.
155 header( 'Connection: close' );
156
157 // Flush all output.
158 ob_end_flush();
159 @ob_flush();
160 flush();
161 }
162
163 /**
164 * When a customer is deleted in Stripe, remove the link to a user.
165 *
166 * @since 6.5, introduced in v2.01 of the Stripe add on.
167 *
168 * @return void
169 */
170 private function reset_customer() {
171 global $wpdb;
172 $customer_id = $this->invoice->id;
173
174 if ( empty( $customer_id ) ) {
175 return;
176 }
177 $wpdb->query(
178 $wpdb->prepare(
179 "DELETE FROM $wpdb->usermeta WHERE meta_value = %s AND meta_key LIKE %s",
180 $customer_id,
181 '_frmstrp_customer_id%'
182 )
183 );
184 }
185
186 /**
187 * @return void
188 */
189 private function maybe_subscription_canceled() {
190 if ( $this->invoice->cancel_at_period_end == true ) {
191 $this->subscription_canceled( 'future_cancel' );
192 }
193 }
194
195 /**
196 * @param string $status
197 *
198 * @return bool
199 */
200 private function subscription_canceled( $status = 'canceled' ) {
201 $sub = $this->get_subscription( $this->invoice->id );
202
203 if ( ! $sub ) {
204 return false;
205 }
206
207 if ( $sub->status === $status ) {
208 FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken since the subscription is already canceled.' );
209 echo json_encode(
210 array(
211 'response' => 'Already canceled',
212 'success' => true,
213 )
214 );
215 return false;
216 }
217
218 FrmTransLiteSubscriptionsController::change_subscription_status(
219 array(
220 'status' => $status,
221 'sub' => $sub,
222 )
223 );
224 return true;
225 }
226
227 /**
228 * @return false|object
229 */
230 private function prepare_from_invoice() {
231 if ( empty( $this->invoice->subscription ) ) {
232 // This isn't a subscription.
233 echo json_encode(
234 array(
235 'response' => 'Invoice missing',
236 'success' => false,
237 )
238 );
239 return false;
240 }
241
242 $sub = $this->get_subscription( $this->invoice->subscription );
243
244 if ( ! $sub ) {
245 return false;
246 }
247
248 $payment = $this->get_payment_for_sub( $sub->id );
249 $payment_values = (array) $payment;
250 $this->set_payment_values( $payment_values );
251
252 $frm_payment = new FrmTransLitePayment();
253
254 if ( $this->is_first_payment( $payment ) ) {
255 // The first payment for the subscription needs to be updated with the receipt id.
256 $frm_payment->update( $payment->id, $payment_values );
257 $payment_id = $payment->id;
258 } else {
259 $payment_values['test'] = $this->event->livemode ? 0 : 1;
260
261 // If this isn't the first, create a new payment.
262 $payment_id = $frm_payment->create( $payment_values );
263 }
264
265 $this->maybe_cancel_subscription( $sub );
266 $this->update_next_bill_date( $sub, $payment_values );
267
268 $payment = $frm_payment->get_one( $payment_id );
269 return $payment;
270 }
271
272 /**
273 * Check if a subscription has reached its payment limit.
274 * If it has, the subscription will be cancelled by period end.
275 *
276 * @since 6.11
277 *
278 * @param object $sub
279 *
280 * @return void
281 */
282 private function maybe_cancel_subscription( $sub ) {
283 $action = FrmFormAction::get_single_action_type( $sub->action_id, 'payment' );
284
285 // @phpstan-ignore-next-line
286 if ( ! is_object( $action ) || empty( $action->post_content['payment_limit'] ) ) {
287 return;
288 }
289
290 $payment_limit = FrmStrpLiteSubscriptionHelper::prepare_payment_limit(
291 $action->post_content['payment_limit'],
292 // Form ID.
293 (int) $action->menu_order,
294 (int) $sub->item_id
295 );
296
297 if ( is_wp_error( $payment_limit ) ) {
298 FrmTransLiteLog::log_message( 'Invalid payment limit value', $payment_limit->get_error_message() );
299 return;
300 }
301
302 if ( $this->get_payments_count( $sub->id ) < $payment_limit ) {
303 return;
304 }
305
306 // Flag to cancel subscription at period end.
307 // In this case, we do not want to cancel immediately.
308 $hook = 'frm_stripe_cancel_subscription_at_period_end';
309 $filter = function () {
310 return true;
311 };
312
313 add_filter( $hook, $filter, 99 );
314 $cancelled = FrmStrpLiteApiHelper::cancel_subscription( $sub->sub_id );
315
316 if ( $cancelled ) {
317 FrmTransLiteSubscriptionsController::change_subscription_status(
318 array(
319 'status' => 'future_cancel',
320 'sub' => $sub,
321 )
322 );
323 }
324 remove_filter( $hook, $filter, 99 );
325 }
326
327 /**
328 * Get the count of completed payments.
329 *
330 * @since 6.11
331 *
332 * @param string $sub_id Stripe subscriptino id prefixed with 'sub_'.
333 *
334 * @return int
335 */
336 private function get_payments_count( $sub_id ) {
337 $frm_payment = new FrmTransLitePayment();
338 $all_payments = $frm_payment->get_all_by( $sub_id, 'sub_id' );
339 $count = FrmTransLiteAppHelper::count_completed_payments( $all_payments );
340
341 return $count;
342 }
343
344 /**
345 * @since 6.5, introduced in v2.07 of the Stripe add on.
346 *
347 * @param stdClass $payment
348 *
349 * @return bool
350 */
351 private function is_first_payment( $payment ) {
352 return ! $payment->receipt_id || 0 === strpos( $payment->receipt_id, 'pi_' );
353 }
354
355 /**
356 * @param string $sub_id
357 *
358 * @return object|null
359 */
360 private function get_subscription( $sub_id ) {
361 $frm_sub = new FrmTransLiteSubscription();
362 $sub = $frm_sub->get_one_by( $sub_id, 'sub_id' );
363
364 if ( ! $sub ) {
365 // If this isn't an existing subscription, it must be a charge for another site/plugin.
366 FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken since there is not a matching subscription for ' . $sub_id );
367 echo json_encode(
368 array(
369 'response' => 'Invoice missing',
370 'success' => false,
371 )
372 );
373 }
374
375 return $sub;
376 }
377
378 /**
379 * @param string $sub_id
380 *
381 * @return object|null
382 */
383 private function get_payment_for_sub( $sub_id ) {
384 $frm_payment = new FrmTransLitePayment();
385 return $frm_payment->get_one_by( $sub_id, 'sub_id' );
386 }
387
388 /**
389 * @param array $payment_values
390 *
391 * @return void
392 */
393 private function set_payment_values( &$payment_values ) {
394 $payment_values['begin_date'] = gmdate( 'Y-m-d' );
395 $payment_values['expire_date'] = '0000-00-00';
396
397 foreach ( $this->invoice->lines->data as $line ) {
398 $payment_values['amount'] = number_format( $line->amount / 100, 2, '.', '' );
399 $payment_values['begin_date'] = gmdate( 'Y-m-d', $line->period->start );
400 $payment_values['expire_date'] = gmdate( 'Y-m-d', $line->period->end );
401 }
402
403 $payment_values['receipt_id'] = $this->charge ? $this->charge : __( 'None', 'formidable' );
404 $payment_values['status'] = $this->status;
405 $payment_values['meta_value'] = array();
406 $payment_values['created_at'] = current_time( 'mysql', 1 );
407
408 FrmTransLiteAppHelper::add_note_to_payment( $payment_values );
409 }
410
411 /**
412 * @param object $sub
413 * @param array $payment
414 *
415 * @return void
416 */
417 private function update_next_bill_date( $sub, $payment ) {
418 $frm_sub = new FrmTransLiteSubscription();
419
420 if ( $payment['status'] === 'complete' ) {
421 $frm_sub->update( $sub->id, array( 'next_bill_date' => $payment['expire_date'] ) );
422 } elseif ( $payment['status'] === 'refunded' ) {
423 $frm_sub->update( $sub->id, array( 'next_bill_date' => $payment['begin_date'] ) );
424 }
425 }
426
427 /**
428 * @return bool
429 */
430 private function is_partial_refund() {
431 $partial = false;
432
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;
439 }
440
441 /**
442 * @param array $payment_values
443 *
444 * @return void
445 */
446 private function set_partial_refund( &$payment_values ) {
447 $payment_values['amount'] = $this->invoice->amount - $this->invoice->amount_refunded;
448 $payment_values['amount'] = number_format( $payment_values['amount'] / 100, 2 );
449 }
450
451 /**
452 * @return void
453 */
454 public function process_connect_events() {
455 $this->flush_response();
456
457 $unprocessed_event_ids = FrmStrpLiteConnectHelper::get_unprocessed_event_ids();
458
459 if ( $unprocessed_event_ids ) {
460 $this->process_event_ids( $unprocessed_event_ids );
461 }
462 wp_send_json_success();
463 }
464
465 /**
466 * @since 6.5, introduced in v2.07 of the Stripe add on.
467 *
468 * @param array<string> $event_ids
469 *
470 * @return void
471 */
472 private function process_event_ids( $event_ids ) {
473 foreach ( $event_ids as $event_id ) {
474 if ( $this->should_skip_event( $event_id ) ) {
475 continue;
476 }
477
478 set_transient( 'frm_last_process_' . $event_id, time(), 60 );
479
480 $this->event = FrmStrpLiteConnectHelper::get_event( $event_id );
481
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 {
487 $this->count_failed_event( $event_id );
488 }
489 }
490 }
491
492 /**
493 * @since 6.5, introduced in v2.07 of the Stripe add on.
494 *
495 * @param string $event_id
496 *
497 * @return bool True if the event should be skipped.
498 */
499 private function should_skip_event( $event_id ) {
500 if ( $this->last_attempt_to_process_event_is_too_recent( $event_id ) ) {
501 return true;
502 }
503
504 $option = get_option( self::$events_to_skip_option_name );
505
506 if ( ! is_array( $option ) ) {
507 return false;
508 }
509
510 return in_array( $event_id, $option, true );
511 }
512
513 /**
514 * @param string $event_id
515 *
516 * @return bool
517 */
518 private function last_attempt_to_process_event_is_too_recent( $event_id ) {
519 $last_process_attempt = get_transient( 'frm_last_process_' . $event_id );
520 return is_numeric( $last_process_attempt ) && $last_process_attempt > time() - 60;
521 }
522
523 /**
524 * @since 6.5, introduced in v2.07 of the Stripe add on.
525 *
526 * @param string $event_id
527 *
528 * @return void
529 */
530 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
540 $maximum_retries = 3;
541
542 if ( $failed_count >= $maximum_retries ) {
543 $this->track_handled_event( $event_id );
544 } else {
545 set_transient( $transient_name, $failed_count, 4 * DAY_IN_SECONDS );
546 }
547 }
548
549 /**
550 * Track an event to no longer process.
551 * This is called for successful events, and also for failed events after a number of retries.
552 *
553 * @since 6.5, introduced in v2.07 of the Stripe add on.
554 *
555 * @param string $event_id
556 *
557 * @return void
558 */
559 private function track_handled_event( $event_id ) {
560 $option = get_option( self::$events_to_skip_option_name );
561
562 if ( is_array( $option ) ) {
563 if ( count( $option ) > 1000 ) {
564 // Prevent the option from getting too big by removing the front item before adding the next.
565 array_shift( $option );
566 }
567 } else {
568 $option = array();
569 }
570
571 $option[] = $event_id;
572 update_option( self::$events_to_skip_option_name, $option, false );
573 }
574
575 /**
576 * @return void
577 */
578 private function handle_event() {
579 $this->invoice = $this->event->data->object;
580 $this->charge = $this->invoice->charge ?? false;
581
582 if ( ! $this->charge && $this->invoice->object === 'payment_intent' ) {
583 $this->charge = $this->invoice->id;
584 }
585
586 $events = array(
587 'payment_intent.succeeded' => 'complete',
588 'payment_intent.payment_failed' => 'failed',
589 'invoice.payment_succeeded' => 'complete',
590 'invoice.payment_failed' => 'failed',
591 'charge.refunded' => 'refunded',
592 );
593
594 if ( isset( $events[ $this->event->type ] ) ) {
595 $this->status = $events[ $this->event->type ];
596 $this->set_payment_status();
597 } elseif ( $this->event->type === 'customer.deleted' ) {
598 $this->reset_customer();
599 } elseif ( $this->event->type === 'customer.subscription.deleted' ) {
600 $this->subscription_canceled();
601 } elseif ( $this->event->type === 'customer.subscription.updated' ) {
602 $this->maybe_subscription_canceled();
603 }
604 }
605 }
606