diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php index 785df5eae48..19ba9b67ff5 100644 --- a/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php @@ -4,8 +4,6 @@ * * Handles requests to the /taxes endpoint. * - * @author WooThemes - * @category API * @package WooCommerce\RestApi * @since 3.0.0 */ @@ -40,67 +38,79 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { * Register the routes for taxes. */ public function register_routes() { - register_rest_route( $this->namespace, '/' . $this->rest_base, array( + register_rest_route( + $this->namespace, + '/' . $this->rest_base, array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - 'args' => $this->get_collection_params(), - ), - array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'create_item' ), - 'permission_callback' => array( $this, 'create_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) ); + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); - register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( - 'args' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - ), - ), + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_item' ), - 'permission_callback' => array( $this, 'update_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'default' => false, - 'type' => 'boolean', - 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', ), ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) ); + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); - register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) ); + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); } /** @@ -200,7 +210,7 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { public function get_items( $request ) { global $wpdb; - $prepared_args = array(); + $prepared_args = array(); $prepared_args['order'] = $request['order']; $prepared_args['number'] = $request['per_page']; if ( ! empty( $request['offset'] ) ) { @@ -208,9 +218,10 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { } else { $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; } - $orderby_possibles = array( - 'id' => 'tax_rate_id', - 'order' => 'tax_rate_order', + $orderby_possibles = array( + 'id' => 'tax_rate_id', + 'order' => 'tax_rate_order', + 'priority' => 'tax_rate_priority', ); $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; $prepared_args['class'] = $request['class']; @@ -223,30 +234,42 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { */ $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); - $query = " + $orderby = sanitize_key( $prepared_args['orderby'] ) . ' ' . sanitize_key( $prepared_args['order'] ); + $query = " SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates - WHERE 1 = 1 + %s + ORDER BY {$orderby} + LIMIT %%d, %%d "; + $wpdb_prepare_args = array( + $prepared_args['offset'], + $prepared_args['number'], + ); + // Filter by tax class. - if ( ! empty( $prepared_args['class'] ) ) { + if ( empty( $prepared_args['class'] ) ) { + $query = sprintf( $query, '' ); + } else { $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; - $query .= " AND tax_rate_class = '$class'"; + array_unshift( $wpdb_prepare_args, $class ); + $query = sprintf( $query, 'WHERE tax_rate_class = %s' ); } - // Order tax rates. - $order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) ); - - // Pagination. - $pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] ); - // Query taxes. - $results = $wpdb->get_results( $query . $order_by . $pagination ); + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( + $wpdb->prepare( + $query, + $wpdb_prepare_args + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared $taxes = array(); foreach ( $results as $tax ) { - $data = $this->prepare_item_for_response( $tax, $request ); + $data = $this->prepare_item_for_response( $tax, $request ); $taxes[] = $this->prepare_response_for_collection( $data ); } @@ -254,10 +277,18 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { // Store pagination values for headers then unset for count query. $per_page = (int) $prepared_args['number']; - $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); // Query only for ids. - $wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) ); + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $query = str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ); + $wpdb->get_results( + $wpdb->prepare( + $query, + $wpdb_prepare_args + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared // Calculate totals. $total_taxes = (int) $wpdb->num_rows; @@ -287,13 +318,13 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { * Take tax data from the request and return the updated or newly created rate. * * @param WP_REST_Request $request Full details about the request. - * @param stdClass|null $current Existing tax object. + * @param stdClass|null $current Existing tax object. * @return object */ protected function create_or_update_tax( $request, $current = null ) { - $id = absint( isset( $request['id'] ) ? $request['id'] : 0 ); - $data = array(); - $fields = array( + $id = absint( isset( $request['id'] ) ? $request['id'] : 0 ); + $data = array(); + $fields = array( 'tax_rate_country', 'tax_rate_state', 'tax_rate', @@ -321,25 +352,25 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { // Add to data array. switch ( $key ) { - case 'tax_rate_priority' : - case 'tax_rate_compound' : - case 'tax_rate_shipping' : - case 'tax_rate_order' : + case 'tax_rate_priority': + case 'tax_rate_compound': + case 'tax_rate_shipping': + case 'tax_rate_order': $data[ $field ] = absint( $request[ $key ] ); break; - case 'tax_rate_class' : + case 'tax_rate_class': $data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : ''; break; - default : + default: $data[ $field ] = wc_clean( $request[ $key ] ); break; } } - if ( $id ) { - WC_Tax::_update_tax_rate( $id, $data ); - } else { + if ( ! $id ) { $id = WC_Tax::_insert_tax_rate( $data ); + } elseif ( $data ) { + WC_Tax::_update_tax_rate( $id, $data ); } // Add locales. @@ -538,7 +569,7 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { */ protected function prepare_links( $tax ) { $links = array( - 'self' => array( + 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ), ), 'collection' => array( @@ -592,18 +623,18 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { 'title' => 'tax', 'type' => 'object', 'properties' => array( - 'id' => array( + 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'country' => array( + 'country' => array( 'description' => __( 'Country ISO 3166 code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'state' => array( + 'state' => array( 'description' => __( 'State code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), @@ -613,17 +644,17 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'city' => array( + 'city' => array( 'description' => __( 'City name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'rate' => array( + 'rate' => array( 'description' => __( 'Tax rate.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'name' => array( + 'name' => array( 'description' => __( 'Tax rate name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), @@ -646,12 +677,12 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { 'default' => true, 'context' => array( 'view', 'edit' ), ), - 'order' => array( + 'order' => array( 'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), - 'class' => array( + 'class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'default' => 'standard', @@ -674,54 +705,55 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller { $params['context'] = $this->get_context_param(); $params['context']['default'] = 'view'; - $params['page'] = array( - 'description' => __( 'Current page of the collection.', 'woocommerce' ), - 'type' => 'integer', - 'default' => 1, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - 'minimum' => 1, + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, ); $params['per_page'] = array( - 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), - 'type' => 'integer', - 'default' => 10, - 'minimum' => 1, - 'maximum' => 100, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', + 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', ); - $params['offset'] = array( - 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', ); - $params['order'] = array( - 'default' => 'asc', - 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), - 'enum' => array( 'asc', 'desc' ), - 'sanitize_callback' => 'sanitize_key', - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', + $params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', ); - $params['orderby'] = array( - 'default' => 'order', - 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), - 'enum' => array( + $params['orderby'] = array( + 'default' => 'order', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( 'id', 'order', + 'priority', ), - 'sanitize_callback' => 'sanitize_key', - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', ); - $params['class'] = array( - 'description' => __( 'Sort by tax class.', 'woocommerce' ), - 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), - 'sanitize_callback' => 'sanitize_title', - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', + $params['class'] = array( + 'description' => __( 'Sort by tax class.', 'woocommerce' ), + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'sanitize_callback' => 'sanitize_title', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', ); return $params; diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php index 758686fbc44..6fef3703eb6 100644 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php @@ -74,7 +74,7 @@ class WC_REST_Taxes_Controller extends WC_REST_Taxes_V2_Controller { $schema = parent::get_item_schema(); $schema['properties']['postcodes'] = array( - 'description' => __( 'List of postcodes / ZIPs.', 'woocommerce' ), + 'description' => __( 'List of postcodes / ZIPs. Introduced in WooCommerce 5.3.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'string', @@ -83,7 +83,7 @@ class WC_REST_Taxes_Controller extends WC_REST_Taxes_V2_Controller { ); $schema['properties']['cities'] = array( - 'description' => __( 'List of city names.', 'woocommerce' ), + 'description' => __( 'List of city names. Introduced in WooCommerce 5.3.', 'woocommerce' ), 'type' => 'array', 'items' => array( 'type' => 'string', @@ -91,6 +91,51 @@ class WC_REST_Taxes_Controller extends WC_REST_Taxes_V2_Controller { 'context' => array( 'view', 'edit' ), ); + $schema['properties']['postcode']['description'] = + __( "Postcode/ZIP, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'postcodes' should be used instead.", 'woocommerce' ); + + $schema['properties']['city']['description'] = + __( "City name, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'cities' should be used instead.", 'woocommerce' ); + return $schema; } + + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response The response, or an error. + */ + public function create_item( $request ) { + $this->adjust_cities_and_postcodes( $request ); + + return parent::create_item( $request ); + } + + /** + * Update a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response The response, or an error. + */ + public function update_item( $request ) { + $this->adjust_cities_and_postcodes( $request ); + + return parent::update_item( $request ); + } + + /** + * Convert array "cities" and "postcodes" parameters + * into semicolon-separated strings "city" and "postcode". + * + * @param WP_REST_Request $request The request to adjust. + */ + private function adjust_cities_and_postcodes( &$request ) { + if ( isset( $request['cities'] ) ) { + $request['city'] = join( ';', $request['cities'] ); + } + if ( isset( $request['postcodes'] ) ) { + $request['postcode'] = join( ';', $request['postcodes'] ); + } + } } diff --git a/tests/legacy/framework/class-wc-rest-unit-test-case.php b/tests/legacy/framework/class-wc-rest-unit-test-case.php index 5ad76c053e7..013ec0ace8d 100644 --- a/tests/legacy/framework/class-wc-rest-unit-test-case.php +++ b/tests/legacy/framework/class-wc-rest-unit-test-case.php @@ -4,11 +4,18 @@ * * Provides REST API specific methods and setup/teardown. * + * @package WooCommerce\Tests * @since 3.0 */ +/** + * Base class for REST related unit test classes. + */ class WC_REST_Unit_Test_Case extends WC_Unit_Test_Case { + /** + * @var WP_REST_Server + */ protected $server; /** @@ -36,4 +43,64 @@ class WC_REST_Unit_Test_Case extends WC_Unit_Test_Case { unset( $this->server ); $wp_rest_server = null; } + + /** + * Perform a REST request. + * + * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'. + * @param string $verb HTTP verb for the request, default is GET. + * @param array|null $body_params Body parameters for the request, null if none are required. + * @param array|null $query_params Query string parameters for the request, null if none are required. + * @return array Result from the request. + */ + public function do_rest_request( $url, $verb = 'GET', $body_params = null, $query_params = null ) { + if ( '/' !== $url[0] ) { + $url = '/wc/v3/' . $url; + } + + $request = new WP_REST_Request( $verb, $url ); + if ( ! is_null( $query_params ) ) { + $request->set_query_params( $query_params ); + } + if ( ! is_null( $body_params ) ) { + $request->set_body_params( $body_params ); + } + + return $this->server->dispatch( $request ); + } + + /** + * Perform a GET REST request. + * + * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'. + * @param array|null $query_params Query string parameters for the request, null if none are required. + * @return WP_REST_Response The response for the request. + */ + public function do_rest_get_request( $url, $query_params = null ) { + return $this->do_rest_request( $url, 'GET', null, $query_params ); + } + + /** + * Perform a POST REST request. + * + * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'. + * @param array|null $body_params Body parameters for the request, null if none are required. + * @param array|null $query_params Query string parameters for the request, null if none are required. + * @return array Result from the request. + */ + public function do_rest_post_request( $url, $body_params = null, $query_params = null ) { + return $this->do_rest_request( $url, 'POST', $body_params, $query_params ); + } + + /** + * Perform a PUT REST request. + * + * @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'. + * @param array|null $body_params Body parameters for the request, null if none are required. + * @param array|null $query_params Query string parameters for the request, null if none are required. + * @return array Result from the request. + */ + public function do_rest_put_request( $url, $body_params = null, $query_params = null ) { + return $this->do_rest_request( $url, 'PUT', $body_params, $query_params ); + } } diff --git a/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller-tests.php b/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller-tests.php new file mode 100644 index 00000000000..e991716cc93 --- /dev/null +++ b/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller-tests.php @@ -0,0 +1,288 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Data provider for test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes. + * + * @return array + */ + public function data_provider_for_test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes() { + return array( + array( + array( + 'city' => 'Osaka;Kyoto;Kobe', + 'postcode' => '5555;7777;8888', + ), + 'create', + ), + array( + array( + 'cities' => array( + 'Osaka', + 'Kyoto', + 'Kobe', + ), + 'postcodes' => array( + '5555', + '7777', + '8888', + ), + ), + 'create', + ), + array( + array( + 'city' => 'Osaka;Kyoto;Kobe', + 'postcode' => '5555;7777;8888', + ), + 'update', + ), + array( + array( + 'cities' => array( + 'Osaka', + 'Kyoto', + 'Kobe', + ), + 'postcodes' => array( + '5555', + '7777', + '8888', + ), + ), + 'update', + ), + ); + } + + /** + * @testdox It is possible to create or update a tax rate passing either "city"/"postcode" (strings) or "cities"/"postcodes" (arrays) fields. + * + * @dataProvider data_provider_for_test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes + * + * @param array $request_body The body for the REST request. + * @param string $action The action to perform, 'create' or 'update'. + */ + public function test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes( $request_body, $action ) { + global $wpdb; + + wp_set_current_user( $this->user ); + + if ( 'create' === $action ) { + $tax_rate_id = null; + + $request_body = array_merge( + $request_body, + array( + 'country' => 'JP', + 'rate' => '1', + 'name' => 'Fake Tax', + ) + ); + + $verb = 'POST'; + $url = 'taxes'; + $success_status = 201; + } else { + $tax_rate_id = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => 'JP', + 'tax_rate' => '1', + 'tax_rate_name' => 'Fake Tax', + ) + ); + + WC_Tax::_update_tax_rate_cities( $tax_rate_id, 'Tokyo' ); + WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, '0000' ); + + $verb = 'PUT'; + $url = 'taxes/' . $tax_rate_id; + $success_status = 200; + } + + $response = $this->do_rest_request( $url, $verb, $request_body ); + $this->assertEquals( $success_status, $response->get_status() ); + if ( ! $tax_rate_id ) { + $tax_rate_id = $response->get_data()['id']; + } + + $data = $wpdb->get_results( + $wpdb->prepare( + "SELECT location_type, GROUP_CONCAT(location_code SEPARATOR ';') as items + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id=%d + GROUP BY location_type", + $tax_rate_id + ), + OBJECT_K + ); + + $this->assertEquals( 'OSAKA;KYOTO;KOBE', $data['city']->items ); + $this->assertEquals( '5555;7777;8888', $data['postcode']->items ); + } + + /** + * @testdox The response for tax rate(s) includes the "city"/"postcode" (strings) and "cities"/"postcodes" (arrays) fields. + * + * @testWith [true] + * [false] + * + * @param bool $request_one True to request only one tax, false to request all the taxes. + */ + public function test_get_tax_response_includes_cities_and_postcodes_as_arrays( $request_one ) { + wp_set_current_user( $this->user ); + + $tax_id = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => 'JP', + 'tax_rate' => '1', + 'tax_rate_name' => 'Fake Tax', + ) + ); + + WC_Tax::_update_tax_rate_cities( $tax_id, 'Osaka;Kyoto;Kobe' ); + WC_Tax::_update_tax_rate_postcodes( $tax_id, '5555;7777;8888' ); + + if ( $request_one ) { + $response = $this->do_rest_get_request( 'taxes/' . $tax_id ); + } else { + $response = $this->do_rest_get_request( 'taxes' ); + } + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + if ( ! $request_one ) { + $data = current( $data ); + } + + $this->assertEquals( 'KOBE', $data['city'] ); + $this->assertEquals( '8888', $data['postcode'] ); + $this->assertEquals( array( 'OSAKA', 'KYOTO', 'KOBE' ), $data['cities'] ); + $this->assertEquals( array( '5555', '7777', '8888' ), $data['postcodes'] ); + } + + /** + * @testdox The response of a REST API request for taxes can be sorted by priority. + * + * @testWith ["asc"] + * ["desc"] + * + * @param string $order_type Sort type, 'asc' or 'desc'. + */ + public function test_get_tax_response_can_be_sorted_by_priority( $order_type ) { + wp_set_current_user( $this->user ); + + $tax_id_1 = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => 'JP', + 'tax_rate' => '1', + 'tax_rate_priority' => 1, + 'tax_rate_name' => 'Fake Tax 1', + ) + ); + $tax_id_3 = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => 'JP', + 'tax_rate' => '1', + 'tax_rate_priority' => 3, + 'tax_rate_name' => 'Fake Tax 3', + ) + ); + $tax_id_2 = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => 'JP', + 'tax_rate' => '1', + 'tax_rate_priority' => 2, + 'tax_rate_name' => 'Fake Tax 2', + ) + ); + + $response = $this->do_rest_get_request( + 'taxes', + array( + 'orderby' => 'priority', + 'order' => $order_type, + ) + ); + + $this->assertEquals( 200, $response->get_status() ); + $data = array_values( $response->get_data() ); + $ids = array_map( + function( $item ) { + return $item['id']; + }, + $data + ); + + if ( 'asc' === $order_type ) { + $expected = array( $tax_id_1, $tax_id_2, $tax_id_3 ); + } else { + $expected = array( $tax_id_3, $tax_id_2, $tax_id_1 ); + } + $this->assertEquals( $expected, $ids ); + } + + /** + * @testdox Tax rates can be queries filtering by tax class. + * + * @testWith ["standard"] + * ["reduced-rate"] + * ["zero-rate"] + * + * @param string $class The tax class name to try getting the taxes for. + */ + public function test_can_get_taxes_filtering_by_class( $class ) { + wp_set_current_user( $this->user ); + + $classes = array( 'standard', 'reduced-rate', 'zero-rate' ); + + $tax_ids_by_class = array(); + foreach ( $classes as $class ) { + $tax_id = WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => 'JP', + 'tax_rate' => '1', + 'tax_rate_priority' => 1, + 'tax_rate_name' => 'Fake Tax', + 'tax_rate_class' => $class, + ) + ); + $tax_ids_by_class[ $class ] = $tax_id; + } + + $response = $this->do_rest_get_request( + 'taxes', + array( + 'class' => $class, + ) + ); + + $this->assertEquals( 200, $response->get_status() ); + $data = array_values( $response->get_data() ); + $ids = array_map( + function( $item ) { + return $item['id']; + }, + $data + ); + + $this->assertEquals( array( $tax_ids_by_class[ $class ] ), $ids ); + } +}