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

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