Current File : /home/digitaw/www/wp-content/plugins/event-tickets/src/Tickets/Commerce/Gateways/Square/WhoDat.php
<?php
/**
 * WhoDat Connection for Square.
 *
 * @since 5.24.0
 *
 * @package TEC\Tickets\Commerce\Gateways\Square
 */

// phpcs:disable StellarWP.Classes.ValidClassName.NotSnakeCase
namespace TEC\Tickets\Commerce\Gateways\Square;

use TEC\Tickets\Commerce\Gateways\Contracts\Abstract_WhoDat;
use TEC\Tickets\Commerce\Gateways\Square\REST\On_Boarding_Endpoint;
use RuntimeException;
/**
 * Class WhoDat. Handles connection to Square when the platform keys are needed.
 *
 * @since 5.24.0
 *
 * @package TEC\Tickets\Commerce\Gateways\Square
 */
class WhoDat extends Abstract_WhoDat {

	/**
	 * The API Path.
	 *
	 * @since 5.24.0
	 *
	 * @var string
	 */
	protected const API_ENDPOINT = 'commerce/v1/square';

	/**
	 * The nonce action for the state.
	 *
	 * @since 5.24.0
	 *
	 * @var string
	 */
	protected const STATE_NONCE_ACTION = 'tec-tc-square-connect';

	/**
	 * Creates a new account link for the client and redirects the user to setup the account details.
	 *
	 * @since 5.24.0
	 *
	 * @param bool $is_wizard Whether this is in the wizard context.
	 *
	 * @return string
	 */
	public function connect_account( bool $is_wizard = false ): string {
		$merchant = tribe( Merchant::class );

		// Generate and store the code challenge using the Merchant class.
		$code_challenge = $merchant->generate_code_challenge();
		$user_id        = get_current_user_id();

		wp_set_current_user( 0 );
		$nonce = wp_create_nonce( $this->get_state_nonce_action() );
		wp_set_current_user( $user_id );

		$query_args = [
			'mode'                  => tec_tickets_commerce_is_sandbox_mode() ? 'sandbox' : 'live',
			'code_challenge'        => $code_challenge,
			'code_challenge_method' => 'S256',
			'url'                   => tribe( On_Boarding_Endpoint::class )->get_return_url( null, $is_wizard ),
			'state'                 => $nonce,
		];

		$connection_response = $this->get_with_cache( 'oauth/authorize', $query_args );

		if ( empty( $connection_response['auth_url'] ) ) {
			do_action(
				'tribe_log',
				'error',
				'Failed to retrieve Square OAuth authorize URL',
				[
					'source'   => 'tickets-commerce',
					'response' => $connection_response,
				]
			);

			return '';
		}

		return $connection_response['auth_url'];
	}

	/**
	 * Get the state nonce action.
	 *
	 * @since 5.24.0
	 *
	 * @return string
	 */
	public function get_state_nonce_action(): string {
		return self::STATE_NONCE_ACTION;
	}

	/**
	 * Get the return URL for OAuth redirects.
	 *
	 * @since 5.24.0
	 *
	 * @return string
	 */
	public function get_return_url(): string {
		return rest_url( 'tribe/tickets/v1/commerce/square/on-boarding' );
	}

	/**
	 * De-authorize the current seller account in Square oAuth.
	 *
	 * @since 5.24.0
	 *
	 * @return ?array
	 */
	public function disconnect_account(): ?array {
		$merchant = tribe( Merchant::class );

		$query_args = [
			'access_token' => $merchant->get_access_token(),
		];

		return $this->post( 'oauth/token/revoke', $query_args );
	}

	/**
	 * Requests WhoDat to refresh the oAuth tokens.
	 *
	 * @since 5.24.0
	 *
	 * @return ?array
	 */
	public function refresh_token(): ?array {
		$refresh_token = tribe( Merchant::class )->get_refresh_token();

		$query_args = [
			'grant_type'    => 'refresh_token',
			'refresh_token' => $refresh_token,
		];

		return $this->get( 'oauth/token/refresh', $query_args );
	}

	/**
	 * Get the token status from Square.
	 *
	 * @since 5.24.0
	 *
	 * @return array|null
	 */
	public function get_token_status(): ?array {
		$merchant = tribe( Merchant::class );

		$query_args = [
			'access_token' => $merchant->get_access_token(),
			'mode'         => $merchant->get_mode(),
		];

		return $this->get_with_cache( 'oauth/token/status', $query_args );
	}

	/**
	 * Get merchant information from Square.
	 *
	 * @since 5.24.0
	 *
	 * @param string $merchant_id The merchant ID.
	 *
	 * @return array|null
	 */
	public function get_merchant( string $merchant_id ): ?array {
		$query_args = [
			'merchant_id' => $merchant_id,
		];

		return $this->get_with_cache( 'merchants/' . $merchant_id, $query_args );
	}

	/**
	 * Register a webhook endpoint with WhoDat.
	 *
	 * @since 5.24.0
	 *
	 * @param string $endpoint_url The webhook endpoint URL.
	 * @param string $merchant_id  The merchant ID.
	 *
	 * @return array The webhook data or null if the request fails.
	 *
	 * @throws RuntimeException If the webhook registration fails.
	 */
	public function register_webhook_endpoint( string $endpoint_url, string $merchant_id ): array {
		$query_args = [
			'url'         => $endpoint_url,
			'merchant_id' => $merchant_id,
		];

		$response = $this->post( 'webhooks/register', $query_args );

		if ( empty( $response['subscription'] ) ) {
			do_action(
				'tribe_log',
				'error',
				'Failed to register Square webhook',
				[
					'source' => 'tickets-commerce',
					'error'  => $response,
				]
			);

			throw new RuntimeException( __( 'Failed to register Square webhook', 'event-tickets' ), 3 );
		}

		return $response['subscription'];
	}

	/**
	 * Get required OAuth scopes for the Square integration.
	 *
	 * @since 5.24.0
	 *
	 * @param bool $include_descriptions Whether to include detailed descriptions for each scope.
	 *
	 * @return array|null Array of required scopes or null if the request fails.
	 */
	public function get_required_scopes( bool $include_descriptions = false ): ?array {
		$query_args = [
			'include_descriptions' => (bool) $include_descriptions,
		];

		$response = $this->get_with_cache( 'oauth/scopes', $query_args );

		if ( empty( $response ) || ! isset( $response['scopes'] ) ) {
			do_action(
				'tribe_log',
				'error',
				'Failed to retrieve Square OAuth scopes',
				[
					'source'   => 'tickets-commerce',
					'response' => $response,
				]
			);

			return null;
		}

		return $response;
	}

	/**
	 * Verify if the currently connected merchant has all required scopes.
	 *
	 * @since 5.24.0
	 *
	 * @return array {
	 *     Array containing verification results
	 *
	 *     @type bool   $has_all_scopes    Whether the merchant has all required scopes.
	 *     @type array  $missing_scopes    Array of missing scope IDs, if any.
	 *     @type string $reconnect_url     URL to reconnect with the correct scopes, if needed.
	 * }
	 */
	public function verify_merchant_scopes(): array {
		$required_scopes_data = $this->get_required_scopes();

		if ( null === $required_scopes_data ) {
			return [
				'has_all_scopes' => false,
				'missing_scopes' => [],
				'reconnect_url'  => '',
				'error'          => 'Failed to retrieve required scopes',
			];
		}

		$required_scopes = $required_scopes_data['scopes'] ?? [];

		$token_status = $this->get_token_status();

		if ( empty( $token_status ) || isset( $token_status['error'] ) ) {
			return [
				'has_all_scopes' => false,
				'missing_scopes' => $required_scopes,
				'reconnect_url'  => $this->get_reconnect_url( $required_scopes ),
				'error'          => 'Could not verify current token status',
			];
		}

		$current_scopes = $token_status['scopes'] ?? [];

		$missing_scopes = array_diff( $required_scopes, $current_scopes );

		$result = [
			'has_all_scopes' => empty( $missing_scopes ),
			'missing_scopes' => $missing_scopes,
		];

		// If scopes are missing, provide a reconnect URL.
		if ( ! empty( $missing_scopes ) ) {
			$result['reconnect_url'] = $this->get_reconnect_url( $required_scopes );
		}

		return $result;
	}

	/**
	 * Get a URL to reconnect with specific scopes.
	 *
	 * @since 5.24.0
	 *
	 * @param array $scopes Array of required scope IDs.
	 *
	 * @return string Reconnect URL.
	 */
	protected function get_reconnect_url( array $scopes = [] ): string {
		$merchant = tribe( Merchant::class );

		$code_challenge = $merchant->generate_code_challenge();
		$user_id        = get_current_user_id();

		wp_set_current_user( 0 );
		$nonce = wp_create_nonce( $this->get_state_nonce_action() );
		wp_set_current_user( $user_id );

		$query_args = [
			'mode'                  => tec_tickets_commerce_is_sandbox_mode() ? 'sandbox' : 'live',
			'code_challenge'        => $code_challenge,
			'code_challenge_method' => 'S256',
			'url'                   => tribe( On_Boarding_Endpoint::class )->get_return_url(),
			'state'                 => $nonce,
		];

		if ( ! empty( $scopes ) ) {
			$query_args['scopes'] = implode( ',', $scopes );
		}

		$connection_response = $this->get_with_cache( 'oauth/authorize', $query_args );

		return $connection_response['auth_url'] ?? '';
	}
}