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

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