WooCommerce Code Reference

class-wc-gateway-paypal-transact-account-manager.php

Source code

<?php
/**
 * Class WC_Gateway_Paypal_Transact_Account_Manager file.
 *
 * @package WooCommerce\Gateways
 */

declare(strict_types=1);

use Automattic\Jetpack\Connection\Client as Jetpack_Connection_Client;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Handles Transact account management.
 */
final class WC_Gateway_Paypal_Transact_Account_Manager {
	/**
	 * The API version for the proxy endpoint.
	 *
	 * @var int
	 */
	private const WPCOM_PROXY_ENDPOINT_API_VERSION = 2;

	/**
	 * Transact provider type, for provider onboarding.
	 *
	 * @var string
	 */
	private const TRANSACT_PROVIDER_TYPE = 'paypal_standard';

	/**
	 * Cache keys for the merchant and provider accounts.
	 *
	 * @var string
	 */
	private const TRANSACT_MERCHANT_ACCOUNT_CACHE_KEY_LIVE = 'woocommerce_paypal_transact_merchant_account_live';
	private const TRANSACT_MERCHANT_ACCOUNT_CACHE_KEY_TEST = 'woocommerce_paypal_transact_merchant_account_test';
	private const TRANSACT_PROVIDER_ACCOUNT_CACHE_KEY_LIVE = 'woocommerce_paypal_transact_provider_account_live';
	private const TRANSACT_PROVIDER_ACCOUNT_CACHE_KEY_TEST = 'woocommerce_paypal_transact_provider_account_test';

	/**
	 * The expiry time for the Transact account cache.
	 *
	 * @var int
	 */
	private const TRANSACT_ACCOUNT_CACHE_EXPIRY = 60 * 60 * 24; // 24 hours.

	/**
	 * Paypal gateway object.
	 *
	 * @var WC_Gateway_Paypal
	 */
	private $gateway;

	/**
	 * Constructor.
	 *
	 * @param WC_Gateway_Paypal $gateway Paypal gateway object.
	 */
	public function __construct( WC_Gateway_Paypal $gateway ) {
		$this->gateway = $gateway;
	}

	/**
	 * Onboard the merchant with the Transact platform.
	 *
	 * @return void
	 */
	public function do_onboarding() {
		// Check that we have a PayPal email. This is required for processing payments --
		// used as the payee email. Only begin onboarding if this minimum requirement is met.
		if ( empty( $this->gateway->email ) ) {
			return;
		}

		// Register with Jetpack if not already connected.
		$jetpack_connection_manager = $this->gateway->get_jetpack_connection_manager();
		if ( ! $jetpack_connection_manager ) {
			WC_Gateway_Paypal::log( 'Jetpack connection manager not found.', 'error' );
			return;
		}

		if ( ! $jetpack_connection_manager->is_connected() ) {
			$result = $jetpack_connection_manager->try_registration();
			if ( is_wp_error( $result ) ) {
				WC_Gateway_Paypal::log( 'Jetpack registration failed: ' . $result->get_error_message(), 'error' );
				return;
			}
		}

		// Fetch (cached) or create the Transact merchant and provider accounts.
		$merchant_account_data = $this->get_transact_account_data( 'merchant' );
		if ( empty( $merchant_account_data ) ) {
			$merchant_account = $this->create_merchant_account();
			if ( empty( $merchant_account ) ) {
				WC_Gateway_Paypal::log( 'Transact merchant onboarding failed.', 'error' );
				return;
			}

			// Cache the merchant account data.
			$this->update_transact_account_cache(
				$this->get_cache_key( 'merchant' ),
				$merchant_account
			);
		}

		$provider_account_data = $this->get_transact_account_data( 'provider' );
		if ( empty( $provider_account_data ) ) {
			$provider_account = $this->create_provider_account();
			if ( ! $provider_account ) {
				WC_Gateway_Paypal::log( 'Transact provider onboarding failed.', 'error' );
				return;
			}

			// Cache the provider account data.
			$this->update_transact_account_cache(
				$this->get_cache_key( 'provider' ),
				$provider_account
			);
		}

		// Set an extra flag to indicate that we've completed onboarding,
		// so we can do inexpensive early returns for checkers like
		// WC_Gateway_Paypal::should_use_orders_v2().
		$this->gateway->set_transact_onboarding_complete();
	}

	/**
	 * Get the Transact account (merchant or provider) data. Performs a fetch if the account
	 * is not in cache or expired.
	 *
	 * @param string $account_type The type of account to get (merchant or provider).
	 * @return array|null Returns null if the transact account cannot be retrieved.
	 */
	public function get_transact_account_data( $account_type ) {
		$cache_key = $this->get_cache_key( $account_type );

		// Get transact account from cache. If not found, fetch/create it.
		$transact_account = $this->get_transact_account_from_cache( $cache_key );
		if ( empty( $transact_account ) ) {
			$transact_account = 'merchant' === $account_type ? $this->fetch_merchant_account() : $this->fetch_provider_account();

			// Fetch failed.
			if ( empty( $transact_account ) ) {
				return null;
			}

			// Update cache.
			$this->update_transact_account_cache( $cache_key, $transact_account );
		}

		return $transact_account;
	}

	/**
	 * Get the cache key for the transact account.
	 *
	 * @param string $account_type The type of account to get (merchant or provider).
	 * @return string|null The cache key, or null if the account type is invalid.
	 */
	private function get_cache_key( $account_type ) {
		if ( 'merchant' === $account_type ) {
			return $this->gateway->testmode ? self::TRANSACT_MERCHANT_ACCOUNT_CACHE_KEY_TEST : self::TRANSACT_MERCHANT_ACCOUNT_CACHE_KEY_LIVE;
		}

		if ( 'provider' === $account_type ) {
			return $this->gateway->testmode ? self::TRANSACT_PROVIDER_ACCOUNT_CACHE_KEY_TEST : self::TRANSACT_PROVIDER_ACCOUNT_CACHE_KEY_LIVE;
		}

		return null;
	}

	/**
	 * Fetch the merchant account from the Transact platform.
	 *
	 * @return array|null The API response body, or null if the request fails.
	 */
	private function fetch_merchant_account() {
		$site_id = \Jetpack_Options::get_option( 'id' );
		if ( ! $site_id ) {
			return null;
		}

		$request_body = array(
			'test_mode' => $this->gateway->testmode,
		);

		$response = $this->send_transact_api_request(
			'GET',
			sprintf( '/sites/%d/transact/account', $site_id ),
			$request_body
		);

		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			return null;
		}

		$response_data = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( empty( $response_data['public_id'] ) ) {
			return null;
		}

		return array( 'public_id' => $response_data['public_id'] );
	}

	/**
	 * Fetch the provider account from the Transact platform.
	 *
	 * @return bool True if the provider account exists, false otherwise.
	 */
	private function fetch_provider_account() {
		$site_id = \Jetpack_Options::get_option( 'id' );
		if ( ! $site_id ) {
			return false;
		}

		$request_body = array(
			'test_mode'     => $this->gateway->testmode,
			'provider_type' => self::TRANSACT_PROVIDER_TYPE,
		);

		$response = $this->send_transact_api_request(
			'GET',
			sprintf( '/sites/%d/transact/account/%s', $site_id, self::TRANSACT_PROVIDER_TYPE ),
			$request_body
		);

		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			return false;
		}

		// Provider account response only returns an empty onboarding link,
		// which we do not need.
		return true;
	}

	/**
	 * Create the merchant account with the Transact platform.
	 *
	 * @return array|null The API response body, or null if the request fails.
	 */
	private function create_merchant_account() {
		$site_id = \Jetpack_Options::get_option( 'id' );
		if ( ! $site_id ) {
			return null;
		}

		$request_body = array( 'test_mode' => $this->gateway->testmode );

		$response = $this->send_transact_api_request(
			'POST',
			sprintf( '/sites/%d/transact/account', $site_id ),
			$request_body
		);

		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			return null;
		}

		$response_data = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( empty( $response_data['public_id'] ) ) {
			WC_Gateway_Paypal::log( 'Transact merchant account creation failed. Response body: ' . wc_print_r( $response_data, true ) );
			return null;
		}

		return array( 'public_id' => $response_data['public_id'] );
	}

	/**
	 * Create the provider account with the Transact platform.
	 *
	 * @return bool True if the provider account creation was successful, false otherwise.
	 */
	private function create_provider_account() {
		$site_id = \Jetpack_Options::get_option( 'id' );
		if ( ! $site_id ) {
			return false;
		}

		$request_body = array(
			'test_mode'     => $this->gateway->testmode,
			'provider_type' => self::TRANSACT_PROVIDER_TYPE,
		);
		$response     = $this->send_transact_api_request(
			'POST',
			sprintf( '/sites/%d/transact/account/%s/onboard', $site_id, self::TRANSACT_PROVIDER_TYPE ),
			$request_body
		);

		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			return false;
		}

		// Provider account response only returns an empty onboarding link,
		// which we do not need.
		return true;
	}

	/**
	 * Update the transact account (merchant or provider) cache.
	 *
	 * @param string $cache_key The cache key to update.
	 * @param array  $account_data The transact account data.
	 */
	private function update_transact_account_cache( $cache_key, $account_data ) {
		$expires = time() + self::TRANSACT_ACCOUNT_CACHE_EXPIRY;
		update_option(
			$cache_key,
			array(
				'account' => $account_data,
				'expiry'  => $expires,
			)
		);
	}

	/**
	 * Get the transact account (merchant or provider) from the database cache.
	 *
	 * @param string $cache_key The cache key to get the account.
	 * @return bool|null The transact account data, or null if the cache is
	 *                    empty or expired.
	 */
	private function get_transact_account_from_cache( $cache_key ) {
		$transact_account = get_option( $cache_key, null );

		if ( empty( $transact_account ) || ( isset( $transact_account['expiry'] ) && $transact_account['expiry'] < time() ) ) {
			return null;
		}

		return $transact_account['account'] ?? null;
	}

	/**
	 * Send a request to the Transact platform.
	 *
	 * @param string $method The HTTP method to use.
	 * @param string $endpoint The endpoint to request.
	 * @param array  $request_body The request body.
	 *
	 * @return array|null The API response body, or null if the request fails.
	 */
	private function send_transact_api_request( $method, $endpoint, $request_body ) {
		if ( 'GET' === $method ) {
			$endpoint .= '?' . http_build_query( $request_body );
		}

		$response = Jetpack_Connection_Client::wpcom_json_api_request_as_blog(
			$endpoint,
			self::WPCOM_PROXY_ENDPOINT_API_VERSION,
			array(
				'headers' => array( 'Content-Type' => 'application/json' ),
				'method'  => $method,
				'timeout' => WC_Gateway_Paypal_Constants::WPCOM_PROXY_REQUEST_TIMEOUT,
			),
			'GET' === $method ? null : wp_json_encode( $request_body ),
			'wpcom'
		);

		return $response;
	}
}