Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
forbidals
/
wp-content
/
plugins
/
formidable
/
stripe
/
controllers
:
FrmStrpLiteEventsController.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php if ( ! defined( 'ABSPATH' ) ) { die( 'You are not allowed to call this page directly.' ); } class FrmStrpLiteEventsController { /** * @var string */ public static $events_to_skip_option_name = 'frm_strp_events_to_skip'; /** * @var object|null */ private $event; /** * @var object|null */ private $invoice; /** * @var string|null */ private $charge; /** * @var string|null */ private $status; /** * @return void */ private function set_payment_status() { if ( $this->status === 'refunded' ) { $this->charge = $this->invoice->id; } $frm_payment = new FrmTransLitePayment(); $payment = false; if ( $this->charge ) { $payment = $frm_payment->get_one_by( $this->charge, 'receipt_id' ); } if ( ! $payment && $this->status === 'refunded' ) { // If the refunded payment doesn't exist, stop here. FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken. The refunded payment does not exist' ); echo json_encode( array( 'response' => 'no payment exists', 'success' => false, ) ); return; } $run_triggers = false; if ( ! $payment ) { $payment = $this->prepare_from_invoice(); $run_triggers = true; } elseif ( $payment->status !== $this->status ) { if ( $this->should_skip_status_update_for_first_recurring_payment( $payment ) ) { return; } $payment_values = (array) $payment; $is_partial_refund = $this->is_partial_refund(); if ( $is_partial_refund ) { $this->set_partial_refund( $payment_values ); $amount_refunded = number_format( $this->invoice->amount_refunded / 100, 2 ); // translators: %s: The amount of money that was refunded. $note = sprintf( __( 'Payment partially refunded %s', 'formidable' ), $amount_refunded ); } else { $payment_values['status'] = $this->status; $payment->status = $this->status; // translators: %s: The status of the payment. $note = sprintf( __( 'Payment %s', 'formidable' ), $payment_values['status'] ); } FrmTransLiteAppHelper::add_note_to_payment( $payment_values, $note ); $u = $frm_payment->update( $payment->id, $payment_values ); echo json_encode( array( 'response' => 'Payment ' . $payment->id . ' was updated', 'success' => true, ) ); if ( ! $is_partial_refund ) { $run_triggers = true; } }//end if if ( $run_triggers && $payment && $payment->action_id ) { FrmTransLiteActionsController::trigger_payment_status_change( array( 'status' => $this->status, 'payment' => $payment, ) ); } } /** * Skip updating the payment object for the first recurring payment. * This is to prevent double notifications because the first recurring payment creates an invoice and that invoice triggers the payment events. * * @since 6.5, introduced in v2.07 of the Stripe add on. * * @param stdClass $payment * * @return bool */ private function should_skip_status_update_for_first_recurring_payment( $payment ) { if ( ! in_array( $this->event->type, array( 'payment_intent.succeeded', 'payment_intent.payment_failed' ), true ) ) { return false; } if ( empty( $payment->sub_id ) ) { // Only skip for subscriptions. This is because subscription events create an invoice, and the status change events trigger for the invoice as well. return false; } return $this->is_first_payment( $payment ); } /** * Tell Stripe Connect API that the request came through by flushing early before processing. * Flushing early allows the API to end the request earlier. * * @since 6.5, introduced in v2.07 of the Stripe add on. * * @return void */ private function flush_response() { ob_start(); // Get the size of the output. $size = ob_get_length(); // Disable compression (in case content length is compressed). header( 'Content-Encoding: none' ); // Set the content length of the response. header( 'Content-Length: ' . $size ); // Close the connection. header( 'Connection: close' ); // Flush all output. ob_end_flush(); @ob_flush(); flush(); } /** * When a customer is deleted in Stripe, remove the link to a user. * * @since 6.5, introduced in v2.01 of the Stripe add on. * * @return void */ private function reset_customer() { global $wpdb; $customer_id = $this->invoice->id; if ( ! $customer_id ) { return; } $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->usermeta WHERE meta_value = %s AND meta_key LIKE %s", $customer_id, '_frmstrp_customer_id%' ) ); } /** * @return void */ private function maybe_subscription_canceled() { // phpcs:ignore Universal.Operators.StrictComparisons if ( $this->invoice->cancel_at_period_end == true ) { $this->subscription_canceled( 'future_cancel' ); } } /** * @param string $status * * @return bool */ private function subscription_canceled( $status = 'canceled' ) { $sub = $this->get_subscription( $this->invoice->id ); if ( ! $sub ) { return false; } if ( $sub->status === $status ) { FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken since the subscription is already canceled.' ); echo json_encode( array( 'response' => 'Already canceled', 'success' => true, ) ); return false; } FrmTransLiteSubscriptionsController::change_subscription_status( array( 'status' => $status, 'sub' => $sub, ) ); return true; } /** * @return false|object */ private function prepare_from_invoice() { if ( empty( $this->invoice->subscription ) ) { // This isn't a subscription. echo json_encode( array( 'response' => 'Invoice missing', 'success' => false, ) ); return false; } $sub = $this->get_subscription( $this->invoice->subscription ); if ( ! $sub ) { return false; } $payment = $this->get_payment_for_sub( $sub->id ); $payment_values = (array) $payment; $this->set_payment_values( $payment_values ); $frm_payment = new FrmTransLitePayment(); if ( $this->is_first_payment( $payment ) ) { // The first payment for the subscription needs to be updated with the receipt id. $frm_payment->update( $payment->id, $payment_values ); $payment_id = $payment->id; } else { $payment_values['test'] = $this->event->livemode ? 0 : 1; // If this isn't the first, create a new payment. $payment_id = $frm_payment->create( $payment_values ); } $this->maybe_cancel_subscription( $sub ); $this->update_next_bill_date( $sub, $payment_values ); return $frm_payment->get_one( $payment_id ); } /** * Check if a subscription has reached its payment limit. * If it has, the subscription will be cancelled by period end. * * @since 6.11 * * @param object $sub * * @return void */ private function maybe_cancel_subscription( $sub ) { $action = FrmFormAction::get_single_action_type( $sub->action_id, 'payment' ); // @phpstan-ignore-next-line if ( ! is_object( $action ) || empty( $action->post_content['payment_limit'] ) ) { return; } $payment_limit = FrmStrpLiteSubscriptionHelper::prepare_payment_limit( $action->post_content['payment_limit'], // Form ID. (int) $action->menu_order, (int) $sub->item_id ); if ( is_wp_error( $payment_limit ) ) { FrmTransLiteLog::log_message( 'Invalid payment limit value', $payment_limit->get_error_message() ); return; } if ( $this->get_payments_count( $sub->id ) < $payment_limit ) { return; } // Flag to cancel subscription at period end. // In this case, we do not want to cancel immediately. $hook = 'frm_stripe_cancel_subscription_at_period_end'; $filter = function () { return true; }; add_filter( $hook, $filter, 99 ); $cancelled = FrmStrpLiteApiHelper::cancel_subscription( $sub->sub_id ); if ( $cancelled ) { FrmTransLiteSubscriptionsController::change_subscription_status( array( 'status' => 'future_cancel', 'sub' => $sub, ) ); } remove_filter( $hook, $filter, 99 ); } /** * Get the count of completed payments. * * @since 6.11 * * @param string $sub_id Stripe subscriptino id prefixed with 'sub_'. * * @return int */ private function get_payments_count( $sub_id ) { $frm_payment = new FrmTransLitePayment(); $all_payments = $frm_payment->get_all_by( $sub_id, 'sub_id' ); return FrmTransLiteAppHelper::count_completed_payments( $all_payments ); } /** * @since 6.5, introduced in v2.07 of the Stripe add on. * * @param stdClass $payment * * @return bool */ private function is_first_payment( $payment ) { return ! $payment->receipt_id || str_starts_with( $payment->receipt_id, 'pi_' ); } /** * @param string $sub_id * * @return object|null */ private function get_subscription( $sub_id ) { $frm_sub = new FrmTransLiteSubscription(); $sub = $frm_sub->get_one_by( $sub_id, 'sub_id' ); if ( ! $sub ) { // If this isn't an existing subscription, it must be a charge for another site/plugin. FrmTransLiteLog::log_message( 'Stripe Webhook Message', 'No action taken since there is not a matching subscription for ' . $sub_id ); echo json_encode( array( 'response' => 'Invoice missing', 'success' => false, ) ); } return $sub; } /** * @param string $sub_id * * @return object|null */ private function get_payment_for_sub( $sub_id ) { $frm_payment = new FrmTransLitePayment(); return $frm_payment->get_one_by( $sub_id, 'sub_id' ); } /** * @param array $payment_values * * @return void */ private function set_payment_values( &$payment_values ) { $payment_values['begin_date'] = gmdate( 'Y-m-d' ); $payment_values['expire_date'] = '0000-00-00'; foreach ( $this->invoice->lines->data as $line ) { $payment_values['amount'] = number_format( $line->amount / 100, 2, '.', '' ); $payment_values['begin_date'] = gmdate( 'Y-m-d', $line->period->start ); $payment_values['expire_date'] = gmdate( 'Y-m-d', $line->period->end ); } $payment_values['receipt_id'] = $this->charge ? $this->charge : __( 'None', 'formidable' ); $payment_values['status'] = $this->status; $payment_values['meta_value'] = array(); $payment_values['created_at'] = current_time( 'mysql', 1 ); FrmTransLiteAppHelper::add_note_to_payment( $payment_values ); } /** * @param object $sub * @param array $payment * * @return void */ private function update_next_bill_date( $sub, $payment ) { $frm_sub = new FrmTransLiteSubscription(); if ( $payment['status'] === 'complete' ) { $frm_sub->update( $sub->id, array( 'next_bill_date' => $payment['expire_date'] ) ); } elseif ( $payment['status'] === 'refunded' ) { $frm_sub->update( $sub->id, array( 'next_bill_date' => $payment['begin_date'] ) ); } } /** * @return bool */ private function is_partial_refund() { $partial = false; if ( $this->status === 'refunded' ) { $amount = $this->invoice->amount; $amount_refunded = $this->invoice->amount_refunded; $partial = $amount != $amount_refunded; // phpcs:ignore Universal.Operators.StrictComparisons } return $partial; } /** * @param array $payment_values * * @return void */ private function set_partial_refund( &$payment_values ) { $payment_values['amount'] = $this->invoice->amount - $this->invoice->amount_refunded; $payment_values['amount'] = number_format( $payment_values['amount'] / 100, 2 ); } /** * @return void */ public function process_connect_events() { $this->flush_response(); $unprocessed_event_ids = FrmStrpLiteConnectHelper::get_unprocessed_event_ids(); if ( $unprocessed_event_ids ) { $this->process_event_ids( $unprocessed_event_ids ); } wp_send_json_success(); } /** * @since 6.5, introduced in v2.07 of the Stripe add on. * * @param array<string> $event_ids * * @return void */ private function process_event_ids( $event_ids ) { foreach ( $event_ids as $event_id ) { if ( $this->should_skip_event( $event_id ) ) { continue; } set_transient( 'frm_last_process_' . $event_id, time(), 60 ); $this->event = FrmStrpLiteConnectHelper::get_event( $event_id ); if ( is_object( $this->event ) ) { $this->handle_event(); $this->track_handled_event( $event_id ); FrmStrpLiteConnectHelper::process_event( $event_id ); } else { $this->count_failed_event( $event_id ); } } } /** * @since 6.5, introduced in v2.07 of the Stripe add on. * * @param string $event_id * * @return bool True if the event should be skipped. */ private function should_skip_event( $event_id ) { if ( $this->last_attempt_to_process_event_is_too_recent( $event_id ) ) { return true; } $option = get_option( self::$events_to_skip_option_name ); return is_array( $option ) && in_array( $event_id, $option, true ); } /** * @param string $event_id * * @return bool */ private function last_attempt_to_process_event_is_too_recent( $event_id ) { $last_process_attempt = get_transient( 'frm_last_process_' . $event_id ); return is_numeric( $last_process_attempt ) && $last_process_attempt > time() - 60; } /** * @since 6.5, introduced in v2.07 of the Stripe add on. * * @param string $event_id * * @return void */ private function count_failed_event( $event_id ) { $transient_name = 'frm_failed_event_' . $event_id; $transient = get_transient( $transient_name ); $failed_count = is_int( $transient ) ? $transient + 1 : 1; $maximum_retries = 3; if ( $failed_count >= $maximum_retries ) { $this->track_handled_event( $event_id ); } else { set_transient( $transient_name, $failed_count, 4 * DAY_IN_SECONDS ); } } /** * Track an event to no longer process. * This is called for successful events, and also for failed events after a number of retries. * * @since 6.5, introduced in v2.07 of the Stripe add on. * * @param string $event_id * * @return void */ private function track_handled_event( $event_id ) { $option = get_option( self::$events_to_skip_option_name ); if ( is_array( $option ) ) { if ( count( $option ) > 1000 ) { // Prevent the option from getting too big by removing the front item before adding the next. array_shift( $option ); } } else { $option = array(); } $option[] = $event_id; update_option( self::$events_to_skip_option_name, $option, false ); } /** * @return void */ private function handle_event() { $this->invoice = $this->event->data->object; $this->charge = $this->invoice->charge ?? false; if ( ! $this->charge && $this->invoice->object === 'payment_intent' ) { $this->charge = $this->invoice->id; } $events = array( 'payment_intent.succeeded' => 'complete', 'payment_intent.payment_failed' => 'failed', 'invoice.payment_succeeded' => 'complete', 'invoice.payment_failed' => 'failed', 'charge.refunded' => 'refunded', ); if ( isset( $events[ $this->event->type ] ) ) { $this->status = $events[ $this->event->type ]; $this->set_payment_status(); } elseif ( $this->event->type === 'customer.deleted' ) { $this->reset_customer(); } elseif ( $this->event->type === 'customer.subscription.deleted' ) { $this->subscription_canceled(); } elseif ( $this->event->type === 'customer.subscription.updated' ) { $this->maybe_subscription_canceled(); } } }