diff --git a/includes/admin/class-wc-rest-stripe-settings-controller.php b/includes/admin/class-wc-rest-stripe-settings-controller.php index e02906536f..60f3f2311c 100644 --- a/includes/admin/class-wc-rest-stripe-settings-controller.php +++ b/includes/admin/class-wc-rest-stripe-settings-controller.php @@ -293,9 +293,27 @@ public function update_settings( WP_REST_Request $request ) { $this->update_is_debug_log_enabled( $request ); $this->update_oc_settings( $request ); + $this->maybe_onboard_with_transact(); + return new WP_REST_Response( [], 200 ); } + /** + * Maybe onboard with the Transact Platform. + * + * @return void + */ + public function maybe_onboard_with_transact() { + wc_get_logger()->info( 'maybe_onboard_with_transact' ); + // Do not run if Stripe is not enabled. + if ( 'yes' !== $this->gateway->enabled ) { + return; + } + + $transact_account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $transact_account_manager->do_onboarding(); + } + /** * Returns the payment method IDs to enable. * diff --git a/includes/class-wc-stripe-transact-account-manager.php b/includes/class-wc-stripe-transact-account-manager.php new file mode 100644 index 0000000000..810879412e --- /dev/null +++ b/includes/class-wc-stripe-transact-account-manager.php @@ -0,0 +1,397 @@ +gateway = $gateway; + } + + /** + * Gets the singleton instance of the class. + * + * @param WC_Stripe_UPE_Payment_Gateway $gateway Stripe gateway instance. + * @return WC_Stripe_Transact_Account_Manager|null + */ + public static function get_instance( WC_Stripe_UPE_Payment_Gateway $gateway ): ?self { + if ( is_null( self::$instance ) ) { + self::$instance = new self( $gateway ); + } + + return self::$instance; + } + + /** + * Sets the singleton instance of the class. + * + * @param WC_Stripe_Transact_Account_Manager|null $instance + * @return void + */ + public static function set_instance( ?self $instance ) { + self::$instance = $instance; + } + + /** + * Onboard the merchant with the Transact platform. + * + * @return void + */ + public function do_onboarding(): void { + $stripe_connect = woocommerce_gateway_stripe()->connect; + $mode = WC_Stripe_Mode::is_test() ? 'test' : 'live'; + $oauth_connected = (bool) $stripe_connect->is_connected_via_oauth( $mode ); + + // Check that the merchant is connected via OAuth. Only begin onboarding if this minimum requirement is met. + if ( ! $oauth_connected ) { + return; + } + + // Register with Jetpack if not already connected. + $jetpack_connection_manager = $this->gateway->get_jetpack_connection_manager(); + if ( ! $jetpack_connection_manager ) { + WC_Stripe_Logger::error( 'Jetpack connection manager not found.' ); + return; + } + + if ( ! $jetpack_connection_manager->is_connected() ) { + $result = $jetpack_connection_manager->try_registration(); + if ( is_wp_error( $result ) ) { + WC_Stripe_Logger::error( 'Jetpack registration failed: ' . $result->get_error_message() ); + 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_Stripe_Logger::error( 'Transact merchant onboarding failed.' ); + return; + } + + // Cache the merchant account data. + $this->update_transact_account_cache( + $this->get_cache_key( 'merchant' ), + $merchant_account + ); + } + + wc_get_logger()->info( 'merchant_account_data: ' . wc_print_r( $merchant_account_data, true ) ); + + $provider_account_data = $this->get_transact_account_data( 'provider' ); + if ( empty( $provider_account_data ) ) { + $provider_account = $this->create_provider_account(); + if ( ! $provider_account ) { + WC_Stripe_Logger::error( 'Transact provider onboarding failed.' ); + return; + } + + // Cache the provider account data. + $this->update_transact_account_cache( + $this->get_cache_key( 'provider' ), + $provider_account + ); + } + + wc_get_logger()->info( 'provider_account_data: ' . wc_print_r( $provider_account_data, true ) ); + + // Set an extra flag to indicate that we've completed onboarding. + $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|bool|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 ): ?string { + if ( 'merchant' === $account_type ) { + return WC_Stripe_Mode::is_test() ? self::TRANSACT_MERCHANT_ACCOUNT_CACHE_KEY_TEST : self::TRANSACT_MERCHANT_ACCOUNT_CACHE_KEY_LIVE; + } + + if ( 'provider' === $account_type ) { + return WC_Stripe_Mode::is_test() ? 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(): ?array { + $site_id = \Jetpack_Options::get_option( 'id' ); + if ( ! $site_id ) { + return null; + } + + $request_body = [ + 'test_mode' => WC_Stripe_Mode::is_test(), + ]; + + $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 [ '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(): bool { + $site_id = \Jetpack_Options::get_option( 'id' ); + if ( ! $site_id ) { + return false; + } + + $request_body = [ + 'test_mode' => WC_Stripe_Mode::is_test(), + '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(): ?array { + $site_id = \Jetpack_Options::get_option( 'id' ); + if ( ! $site_id ) { + return null; + } + + $request_body = [ 'test_mode' => WC_Stripe_Mode::is_test() ]; + + $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_Stripe_Logger::error( 'Transact merchant account creation failed. Response body: ' . wc_print_r( $response_data, true ) ); + return null; + } + + return [ '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(): bool { + $site_id = \Jetpack_Options::get_option( 'id' ); + if ( ! $site_id ) { + return false; + } + + $request_body = [ + 'test_mode' => WC_Stripe_Mode::is_test(), + '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 ): void { + $expires = time() + self::TRANSACT_ACCOUNT_CACHE_EXPIRY; + WC_Stripe_Database_Cache::set( + $cache_key, + [ + 'account' => $account_data, + 'expiry' => $expires, + ], + self::TRANSACT_ACCOUNT_CACHE_EXPIRY + ); + } + + /** + * Get the transact account (merchant or provider) from the database cache. + * + * @param string $cache_key The cache key to get the account. + * @return array|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 = WC_Stripe_Database_Cache::get( $cache_key ); + + 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 = \Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_blog( + $endpoint, + self::WPCOM_PROXY_ENDPOINT_API_VERSION, + [ + 'headers' => [ 'Content-Type' => 'application/json' ], + 'method' => $method, + 'timeout' => self::WPCOM_PROXY_REQUEST_TIMEOUT, + ], + 'GET' === $method ? null : wp_json_encode( $request_body ), + 'wpcom' + ); + + return $response; + } +} diff --git a/includes/class-wc-stripe.php b/includes/class-wc-stripe.php index 4cae23488c..d710145364 100644 --- a/includes/class-wc-stripe.php +++ b/includes/class-wc-stripe.php @@ -131,6 +131,7 @@ public function init() { require_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-database-cache-prefetch.php'; include_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-api.php'; include_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-mode.php'; + include_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-transact-account-manager.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/compat/class-wc-stripe-subscriptions-helper.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/compat/trait-wc-stripe-subscriptions-utilities.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/compat/trait-wc-stripe-subscriptions.php'; @@ -298,6 +299,9 @@ public function init() { // Handle the async cache prefetch action. add_action( WC_Stripe_Database_Cache_Prefetch::ASYNC_PREFETCH_ACTION, [ WC_Stripe_Database_Cache_Prefetch::get_instance(), 'handle_prefetch_action' ], 10, 1 ); + + // Hook for plugin upgrades. + add_action( 'woocommerce_updated', [ $this, 'maybe_onboard_with_transact' ] ); } /** @@ -917,6 +921,27 @@ public function maybe_toggle_payment_methods( WC_Payment_Gateways $gateways ) { ); } + /** + * Maybe onboard with the Transact Platform. + * + * @return void + */ + public function maybe_onboard_with_transact(): void { + if ( ! is_admin() || ! current_user_can( 'manage_woocommerce' ) ) { + return; + } + + $gateway = $this->get_main_stripe_gateway(); + + // Do not run if Stripe is not enabled. + if ( 'yes' !== $gateway->enabled ) { + return; + } + + $transact_account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $gateway ); + $transact_account_manager->do_onboarding(); + } + /** * Deactivate Affirm or Klarna payment methods if other official plugins are active. * diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index 7d2040708b..f32ee72662 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -181,6 +181,20 @@ class WC_Stripe_UPE_Payment_Gateway extends WC_Gateway_Stripe { */ public $payment_methods = []; + /** + * Jetpack connection manager. + * + * @var Automattic\Jetpack\Connection\Manager + */ + private $jetpack_connection_manager; + + /** + * Whether the Transact onboarding is complete. + * + * @var bool + */ + private $transact_onboarding_complete; + /** * Constructor */ @@ -3410,4 +3424,30 @@ public function is_payment_request_enabled() { return in_array( WC_Stripe_Payment_Methods::APPLE_PAY, $enabled_payment_method_ids, true ) || in_array( WC_Stripe_Payment_Methods::GOOGLE_PAY, $enabled_payment_method_ids, true ); } + + /** + * Get the Jetpack Connection Manager instance. + * + * @return \Automattic\Jetpack\Connection\Manager + */ + public function get_jetpack_connection_manager(): \Automattic\Jetpack\Connection\Manager { + if ( ! $this->jetpack_connection_manager ) { + $this->jetpack_connection_manager = new \Automattic\Jetpack\Connection\Manager( 'woocommerce' ); + } + return $this->jetpack_connection_manager; + } + + /** + * Set the Transact onboarding as complete. + * + * @return void + */ + public function set_transact_onboarding_complete(): void { + if ( $this->transact_onboarding_complete ) { + return; + } + + $this->update_option( 'transact_onboarding_complete', 'yes' ); + $this->transact_onboarding_complete = true; + } } diff --git a/tests/phpunit/Admin/WC_REST_Stripe_Settings_Controller_Test.php b/tests/phpunit/Admin/WC_REST_Stripe_Settings_Controller_Test.php index be66b40049..36df128c3f 100644 --- a/tests/phpunit/Admin/WC_REST_Stripe_Settings_Controller_Test.php +++ b/tests/phpunit/Admin/WC_REST_Stripe_Settings_Controller_Test.php @@ -4,6 +4,7 @@ use Automattic\WooCommerce\Blocks\Package; use Exception; +use WC_Stripe_Transact_Account_Manager; use WooCommerce\Stripe\Tests\Helpers\UPE_Test_Helper; use WC_Stripe_UPE_Payment_Gateway; use WC_REST_Stripe_Settings_Controller; @@ -512,7 +513,6 @@ public function is_payment_request_enabled_provider() { * @dataProvider is_payment_request_enabled_legacy_provider */ public function test_is_payment_request_enabled_legacy( $is_enabled, $option_value ) { - // Settings controller with non-UPE gateway. $gateway = new WC_Stripe_UPE_Payment_Gateway(); $gateway->update_option( 'payment_request', $option_value ); $controller = new WC_REST_Stripe_Settings_Controller( $gateway ); @@ -594,6 +594,55 @@ public function enum_field_provider() { ]; } + /** + * Tests for `maybe_onboard_with_transact`. + * + * @param bool $gateway_enabled Whether the gateway is enabled. + * @param bool $expected_to_onboard Whether onboarding is expected to occur. + * @return void + * + * @dataProvider provide_test_maybe_onboard_with_transact + */ + public function test_maybe_onboard_with_transact( bool $gateway_enabled = true, bool $expected_to_onboard = false ): void { + $gateway = $this->get_gateway(); + $gateway->enabled = $gateway_enabled ? 'yes' : 'no'; + + $transact_account_manager = $this->getMockBuilder( WC_Stripe_Transact_Account_Manager::class ) + ->onlyMethods( [ 'do_onboarding' ] ) + ->setConstructorArgs( [ $gateway ] ) + ->getMock(); + + $transact_account_manager + ->expects( $expected_to_onboard ? $this->once() : $this->never() ) + ->method( 'do_onboarding' ); + + WC_Stripe_Transact_Account_Manager::set_instance( $transact_account_manager ); + + $controller = new WC_REST_Stripe_Settings_Controller( $gateway ); + $controller->maybe_onboard_with_transact(); + + // Clean up + $gateway->enabled = 'yes'; + } + + /** + * Provider for `test_maybe_onboard_with_transact`. + * + * @return array + */ + public function provide_test_maybe_onboard_with_transact(): array { + return [ + 'gateway not enabled' => [ + 'gateway enabled' => false, + 'expected to onboard' => false, + ], + 'gateway enabled' => [ + 'gateway enabled' => true, + 'expected to onboard' => true, + ], + ]; + } + /** * @param bool $can_manage_woocommerce * diff --git a/tests/phpunit/PaymentMethods/WC_Stripe_UPE_Payment_Gateway_Test.php b/tests/phpunit/PaymentMethods/WC_Stripe_UPE_Payment_Gateway_Test.php index fd6c681659..83d86908b5 100644 --- a/tests/phpunit/PaymentMethods/WC_Stripe_UPE_Payment_Gateway_Test.php +++ b/tests/phpunit/PaymentMethods/WC_Stripe_UPE_Payment_Gateway_Test.php @@ -3132,4 +3132,26 @@ public function test_add_bnpl_debug_metadata() { $this->assertArrayHasKey( 'pmc_enabled', $result ); $this->assertEquals( 'yes', $result['pmc_enabled'] ); } + + /** + * Tests for `get_jetpack_connection_manager`. + * + * @return void + */ + public function test_get_jetpack_connection_manager(): void { + $this->assertInstanceOf( + \Automattic\Jetpack\Connection\Manager::class, + $this->mock_gateway->get_jetpack_connection_manager() + ); + } + + /** + * Tests for `set_transact_onboarding_complete`. + * + * @return void + */ + public function test_set_transact_onboarding_complete(): void { + $this->mock_gateway->set_transact_onboarding_complete(); + $this->assertEquals( 'yes', $this->mock_gateway->get_option( 'transact_onboarding_complete' ) ); + } } diff --git a/tests/phpunit/WC_Stripe_Test.php b/tests/phpunit/WC_Stripe_Test.php index a00d5bcacf..4acb72b700 100644 --- a/tests/phpunit/WC_Stripe_Test.php +++ b/tests/phpunit/WC_Stripe_Test.php @@ -5,6 +5,7 @@ use WC_Stripe; use WC_Stripe_Helper; use WC_Stripe_Payment_Methods; +use WC_Stripe_Transact_Account_Manager; use WC_Stripe_UPE_Payment_Gateway; /** @@ -154,4 +155,88 @@ public function provide_test_maybe_toggle_payment_methods() { ], ]; } + + /** + * Tests for `maybe_onboard_with_transact`. + * + * @param bool $can_manage_woocommerce Whether the user can manage WooCommerce. + * @param bool $gateway_enabled Whether the gateway is enabled. + * @param bool $onboarding_called Whether onboarding is expected to be called. + * @return void + * + * @dataProvider provide_test_maybe_onboard_with_transact + */ + public function test_maybe_onboard_with_transact( $can_manage_woocommerce = false, $gateway_enabled = true, $onboarding_called = false ): void { + // Mock the GLOBALS to return `true` for `is_admin` + $current_screen = $this->getMockBuilder( \stdClass::class ) + ->addMethods( [ 'in_admin' ] ) + ->getMock(); + + $current_screen->expects( $this->any() ) + ->method( 'in_admin' ) + ->willReturn( true ); + + $GLOBALS['current_screen'] = $current_screen; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + + $user_cap_filter = function ( $allcaps ) use ( $can_manage_woocommerce ) { + $allcaps['manage_woocommerce'] = $can_manage_woocommerce; + return $allcaps; + }; + add_filter( 'user_has_cap', $user_cap_filter ); + + $transact_account_manager = $this->getMockBuilder( WC_Stripe_Transact_Account_Manager::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'do_onboarding' ] ) + ->getMock(); + + $transact_account_manager->expects( $onboarding_called ? $this->once() : $this->never() ) + ->method( 'do_onboarding' ); + + WC_Stripe_Transact_Account_Manager::set_instance( $transact_account_manager ); + + $upe_payment_gateway = $this->getMockBuilder( WC_Stripe_UPE_Payment_Gateway::class ) + ->disableOriginalConstructor() + ->getMock(); + + $upe_payment_gateway->enabled = $gateway_enabled ? 'yes' : 'no'; + + $wc_stripe = $this->getMockBuilder( WC_Stripe::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'get_main_stripe_gateway' ] ) + ->getMock(); + + $wc_stripe->method( 'get_main_stripe_gateway' ) + ->willReturn( $upe_payment_gateway ); + + $wc_stripe->maybe_onboard_with_transact(); + + // Clean up. + remove_filter( 'user_has_cap', $user_cap_filter ); + unset( $GLOBALS['current_screen'] ); + } + + /** + * Provider for `test_maybe_onboard_with_transact`. + * + * @return array + */ + public function provide_test_maybe_onboard_with_transact(): array { + return [ + 'user cannot manage woocommerce' => [ + 'user can manage woocommerce' => false, + 'gateway is enabled' => true, + 'onboarding called' => false, + ], + 'gateway is not enabled' => [ + 'user can manage woocommerce' => true, + 'gateway is enabled' => false, + 'onboarding called' => false, + ], + 'onboarding called successfully' => [ + 'user can manage woocommerce' => true, + 'gateway is enabled' => true, + 'onboarding called' => true, + ], + ]; + } } diff --git a/tests/phpunit/WC_Stripe_Transact_Account_Manager_Test.php b/tests/phpunit/WC_Stripe_Transact_Account_Manager_Test.php new file mode 100644 index 0000000000..a3cc40100a --- /dev/null +++ b/tests/phpunit/WC_Stripe_Transact_Account_Manager_Test.php @@ -0,0 +1,744 @@ +gateway = $this->getMockBuilder( WC_Stripe_UPE_Payment_Gateway::class ) + ->disableOriginalConstructor() + ->getMock(); + + // Set default properties. + $stripe_settings = WC_Stripe_Helper::get_stripe_settings(); + $stripe_settings['testmode'] = 'yes'; + WC_Stripe_Helper::update_main_stripe_settings( $stripe_settings ); + + // overriding the `WC_Stripe_Connect` in woocommerce_gateway_stripe(), + $stripe_connect_mock = $this->createPartialMock( + WC_Stripe_Connect::class, + [ 'is_connected_via_oauth' ] + ); + $stripe_connect_mock + ->expects( $this->any() ) + ->method( 'is_connected_via_oauth' ) + ->willReturn( true ); + + $this->stripe_connect_original = woocommerce_gateway_stripe()->connect; + woocommerce_gateway_stripe()->connect = $stripe_connect_mock; + + // Create account manager instance. + $this->account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + } + + /** + * @inheritDoc + * + * @return void + */ + public function tear_down() { + parent::tear_down(); + + // Restoring the original `WC_Stripe_Connect` instance. + woocommerce_gateway_stripe()->connect = $this->stripe_connect_original; + } + + /** + * Test `get_instance` sets gateway. + */ + public function test_get_instance_set_instance() { + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $this->assertInstanceOf( WC_Stripe_Transact_Account_Manager::class, $account_manager ); + + // Test setting a custom instance. + $custom_transaction_manager = new class( $this->gateway ) extends WC_Stripe_Transact_Account_Manager { + public string $test_property = 'foo'; + }; + + WC_Stripe_Transact_Account_Manager::set_instance( $custom_transaction_manager ); + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $this->assertEquals( 'foo', $account_manager->test_property ); + } + + /** + * Test do_onboarding when not connected via OAuth. + */ + public function test_do_onboarding_when_not_connected_via_oauth() { + // overriding the `WC_Stripe_Connect` in woocommerce_gateway_stripe(), + $stripe_connect_mock = $this->createPartialMock( + WC_Stripe_Connect::class, + [ 'is_connected_via_oauth' ] + ); + $stripe_connect_mock + ->expects( $this->any() ) + ->method( 'is_connected_via_oauth' ) + ->willReturn( false ); + + $this->stripe_connect_original = woocommerce_gateway_stripe()->connect; + woocommerce_gateway_stripe()->connect = $stripe_connect_mock; + + // Should not throw any errors and should return early. + $this->account_manager->do_onboarding(); + + $this->assertTrue( true ); + } + + /** + * Test do_onboarding when Jetpack registration fails. + */ + public function test_do_onboarding_when_jetpack_registration_fails() { + // Mock the gateway to return a mock Jetpack connection manager. + $jetpack_manager = $this->getMockBuilder( \Automattic\Jetpack\Connection\Manager::class ) + ->onlyMethods( [ 'is_connected', 'try_registration' ] ) + ->getMock(); + + $jetpack_manager->method( 'is_connected' ) + ->willReturn( false ); + + $jetpack_manager->method( 'try_registration' ) + ->willReturn( new \WP_Error( 'registration_failed', 'Registration failed' ) ); + + $this->gateway->method( 'get_jetpack_connection_manager' ) + ->willReturn( $jetpack_manager ); + + // Should not throw any errors and should return early. + $this->account_manager->do_onboarding(); + + $this->assertTrue( true ); + } + + /** + * Test do_onboarding when merchant account creation fails. + */ + public function test_do_onboarding_when_merchant_account_creation_fails() { + // Mock the gateway to return a mock Jetpack connection manager. + $jetpack_manager = $this->getMockBuilder( \Automattic\Jetpack\Connection\Manager::class ) + ->onlyMethods( [ 'is_connected' ] ) + ->getMock(); + + $jetpack_manager->method( 'is_connected' ) + ->willReturn( true ); + + $this->gateway->method( 'get_jetpack_connection_manager' ) + ->willReturn( $jetpack_manager ); + + // Mock Jetpack options to return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Mock the HTTP request to return an error. + add_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + // Should do nothing. Should not throw any errors. + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $account_manager->do_onboarding(); + + // Clean up the filters. + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + $this->assertTrue( true ); + } + + /** + * Test do_onboarding when provider account creation fails. + */ + public function test_do_onboarding_when_provider_account_creation_fails() { + // Mock the gateway to return a mock Jetpack connection manager. + $jetpack_manager = $this->getMockBuilder( \Automattic\Jetpack\Connection\Manager::class ) + ->onlyMethods( [ 'is_connected' ] ) + ->getMock(); + + $jetpack_manager->method( 'is_connected' ) + ->willReturn( true ); + + $this->gateway->method( 'get_jetpack_connection_manager' ) + ->willReturn( $jetpack_manager ); + + // Mock Jetpack options to return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Mock the HTTP request to return an error. + add_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + // Should do nothing. Should not throw any errors. + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $account_manager->do_onboarding(); + + // Check that it returns true. + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + $this->assertTrue( true ); + } + + /** + * Test get_merchant_account_data returns cached data when available. + */ + public function test_get_merchant_account_data_returns_cached_data() { + // Return valid cache data. + WC_Stripe_Database_Cache::set( 'transact_merchant_account_test', $this->return_valid_merchant_account_cache() ); + + $result = $this->account_manager->get_transact_account_data( 'merchant' ); + + // Clean up the filter. + WC_Stripe_Database_Cache::delete( 'transact_merchant_account_test' ); + + $expected_merchant_account = $this->return_valid_merchant_account_cache(); + $this->assertEquals( $expected_merchant_account['account'], $result ); + } + + /** + * Test get_merchant_account_data returns null when cache is expired. + */ + public function test_get_merchant_account_data_returns_null_when_cache_expired() { + // Mock cache to return expired data. + WC_Stripe_Database_Cache::set( 'transact_merchant_account_test', $this->return_expired_merchant_account_cache() ); + + $result = $this->account_manager->get_transact_account_data( 'merchant' ); + + // Clean up the filter. + WC_Stripe_Database_Cache::delete( 'transact_merchant_account_test' ); + + $this->assertNull( $result ); + } + + /** + * Test get_merchant_account_data fetches when cache is empty and caches fetched data. + */ + public function test_get_merchant_account_data_fetches_and_caches_data() { + // Return empty cache. + WC_Stripe_Database_Cache::set( 'transact_merchant_account_test', $this->return_empty_merchant_account_cache() ); + + // Return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Return a successful response, with the merchant account data. + add_filter( 'pre_http_request', [ $this, 'return_merchant_account_api_success' ] ); + + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $result = $account_manager->get_transact_account_data( 'merchant' ); + + // Clean up the filters and cache. + WC_Stripe_Database_Cache::delete( 'transact_merchant_account_test' ); + + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_merchant_account_api_success' ] ); + + // Check that it returns the data. + $response_data = json_decode( $this->return_merchant_account_api_success()['body'], true ); + $expected_merchant_account = [ 'public_id' => $response_data['public_id'] ]; + $this->assertEquals( $expected_merchant_account, $result ); + + // Check that the cache was updated. + $cached_data = WC_Stripe_Database_Cache::get( 'transact_merchant_account_test' ); + $this->assertNull( $cached_data ); + } + + + /** + * Test get_provider_account_data returns cached data when available. + */ + public function test_get_provider_account_data_returns_cached_data() { + // Return valid cache data. + WC_Stripe_Database_Cache::set( 'transact_provider_account_test', $this->return_valid_provider_account_cache() ); + + $result = $this->account_manager->get_transact_account_data( 'provider' ); + + // Clean up the cache. + WC_Stripe_Database_Cache::delete( 'transact_provider_account_test' ); + + $expected_provider_account = $this->return_valid_provider_account_cache(); + $this->assertEquals( $expected_provider_account['account'], $result ); + } + + /** + * Test get_provider_account_data returns null when cache is expired. + */ + public function test_get_provider_account_data_returns_null_when_cache_expired() { + // Mock cache to return expired data. + WC_Stripe_Database_Cache::set( 'transact_provider_account_test', $this->return_expired_provider_account_cache() ); + + $result = $this->account_manager->get_transact_account_data( 'provider' ); + + // Clean up the cache. + WC_Stripe_Database_Cache::delete( 'transact_provider_account_test' ); + + $this->assertNull( $result ); + } + + /** + * Test get_provider_account_data fetches when cache is empty and caches fetched data. + */ + public function test_get_provider_account_data_fetches_and_caches_data() { + // Return empty cache. + WC_Stripe_Database_Cache::set( 'transact_provider_account_test', $this->return_empty_provider_account_cache() ); + + // Return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Return a successful response, with the provider account data. + add_filter( 'pre_http_request', [ $this, 'return_provider_account_api_success' ] ); + + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $result = $account_manager->get_transact_account_data( 'provider' ); + + // Clean up the filters and cache. + WC_Stripe_Database_Cache::delete( 'transact_provider_account_test' ); + + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_provider_account_api_success' ] ); + + // Check that it returns the data. + $this->assertTrue( $result ); + + // Check that the cache was updated. + WC_Stripe_Database_Cache::delete( 'transact_provider_account_test' ); + $cached_data = WC_Stripe_Database_Cache::get( 'transact_provider_account_test' ); + $this->assertNull( $cached_data ); + } + + /** + * Test fetch_merchant_account when API request fails. + */ + public function test_fetch_merchant_account_when_api_request_fails() { + // Mock Jetpack options to return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Mock the HTTP request to return an error. + add_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $reflection = new \ReflectionClass( $account_manager ); + $method = $reflection->getMethod( 'fetch_merchant_account' ); + $method->setAccessible( true ); + + $result = $method->invoke( $account_manager ); + + // Clean up the filters. + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + $this->assertNull( $result ); + } + + /** + * Test fetch_merchant_account when API response is successful. + */ + public function test_fetch_merchant_account_when_api_response_successful() { + // Mock Jetpack options to return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Mock the HTTP request to return a successful response. + add_filter( 'pre_http_request', [ $this, 'return_merchant_account_api_success' ] ); + + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $reflection = new \ReflectionClass( $account_manager ); + $method = $reflection->getMethod( 'fetch_merchant_account' ); + $method->setAccessible( true ); + + $result = $method->invoke( $account_manager ); + + // Clean up the filters. + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_merchant_account_api_success' ] ); + + $this->assertEquals( [ 'public_id' => 'test_public_id' ], $result ); + } + + /** + * Test fetch_provider_account when API request fails. + */ + public function test_fetch_provider_account_when_api_request_fails() { + // Mock Jetpack options to return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Mock the HTTP request to return an error. + add_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + // Create a real account manager instance. + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + + // Use reflection to access the private fetch_provider_account method. + $reflection = new \ReflectionClass( $account_manager ); + $method = $reflection->getMethod( 'fetch_provider_account' ); + $method->setAccessible( true ); + $result = $method->invoke( $account_manager ); + + // Clean up the filters. + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + $this->assertFalse( $result ); + } + + /** + * Test fetch_provider_account when API response is successful. + */ + public function test_fetch_provider_account_when_api_response_successful() { + // Mock Jetpack options to return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Mock the HTTP request to return a successful response. + add_filter( 'pre_http_request', [ $this, 'return_provider_account_api_success' ] ); + + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $reflection = new \ReflectionClass( $account_manager ); + $method = $reflection->getMethod( 'fetch_provider_account' ); + $method->setAccessible( true ); + $result = $method->invoke( $account_manager ); + + // Clean up the filters. + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_provider_account_api_success' ] ); + + // Check that it returns true. + $this->assertTrue( $result ); + } + + /** + * Test create_merchant_account when API request fails. + */ + public function test_create_merchant_account_when_api_request_fails() { + // Mock Jetpack options to return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Mock the HTTP request to return an error. + add_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + // Create a real account manager instance. + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + + // Use reflection to access the private create_merchant_account method. + $reflection = new \ReflectionClass( $account_manager ); + $create_method = $reflection->getMethod( 'create_merchant_account' ); + $create_method->setAccessible( true ); + + $result = $create_method->invoke( $account_manager ); + + // Clean up the filters. + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + // The method should return null when API fails. + $this->assertNull( $result ); + } + + /** + * Test create_merchant_account when API response is successful. + */ + public function test_create_merchant_account_when_api_response_successful() { + // Mock Jetpack options to return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Mock the HTTP request to return a successful response. + add_filter( 'pre_http_request', [ $this, 'return_merchant_account_api_success' ] ); + + // Create a real account manager instance. + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + + // Use reflection to access the private create_merchant_account method. + $reflection = new \ReflectionClass( $account_manager ); + $method = $reflection->getMethod( 'create_merchant_account' ); + $method->setAccessible( true ); + + $result = $method->invoke( $account_manager ); + + // Clean up the filters. + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_merchant_account_api_success' ] ); + + // Check that it returns the data. + $this->assertEquals( [ 'public_id' => 'test_public_id' ], $result ); + } + + /** + * Test create_provider_account when API request fails. + */ + public function test_create_provider_account_when_api_request_fails() { + // Mock Jetpack options to return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Mock the HTTP request to return an error. + add_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $reflection = new \ReflectionClass( $account_manager ); + $method = $reflection->getMethod( 'create_provider_account' ); + $method->setAccessible( true ); + $result = $method->invoke( $account_manager ); + + // Clean up the filters. + remove_filter( 'pre_http_request', [ $this, 'return_api_error' ] ); + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Check that it returns false. + $this->assertFalse( $result ); + } + + /** + * Test create_provider_account when API response is successful. + */ + public function test_create_provider_account_when_api_response_successful() { + // Mock Jetpack options to return a valid site ID. + add_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + + // Return a Jetpack blog token. + add_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + + // Mock the HTTP request to return a successful response. + add_filter( 'pre_http_request', [ $this, 'return_provider_account_api_success' ] ); + + $account_manager = WC_Stripe_Transact_Account_Manager::get_instance( $this->gateway ); + $reflection = new \ReflectionClass( $account_manager ); + $method = $reflection->getMethod( 'create_provider_account' ); + $method->setAccessible( true ); + $result = $method->invoke( $account_manager ); + + // Clean up the filters. + remove_filter( 'pre_option_jetpack_private_options', [ $this, 'return_blog_token' ] ); + remove_filter( 'pre_option_jetpack_options', [ $this, 'return_valid_site_id' ] ); + remove_filter( 'pre_http_request', [ $this, 'return_provider_account_api_success' ] ); + + // Check that it returns true. + $this->assertTrue( $result ); + } + + /** + * Helper method to return API error response. + * + * @return \WP_Error Error response. + */ + public function return_api_error() { + return new \WP_Error( 'api_error', 'API request failed' ); + } + + /** + * Helper method to return successful merchant account API response. + * + * @return array Success response. + */ + public function return_merchant_account_api_success() { + return [ + 'response' => [ 'code' => 200 ], + 'body' => wp_json_encode( + [ + 'public_id' => 'test_public_id', + ] + ), + ]; + } + + /** + * Helper method to return successful provider account API response. + * + * @return array Success response. + */ + public function return_provider_account_api_success() { + return [ 'response' => [ 'code' => 200 ] ]; + } + + /** + * Helper method to return null site ID for Jetpack options. + * + * @param mixed $value The option value. + * + * @return null + */ + public function return_null_site_id( $value ) { + return [ 'id' => null ]; + } + + /** + * Helper method to return valid site ID for Jetpack options. + * + * @param mixed $value The option value. + * + * @return int + */ + public function return_valid_site_id( $value ) { + return [ 'id' => 12345 ]; + } + + /** + * Helper method to return empty merchant account cache. + * + * @return false + */ + public function return_empty_merchant_account_cache() { + return false; + } + + /** + * Helper method to return expired merchant account cache. + * + * @return array + */ + public function return_expired_merchant_account_cache() { + return [ + 'account' => [ 'public_id' => 'test_public_id' ], + 'expiry' => time() - 3600, // Expired 1 hour ago. + ]; + } + + /** + * Helper method to return valid merchant account cache. + * + * @return array + */ + public function return_valid_merchant_account_cache() { + return [ + 'account' => [ 'public_id' => 'test_public_id' ], + 'expiry' => time() + 3600, // Expires in 1 hour. + ]; + } + + /** + * Helper method to return empty provider account cache. + * + * @return false + */ + public function return_empty_provider_account_cache() { + return false; + } + + /** + * Helper method to return expired provider account cache. + * + * @return array + */ + public function return_expired_provider_account_cache() { + return [ + 'account' => true, + 'expiry' => time() - 3600, // Expired 1 hour ago. + ]; + } + + /** + * Helper method to return valid provider account cache. + * + * @return array + */ + public function return_valid_provider_account_cache() { + return [ + 'account' => true, + 'expiry' => time() + 3600, // Expires in 1 hour. + ]; + } + + /** + * Helper method to return valid blog token for Jetpack options. + * + * @param mixed $value The option value. + * + * @return array + */ + public function return_blog_token( $value ) { + return [ 'blog_token' => 'IAM.AJETPACKBLOGTOKEN' ]; + } + + /** + * Clean up after tests. + */ + public function tearDown(): void { + // Clean up any options we created. + WC_Stripe_Database_Cache::delete( 'transact_merchant_account_live' ); + WC_Stripe_Database_Cache::delete( 'transact_merchant_account_test' ); + WC_Stripe_Database_Cache::delete( 'transact_provider_account_live' ); + WC_Stripe_Database_Cache::delete( 'transact_provider_account_test' ); + + parent::tearDown(); + } +}