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

596 lines 15.1 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 ( ! $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 // phpcs:ignore Universal.Operators.StrictComparisons
191 if ( $this->invoice->cancel_at_period_end == true ) {
192 $this->subscription_canceled( 'future_cancel' );
193 }
194 }
195
196 /**
197 * @param string $status
198 *
199 * @return bool
200 */
201 private function subscription_canceled( $status = 'canceled' ) {
202 $sub = $this->get_subscription( $this->invoice->id );
203
204 if ( ! $sub ) {
205 return false;
206 }
207
208 if ( $sub->status === $status ) {
209 FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken since the subscription is already canceled.' );
210 echo json_encode(
211 array(
212 'response' => 'Already canceled',
213 'success' => true,
214 )
215 );
216 return false;
217 }
218
219 FrmTransLiteSubscriptionsController::change_subscription_status(
220 array(
221 'status' => $status,
222 'sub' => $sub,
223 )
224 );
225 return true;
226 }
227
228 /**
229 * @return false|object
230 */
231 private function prepare_from_invoice() {
232 if ( empty( $this->invoice->subscription ) ) {
233 // This isn't a subscription.
234 echo json_encode(
235 array(
236 'response' => 'Invoice missing',
237 'success' => false,
238 )
239 );
240 return false;
241 }
242
243 $sub = $this->get_subscription( $this->invoice->subscription );
244
245 if ( ! $sub ) {
246 return false;
247 }
248
249 $payment = $this->get_payment_for_sub( $sub->id );
250 $payment_values = (array) $payment;
251 $this->set_payment_values( $payment_values );
252
253 $frm_payment = new FrmTransLitePayment();
254
255 if ( $this->is_first_payment( $payment ) ) {
256 // The first payment for the subscription needs to be updated with the receipt id.
257 $frm_payment->update( $payment->id, $payment_values );
258 $payment_id = $payment->id;
259 } else {
260 $payment_values['test'] = $this->event->livemode ? 0 : 1;
261
262 // If this isn't the first, create a new payment.
263 $payment_id = $frm_payment->create( $payment_values );
264 }
265
266 $this->maybe_cancel_subscription( $sub );
267 $this->update_next_bill_date( $sub, $payment_values );
268 return $frm_payment->get_one( $payment_id );
269 }
270
271 /**
272 * Check if a subscription has reached its payment limit.
273 * If it has, the subscription will be cancelled by period end.
274 *
275 * @since 6.11
276 *
277 * @param object $sub
278 *
279 * @return void
280 */
281 private function maybe_cancel_subscription( $sub ) {
282 $action = FrmFormAction::get_single_action_type( $sub->action_id, 'payment' );
283
284 // @phpstan-ignore-next-line
285 if ( ! is_object( $action ) || empty( $action->post_content['payment_limit'] ) ) {
286 return;
287 }
288
289 $payment_limit = FrmStrpLiteSubscriptionHelper::prepare_payment_limit(
290 $action->post_content['payment_limit'],
291 // Form ID.
292 (int) $action->menu_order,
293 (int) $sub->item_id
294 );
295
296 if ( is_wp_error( $payment_limit ) ) {
297 FrmTransLiteLog::log_message( 'Invalid payment limit value', $payment_limit->get_error_message() );
298 return;
299 }
300
301 if ( $this->get_payments_count( $sub->id ) < $payment_limit ) {
302 return;
303 }
304
305 // Flag to cancel subscription at period end.
306 // In this case, we do not want to cancel immediately.
307 $hook = 'frm_stripe_cancel_subscription_at_period_end';
308 $filter = function () {
309 return true;
310 };
311
312 add_filter( $hook, $filter, 99 );
313 $cancelled = FrmStrpLiteApiHelper::cancel_subscription( $sub->sub_id );
314
315 if ( $cancelled ) {
316 FrmTransLiteSubscriptionsController::change_subscription_status(
317 array(
318 'status' => 'future_cancel',
319 'sub' => $sub,
320 )
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
340 return FrmTransLiteAppHelper::count_completed_payments( $all_payments );
341 }
342
343 /**
344 * @since 6.5, introduced in v2.07 of the Stripe add on.
345 *
346 * @param stdClass $payment
347 *
348 * @return bool
349 */
350 private function is_first_payment( $payment ) {
351 return ! $payment->receipt_id || str_starts_with( $payment->receipt_id, 'pi_' );
352 }
353
354 /**
355 * @param string $sub_id
356 *
357 * @return object|null
358 */
359 private function get_subscription( $sub_id ) {
360 $frm_sub = new FrmTransLiteSubscription();
361 $sub = $frm_sub->get_one_by( $sub_id, 'sub_id' );
362
363 if ( ! $sub ) {
364 // If this isn't an existing subscription, it must be a charge for another site/plugin.
365 FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken since there is not a matching subscription for ' . $sub_id );
366 echo json_encode(
367 array(
368 'response' => 'Invoice missing',
369 'success' => false,
370 )
371 );
372 }
373
374 return $sub;
375 }
376
377 /**
378 * @param string $sub_id
379 *
380 * @return object|null
381 */
382 private function get_payment_for_sub( $sub_id ) {
383 $frm_payment = new FrmTransLitePayment();
384 return $frm_payment->get_one_by( $sub_id, 'sub_id' );
385 }
386
387 /**
388 * @param array $payment_values
389 *
390 * @return void
391 */
392 private function set_payment_values( &$payment_values ) {
393 $payment_values['begin_date'] = gmdate( 'Y-m-d' );
394 $payment_values['expire_date'] = '0000-00-00';
395
396 foreach ( $this->invoice->lines->data as $line ) {
397 $payment_values['amount'] = number_format( $line->amount / 100, 2, '.', '' );
398 $payment_values['begin_date'] = gmdate( 'Y-m-d', $line->period->start );
399 $payment_values['expire_date'] = gmdate( 'Y-m-d', $line->period->end );
400 }
401
402 $payment_values['receipt_id'] = $this->charge ? $this->charge : __( 'None', 'formidable' );
403 $payment_values['status'] = $this->status;
404 $payment_values['meta_value'] = array();
405 $payment_values['created_at'] = current_time( 'mysql', 1 );
406
407 FrmTransLiteAppHelper::add_note_to_payment( $payment_values );
408 }
409
410 /**
411 * @param object $sub
412 * @param array $payment
413 *
414 * @return void
415 */
416 private function update_next_bill_date( $sub, $payment ) {
417 $frm_sub = new FrmTransLiteSubscription();
418
419 if ( $payment['status'] === 'complete' ) {
420 $frm_sub->update( $sub->id, array( 'next_bill_date' => $payment['expire_date'] ) );
421 } elseif ( $payment['status'] === 'refunded' ) {
422 $frm_sub->update( $sub->id, array( 'next_bill_date' => $payment['begin_date'] ) );
423 }
424 }
425
426 /**
427 * @return bool
428 */
429 private function is_partial_refund() {
430 if ( $this->status !== 'refunded' ) {
431 return false;
432 }
433
434 $amount = $this->invoice->amount;
435 $amount_refunded = $this->invoice->amount_refunded;
436 return $amount !== $amount_refunded;
437 }
438
439 /**
440 * @param array $payment_values
441 *
442 * @return void
443 */
444 private function set_partial_refund( &$payment_values ) {
445 $payment_values['amount'] = $this->invoice->amount - $this->invoice->amount_refunded;
446 $payment_values['amount'] = number_format( $payment_values['amount'] / 100, 2 );
447 }
448
449 /**
450 * @return void
451 */
452 public function process_connect_events() {
453 $this->flush_response();
454
455 $unprocessed_event_ids = FrmStrpLiteConnectHelper::get_unprocessed_event_ids();
456
457 if ( $unprocessed_event_ids ) {
458 $this->process_event_ids( $unprocessed_event_ids );
459 }
460
461 wp_send_json_success();
462 }
463
464 /**
465 * @since 6.5, introduced in v2.07 of the Stripe add on.
466 *
467 * @param array<string> $event_ids
468 *
469 * @return void
470 */
471 private function process_event_ids( $event_ids ) {
472 foreach ( $event_ids as $event_id ) {
473 if ( $this->should_skip_event( $event_id ) ) {
474 continue;
475 }
476
477 set_transient( 'frm_last_process_' . $event_id, time(), 60 );
478
479 $this->event = FrmStrpLiteConnectHelper::get_event( $event_id );
480
481 if ( ! is_object( $this->event ) ) {
482 $this->count_failed_event( $event_id );
483 continue;
484 }
485
486 $this->handle_event();
487 $this->track_handled_event( $event_id );
488 FrmStrpLiteConnectHelper::process_event( $event_id );
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 return is_array( $option ) && in_array( $event_id, $option, true );
507 }
508
509 /**
510 * @param string $event_id
511 *
512 * @return bool
513 */
514 private function last_attempt_to_process_event_is_too_recent( $event_id ) {
515 $last_process_attempt = get_transient( 'frm_last_process_' . $event_id );
516 return is_numeric( $last_process_attempt ) && $last_process_attempt > time() - 60;
517 }
518
519 /**
520 * @since 6.5, introduced in v2.07 of the Stripe add on.
521 *
522 * @param string $event_id
523 *
524 * @return void
525 */
526 private function count_failed_event( $event_id ) {
527 $transient_name = 'frm_failed_event_' . $event_id;
528 $transient = get_transient( $transient_name );
529 $failed_count = is_int( $transient ) ? $transient + 1 : 1;
530 $maximum_retries = 3;
531
532 if ( $failed_count >= $maximum_retries ) {
533 $this->track_handled_event( $event_id );
534 } else {
535 set_transient( $transient_name, $failed_count, 4 * DAY_IN_SECONDS );
536 }
537 }
538
539 /**
540 * Track an event to no longer process.
541 * This is called for successful events, and also for failed events after a number of retries.
542 *
543 * @since 6.5, introduced in v2.07 of the Stripe add on.
544 *
545 * @param string $event_id
546 *
547 * @return void
548 */
549 private function track_handled_event( $event_id ) {
550 $option = get_option( self::$events_to_skip_option_name );
551
552 if ( is_array( $option ) ) {
553 if ( count( $option ) > 1000 ) {
554 // Prevent the option from getting too big by removing the front item before adding the next.
555 array_shift( $option );
556 }
557 } else {
558 $option = array();
559 }
560
561 $option[] = $event_id;
562 update_option( self::$events_to_skip_option_name, $option, false );
563 }
564
565 /**
566 * @return void
567 */
568 private function handle_event() {
569 $this->invoice = $this->event->data->object;
570 $this->charge = $this->invoice->charge ?? false;
571
572 if ( ! $this->charge && $this->invoice->object === 'payment_intent' ) {
573 $this->charge = $this->invoice->id;
574 }
575
576 $events = array(
577 'payment_intent.succeeded' => 'complete',
578 'payment_intent.payment_failed' => 'failed',
579 'invoice.payment_succeeded' => 'complete',
580 'invoice.payment_failed' => 'failed',
581 'charge.refunded' => 'refunded',
582 );
583
584 if ( isset( $events[ $this->event->type ] ) ) {
585 $this->status = $events[ $this->event->type ];
586 $this->set_payment_status();
587 } elseif ( $this->event->type === 'customer.deleted' ) {
588 $this->reset_customer();
589 } elseif ( $this->event->type === 'customer.subscription.deleted' ) {
590 $this->subscription_canceled();
591 } elseif ( $this->event->type === 'customer.subscription.updated' ) {
592 $this->maybe_subscription_canceled();
593 }
594 }
595 }
596