Current File : /home/digitaw/www/wp-content/plugins/event-tickets/src/Tickets/Commerce/Gateways/Stripe/Hooks.php
<?php

namespace TEC\Tickets\Commerce\Gateways\Stripe;

use TEC\Tickets\Commerce\Module;
use TEC\Tickets\Commerce\Notice_Handler;
use TEC\Tickets\Commerce\Status\Completed;
use TEC\Tickets\Commerce\Success;
use Tribe\Tickets\Admin\Settings as Admin_Settings;
use Tribe\Admin\Pages;
use Tribe__Tickets__Main as Tickets_Plugin;
use WP_Post;
use Exception;
use TEC\Tickets\Commerce\Order;
use TEC\Tickets\Commerce\Status\Status_Handler;
use TEC\Tickets\Commerce\Gateways\Stripe\Webhooks;
use Tribe__Utils__Array as Arr;

/**
 * Class Hooks
 *
 * @since 5.3.0
 *
 * @package TEC\Tickets\Commerce\Gateways\Stripe
 */
class Hooks extends \TEC\Common\Contracts\Service_Provider {

	/**
	 * @inheritDoc
	 */
	public function register() {
		$this->add_actions();
		$this->add_filters();
	}

	/**
	 * Adds the actions required by each Stripe component.
	 *
	 * @since 5.3.0
	 * @since 5.24.0 Moved async webhook process to Commerce Hooks routing action.
	 */
	protected function add_actions() {
		add_action( 'rest_api_init', [ $this, 'register_endpoints' ] );
		add_action( 'tec_tickets_commerce_checkout_page_parse_request', [ $this, 'maybe_create_stripe_payment_intent' ], 10000 );

		add_action( 'admin_init', [ $this, 'handle_stripe_errors' ] );
		// Set up during feature release.
		add_action( 'admin_init', [ $this, 'setup_stripe_webhook_on_release' ] );
		// Set up during plugin activation.
		add_action( 'admin_init', [ $this, 'setup_stripe_webhook_on_activation' ] );

		add_action( 'tec_tickets_commerce_checkout_page_parse_request', [ $this, 'handle_checkout_request' ] );

		add_action( 'wp_ajax_tec_tickets_commerce_gateway_stripe_test_webhooks', [ $this, 'action_handle_testing_webhooks_field' ] );
		add_action( 'wp_ajax_tec_tickets_commerce_gateway_stripe_verify_webhooks', [ $this, 'action_handle_verify_webhooks' ] );

		add_action( 'wp_ajax_' . Webhooks::NONCE_KEY_SETUP, [ $this, 'action_handle_set_up_webhook' ] );
	}

	/**
	 * Adds the filters required by each Stripe component.
	 *
	 * @since 5.3.0
	 */
	protected function add_filters() {
		add_filter( 'tec_tickets_commerce_gateways', [ $this, 'filter_add_gateway' ], 5, 2 );
		add_filter( 'tec_tickets_commerce_notice_messages', [ $this, 'include_admin_notices' ] );
		add_filter( 'tec_tickets_commerce_stripe_settings', [ $this, 'include_webhook_settings' ], 20 );
		add_filter( 'tribe_field_div_end', [ $this, 'filter_include_webhooks_copy' ], 10, 2 );
		add_filter( 'tribe_settings_save_field_value', [ $this, 'validate_payment_methods' ], 10, 2 );
		add_filter( 'tribe_settings_validate_field_value', [ $this, 'provide_defaults_for_hidden_fields'], 10, 3 );
		add_filter( 'tec_tickets_commerce_admin_notices', [ $this, 'filter_admin_notices' ] );
		add_filter( 'tec_tickets_commerce_success_page_should_display_billing_fields', [ $this, 'modify_checkout_display_billing_info' ] );
		add_filter( 'tec_tickets_commerce_shortcode_checkout_page_template_vars', [ $this, 'modify_checkout_vars' ] );
		add_filter( 'tec_tickets_commerce_order_stripe_get_value_refunded', [ $this, 'filter_order_get_value_refunded' ], 10, 2 );
		add_filter( 'tec_tickets_commerce_order_stripe_get_value_captured', [ $this, 'filter_order_get_value_captured' ], 10, 2 );
		add_filter( 'tec_tickets_commerce_gateway_value_formatter_stripe_currency_map', [ $this, 'filter_stripe_currency_precision' ], 10, 3 );
	}

	/**
	 * Filter the refunded amount for the order.
	 *
	 * @since 5.24.0
	 *
	 * @param ?int  $nothing The current value.
	 * @param array $refunds The refunds for the order.
	 *
	 * @return int
	 */
	public function filter_order_get_value_refunded( ?int $nothing, array $refunds ): int {
		if ( $nothing ) {
			return $nothing;
		}

		if ( empty( $refunds['0']['amount_refunded'] ) ) {
			return 0;
		}

		return (int) max( wp_list_pluck( $refunds, 'amount_refunded' ) );
	}

	/**
	 * Filter the captured amount for the order.
	 *
	 * @since 5.24.0
	 *
	 * @param ?int  $nothing The current value.
	 * @param array $refunds The refunds for the order.
	 *
	 * @return int
	 */
	public function filter_order_get_value_captured( ?int $nothing, array $refunds ): int {
		if ( $nothing ) {
			return $nothing;
		}

		if ( empty( $refunds['0']['amount_captured'] ) ) {
			return 0;
		}

		return (int) max( wp_list_pluck( $refunds, 'amount_captured' ) );
	}

	/**
	 * Process the async stripe webhook.
	 *
	 * @since 5.18.1
	 * @since 5.19.3 Added the $retry parameter.
	 *
	 * @param int $order_id The order ID.
	 * @param int $retry      The number of times this has been tried.
	 *
	 * @throws Exception If the action fails after too many retries.
	 */
	public function process_async_stripe_webhook( int $order_id, int $retry = 0 ): void {
		$order = tec_tc_get_order( $order_id );

		if ( ! $order ) {
			return;
		}

		if ( ! $order instanceof WP_Post ) {
			return;
		}

		if ( ! $order->ID ) {
			return;
		}

		$webhooks = tribe( Webhooks::class );

		if ( time() < $order->on_checkout_hold ) {
			if ( $retry > $webhooks->get_max_number_of_retries() ) {
				throw new Exception( __( 'Failed to process the webhook after too many tries.', 'event-tickets' ) );
			}

			as_schedule_single_action(
				$order->on_checkout_hold + MINUTE_IN_SECONDS,
				'tec_tickets_commerce_async_webhook_process',
				[
					'order_id' => $order_id,
					'try'      => ++$retry,
				],
				'tec-tickets-commerce-webhooks'
			);
			return;
		}

		$pending_webhooks = $webhooks->get_pending_webhooks( $order->ID );

		// On multiple checkout completes, make sure we dont process the same webhook twice.
		$webhooks->delete_pending_webhooks( $order->ID );

		foreach ( $pending_webhooks as $pending_webhook ) {
			if ( ! ( is_array( $pending_webhook ) ) ) {
				continue;
			}

			if ( ! isset( $pending_webhook['new_status'], $pending_webhook['metadata'], $pending_webhook['old_status'] ) ) {
				continue;
			}

			$new_status_wp_slug = $pending_webhook['new_status'];

			// The order is already there!
			if ( $order->post_status === $new_status_wp_slug ) {
				continue;
			}

			// The order is no longer where it was... that could be dangerous, lets bail?
			if ( $order->post_status !== $pending_webhook['old_status'] ) {
				continue;
			}

			tribe( Order::class )->modify_status(
				$order->ID,
				tribe( Status_Handler::class )->get_by_wp_slug( $new_status_wp_slug )->get_slug(),
				$pending_webhook['metadata']
			);
		}
	}

	/**
	 * Set up Stripe Webhook based on transient value.
	 *
	 * @since 5.11.0
	 *
	 * @return bool
	 */
	public function setup_stripe_webhook_on_activation() {
		/**
		 * Filters whether to enable the Stripe Webhook.
		 *
		 * @since 5.11.0
		 *
		 * @param bool $need_to_enable_stripe_webhook Whether to enable the Stripe Webhook.
		 */
		$need_to_enable_stripe_webhook = apply_filters( 'tec_tickets_commerce_need_to_enable_stripe_webhook', get_transient( 'tec_tickets_commerce_setup_stripe_webhook' ) );

		if ( false === $need_to_enable_stripe_webhook ) {
			return false;
		}

		// Always delete the transient.
		delete_transient( 'tec_tickets_commerce_setup_stripe_webhook' );

		// Bail on non-truthy values as well.
		if ( ! tribe_is_truthy( $need_to_enable_stripe_webhook ) ) {
			return false;
		}

		return tribe( Webhooks::class )->handle_webhook_setup();
	}

	/**
	 * Set up Stripe Webhook based on the plugin version.
	 *
	 * @since 5.11.0
	 *
	 * @return bool
	 */
	public function setup_stripe_webhook_on_release() {
		$stripe_webhook_version = tribe_get_option( 'tec_tickets_commerce_stripe_webhook_version', false );

		if ( $stripe_webhook_version ) {
			return false;
		}

		tribe_update_option( 'tec_tickets_commerce_stripe_webhook_version', Tickets_Plugin::VERSION );

		return tribe( Webhooks::class )->handle_webhook_setup();
	}

	/**
	 * Add this gateway to the list of available.
	 *
	 * @since 5.3.0
	 *
	 * @param array $gateways List of available gateways.
	 *
	 * @return array
	 */
	public function filter_add_gateway( array $gateways = [] ) : array {
		return $this->container->make( Gateway::class )->register_gateway( $gateways );
	}

	/**
	 * Modify the HTML of the Webhooks field to include a copy button.
	 *
	 * @since 5.3.0
	 *
	 * @param string        $html
	 * @param \Tribe__Field $field
	 *
	 * @return string
	 */
	public function filter_include_webhooks_copy( string $html, \Tribe__Field $field ) : string {
		return $this->container->make( Webhooks::class )->include_webhooks_copy_button( $html, $field );
	}

	/**
	 * Register the Endpoints from Stripe.
	 *
	 * @since 5.3.0
	 */
	public function register_endpoints() : void {
		$this->container->make( REST::class )->register_endpoints();
	}

	/**
	 * Handles the testing of the signing key on the settings page.
	 *
	 * @since 5.3.0
	 */
	public function action_handle_testing_webhooks_field() : void {
		$this->container->make( Webhooks::class )->handle_validation();
	}

	/**
	 * Handles the validation of the signing key on the settings page.
	 *
	 * @since 5.5.6
	 */
	public function action_handle_verify_webhooks() : void {
		$this->container->make( Webhooks::class )->handle_verification();
	}

	/**
	 * Handles the setting up of the webhook on the settings page.
	 *
	 * @since 5.11.0
	 *
	 * @return void
	 */
	public function action_handle_set_up_webhook(): void {
		$nonce  = tribe_get_request_var( 'tc_nonce' );
		$status = esc_html__( 'Something went wrong with your Webhook Creation. Please reload the page and try again later.', 'event-tickets' );

		$webhooks = $this->container->make( Webhooks::class );

		if ( ! wp_verify_nonce( $nonce, Webhooks::NONCE_KEY_SETUP ) || ! current_user_can( Pages::get_capability() ) ) {
			wp_send_json_error( [ 'status' => $status ] );
			return;
		}

		$result = $webhooks->handle_webhook_setup();

		if ( ! $result ) {
			wp_send_json_error( [ 'status' => $status ] );
			return;
		}

		wp_send_json_success( [ 'status' => esc_html__( 'Webhook successfully set up! The page will reload now.', 'event-tickets' ) ] );
	}

	/**
	 * Handle Stripe errors into the admin UI.
	 *
	 * @since 5.3.0
	 * @since 5.6.3   Added check for ajax call, and additional logic to only run logic on checkout page and when Stripe is connected.
	 */
	public function handle_stripe_errors() {

		// Bail out if not on Stripe Settings Page or TicketsCommerce Checkout page.
		if ( ! tribe( Admin_Settings::class )->is_on_tab_section( 'payments', 'stripe' ) || ! tribe( Module::class )->is_checkout_page() ) {
			return;
		}

		// Bail if this is an ajax call.
		if ( wp_doing_ajax() ) {
			return;
		}

		$merchant_denied = tribe( Merchant::class )->is_merchant_unauthorized();

		if ( $merchant_denied ) {
			return tribe( Notice_Handler::class )->trigger_admin( $merchant_denied );
		}

		$merchant_disconnected = tribe( Merchant::class )->is_merchant_deauthorized();

		if ( $merchant_disconnected ) {
			return tribe( Notice_Handler::class )->trigger_admin( $merchant_disconnected );
		}

		tribe( Settings::class )->alert_currency_mismatch();

		if ( empty( tribe_get_request_var( 'tc-stripe-error' ) ) ) {
			return;
		}

		return tribe( Notice_Handler::class )->trigger_admin( tribe_get_request_var( 'tc-stripe-error' ) );
	}

	/**
	 * Include Stripe admin notices for Ticket Commerce.
	 *
	 * @since 5.3.0
	 *
	 * @param array $messages Array of messages.
	 *
	 * @return array
	 */
	public function include_admin_notices( $messages ) {
		return array_merge( $messages, $this->container->make( Gateway::class )->get_admin_notices() );
	}

	/**
	 * Checks if Stripe is active and can be used to check out in the current cart and, if so,
	 * generates a payment intent
	 *
	 * @since 5.3.0
	 */
	public function maybe_create_stripe_payment_intent() {

		if ( ! ( tribe( Gateway::class )->is_enabled() && tribe( Merchant::class )->is_connected() ) ) {
			return;
		}

		tribe( Payment_Intent_Handler::class )->create_payment_intent_for_cart();
	}

	/**
	 * Handles the checkout request pieces related to Stripe Gateway.
	 *
	 * @since 5.19.3
	 *
	 * @return void
	 */
	public function handle_checkout_request() {
		$payment_intent_id            = tec_get_request_var( 'payment_intent' );
		$payment_intent_client_secret = tec_get_request_var( 'payment_intent_client_secret' );

		if ( ! ( $payment_intent_id && $payment_intent_client_secret ) ) {
			return;
		}

		$existing_payment_intent = tribe( Payment_Intent_Handler::class )->get();

		// Do we need to re-fecth the payment intent?
		if ( ! empty( $existing_payment_intent['id'] ) && ! empty( $existing_payment_intent['client_secret'] ) && $existing_payment_intent['id'] === $payment_intent_id && $existing_payment_intent['client_secret'] === $payment_intent_client_secret ) {
			$payment_intent = $existing_payment_intent;
		} else {
			$payment_intent = Payment_Intent::get( $payment_intent_id );
		}

		// Invalid payment intent, bail.
		if ( empty( $payment_intent['client_secret'] ) || $payment_intent['client_secret'] !== $payment_intent_client_secret ) {
			return;
		}

		// Overwrite the local payment intent, since we confirmed the one we received is the one in use.
		// This will be relevant for the checkout page.
		tribe( Payment_Intent_Handler::class )->set( $payment_intent );

		$success_url = add_query_arg( [ 'tc-order-id' => $payment_intent['id'] ], tribe( Success::class )->get_url() );
		$new_status  = tribe( Status::class )->convert_payment_intent_to_commerce_status( $payment_intent );

		$order = tec_tc_orders()->by_args(
			[
				'status'           => 'any',
				'gateway_order_id' => $payment_intent['id'],
			]
		)->first();

		if ( ! $order ) {
			return;
		}

		// We will attempt to update the order status to the one returned by Stripe.
		tribe( Order::class )->modify_status(
			$order->ID,
			$new_status->get_slug(),
			[
				'gateway_payload'  => $payment_intent,
				'gateway_order_id' => $payment_intent['id'],
			]
		);

		// If we get a success status, we redirect to the success page.
		if ( Completed::SLUG === $new_status->get_slug() ) {
			wp_safe_redirect( $success_url ); // phpcs:ignore WordPressVIPMinimum.Security.ExitAfterRedirect.NoExit, StellarWP.CodeAnalysis.RedirectAndDie.Error
			tribe_exit();
		}
	}

	/**
	 * Modify the checkout variables to include errors and billing fields.
	 *
	 * @since 5.19.3
	 *
	 * @param array $vars The current template vars.
	 *
	 * @return array
	 */
	public function modify_checkout_vars( $vars ) {
		$payment_intent = tribe( Payment_Intent_Handler::class )->get();

		$vars['billing_fields']['name']['value']             = Arr::get( $payment_intent, [ 'metadata', 'purchaser_name' ], '' );
		$vars['billing_fields']['email']['value']            = Arr::get( $payment_intent, [ 'metadata', 'purchaser_email' ], '' );
		$vars['billing_fields']['address']['value']['line1'] = Arr::get( $payment_intent, [ 'shipping', 'address', 'line1' ], '' );
		$vars['billing_fields']['address']['value']['line2'] = Arr::get( $payment_intent, [ 'shipping', 'address', 'line2' ], '' );
		$vars['billing_fields']['city']['value']             = Arr::get( $payment_intent, [ 'shipping', 'address', 'city' ], '' );
		$vars['billing_fields']['state']['value']            = Arr::get( $payment_intent, [ 'shipping', 'address', 'state' ], '' );
		$vars['billing_fields']['zip']['value']              = Arr::get( $payment_intent, [ 'shipping', 'address', 'postal_code' ], '' );
		$vars['billing_fields']['country']['value']          = Arr::get( $payment_intent, [ 'shipping', 'address', 'country' ], '' );

		$redirect_status = tec_get_request_var( 'redirect_status' );
		if ( $redirect_status === 'failed' ) {
			$vars['has_error'] = true;
			$vars['error']     = [
				'title'   => esc_html__( 'Payment Failed', 'event-tickets' ),
				'message' => esc_html__( 'There was an issue processing your payment with your payment method. Please try again.', 'event-tickets' ),
			];
		}

		return $vars;
	}

	/**
	 * Modify the checkout whether to display billing fields.
	 *
	 * @since 5.19.3
	 *
	 * @param bool $value The current value.
	 *
	 * @return bool
	 */
	public function modify_checkout_display_billing_info( bool $value ): bool {
		$payment_methods       = tribe( Merchant::class )->get_payment_method_types();
		$count_payment_methods = count( $payment_methods );
		if ( 1 < $count_payment_methods ) {
			return true;
		}

		if ( 1 === $count_payment_methods && 'card' !== $payment_methods[0] ) {
			return true;
		}

		return $value;
	}

	/**
	 * Intercept saving settings to check if any new payment methods would break Stripe payment intents.
	 *
	 * @since 5.3.0
	 *
	 * @param mixed  $value    The new value.
	 * @param string $field_id The field id in the options.
	 *
	 * @return mixed
	 */
	public function validate_payment_methods( $value, $field_id ) {

		if ( $field_id !== Settings::$option_checkout_element_payment_methods ) {
			return $value;
		}

		return Payment_Intent::validate_payment_methods( $value, $field_id );
	}

	/**
	 * Add Webhook settings fields
	 *
	 * @since 5.3.0
	 *
	 * @param array $settings Array of settings for the Stripe gateway.
	 *
	 * @return mixed
	 */
	public function include_webhook_settings( $settings ) {
		if ( ! tribe( Merchant::class )->is_connected() ) {
			return $settings;
		}

		return array_merge( $settings, tribe( Webhooks::class )->get_fields() );
	}

	/**
	 * Makes sure mandatory fields have values when hidden.
	 *
	 * @since 5.3.0
	 *
	 * @param mixed  $value    Field value submitted.
	 * @param string $field_id Field key in the settings array.
	 * @param array  $field    Entire field array.
	 *
	 * @return mixed
	 */
	public function provide_defaults_for_hidden_fields( $value, $field_id, $field ) {
		return tribe( Settings::class )->reset_hidden_field_values( $value, $field_id, $field );
	}

	/**
	 * Filter admin notices.
	 *
	 * @since 5.3.2
	 *
	 * @param array $notices Array of admin notices.
	 *
	 * @return array
	 */
	public function filter_admin_notices( $notices ) {
		return $this->container->make( Gateway::class )->filter_admin_notices( $notices );
	}

	/**
	 * Filter Stripe currency precision based on Stripe's specific requirements.
	 *
	 * @since 5.26.7
	 *
	 * @param array  $currency_data The currency data from the map.
	 * @param string $currency_code The currency code.
	 * @param string $gateway The gateway name.
	 *
	 * @return array The modified currency data.
	 */
	public function filter_stripe_currency_precision( $currency_data, $currency_code, $gateway ) {
		return $this->container->make( Gateway::class )->filter_stripe_currency_precision( $currency_data, $currency_code, $gateway );
	}
}