From a2221e52337ea7f90ec9d22ecb42f4c0cce3018d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 17 Dec 2024 19:34:22 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Introduce=20new=20connection=20mana?= =?UTF-8?q?ger=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 9 + .../ppcp-settings/src/Data/CommonSettings.php | 14 + .../src/Service/ConnectionManager.php | 242 ++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 modules/ppcp-settings/src/Service/ConnectionManager.php diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index c785111aa..1397b137b 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -23,6 +23,7 @@ use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; +use WooCommerce\PayPalCommerce\Settings\Service\ConnectionManager; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -192,6 +193,14 @@ return array( return $generators; }, + 'settings.service.connection_manager' => static function ( ContainerInterface $container ) : ConnectionManager { + return new ConnectionManager( + $container->get( 'settings.data.common' ), + $container->get( 'api.paypal-host-production' ), + $container->get( 'api.paypal-host-sandbox' ), + $container->get( 'woocommerce.logger.woocommerce' ), + ); + }, 'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { return new SwitchSettingsUiEndpoint( $container->get( 'woocommerce.logger.woocommerce' ), diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php index 1894255ff..0935734b8 100644 --- a/modules/ppcp-settings/src/Data/CommonSettings.php +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -169,6 +169,20 @@ class CommonSettings extends AbstractDataModel { $this->data['merchant_connected'] = true; } + /** + * Reset all connection details to the initial, disconnected state. + * + * @return void + */ + public function reset_merchant_data() : void { + $defaults = $this->get_defaults(); + + $this->data['sandbox_merchant'] = $defaults['sandbox_merchant']; + $this->data['merchant_id'] = $defaults['merchant_id']; + $this->data['merchant_email'] = $defaults['merchant_email']; + $this->data['merchant_connected'] = $defaults['merchant_connected']; + } + /** * Whether the currently connected merchant is a sandbox account. * diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php new file mode 100644 index 000000000..1cffb155c --- /dev/null +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -0,0 +1,242 @@ + + */ + private array $connection_hosts; + + /** + * Constructor. + * + * @param CommonSettings $common_settings Data model that stores the connection details. + * @param string $live_host The API host for the live mode. + * @param string $sandbox_host The API host for the sandbox mode. + * @param LoggerInterface $logger Logging instance. + */ + public function __construct( CommonSettings $common_settings, string $live_host, string $sandbox_host, LoggerInterface $logger ) { + $this->common_settings = $common_settings; + $this->logger = $logger; + $this->connection_hosts = array( + 'live' => $live_host, + 'sandbox' => $sandbox_host, + ); + } + + /** + * Returns details about the currently connected merchant. + * + * @return array + */ + public function get_account_details() : array { + return array( + 'is_sandbox' => $this->common_settings->is_sandbox_merchant(), + 'is_connected' => $this->common_settings->is_merchant_connected(), + 'merchant_id' => $this->common_settings->get_merchant_id(), + 'merchant_email' => $this->common_settings->get_merchant_email(), + ); + } + + /** + * Removes any connection details we currently have stored. + * + * @return void + */ + public function disconnect() : void { + $this->logger->info( 'Disconnecting merchant from PayPal...' ); + + $this->common_settings->reset_merchant_data(); + $this->common_settings->save(); + } + + /** + * Checks if the provided ID and secret have a valid format. + * + * On failure, an Exception is thrown, while a successful check does not + * generate any return value. + * + * @param string $client_id The client ID. + * @param string $client_secret The client secret. + * @return void + * @throws RuntimeException When invalid client ID or secret provided. + */ + public function validate_id_and_secret( string $client_id, string $client_secret ) : void { + if ( empty( $client_id ) ) { + throw new RuntimeException( 'No client ID provided.' ); + } + + if ( false === preg_match( '/^A[\w-]{79}$/', $client_secret ) ) { + throw new RuntimeException( 'Invalid client ID provided.' ); + } + + if ( empty( $client_secret ) ) { + throw new RuntimeException( 'No client secret provided.' ); + } + } + + /** + * Disconnects the current merchant, and then attempts to connect to a + * PayPal account using a client ID and secret. + * + * @param bool $use_sandbox Whether to use the sandbox mode. + * @param string $client_id The client ID. + * @param string $client_secret The client secret. + * @return void + * @throws RuntimeException When failed to retrieve payee. + */ + public function connect_via_secret( bool $use_sandbox, string $client_id, string $client_secret ) : void { + $this->disconnect(); + + $this->logger->info( + 'Attempting manual connection to PayPal...', + array( + 'sandbox' => $use_sandbox, + 'client_id' => $client_id, + ) + ); + + $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); + + $this->update_connection_details( $use_sandbox, $payee['merchant_id'], $payee['email_address'] ); + } + + + // ---------------------------------------------------------------------------- + // Internal helper methods + + + /** + * Returns the API host for the relevant environment. + * + * @param bool $for_sandbox Whether to return the sandbox API host. + * @return string + */ + private function get_host( bool $for_sandbox = false ) : string { + return $for_sandbox ? $this->connection_hosts['sandbox'] : $this->connection_hosts['live']; + } + + /** + * Retrieves the payee object with the merchant data by creating a minimal PayPal order. + * + * @param string $client_id The client ID. + * @param string $client_secret The client secret. + * @param bool $use_sandbox Whether to use the sandbox mode. + * + * @return array Payee details, containing 'merchant_id' and 'merchant_email' keys. + * @throws RuntimeException When failed to retrieve payee. + */ + private function request_payee( + string $client_id, + string $client_secret, + bool $use_sandbox + ) : array { + $host = $this->get_host( $use_sandbox ); + + $bearer = new PayPalBearer( + new InMemoryCache(), + $host, + $client_id, + $client_secret, + $this->logger, + null + ); + + $orders = new Orders( + $host, + $bearer, + $this->logger + ); + + $request_body = array( + 'intent' => 'CAPTURE', + 'purchase_units' => array( + array( + 'amount' => array( + 'currency_code' => 'USD', + 'value' => 1.0, + ), + ), + ), + ); + + $response = $orders->create( $request_body ); + $body = json_decode( $response['body'] ); + + $order_id = $body->id; + + $order_response = $orders->order( $order_id ); + $order_body = json_decode( $order_response['body'] ); + + $pu = $order_body->purchase_units[0]; + $payee = $pu->payee; + + if ( ! is_object( $payee ) ) { + throw new RuntimeException( 'Payee not found.' ); + } + if ( ! isset( $payee->merchant_id ) || ! isset( $payee->email_address ) ) { + throw new RuntimeException( 'Payee info not found.' ); + } + + return array( + 'merchant_id' => $payee->merchant_id, + 'email_address' => $payee->email_address, + ); + } + + /** + * Stores the provided details in the data model. + * + * @param bool $is_sandbox Whether the details are for a sandbox account. + * @param string $merchant_id PayPal's internal merchant ID. + * @param string $merchant_email Email address associated with the PayPal account. + * @return void + */ + private function update_connection_details( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { + $this->logger->info( + 'Updating connection details', + array( + 'sandbox' => $is_sandbox, + 'merchant_id' => $merchant_id, + 'merchant_email' => $merchant_email, + ) + ); + + $this->common_settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email ); + $this->common_settings->save(); + } +}