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

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