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

481 lines 13.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 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->update_next_bill_date( $sub, $payment_values );
242
243 $payment = $frm_payment->get_one( $payment_id );
244 return $payment;
245 }
246
247 /**
248 * @since 6.5, introduced in v2.07 of the Stripe add on.
249 *
250 * @param stdClass $payment
251 * @return bool
252 */
253 private function is_first_payment( $payment ) {
254 return ! $payment->receipt_id || 0 === strpos( $payment->receipt_id, 'pi_' );
255 }
256
257 private function get_subscription( $sub_id ) {
258 $frm_sub = new FrmTransLiteSubscription();
259 $sub = $frm_sub->get_one_by( $sub_id, 'sub_id' );
260 if ( ! $sub ) {
261 // If this isn't an existing subscription, it must be a charge for another site/plugin.
262 FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken since there is not a matching subscription for ' . $sub_id );
263 echo json_encode(
264 array(
265 'response' => 'Invoice missing',
266 'success' => false,
267 )
268 );
269 }
270
271 return $sub;
272 }
273
274 private function get_payment_for_sub( $sub_id ) {
275 $frm_payment = new FrmTransLitePayment();
276 return $frm_payment->get_one_by( $sub_id, 'sub_id' );
277 }
278
279 /**
280 * @param array $payment_values
281 * @return void
282 */
283 private function set_payment_values( &$payment_values ) {
284 $payment_values['begin_date'] = gmdate( 'Y-m-d' );
285 $payment_values['expire_date'] = '0000-00-00';
286
287 foreach ( $this->invoice->lines->data as $line ) {
288 $payment_values['amount'] = number_format( ( $line->amount / 100 ), 2, '.', '' );
289 $payment_values['begin_date'] = gmdate( 'Y-m-d', $line->period->start );
290 $payment_values['expire_date'] = gmdate( 'Y-m-d', $line->period->end );
291 }
292
293 $payment_values['receipt_id'] = $this->charge ? $this->charge : __( 'None', 'formidable' );
294 $payment_values['status'] = $this->status;
295 $payment_values['meta_value'] = array();
296 $payment_values['created_at'] = current_time( 'mysql', 1 );
297
298 FrmTransLiteAppHelper::add_note_to_payment( $payment_values );
299 }
300
301 /**
302 * @param object $sub
303 * @param array $payment
304 * @return void
305 */
306 private function update_next_bill_date( $sub, $payment ) {
307 $frm_sub = new FrmTransLiteSubscription();
308 if ( $payment['status'] === 'complete' ) {
309 $frm_sub->update( $sub->id, array( 'next_bill_date' => $payment['expire_date'] ) );
310 } elseif ( $payment['status'] === 'refunded' ) {
311 $frm_sub->update( $sub->id, array( 'next_bill_date' => $payment['begin_date'] ) );
312 }
313 }
314
315 /**
316 * @return bool
317 */
318 private function is_partial_refund() {
319 $partial = false;
320 if ( $this->status === 'refunded' ) {
321 $amount = $this->invoice->amount;
322 $amount_refunded = $this->invoice->amount_refunded;
323 $partial = $amount != $amount_refunded;
324 }
325 return $partial;
326 }
327
328 /**
329 * @param array $payment_values
330 * @return void
331 */
332 private function set_partial_refund( &$payment_values ) {
333 $payment_values['amount'] = $this->invoice->amount - $this->invoice->amount_refunded;
334 $payment_values['amount'] = number_format( $payment_values['amount'] / 100, 2 );
335 }
336
337 /**
338 * @return void
339 */
340 public function process_connect_events() {
341 $this->flush_response();
342
343 $unprocessed_event_ids = FrmStrpLiteConnectHelper::get_unprocessed_event_ids();
344 if ( $unprocessed_event_ids ) {
345 $this->process_event_ids( $unprocessed_event_ids );
346 }
347 wp_send_json_success();
348 }
349
350 /**
351 * @since 6.5, introduced in v2.07 of the Stripe add on.
352 *
353 * @param array<string> $event_ids
354 * @return void
355 */
356 private function process_event_ids( $event_ids ) {
357 foreach ( $event_ids as $event_id ) {
358 if ( $this->should_skip_event( $event_id ) ) {
359 continue;
360 }
361
362 set_transient( 'frm_last_process_' . $event_id, time(), 60 );
363
364 $this->event = FrmStrpLiteConnectHelper::get_event( $event_id );
365 if ( is_object( $this->event ) ) {
366 $this->handle_event();
367 $this->track_handled_event( $event_id );
368 FrmStrpLiteConnectHelper::process_event( $event_id );
369 } else {
370 $this->count_failed_event( $event_id );
371 }
372 }
373 }
374
375 /**
376 * @since 6.5, introduced in v2.07 of the Stripe add on.
377 *
378 * @param string $event_id
379 * @return bool True if the event should be skipped.
380 */
381 private function should_skip_event( $event_id ) {
382 if ( $this->last_attempt_to_process_event_is_too_recent( $event_id ) ) {
383 return true;
384 }
385
386 $option = get_option( self::$events_to_skip_option_name );
387 if ( ! is_array( $option ) ) {
388 return false;
389 }
390
391 return in_array( $event_id, $option, true );
392 }
393
394 /**
395 * @param string $event_id
396 * @return bool
397 */
398 private function last_attempt_to_process_event_is_too_recent( $event_id ) {
399 $last_process_attempt = get_transient( 'frm_last_process_' . $event_id );
400 return is_numeric( $last_process_attempt ) && $last_process_attempt > ( time() - 60 );
401 }
402
403 /**
404 * @since 6.5, introduced in v2.07 of the Stripe add on.
405 *
406 * @param string $event_id
407 * @return void
408 */
409 private function count_failed_event( $event_id ) {
410 $transient_name = 'frm_failed_event_' . $event_id;
411 $transient = get_transient( $transient_name );
412 if ( is_int( $transient ) ) {
413 $failed_count = $transient + 1;
414 } else {
415 $failed_count = 1;
416 }
417
418 $maximum_retries = 3;
419 if ( $failed_count >= $maximum_retries ) {
420 $this->track_handled_event( $event_id );
421 } else {
422 set_transient( $transient_name, $failed_count );
423 }
424 }
425
426 /**
427 * Track an event to no longer process.
428 * This is called for successful events, and also for failed events after a number of retries.
429 *
430 * @since 6.5, introduced in v2.07 of the Stripe add on.
431 *
432 * @param string $event_id
433 * @return void
434 */
435 private function track_handled_event( $event_id ) {
436 $option = get_option( self::$events_to_skip_option_name );
437
438 if ( is_array( $option ) ) {
439 if ( count( $option ) > 1000 ) {
440 // Prevent the option from getting too big by removing the front item before adding the next.
441 array_shift( $option );
442 }
443 } else {
444 $option = array();
445 }
446
447 $option[] = $event_id;
448 update_option( self::$events_to_skip_option_name, $option, false );
449 }
450
451 /**
452 * @return void
453 */
454 private function handle_event() {
455 $this->invoice = $this->event->data->object;
456 $this->charge = isset( $this->invoice->charge ) ? $this->invoice->charge : false;
457 if ( ! $this->charge && $this->invoice->object === 'payment_intent' ) {
458 $this->charge = $this->invoice->id;
459 }
460
461 $events = array(
462 'payment_intent.succeeded' => 'complete',
463 'payment_intent.payment_failed' => 'failed',
464 'invoice.payment_succeeded' => 'complete',
465 'invoice.payment_failed' => 'failed',
466 'charge.refunded' => 'refunded',
467 );
468
469 if ( isset( $events[ $this->event->type ] ) ) {
470 $this->status = $events[ $this->event->type ];
471 $this->set_payment_status();
472 } elseif ( $this->event->type === 'customer.deleted' ) {
473 $this->reset_customer();
474 } elseif ( $this->event->type === 'customer.subscription.deleted' ) {
475 $this->subscription_canceled();
476 } elseif ( $this->event->type === 'customer.subscription.updated' ) {
477 $this->maybe_subscription_canceled();
478 }
479 }
480 }
481