PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
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 / paypal / controllers / FrmPayPalLiteEventsController.php

FrmPayPalLiteEventsController.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More trunk, at paypal/controllers/FrmPayPalLiteEventsController.php

882 lines 22.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 FrmPayPalLiteEventsController {
7
8 /**
9 * @var string
10 */
11 public static $events_to_skip_option_name = 'frm_paypal_events_to_skip';
12
13 /**
14 * @var object|null
15 */
16 private $event;
17
18 /**
19 * The data object from the event.
20 *
21 * @since 6.31
22 *
23 * @var object|null
24 */
25 private $resource;
26
27 /**
28 * The mapped payment status for the current event.
29 *
30 * @since 6.31
31 *
32 * @var string|null
33 */
34 private $status;
35
36 /**
37 * Tell PayPal Connect API that the request came through by flushing early before processing.
38 * Flushing early allows the API to end the request earlier.
39 *
40 * @since 6.31
41 *
42 * @return void
43 */
44 private function flush_response() {
45 ob_start();
46
47 // Get the size of the output.
48 $size = ob_get_length();
49
50 // Disable compression (in case content length is compressed).
51 header( 'Content-Encoding: none' );
52
53 // Set the content length of the response.
54 header( 'Content-Length: ' . $size );
55
56 // Close the connection.
57 header( 'Connection: close' );
58
59 // Flush all output.
60 ob_end_flush();
61 @ob_flush();
62 flush();
63 }
64
65 /**
66 * @return void
67 */
68 public function process_events() {
69 $this->flush_response();
70
71 $unprocessed_event_ids = FrmPayPalLiteConnectHelper::get_unprocessed_event_ids();
72
73 if ( $unprocessed_event_ids ) {
74 $this->process_event_ids( $unprocessed_event_ids );
75 }
76
77 wp_send_json_success();
78 }
79
80 /**
81 * @since 6.31
82 *
83 * @param array<string> $event_ids
84 *
85 * @return void
86 */
87 private function process_event_ids( $event_ids ) {
88 foreach ( $event_ids as $event_id ) {
89 if ( $this->should_skip_event( $event_id ) ) {
90 continue;
91 }
92
93 set_transient( 'frm_paypal_last_process_' . $event_id, time(), 60 );
94
95 $this->event = FrmPayPalLiteConnectHelper::get_event( $event_id );
96
97 if ( ! is_object( $this->event ) ) {
98 $this->count_failed_event( $event_id );
99 continue;
100 }
101
102 $this->handle_event();
103 $this->track_handled_event( $event_id );
104 FrmPayPalLiteConnectHelper::process_event( $event_id );
105 }
106 }
107
108 /**
109 * @since 6.31
110 *
111 * @param string $event_id
112 *
113 * @return bool True if the event should be skipped.
114 */
115 private function should_skip_event( $event_id ) {
116 if ( $this->last_attempt_to_process_event_is_too_recent( $event_id ) ) {
117 return true;
118 }
119
120 $option = get_option( self::$events_to_skip_option_name );
121
122 return is_array( $option ) && in_array( $event_id, $option, true );
123 }
124
125 /**
126 * @param string $event_id
127 *
128 * @return bool
129 */
130 private function last_attempt_to_process_event_is_too_recent( $event_id ) {
131 $last_process_attempt = get_transient( 'frm_paypal_last_process_' . $event_id );
132 return is_numeric( $last_process_attempt ) && $last_process_attempt > time() - 60;
133 }
134
135 /**
136 * @since 6.31
137 *
138 * @param string $event_id
139 *
140 * @return void
141 */
142 private function count_failed_event( $event_id ) {
143 $transient_name = 'frm_paypal_failed_event_' . $event_id;
144 $transient = get_transient( $transient_name );
145 $failed_count = is_int( $transient ) ? $transient + 1 : 1;
146 $maximum_retries = 3;
147
148 if ( $failed_count >= $maximum_retries ) {
149 $this->track_handled_event( $event_id );
150 } else {
151 set_transient( $transient_name, $failed_count, 4 * DAY_IN_SECONDS );
152 }
153 }
154
155 /**
156 * Track an event to no longer process.
157 * This is called for successful events, and also for failed events after a number of retries.
158 *
159 * @since 6.31
160 *
161 * @param string $event_id
162 *
163 * @return void
164 */
165 private function track_handled_event( $event_id ) {
166 $option = get_option( self::$events_to_skip_option_name );
167
168 if ( is_array( $option ) ) {
169 if ( count( $option ) > 1000 ) {
170 // Prevent the option from getting too big by removing the front item before adding the next.
171 array_shift( $option );
172 }
173 } else {
174 $option = array();
175 }
176
177 $option[] = $event_id;
178 update_option( self::$events_to_skip_option_name, $option, false );
179 }
180
181 /**
182 * @return void
183 */
184 private function handle_event() {
185 $this->resource = $this->event->resource ?? null;
186
187 if ( ! is_object( $this->resource ) ) {
188 FrmTransLiteLog::log_message( 'PayPal Webhook Message', 'No resource object found in event' );
189 return;
190 }
191
192 $payment_events = array(
193 'PAYMENT.CAPTURE.COMPLETED' => 'complete',
194 'PAYMENT.CAPTURE.DECLINED' => 'failed',
195 'PAYMENT.CAPTURE.DENIED' => 'failed',
196 'PAYMENT.CAPTURE.REFUNDED' => 'refunded',
197 'PAYMENT.CAPTURE.REVERSED' => 'refunded',
198 'PAYMENT.SALE.COMPLETED' => 'complete',
199 'PAYMENT.SALE.DENIED' => 'failed',
200 'PAYMENT.SALE.REFUNDED' => 'refunded',
201 'PAYMENT.SALE.REVERSED' => 'refunded',
202 );
203
204 if ( isset( $payment_events[ $this->event->event_type ] ) ) {
205 $this->status = $payment_events[ $this->event->event_type ];
206 $this->handle_payment_event();
207 return;
208 }
209
210 switch ( $this->event->event_type ) {
211 case 'BILLING.SUBSCRIPTION.ACTIVATED':
212 case 'BILLING.SUBSCRIPTION.RE-ACTIVATED':
213 $this->handle_subscription_activated();
214 break;
215
216 case 'BILLING.SUBSCRIPTION.CANCELLED':
217 case 'BILLING.SUBSCRIPTION.EXPIRED':
218 case 'BILLING.SUBSCRIPTION.SUSPENDED':
219 $this->handle_subscription_canceled();
220 break;
221
222 case 'BILLING.SUBSCRIPTION.PAYMENT.FAILED':
223 $this->handle_subscription_payment_failed();
224 break;
225
226 case 'BILLING.SUBSCRIPTION.UPDATED':
227 $this->handle_subscription_updated();
228 break;
229 }
230 }
231
232 /**
233 * Handle a payment capture or sale event by syncing the payment record.
234 *
235 * @since 6.31
236 *
237 * @return void
238 */
239 private function handle_payment_event() {
240 $receipt_id = $this->get_receipt_id_for_event();
241
242 if ( ! $receipt_id ) {
243 FrmTransLiteLog::log_message( 'PayPal Webhook Message', 'No resource ID found in payment event' );
244 return;
245 }
246
247 $frm_payment = new FrmTransLitePayment();
248 $payment = $frm_payment->get_one_by( $receipt_id, 'receipt_id' );
249
250 // If no payment was found by capture ID, check for a pending payment
251 // stored with the order ID as receipt_id. Update it to the capture ID.
252 if ( ! $payment ) {
253 $payment = $this->maybe_resolve_pending_payment( $frm_payment, $receipt_id );
254 }
255
256 if ( ! $payment && $this->status === 'refunded' ) {
257 FrmTransLiteLog::log_message( 'PayPal Webhook Message', 'No action taken. The refunded payment does not exist for ' . $receipt_id );
258 return;
259 }
260
261 $run_triggers = false;
262
263 if ( ! $payment ) {
264 $payment = $this->maybe_create_subscription_payment();
265
266 if ( $payment ) {
267 $run_triggers = true;
268 }
269 } elseif ( $payment->status !== $this->status ) {
270 $payment_values = (array) $payment;
271 $is_partial_refund = $this->is_partial_refund();
272
273 if ( $is_partial_refund ) {
274 $this->set_partial_refund( $payment_values );
275 // translators: %s: The amount of money that was refunded.
276 $note = sprintf( __( 'Payment partially refunded %s', 'formidable' ), $this->get_refunded_amount() );
277 } else {
278 $payment_values['status'] = $this->status;
279 $payment->status = $this->status;
280 // translators: %s: The status of the payment.
281 $note = sprintf( __( 'Payment %s', 'formidable' ), $payment_values['status'] );
282 }
283
284 FrmTransLiteAppHelper::add_note_to_payment( $payment_values, $note );
285
286 $frm_payment->update( $payment->id, $payment_values );
287
288 if ( ! $is_partial_refund ) {
289 $run_triggers = true;
290 }
291 }//end if
292
293 if ( $run_triggers && $payment && $payment->action_id ) {
294 FrmTransLiteActionsController::trigger_payment_status_change(
295 array(
296 'status' => $this->status,
297 'payment' => $payment,
298 )
299 );
300 }
301 }
302
303 /**
304 * Try to create a new payment record from a subscription payment event.
305 * This handles recurring payments after the first one.
306 *
307 * @since 6.31
308 *
309 * @return false|object The new payment object or false if not a subscription payment.
310 */
311 private function maybe_create_subscription_payment() {
312 $subscription_id = $this->get_subscription_id_from_resource();
313
314 if ( ! $subscription_id ) {
315 return false;
316 }
317
318 $sub = $this->get_subscription( $subscription_id );
319
320 if ( ! $sub ) {
321 return false;
322 }
323
324 $receipt_id = $this->resource->id ?? '';
325 $amount = $this->get_amount_from_resource();
326
327 $payment_values = array(
328 'paysys' => 'paypal',
329 'amount' => $amount,
330 'status' => $this->status,
331 'item_id' => $sub->item_id,
332 'action_id' => $sub->action_id,
333 'receipt_id' => $receipt_id,
334 'sub_id' => $sub->id,
335 'begin_date' => gmdate( 'Y-m-d' ),
336 'expire_date' => '0000-00-00',
337 'meta_value' => array(),
338 'created_at' => current_time( 'mysql', 1 ),
339 'test' => 'test' === FrmPayPalLiteAppHelper::active_mode() ? 1 : 0,
340 );
341
342 FrmTransLiteAppHelper::add_note_to_payment( $payment_values );
343
344 $frm_payment = new FrmTransLitePayment();
345 $existing_payment = $frm_payment->get_one_by( $receipt_id, 'receipt_id' );
346
347 if ( $existing_payment ) {
348 return false;
349 }
350
351 $existing_sub_payment = $frm_payment->get_one_by( $sub->id, 'sub_id' );
352
353 if ( $existing_sub_payment && str_starts_with( $existing_sub_payment->receipt_id, 'I-' ) ) {
354 $frm_payment->update( $existing_sub_payment->id, array( 'receipt_id' => $receipt_id ) );
355
356 $this->update_next_bill_date( $sub );
357 $this->maybe_cancel_subscription_at_limit( $sub );
358
359 return $frm_payment->get_one( $existing_sub_payment->id );
360 }
361
362 $payment_id = $frm_payment->create( $payment_values );
363
364 $this->update_next_bill_date( $sub );
365 $this->maybe_cancel_subscription_at_limit( $sub );
366
367 return $frm_payment->get_one( $payment_id );
368 }
369
370 /**
371 * Get the PayPal subscription ID from the resource object.
372 *
373 * @since 6.31
374 *
375 * @return string
376 */
377 private function get_subscription_id_from_resource() {
378 if ( ! empty( $this->resource->billing_agreement_id ) ) {
379 return $this->resource->billing_agreement_id;
380 }
381
382 if ( ! empty( $this->resource->supplementary_data->related_ids->subscription_id ) ) {
383 return $this->resource->supplementary_data->related_ids->subscription_id;
384 }
385
386 return '';
387 }
388
389 /**
390 * Get the payment amount from the resource object.
391 *
392 * @since 6.31
393 *
394 * @return string
395 */
396 private function get_amount_from_resource() {
397 if ( ! empty( $this->resource->amount->total ) ) {
398 return $this->resource->amount->total;
399 }
400
401 if ( ! empty( $this->resource->amount->value ) ) {
402 return $this->resource->amount->value;
403 }
404
405 return '0.00';
406 }
407
408 /**
409 * Get the refunded amount from the resource object.
410 *
411 * @since 6.31
412 *
413 * @return string
414 */
415 private function get_refunded_amount() {
416 if ( ! empty( $this->resource->amount->total ) ) {
417 return $this->resource->amount->total;
418 }
419
420 if ( ! empty( $this->resource->amount->value ) ) {
421 return $this->resource->amount->value;
422 }
423
424 return '0.00';
425 }
426
427 /**
428 * Check if a refund is partial by comparing the refunded amount to the original payment amount.
429 *
430 * @since 6.31
431 *
432 * @return bool
433 */
434 private function is_partial_refund() {
435 if ( $this->status !== 'refunded' ) {
436 return false;
437 }
438
439 $receipt_id = $this->get_receipt_id_for_event();
440
441 if ( ! $receipt_id ) {
442 return false;
443 }
444
445 $frm_payment = new FrmTransLitePayment();
446 $original_payment = $frm_payment->get_one_by( $receipt_id, 'receipt_id' );
447 $refunded_amount = (float) $this->get_refunded_amount();
448
449 if ( $original_payment ) {
450 return $refunded_amount < (float) $original_payment->amount;
451 }
452
453 return false;
454 }
455
456 /**
457 * Set the partial refund amount on a payment.
458 *
459 * @since 6.31
460 *
461 * @param array $payment_values The payment values to update.
462 *
463 * @return void
464 */
465 private function set_partial_refund( &$payment_values ) {
466 $refunded = (float) $this->get_refunded_amount();
467 $original = (float) $payment_values['amount'];
468 $payment_values['amount'] = number_format( $original - $refunded, 2, '.', '' );
469 }
470
471 /**
472 * Try to find a pending payment stored with the order ID as receipt_id.
473 *
474 * When a capture is pending, the payment record is created with the order
475 * ID as receipt_id. When the capture completes, the webhook provides a
476 * capture ID. This method extracts the order ID from the resource's HATEOAS
477 * 'up' link, looks up the pending payment, and updates its receipt_id to
478 * the capture ID.
479 *
480 * @since 6.31
481 *
482 * @param FrmTransLitePayment $frm_payment The payment model instance.
483 * @param string $capture_id The capture ID from the webhook resource.
484 *
485 * @return object|null The payment if found and updated, or null.
486 */
487 private function maybe_resolve_pending_payment( $frm_payment, $capture_id ) {
488 $order_id = $this->get_order_id_from_resource_links();
489
490 if ( ! $order_id ) {
491 return null;
492 }
493
494 $payment = $frm_payment->get_one_by( $order_id, 'receipt_id' );
495
496 if ( ! $payment || 'pending' !== $payment->status ) {
497 return null;
498 }
499
500 $frm_payment->update( $payment->id, array( 'receipt_id' => $capture_id ) );
501 $payment->receipt_id = $capture_id;
502
503 return $payment;
504 }
505
506 /**
507 * Extract the order ID from the webhook resource's HATEOAS 'up' link.
508 *
509 * For PAYMENT.CAPTURE.COMPLETED events, the 'up' link points to the
510 * order: /v2/checkout/orders/{order_id}.
511 *
512 * @since 6.31
513 *
514 * @return string The order ID, or empty string if not found.
515 */
516 private function get_order_id_from_resource_links() {
517 if ( empty( $this->resource->links ) || ! is_array( $this->resource->links ) ) {
518 return '';
519 }
520
521 foreach ( $this->resource->links as $link ) {
522 if ( ! isset( $link->rel ) || 'up' !== $link->rel ) {
523 continue;
524 }
525
526 if ( empty( $link->href ) ) {
527 continue;
528 }
529
530 $path = wp_parse_url( $link->href, PHP_URL_PATH );
531
532 if ( ! $path ) {
533 continue;
534 }
535
536 $segments = explode( '/', rtrim( $path, '/' ) );
537 $last_segment = end( $segments );
538
539 if ( $last_segment ) {
540 return $last_segment;
541 }
542 }//end foreach
543
544 return '';
545 }
546
547 /**
548 * Get the receipt ID to use for looking up a payment record.
549 *
550 * For most events, this is the resource ID. For refund/reversal events,
551 * the resource is the refund object, so we need to extract the original
552 * capture or sale ID from the resource's sale_id property or HATEOAS links.
553 *
554 * @since 6.31
555 *
556 * @return string The receipt ID of the original payment.
557 */
558 private function get_receipt_id_for_event() {
559 if ( $this->status !== 'refunded' ) {
560 return $this->resource->id ?? '';
561 }
562
563 $refund_id = $this->resource->id ?? '';
564
565 // For sale refunds, the sale_id property references the original sale.
566 if ( ! empty( $this->resource->sale_id ) ) {
567 return $this->resource->sale_id;
568 }
569
570 // For capture refunds, extract the capture ID from the HATEOAS 'up' link.
571 // The 'up' link points to the original capture: /v2/payments/captures/{capture_id}
572 if ( empty( $this->resource->links ) || ! is_array( $this->resource->links ) ) {
573 return $refund_id;
574 }
575
576 foreach ( $this->resource->links as $link ) {
577 if ( ! isset( $link->rel ) || 'up' !== $link->rel ) {
578 continue;
579 }
580
581 if ( empty( $link->href ) ) {
582 continue;
583 }
584
585 // Extract the ID from the URL path: .../captures/{id} or .../sale/{id}
586 $path = wp_parse_url( $link->href, PHP_URL_PATH );
587
588 if ( ! $path ) {
589 continue;
590 }
591
592 $segments = explode( '/', rtrim( $path, '/' ) );
593 $last_segment = end( $segments );
594
595 if ( $last_segment ) {
596 return $last_segment;
597 }
598 }//end foreach
599
600 // Fallback to the resource ID (the refund ID itself).
601 return $refund_id;
602 }
603
604 /**
605 * Get a subscription record by its PayPal subscription ID.
606 *
607 * @since 6.31
608 *
609 * @param string $sub_id The PayPal subscription ID.
610 *
611 * @return object|null
612 */
613 private function get_subscription( $sub_id ) {
614 $frm_sub = new FrmTransLiteSubscription();
615 $sub = $frm_sub->get_one_by( $sub_id, 'sub_id' );
616
617 if ( ! $sub ) {
618 FrmTransLiteLog::log_message( 'PayPal Webhook Message', 'No action taken since there is not a matching subscription for ' . $sub_id );
619 }
620
621 return $sub;
622 }
623
624 /**
625 * Update the next bill date for a subscription using the PayPal API.
626 *
627 * @since 6.31
628 *
629 * @param object $sub The local subscription record.
630 *
631 * @return void
632 */
633 private function update_next_bill_date( $sub ) {
634 $subscription = FrmPayPalLiteConnectHelper::get_subscription( $sub->sub_id );
635
636 if ( ! is_object( $subscription ) ) {
637 return;
638 }
639
640 $next_bill_date = '';
641
642 if ( ! empty( $subscription->billing_info->next_billing_time ) ) {
643 $next_bill_date = gmdate( 'Y-m-d', strtotime( $subscription->billing_info->next_billing_time ) );
644 }
645
646 if ( ! $next_bill_date ) {
647 return;
648 }
649
650 $frm_sub = new FrmTransLiteSubscription();
651 $frm_sub->update( $sub->id, array( 'next_bill_date' => $next_bill_date ) );
652 }
653
654 /**
655 * Check if a subscription has reached its payment limit.
656 * If it has, cancel the subscription.
657 *
658 * @since 6.31
659 *
660 * @param object $sub The local subscription record.
661 *
662 * @return void
663 */
664 private function maybe_cancel_subscription_at_limit( $sub ) {
665 $action = FrmFormAction::get_single_action_type( $sub->action_id, 'payment' );
666
667 if ( ! is_object( $action ) || empty( $action->post_content['payment_limit'] ) ) {
668 return;
669 }
670
671 $frm_payment = new FrmTransLitePayment();
672 $all_payments = $frm_payment->get_all_by( $sub->id, 'sub_id' );
673 $count = FrmTransLiteAppHelper::count_completed_payments( $all_payments );
674
675 if ( $count < (int) $action->post_content['payment_limit'] ) {
676 return;
677 }
678
679 $cancelled = FrmPayPalLiteConnectHelper::cancel_subscription( $sub->sub_id );
680
681 if ( $cancelled ) {
682 FrmTransLiteSubscriptionsController::change_subscription_status(
683 array(
684 'status' => 'future_cancel',
685 'sub' => $sub,
686 )
687 );
688 }
689 }
690
691 /**
692 * Handle a subscription activated or re-activated event.
693 *
694 * @since 6.31
695 *
696 * @return void
697 */
698 private function handle_subscription_activated() {
699 $subscription_id = $this->resource->id ?? '';
700
701 if ( ! $subscription_id ) {
702 return;
703 }
704
705 $sub = $this->get_subscription( $subscription_id );
706
707 if ( ! $sub ) {
708 return;
709 }
710
711 if ( $sub->status === 'active' ) {
712 return;
713 }
714
715 FrmTransLiteSubscriptionsController::change_subscription_status(
716 array(
717 'status' => 'active',
718 'sub' => $sub,
719 )
720 );
721
722 $this->update_next_bill_date( $sub );
723 }
724
725 /**
726 * Handle a subscription cancelled, expired, or suspended event.
727 *
728 * @since 6.31
729 *
730 * @return void
731 */
732 private function handle_subscription_canceled() {
733 $subscription_id = $this->resource->id ?? '';
734
735 if ( ! $subscription_id ) {
736 FrmTransLiteLog::log_message( 'PayPal Webhook Debug', 'BILLING.SUBSCRIPTION.CANCELLED: No subscription ID found in resource' );
737 return;
738 }
739
740 FrmTransLiteLog::log_message( 'PayPal Webhook Debug', 'BILLING.SUBSCRIPTION.CANCELLED: Looking for subscription ID: ' . $subscription_id );
741
742 $sub = $this->get_subscription( $subscription_id );
743
744 if ( ! $sub ) {
745 FrmTransLiteLog::log_message( 'PayPal Webhook Debug', 'BILLING.SUBSCRIPTION.CANCELLED: Subscription not found in database for ID: ' . $subscription_id );
746 return;
747 }
748
749 FrmTransLiteLog::log_message( 'PayPal Webhook Debug', 'BILLING.SUBSCRIPTION.CANCELLED: Found subscription, current status: ' . $sub->status );
750
751 if ( $sub->status === 'canceled' ) {
752 FrmTransLiteLog::log_message( 'PayPal Webhook Debug', 'BILLING.SUBSCRIPTION.CANCELLED: Subscription already canceled, no action needed' );
753 return;
754 }
755
756 FrmTransLiteLog::log_message( 'PayPal Webhook Debug', 'BILLING.SUBSCRIPTION.CANCELLED: Updating subscription status to canceled' );
757
758 FrmTransLiteSubscriptionsController::change_subscription_status(
759 array(
760 'status' => 'canceled',
761 'sub' => $sub,
762 )
763 );
764
765 FrmTransLiteLog::log_message( 'PayPal Webhook Debug', 'BILLING.SUBSCRIPTION.CANCELLED: Status update completed' );
766 }
767
768 /**
769 * Handle a subscription payment failed event.
770 * Increments the fail count and cancels after too many failures.
771 *
772 * @since 6.31
773 *
774 * @return void
775 */
776 private function handle_subscription_payment_failed() {
777 $subscription_id = $this->resource->id ?? '';
778
779 if ( ! $subscription_id ) {
780 return;
781 }
782
783 $sub = $this->get_subscription( $subscription_id );
784
785 if ( ! $sub ) {
786 return;
787 }
788
789 $frm_sub = new FrmTransLiteSubscription();
790 $fail_count = (int) $sub->fail_count + 1;
791
792 $frm_sub->update(
793 $sub->id,
794 array( 'fail_count' => $fail_count )
795 );
796
797 if ( $fail_count > 3 ) {
798 FrmTransLiteSubscriptionsController::change_subscription_status(
799 array(
800 'status' => 'canceled',
801 'sub' => $sub,
802 )
803 );
804 }
805 }
806
807 /**
808 * Handle a subscription updated event.
809 * Syncs subscription data like amount and next bill date.
810 *
811 * @since 6.31
812 *
813 * @return void
814 */
815 private function handle_subscription_updated() {
816 $subscription_id = $this->resource->id ?? '';
817
818 if ( ! $subscription_id ) {
819 return;
820 }
821
822 $sub = $this->get_subscription( $subscription_id );
823
824 if ( ! $sub ) {
825 return;
826 }
827
828 $new_values = array();
829
830 // Sync the subscription status.
831 $paypal_status = $this->resource->status ?? '';
832 $status_map = array(
833 'ACTIVE' => 'active',
834 'SUSPENDED' => 'canceled',
835 'CANCELLED' => 'canceled',
836 'EXPIRED' => 'canceled',
837 );
838
839 if ( $paypal_status && isset( $status_map[ $paypal_status ] ) && $sub->status !== $status_map[ $paypal_status ] ) {
840 FrmTransLiteSubscriptionsController::change_subscription_status(
841 array(
842 'status' => $status_map[ $paypal_status ],
843 'sub' => $sub,
844 )
845 );
846 }
847
848 // Sync next billing date and amount from the PayPal API.
849 $subscription = FrmPayPalLiteConnectHelper::get_subscription( $subscription_id );
850
851 if ( ! is_object( $subscription ) ) {
852 // If the subscription doesn't exist in PayPal's API (404 error), it's likely cancelled.
853 // Update the local status to canceled if it's not already.
854 if ( $sub->status !== 'canceled' ) {
855 FrmTransLiteSubscriptionsController::change_subscription_status(
856 array(
857 'status' => 'canceled',
858 'sub' => $sub,
859 )
860 );
861 }
862
863 return;
864 }
865
866 if ( ! empty( $subscription->billing_info->next_billing_time ) ) {
867 $new_values['next_bill_date'] = gmdate( 'Y-m-d', strtotime( $subscription->billing_info->next_billing_time ) );
868 }
869
870 if ( ! empty( $subscription->plan->billing_cycles[0]->pricing_scheme->fixed_price->value ) ) {
871 $new_values['amount'] = $subscription->plan->billing_cycles[0]->pricing_scheme->fixed_price->value;
872 }
873
874 if ( ! $new_values ) {
875 return;
876 }
877
878 $frm_sub = new FrmTransLiteSubscription();
879 $frm_sub->update( $sub->id, $new_values );
880 }
881 }
882