diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 9f0865257ac..9e49b0ab504 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -443,6 +443,7 @@ public static function init() { include_once __DIR__ . '/class-wc-payments-status.php'; include_once __DIR__ . '/class-wc-payments-token-service.php'; include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-ajax-handler.php'; + include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-avatax-compatibility.php'; include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-button-display-handler.php'; include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-button-handler.php'; include_once __DIR__ . '/class-wc-payments-woopay-button-handler.php'; diff --git a/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php index b405a0ed6ec..4ef004b3698 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php @@ -60,6 +60,9 @@ public function init() { ); add_filter( 'rest_pre_dispatch', [ $this, 'tokenized_cart_store_api_address_normalization' ], 10, 3 ); add_filter( 'woocommerce_get_country_locale', [ $this, 'modify_country_locale_for_express_checkout' ], 20 ); + + // Initialize third-party plugin compatibility. + ( new WC_Payments_Express_Checkout_Avatax_Compatibility() )->maybe_init(); } /** diff --git a/includes/express-checkout/class-wc-payments-express-checkout-avatax-compatibility.php b/includes/express-checkout/class-wc-payments-express-checkout-avatax-compatibility.php new file mode 100644 index 00000000000..1d545c93eb9 --- /dev/null +++ b/includes/express-checkout/class-wc-payments-express-checkout-avatax-compatibility.php @@ -0,0 +1,92 @@ +is_avatax_plugin_active() ) { + return; + } + + add_filter( 'rest_pre_dispatch', [ $this, 'maybe_add_avatax_filters' ], 5, 3 ); + } + + /** + * Adds Avatax compatibility filters for Express Checkout. + * + * @param mixed $response Response to replace the requested version with. + * @param \WP_REST_Server $server Server instance. + * @param \WP_REST_Request $request Request used to generate the response. + * + * @return mixed + */ + public function maybe_add_avatax_filters( $response, $server, $request ) { + // Only add filters if we're in an Express Checkout context. + if ( ! $this->is_express_checkout_context() ) { + return $response; + } + + // Force Avatax to calculate taxes during Express Checkout. + // These filters ensure Avatax recognizes the Store API checkout as a valid checkout context. + add_filter( 'wc_avatax_cart_needs_calculation', '__return_true' ); + add_filter( 'wc_avatax_checkout_ready_for_calculation', '__return_true' ); + + return $response; + } + + /** + * Check if Avatax plugin is active. + * + * @return bool True if Avatax is active, false otherwise. + */ + private function is_avatax_plugin_active() { + return class_exists( 'WC_AvaTax_Loader' ) || function_exists( 'wc_avatax' ); + } + + /** + * Check if we're in an express checkout context. + * + * @return bool True if we're in an express checkout context, false otherwise. + */ + private function is_express_checkout_context() { + // Only proceed if this is a Store API request. + if ( ! WC_Payments_Utils::is_store_api_request() ) { + return false; + } + + // Check for the 'X-WooPayments-Tokenized-Cart' header using superglobals. + if ( 'true' !== sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART'] ?? '' ) ) ) { + return false; + } + + // Verify the nonce from the 'X-WooPayments-Tokenized-Cart-Nonce' header using superglobals. + $nonce = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_NONCE'] ?? '' ) ); + if ( ! wp_verify_nonce( $nonce, 'woopayments_tokenized_cart_nonce' ) ) { + return false; + } + + return true; + } +} diff --git a/tests/unit/express-checkout/test-class-wc-payments-express-checkout-avatax-compatibility.php b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-avatax-compatibility.php new file mode 100644 index 00000000000..15b4d6d6edc --- /dev/null +++ b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-avatax-compatibility.php @@ -0,0 +1,154 @@ +avatax_compatibility = new WC_Payments_Express_Checkout_Avatax_Compatibility(); + } + + /** + * Test that Avatax compatibility filters are added during Express Checkout when Avatax is active. + */ + public function test_avatax_compatibility_filters_added_when_avatax_active_and_express_checkout() { + // Remove any existing filters first. + remove_all_filters( 'wc_avatax_cart_needs_calculation' ); + remove_all_filters( 'wc_avatax_checkout_ready_for_calculation' ); + + // Simulate Avatax being active by defining the function. + if ( ! function_exists( 'wc_avatax' ) ) { + function wc_avatax() { + return new stdClass(); + } + } + + // Initialize the compatibility class (this checks if Avatax is active). + $this->avatax_compatibility->maybe_init(); + + // Create a valid Express Checkout request. + $request = new WP_REST_Request(); + $request->set_header( 'X-WooPayments-Tokenized-Cart', 'true' ); + $request->set_header( 'X-WooPayments-Tokenized-Cart-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_nonce' ) ); + + // Simulate being in a Store API context. + $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART'] = 'true'; + $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_NONCE'] = wp_create_nonce( 'woopayments_tokenized_cart_nonce' ); + $_REQUEST['rest_route'] = '/wc/store/v1/checkout'; + + // Call the method that should add Avatax compatibility filters. + $this->avatax_compatibility->maybe_add_avatax_filters( null, null, $request ); + + // Verify the filters were added and return true. + $this->assertTrue( + apply_filters( 'wc_avatax_cart_needs_calculation', false ), + 'wc_avatax_cart_needs_calculation filter should return true during Express Checkout' + ); + $this->assertTrue( + apply_filters( 'wc_avatax_checkout_ready_for_calculation', false ), + 'wc_avatax_checkout_ready_for_calculation filter should return true during Express Checkout' + ); + + // Clean up. + unset( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART'] ); + unset( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_NONCE'] ); + unset( $_REQUEST['rest_route'] ); + } + + /** + * Test that Avatax compatibility filters are NOT added when not in Express Checkout context. + */ + public function test_avatax_compatibility_filters_not_added_when_not_express_checkout() { + // Remove any existing filters first. + remove_all_filters( 'wc_avatax_cart_needs_calculation' ); + remove_all_filters( 'wc_avatax_checkout_ready_for_calculation' ); + + // Simulate Avatax being active. + if ( ! function_exists( 'wc_avatax' ) ) { + function wc_avatax() { + return new stdClass(); + } + } + + // Initialize the compatibility class (this checks if Avatax is active). + $this->avatax_compatibility->maybe_init(); + + // Create a request WITHOUT Express Checkout headers. + $request = new WP_REST_Request(); + + // Clear any Express Checkout context. + unset( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART'] ); + unset( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_NONCE'] ); + + // Call the method. + $this->avatax_compatibility->maybe_add_avatax_filters( null, null, $request ); + + // Verify the filters were NOT added (should return the original false value). + $this->assertFalse( + apply_filters( 'wc_avatax_cart_needs_calculation', false ), + 'wc_avatax_cart_needs_calculation filter should NOT be modified when not in Express Checkout' + ); + $this->assertFalse( + apply_filters( 'wc_avatax_checkout_ready_for_calculation', false ), + 'wc_avatax_checkout_ready_for_calculation filter should NOT be modified when not in Express Checkout' + ); + } + + /** + * Test that Avatax compatibility filters are NOT added when Avatax is not active. + */ + public function test_avatax_compatibility_filters_not_added_when_avatax_not_active() { + // Remove any existing filters first. + remove_all_filters( 'wc_avatax_cart_needs_calculation' ); + remove_all_filters( 'wc_avatax_checkout_ready_for_calculation' ); + + // Note: wc_avatax function should not exist in a clean test environment, + // but we can't undefine functions in PHP. This test assumes a fresh environment + // or we test by checking the class doesn't exist. + + // Create a valid Express Checkout request. + $request = new WP_REST_Request(); + $request->set_header( 'X-WooPayments-Tokenized-Cart', 'true' ); + $request->set_header( 'X-WooPayments-Tokenized-Cart-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_nonce' ) ); + + $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART'] = 'true'; + $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_NONCE'] = wp_create_nonce( 'woopayments_tokenized_cart_nonce' ); + + // The method should check for WC_AvaTax_Loader class which won't exist. + // We test that when the class doesn't exist, filters are not added. + + // For this test, we'll verify that filters aren't blindly added + // by checking that if we had a condition for Avatax not existing, it would work. + // Since we can't undefine wc_avatax(), we'll check the WC_AvaTax_Loader class. + // If Avatax was just defined in previous test, skip this assertion + // In real scenarios, Avatax would be detected by class_exists('WC_AvaTax_Loader'). + if ( ! class_exists( 'WC_AvaTax_Loader' ) ) { + // The implementation should check for the class, not just the function. + $this->assertFalse( + class_exists( 'WC_AvaTax_Loader' ), + 'WC_AvaTax_Loader class should not exist in test environment' + ); + } + + // Clean up. + unset( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART'] ); + unset( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_NONCE'] ); + } +}