From e81857ba7a6a7c1cb1bbe93dc30d35b3b760bdf3 Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 1 Apr 2026 11:43:04 -0600 Subject: [PATCH] fix(paypal): block gateway at checkout when merchant status is invalid When payments_receivable or email_confirmed is false for the connected OAuth merchant, remove the PayPal REST gateway from the active gateways list so customers cannot attempt a payment that PayPal would reject. - Add is_merchant_status_valid() to check both flags for the current mode - Add maybe_remove_for_invalid_merchant_status() filter callback hooked to wu_get_active_gateways (mirrors the existing currency check pattern) - Skip the check when no OAuth merchant is connected (manual credentials are unaffected) - Add 11 unit tests covering all flag combinations, live/sandbox modes, and the manual-credentials bypass Closes #729 Part of #725 --- inc/gateways/class-paypal-rest-gateway.php | 51 +++-- .../Gateways/PayPal_REST_Gateway_Test.php | 195 ++++++++++++++++++ 2 files changed, 230 insertions(+), 16 deletions(-) diff --git a/inc/gateways/class-paypal-rest-gateway.php b/inc/gateways/class-paypal-rest-gateway.php index 3b81ac7d3..5d151020b 100644 --- a/inc/gateways/class-paypal-rest-gateway.php +++ b/inc/gateways/class-paypal-rest-gateway.php @@ -193,7 +193,7 @@ public function hooks(): void { // Hide PayPal from checkout when currency is not supported add_filter('wu_get_active_gateways', [$this, 'maybe_remove_for_unsupported_currency']); - // Hide PayPal from checkout when merchant cannot receive payments + // Hide PayPal from checkout when merchant status is invalid (payments_receivable or email_confirmed false) add_filter('wu_get_active_gateways', [$this, 'maybe_remove_for_invalid_merchant_status']); // Register PayPal checkout scripts (button branding) @@ -250,30 +250,49 @@ public function maybe_remove_for_unsupported_currency(array $gateways): array { } /** - * Removes PayPal from the active gateways list when the merchant cannot receive payments. + * Checks whether the connected PayPal merchant account is in a valid state to receive payments. * - * PayPal requires that merchants with `payments_receivable=false` or - * `email_confirmed=false` are blocked from processing payments until - * their account setup is complete. + * Returns true only when both conditions are met for the current mode (sandbox or live): + * - payments_receivable is truthy + * - email_confirmed is truthy * - * Hooked to 'wu_get_active_gateways'. + * When no OAuth merchant is connected the check is skipped (returns true) so that + * manual-credentials setups are not affected. * * @since 2.0.0 - * @param array $gateways The registered active gateways. - * @return array + * @return bool */ - public function maybe_remove_for_invalid_merchant_status(array $gateways): array { + public function is_merchant_status_valid(): bool { - // Only applies when connected via OAuth - if (empty($this->merchant_id)) { - return $gateways; + $mode_prefix = $this->test_mode ? 'sandbox' : 'live'; + + // Only enforce when an OAuth merchant is connected. + $merchant_id = wu_get_setting("paypal_rest_{$mode_prefix}_merchant_id", ''); + + if (empty($merchant_id)) { + return true; } - $mode_prefix = $this->test_mode ? 'sandbox' : 'live'; - $payments_receivable = wu_get_setting("paypal_rest_{$mode_prefix}_payments_receivable", true); - $email_confirmed = wu_get_setting("paypal_rest_{$mode_prefix}_email_confirmed", true); + $payments_receivable = wu_get_setting("paypal_rest_{$mode_prefix}_payments_receivable", false); + $email_confirmed = wu_get_setting("paypal_rest_{$mode_prefix}_email_confirmed", false); + + return (bool) $payments_receivable && (bool) $email_confirmed; + } + + /** + * Removes PayPal from the active gateways list when the merchant account status is invalid. + * + * Hooked to 'wu_get_active_gateways'. Hides the gateway when payments_receivable or + * email_confirmed is false so customers cannot attempt a payment that would be rejected + * by PayPal. + * + * @since 2.0.0 + * @param array $gateways The registered active gateways. + * @return array + */ + public function maybe_remove_for_invalid_merchant_status(array $gateways): array { - if (! $payments_receivable || ! $email_confirmed) { + if (! $this->is_merchant_status_valid()) { unset($gateways['paypal-rest']); } diff --git a/tests/WP_Ultimo/Gateways/PayPal_REST_Gateway_Test.php b/tests/WP_Ultimo/Gateways/PayPal_REST_Gateway_Test.php index 279da14a2..f2d84dcf2 100644 --- a/tests/WP_Ultimo/Gateways/PayPal_REST_Gateway_Test.php +++ b/tests/WP_Ultimo/Gateways/PayPal_REST_Gateway_Test.php @@ -719,6 +719,201 @@ public function test_maybe_remove_for_unsupported_currency_preserves_other_gatew $this->assertArrayHasKey('manual', $result); } + // ------------------------------------------------------------------------- + // is_merchant_status_valid() + // ------------------------------------------------------------------------- + + /** + * Test merchant status is valid when no OAuth merchant is connected. + * + * Manual-credentials setups must not be blocked by the merchant status check. + */ + public function test_is_merchant_status_valid_without_oauth_merchant(): void { + + // No merchant ID set — manual credentials only + wu_save_setting('paypal_rest_sandbox_merchant_id', ''); + + $gateway = new PayPal_REST_Gateway(); + $gateway->init(); + + $this->assertTrue($gateway->is_merchant_status_valid()); + } + + /** + * Test merchant status is valid when both flags are true. + */ + public function test_is_merchant_status_valid_both_flags_true(): void { + + wu_save_setting('paypal_rest_sandbox_merchant_id', 'MERCHANT123'); + wu_save_setting('paypal_rest_sandbox_payments_receivable', true); + wu_save_setting('paypal_rest_sandbox_email_confirmed', true); + wu_save_setting('paypal_rest_sandbox_mode', 1); + + $gateway = new PayPal_REST_Gateway(); + $gateway->init(); + + $this->assertTrue($gateway->is_merchant_status_valid()); + } + + /** + * Test merchant status is invalid when payments_receivable is false. + */ + public function test_is_merchant_status_invalid_when_payments_receivable_false(): void { + + wu_save_setting('paypal_rest_sandbox_merchant_id', 'MERCHANT123'); + wu_save_setting('paypal_rest_sandbox_payments_receivable', false); + wu_save_setting('paypal_rest_sandbox_email_confirmed', true); + wu_save_setting('paypal_rest_sandbox_mode', 1); + + $gateway = new PayPal_REST_Gateway(); + $gateway->init(); + + $this->assertFalse($gateway->is_merchant_status_valid()); + } + + /** + * Test merchant status is invalid when email_confirmed is false. + */ + public function test_is_merchant_status_invalid_when_email_confirmed_false(): void { + + wu_save_setting('paypal_rest_sandbox_merchant_id', 'MERCHANT123'); + wu_save_setting('paypal_rest_sandbox_payments_receivable', true); + wu_save_setting('paypal_rest_sandbox_email_confirmed', false); + wu_save_setting('paypal_rest_sandbox_mode', 1); + + $gateway = new PayPal_REST_Gateway(); + $gateway->init(); + + $this->assertFalse($gateway->is_merchant_status_valid()); + } + + /** + * Test merchant status is invalid when both flags are false. + */ + public function test_is_merchant_status_invalid_when_both_flags_false(): void { + + wu_save_setting('paypal_rest_sandbox_merchant_id', 'MERCHANT123'); + wu_save_setting('paypal_rest_sandbox_payments_receivable', false); + wu_save_setting('paypal_rest_sandbox_email_confirmed', false); + wu_save_setting('paypal_rest_sandbox_mode', 1); + + $gateway = new PayPal_REST_Gateway(); + $gateway->init(); + + $this->assertFalse($gateway->is_merchant_status_valid()); + } + + /** + * Test merchant status check uses live settings in live mode. + */ + public function test_is_merchant_status_valid_uses_live_settings_in_live_mode(): void { + + wu_save_setting('paypal_rest_sandbox_mode', 0); + wu_save_setting('paypal_rest_live_merchant_id', 'LIVE_MERCHANT'); + wu_save_setting('paypal_rest_live_payments_receivable', false); + wu_save_setting('paypal_rest_live_email_confirmed', true); + + $gateway = new PayPal_REST_Gateway(); + $gateway->init(); + + $this->assertFalse($gateway->is_merchant_status_valid()); + } + + // ------------------------------------------------------------------------- + // maybe_remove_for_invalid_merchant_status() + // ------------------------------------------------------------------------- + + /** + * Test gateway is kept when merchant status is valid. + */ + public function test_maybe_remove_for_invalid_merchant_status_keeps_when_valid(): void { + + wu_save_setting('paypal_rest_sandbox_merchant_id', 'MERCHANT123'); + wu_save_setting('paypal_rest_sandbox_payments_receivable', true); + wu_save_setting('paypal_rest_sandbox_email_confirmed', true); + wu_save_setting('paypal_rest_sandbox_mode', 1); + + $gateway = new PayPal_REST_Gateway(); + $gateway->init(); + $gateways = ['paypal-rest' => $gateway, 'stripe' => 'stripe']; + $result = $gateway->maybe_remove_for_invalid_merchant_status($gateways); + + $this->assertArrayHasKey('paypal-rest', $result); + } + + /** + * Test gateway is removed when payments_receivable is false. + */ + public function test_maybe_remove_for_invalid_merchant_status_removes_when_payments_receivable_false(): void { + + wu_save_setting('paypal_rest_sandbox_merchant_id', 'MERCHANT123'); + wu_save_setting('paypal_rest_sandbox_payments_receivable', false); + wu_save_setting('paypal_rest_sandbox_email_confirmed', true); + wu_save_setting('paypal_rest_sandbox_mode', 1); + + $gateway = new PayPal_REST_Gateway(); + $gateway->init(); + $gateways = ['paypal-rest' => $gateway, 'stripe' => 'stripe']; + $result = $gateway->maybe_remove_for_invalid_merchant_status($gateways); + + $this->assertArrayNotHasKey('paypal-rest', $result); + $this->assertArrayHasKey('stripe', $result); + } + + /** + * Test gateway is removed when email_confirmed is false. + */ + public function test_maybe_remove_for_invalid_merchant_status_removes_when_email_confirmed_false(): void { + + wu_save_setting('paypal_rest_sandbox_merchant_id', 'MERCHANT123'); + wu_save_setting('paypal_rest_sandbox_payments_receivable', true); + wu_save_setting('paypal_rest_sandbox_email_confirmed', false); + wu_save_setting('paypal_rest_sandbox_mode', 1); + + $gateway = new PayPal_REST_Gateway(); + $gateway->init(); + $gateways = ['paypal-rest' => $gateway, 'stripe' => 'stripe']; + $result = $gateway->maybe_remove_for_invalid_merchant_status($gateways); + + $this->assertArrayNotHasKey('paypal-rest', $result); + $this->assertArrayHasKey('stripe', $result); + } + + /** + * Test gateway is kept when no OAuth merchant is connected (manual credentials). + */ + public function test_maybe_remove_for_invalid_merchant_status_keeps_without_oauth(): void { + + // No merchant ID — manual credentials only + wu_save_setting('paypal_rest_sandbox_merchant_id', ''); + wu_save_setting('paypal_rest_sandbox_payments_receivable', false); + wu_save_setting('paypal_rest_sandbox_email_confirmed', false); + + $gateways = ['paypal-rest' => $this->gateway, 'stripe' => 'stripe']; + $result = $this->gateway->maybe_remove_for_invalid_merchant_status($gateways); + + $this->assertArrayHasKey('paypal-rest', $result); + } + + /** + * Test other gateways are not affected by merchant status check. + */ + public function test_maybe_remove_for_invalid_merchant_status_preserves_other_gateways(): void { + + wu_save_setting('paypal_rest_sandbox_merchant_id', 'MERCHANT123'); + wu_save_setting('paypal_rest_sandbox_payments_receivable', false); + wu_save_setting('paypal_rest_sandbox_email_confirmed', false); + wu_save_setting('paypal_rest_sandbox_mode', 1); + + $gateway = new PayPal_REST_Gateway(); + $gateway->init(); + $gateways = ['paypal-rest' => $gateway, 'stripe' => 'stripe', 'manual' => 'manual']; + $result = $gateway->maybe_remove_for_invalid_merchant_status($gateways); + + $this->assertArrayHasKey('stripe', $result); + $this->assertArrayHasKey('manual', $result); + } + // ------------------------------------------------------------------------- // get_checkout_label_html() // -------------------------------------------------------------------------