Commit f3a3631f26b for woocommerce
commit f3a3631f26b7f80d7490bc7f1d89a5fcd8968df5
Author: Jill Q. <jill.quek@automattic.com>
Date: Mon Aug 31 17:53:25 2026 +0800
Add search and pagination to order tax modal (#66039)
* Add tax rate search to order modal
* Update tax modal empty state copy
* Support tax class label search in tax modal
* Improve add tax modal accessibility
* Address add tax modal review feedback
* Fix AJAX branch lint warnings
* Fix tax rate percent test expectation
* Improve tax rate search clearing
* Refactor tax rate pagination selector
* Polish tax rate search summary alignment
* Hide empty tax rate modal controls
* Fix tax rate modal lint failure
* Address tax rate modal review feedback
Scope the Add tax modal handlers to the modal element instead of the
document body, search the values the modal actually displays, and skip
the tax location join when no term is given.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Disable the tax modal Add button while results reload
Replacing the results rows drops the selected radio, so re-evaluate the
Add button at that point instead of before the rows are cleared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Add filter doc blocks
* Fix indent
* Add @testdox
* Maintain tax box height during search state
* Fix malformed AJAX test merge
---------
Co-authored-by: Sam Najian <dev@najian.info>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Tom Cafferkey <tjcafferkey@gmail.com>
diff --git a/plugins/woocommerce/changelog/add-tax-rate-modal-search-pagination b/plugins/woocommerce/changelog/add-tax-rate-modal-search-pagination
new file mode 100644
index 00000000000..d2265ceeda3
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-tax-rate-modal-search-pagination
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Add search, pagination, and an empty state to the order edit Add tax modal.
diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss
index b43282228b4..752b207e21a 100644
--- a/plugins/woocommerce/client/legacy/css/admin.scss
+++ b/plugins/woocommerce/client/legacy/css/admin.scss
@@ -8196,6 +8196,46 @@ table.bar_chart {
}
}
}
+
+ table.widefat.wc-tax-rate-results {
+ thead th,
+ tbody td,
+ tbody th {
+ &:first-child {
+ padding-left: 1em;
+ }
+
+ &:last-child {
+ padding-right: 1em;
+ }
+ }
+
+ thead th:last-child {
+ white-space: nowrap;
+ }
+
+ tbody td.wc-tax-rate-status,
+ tbody td.wc-tax-rate-empty {
+ text-align: center;
+ }
+
+ tbody td.wc-tax-rate-empty {
+ padding-bottom: 48px;
+ padding-top: 48px;
+ }
+
+ .wc-tax-rate-empty-title {
+ margin: 0;
+ }
+
+ .wc-tax-rate-empty-description {
+ margin: 4px 0 0;
+ }
+
+ .wc-tax-rate-empty-action {
+ margin: 24px 0 0;
+ }
+ }
}
footer {
@@ -8224,6 +8264,38 @@ table.bar_chart {
}
}
+.wc-modal-add-tax {
+ .wc-backbone-modal-main {
+ padding-bottom: calc( 2.5em + 40px );
+
+ .search-box {
+ float: none;
+ justify-content: flex-start;
+ width: 100%;
+ }
+
+ [data-wc-tax-rate-search-box][hidden],
+ [data-wc-tax-rate-pagination-container][hidden] {
+ display: none;
+ }
+
+ .wc-tax-rate-search-summary:not([hidden]) {
+ display: block;
+ flex-basis: 100%;
+ margin: 8px 0 0;
+ text-align: start;
+ }
+
+ .wc-tax-rate-search-summary .button-link {
+ margin-inline-start: 8px;
+ }
+
+ footer {
+ padding: 1.25em 2em;
+ }
+ }
+}
+
@media screen and (max-width: 782px) {
.wc-backbone-modal .wc-backbone-modal-content {
border-radius: 0;
diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
index 9a5b05e5c2e..efa582ca46f 100644
--- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
+++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
@@ -1298,6 +1298,299 @@ jQuery( function ( $ ) {
$( document.body ).trigger( 'wc-enhanced-select-init' );
} );
}
+
+ if ( 'wc-modal-add-tax' === target ) {
+ wc_meta_boxes_order_items.backbone.init_tax_rate_modal(
+ $( '#wc-backbone-modal-dialog' ).find( '.wc-backbone-modal.wc-modal-add-tax' )
+ );
+ }
+ },
+
+ init_tax_rate_modal: function( modal ) {
+ var form = modal.find( 'form' ).first(),
+ search = modal.find( 'input[type="search"][data-wc-tax-rate-search]' ),
+ summary = modal.find( '[data-wc-tax-rate-search-summary]' ),
+ results = modal.find( 'table[data-wc-tax-rate-results]' ),
+ pagination = modal.find( '[data-wc-tax-rate-pagination]' ),
+ load_tax_rates;
+
+ // The modal template is filterable, so only wire the controls up when the
+ // elements this code depends on are actually present.
+ if ( ! form.length || ! search.length || ! results.length || ! pagination.length ) {
+ return;
+ }
+
+ load_tax_rates = function( page, is_search ) {
+ wc_meta_boxes_order_items.backbone.search_tax_rates( modal, page, is_search );
+ };
+
+ form.on( 'submit', function( event ) {
+ event.preventDefault();
+ load_tax_rates( 1, true );
+ } );
+
+ search.on( 'input search', function() {
+ if ( ! ( $( this ).val() || '' ).trim().length ) {
+ load_tax_rates( 1, false );
+ }
+ } );
+
+ // The clear button is rendered with the results summary, so delegate from its container.
+ summary.on( 'click', '[data-wc-tax-rate-clear-search]', function( event ) {
+ event.preventDefault();
+ search.val( '' );
+ load_tax_rates( 1, false );
+ } );
+
+ pagination.on( 'click', '.first-page', function() {
+ load_tax_rates( 1 );
+ } );
+
+ pagination.on( 'click', '.prev-page', function() {
+ var current_page = parseInt( pagination.data( 'page' ), 10 ) || 1;
+ load_tax_rates( Math.max( 1, current_page - 1 ) );
+ } );
+
+ pagination.on( 'click', '.next-page', function() {
+ var current_page = parseInt( pagination.data( 'page' ), 10 ) || 1;
+ load_tax_rates( current_page + 1 );
+ } );
+
+ pagination.on( 'click', '.last-page', function() {
+ var total_pages = parseInt( pagination.data( 'total-pages' ), 10 ) || 1;
+ load_tax_rates( total_pages );
+ } );
+
+ pagination.on( 'keydown', '.current-page', function( event ) {
+ if ( 13 !== event.which ) {
+ return;
+ }
+
+ event.preventDefault();
+
+ var total_pages = parseInt( pagination.data( 'total-pages' ), 10 ) || 1,
+ page = parseInt( $( this ).val(), 10 ) || 1;
+
+ load_tax_rates( Math.min( Math.max( 1, page ), total_pages ) );
+ } );
+
+ // Rows are re-rendered on every search, so delegate from the results table.
+ results.on( 'change', 'input[type="radio"][name="add_order_tax"]', function() {
+ wc_meta_boxes_order_items.backbone.update_tax_rate_add_button( modal );
+ } );
+
+ load_tax_rates( 1 );
+ },
+
+ search_tax_rates: function( modal, page, is_search ) {
+ var table = modal.find( '[data-wc-tax-rate-results]' ),
+ table_body = table.find( 'tbody' ),
+ pagination = modal.find( '[data-wc-tax-rate-pagination]' ),
+ term = ( modal.find( '[data-wc-tax-rate-search]' ).val() || '' ).trim(),
+ per_page = parseInt( table.data( 'per_page' ), 10 ) || 10,
+ request_id = Date.now() + '-' + Math.random(),
+ status = is_search && term.length
+ ? wc_enhanced_select_params.i18n_searching
+ : wc_enhanced_select_params.i18n_loading_tax_rates;
+
+ modal.data( 'wc-tax-rate-request-id', request_id );
+ table_body.css( 'height', table_body.height() );
+
+ // Replacing the rows drops any selected rate, so re-evaluate the Add button
+ // before the request starts rather than leaving it enabled with no selection.
+ table_body.html( wc_meta_boxes_order_items.backbone.tax_rate_status_row( status ) );
+ wc_meta_boxes_order_items.backbone.update_tax_rate_add_button( modal );
+ pagination.find( '.button, .current-page' ).prop( 'disabled', true );
+ wc_meta_boxes_order_items.backbone.announce_tax_rate_status( status );
+
+ $.ajax( {
+ url: woocommerce_admin_meta_boxes.ajax_url,
+ dataType: 'json',
+ type: 'GET',
+ data: {
+ action: 'woocommerce_json_search_tax_rates',
+ security: wc_enhanced_select_params.search_tax_rates_nonce,
+ term: term,
+ page: page,
+ per_page: per_page
+ },
+ success: function( response ) {
+ if ( request_id !== modal.data( 'wc-tax-rate-request-id' ) ) {
+ return;
+ }
+
+ wc_meta_boxes_order_items.backbone.render_tax_rate_results( modal, response, term );
+ },
+ error: function() {
+ if ( request_id !== modal.data( 'wc-tax-rate-request-id' ) ) {
+ return;
+ }
+
+ table_body.html(
+ wc_meta_boxes_order_items.backbone.tax_rate_status_row(
+ wc_enhanced_select_params.i18n_ajax_error
+ )
+ );
+ table_body.css( 'height', '' );
+ wc_meta_boxes_order_items.backbone.announce_tax_rate_status( wc_enhanced_select_params.i18n_ajax_error );
+ wc_meta_boxes_order_items.backbone.reset_tax_rate_pagination( pagination );
+ }
+ } );
+ },
+
+ reset_tax_rate_pagination: function( pagination ) {
+ pagination.data( 'page', 1 );
+ pagination.data( 'total-pages', 1 );
+ pagination.find( '.displaying-num' ).text( '' );
+ pagination.find( '.total-pages' ).text( 1 );
+ pagination.find( '.current-page' ).val( 1 ).prop( 'disabled', false );
+ pagination.find( '.first-page, .prev-page, .next-page, .last-page' ).prop( 'disabled', true );
+ },
+
+ update_tax_rate_add_button: function( modal ) {
+ modal.find( '#btn-ok' ).prop(
+ 'disabled',
+ ! modal.find( 'input[name="add_order_tax"]:checked' ).length
+ );
+ },
+
+ update_tax_rate_empty_state_controls: function( modal, is_empty_state ) {
+ modal.find( '[data-wc-tax-rate-search-box]' ).prop( 'hidden', is_empty_state );
+ modal.find( '[data-wc-tax-rate-pagination-container]' ).prop( 'hidden', is_empty_state );
+ wc_meta_boxes_order_items.backbone.update_tax_rate_add_button( modal );
+ },
+
+ announce_tax_rate_status: function( message ) {
+ if ( message && window.wp && window.wp.a11y && 'function' === typeof window.wp.a11y.speak ) {
+ window.wp.a11y.speak( message, 'polite' );
+ }
+ },
+
+ tax_rate_status_row: function( message ) {
+ return $( '<tr class="no-items"></tr>' ).append(
+ $( '<td class="colspanchange wc-tax-rate-status" colspan="5"></td>' ).text( message )
+ );
+ },
+
+ tax_rate_empty_state_row: function() {
+ return $( '<tr class="no-items"></tr>' ).append(
+ $( '<td class="colspanchange wc-tax-rate-empty" colspan="5"></td>' ).append(
+ $( '<p class="wc-tax-rate-empty-title"></p>' ).text( wc_enhanced_select_params.i18n_no_tax_rates ),
+ $( '<p class="wc-tax-rate-empty-description"></p>' ).text( wc_enhanced_select_params.i18n_no_tax_rates_help ),
+ $( '<p class="wc-tax-rate-empty-action"></p>' ).append(
+ $( '<a></a>', {
+ class: 'woocommerce-BlankState-cta button',
+ href: wc_enhanced_select_params.tax_rates_settings_url
+ } ).text( wc_enhanced_select_params.i18n_tax_settings )
+ )
+ )
+ );
+ },
+
+ tax_rate_result_row: function( rate ) {
+ var input_id = 'add_order_tax_' + rate.id,
+ description_id = input_id + '_description',
+ description = wc_enhanced_select_params.i18n_tax_rate_details
+ .replace( '%1$s', rate.tax_class || '' )
+ .replace( '%2$s', rate.rate_code || '' )
+ .replace( '%3$s', rate.rate_percent || '' ),
+ radio = $( '<input />', {
+ type: 'radio',
+ id: input_id,
+ name: 'add_order_tax',
+ value: rate.id,
+ 'aria-describedby': description_id
+ } );
+
+ return $( '<tr></tr>' ).append(
+ $( '<td></td>' ).append(
+ radio,
+ $( '<span></span>', {
+ class: 'screen-reader-text',
+ id: description_id
+ } ).text( description )
+ ),
+ $( '<td></td>' ).append( $( '<label></label>', { 'for': input_id } ).text( rate.label ) ),
+ $( '<td></td>' ).text( rate.tax_class ),
+ $( '<td></td>' ).text( rate.rate_code ),
+ $( '<td></td>' ).text( rate.rate_percent )
+ );
+ },
+
+ update_tax_rate_search_summary: function( modal, search_term ) {
+ var summary = modal.find( '[data-wc-tax-rate-search-summary]' );
+
+ summary.empty();
+
+ if ( ! search_term.length ) {
+ summary.prop( 'hidden', true );
+ return;
+ }
+
+ summary
+ .prop( 'hidden', false )
+ .append(
+ $( '<span></span>' ).text(
+ wc_enhanced_select_params.i18n_tax_rate_search_results.replace( '%s', search_term )
+ ),
+ ' ',
+ $( '<button></button>', {
+ type: 'button',
+ class: 'button-link',
+ 'data-wc-tax-rate-clear-search': true
+ } ).text( wc_enhanced_select_params.i18n_clear_tax_rate_search )
+ );
+ },
+
+ render_tax_rate_results: function( modal, response, search_term ) {
+ var table_body = modal.find( '[data-wc-tax-rate-results] tbody' ),
+ pagination = modal.find( '[data-wc-tax-rate-pagination]' ),
+ count = pagination.find( '.displaying-num' ),
+ results = response.results || [],
+ meta = response.pagination || {},
+ total = parseInt( meta.total, 10 ) || 0,
+ total_pages = parseInt( meta.total_pages, 10 ) || 1,
+ current_page = parseInt( meta.page, 10 ) || 1,
+ is_empty_state = false,
+ announcement = '';
+
+ search_term = ( search_term || '' ).trim();
+ is_empty_state = ! search_term.length && 0 === total && ! results.length;
+
+ table_body.empty();
+ wc_meta_boxes_order_items.backbone.update_tax_rate_search_summary( modal, search_term );
+ wc_meta_boxes_order_items.backbone.update_tax_rate_empty_state_controls( modal, is_empty_state );
+
+ if ( ! results.length ) {
+ if ( is_empty_state ) {
+ table_body.append( wc_meta_boxes_order_items.backbone.tax_rate_empty_state_row() );
+ announcement = wc_enhanced_select_params.i18n_no_tax_rates + ' ' +
+ wc_enhanced_select_params.i18n_no_tax_rates_help;
+ } else {
+ table_body.append(
+ wc_meta_boxes_order_items.backbone.tax_rate_status_row(
+ wc_enhanced_select_params.i18n_no_matches
+ )
+ );
+ announcement = wc_enhanced_select_params.i18n_no_matches;
+ }
+ } else {
+ $.each( results, function( index, rate ) {
+ table_body.append( wc_meta_boxes_order_items.backbone.tax_rate_result_row( rate ) );
+ } );
+ announcement = meta.displaying_num || '';
+ }
+
+ table_body.css( 'height', '' );
+
+ pagination.data( 'page', current_page );
+ pagination.data( 'total-pages', total_pages );
+ pagination.find( '.current-page' ).val( current_page ).prop( 'disabled', false );
+ pagination.find( '.total-pages' ).text( total_pages );
+ pagination.find( '.first-page, .prev-page' ).prop( 'disabled', ! meta.has_prev );
+ pagination.find( '.next-page, .last-page' ).prop( 'disabled', ! meta.has_next );
+ count.text( total ? meta.displaying_num : '' );
+ wc_meta_boxes_order_items.backbone.announce_tax_rate_status( announcement );
},
response: function( e, target, data ) {
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
index b9830c3bcbd..d4638f503d6 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
@@ -414,8 +414,19 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ),
'i18n_load_more' => _x( 'Loading more results…', 'enhanced select', 'woocommerce' ),
'i18n_searching' => _x( 'Searching…', 'enhanced select', 'woocommerce' ),
+ 'i18n_loading_tax_rates' => _x( 'Loading tax rates…', 'order tax rate search status', 'woocommerce' ),
+ /* translators: %s: tax rate search term. */
+ 'i18n_tax_rate_search_results' => _x( 'Showing results for “%s”', 'order tax rate search results summary', 'woocommerce' ),
+ 'i18n_clear_tax_rate_search' => _x( 'Clear search', 'order tax rate search results action', 'woocommerce' ),
+ 'i18n_no_tax_rates' => _x( 'No tax rates have been set up yet.', 'order tax rate empty state', 'woocommerce' ),
+ 'i18n_no_tax_rates_help' => _x( 'Set up tax rates or enable automated tax calculation before adding tax to this order.', 'order tax rate empty state', 'woocommerce' ),
+ 'i18n_tax_settings' => _x( 'Go to tax settings', 'order tax rate empty state action', 'woocommerce' ),
+ /* translators: 1: tax class, 2: rate code, 3: rate percentage. */
+ 'i18n_tax_rate_details' => _x( 'Tax class: %1$s. Rate code: %2$s. Rate: %3$s.', 'tax rate option screen reader details', 'woocommerce' ),
+ 'tax_rates_settings_url' => admin_url( 'admin.php?page=wc-settings&tab=tax' ),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'search_products_nonce' => wp_create_nonce( 'search-products' ),
+ 'search_tax_rates_nonce' => wp_create_nonce( 'search-tax-rates' ),
'search_customers_nonce' => wp_create_nonce( 'search-customers' ),
'search_categories_nonce' => wp_create_nonce( 'search-categories' ),
'search_taxonomy_terms_nonce' => wp_create_nonce( 'search-taxonomy-terms' ),
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-items.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-items.php
index 13c41f439ea..e21000f3715 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-items.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-items.php
@@ -460,7 +460,7 @@ if ( wc_tax_enabled() ) {
</script>
<script type="text/template" id="tmpl-wc-modal-add-tax">
- <div class="wc-backbone-modal">
+ <div class="wc-backbone-modal wc-modal-add-tax">
<div class="wc-backbone-modal-content">
<section class="wc-backbone-modal-main" role="main">
<header class="wc-backbone-modal-header">
@@ -471,42 +471,86 @@ if ( wc_tax_enabled() ) {
</header>
<article>
<form action="" method="post">
- <table class="widefat">
+ <p class="search-box" data-wc-tax-rate-search-box>
+ <label class="screen-reader-text" for="add_order_tax_search"><?php esc_html_e( 'Search tax rates:', 'woocommerce' ); ?></label>
+ <span class="screen-reader-text" id="add_order_tax_search_description">
+ <?php echo esc_html_x( 'Search by name, code, class, rate, or ID.', 'tax rate search input help', 'woocommerce' ); ?>
+ </span>
+ <input
+ type="search"
+ id="add_order_tax_search"
+ name="s"
+ data-wc-tax-rate-search
+ aria-describedby="add_order_tax_search_description"
+ placeholder="<?php echo esc_attr_x( 'Search', 'tax rate search input placeholder', 'woocommerce' ); ?>"
+ />
+ <button type="submit" class="button"><?php esc_html_e( 'Search tax rates', 'woocommerce' ); ?></button>
+ <span class="description wc-tax-rate-search-summary" data-wc-tax-rate-search-summary hidden></span>
+ </p>
+ <br class="clear" />
+ <table class="widefat striped wc-tax-rate-results" data-wc-tax-rate-results data-per_page="10">
<thead>
<tr>
- <th> </th>
- <th><?php esc_html_e( 'Rate name', 'woocommerce' ); ?></th>
- <th><?php esc_html_e( 'Tax class', 'woocommerce' ); ?></th>
- <th><?php esc_html_e( 'Rate code', 'woocommerce' ); ?></th>
- <th><?php esc_html_e( 'Rate %', 'woocommerce' ); ?></th>
+ <th scope="col">
+ <span class="screen-reader-text"><?php esc_html_e( 'Select tax rate', 'woocommerce' ); ?></span>
+ <span aria-hidden="true"> </span>
+ </th>
+ <th scope="col"><?php esc_html_e( 'Rate name', 'woocommerce' ); ?></th>
+ <th scope="col"><?php esc_html_e( 'Tax class', 'woocommerce' ); ?></th>
+ <th scope="col"><?php esc_html_e( 'Rate code', 'woocommerce' ); ?></th>
+ <th scope="col"><?php esc_html_e( 'Rate %', 'woocommerce' ); ?></th>
</tr>
</thead>
- <?php
- $rates = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates ORDER BY tax_rate_name LIMIT 100" );
- foreach ( $rates as $rate ) {
- echo '
- <tr>
- <td><input type="radio" id="add_order_tax_' . absint( $rate->tax_rate_id ) . '" name="add_order_tax" value="' . absint( $rate->tax_rate_id ) . '" /></td>
- <td><label for="add_order_tax_' . absint( $rate->tax_rate_id ) . '">' . esc_html( WC_Tax::get_rate_label( $rate ) ) . '</label></td>
- <td>' . ( isset( $classes_options[ $rate->tax_rate_class ] ) ? esc_html( $classes_options[ $rate->tax_rate_class ] ) : '-' ) . '</td>
- <td>' . esc_html( WC_Tax::get_rate_code( $rate ) ) . '</td>
- <td>' . esc_html( WC_Tax::get_rate_percent( $rate ) ) . '</td>
- </tr>
- ';
- }
- ?>
+ <tbody>
+ <tr class="no-items">
+ <td class="colspanchange wc-tax-rate-status" colspan="5"><?php esc_html_e( 'Loading tax rates…', 'woocommerce' ); ?></td>
+ </tr>
+ </tbody>
</table>
- <?php if ( absint( $wpdb->get_var( "SELECT COUNT(tax_rate_id) FROM {$wpdb->prefix}woocommerce_tax_rates;" ) ) > 100 ) : ?>
- <p>
- <label for="manual_tax_rate_id"><?php esc_html_e( 'Or, enter tax rate ID:', 'woocommerce' ); ?></label><br/>
- <input type="number" name="manual_tax_rate_id" id="manual_tax_rate_id" step="1" placeholder="<?php esc_attr_e( 'Optional', 'woocommerce' ); ?>" />
- </p>
- <?php endif; ?>
+ <div class="tablenav bottom" data-wc-tax-rate-pagination-container>
+ <div class="tablenav-pages" data-wc-tax-rate-pagination data-page="1" data-total-pages="1">
+ <span class="displaying-num"></span>
+ <span class="pagination-links">
+ <button type="button" class="first-page button" disabled>
+ <span class="screen-reader-text"><?php esc_html_e( 'First page', 'woocommerce' ); ?></span>
+ <span aria-hidden="true">«</span>
+ </button>
+ <button type="button" class="prev-page button" disabled>
+ <span class="screen-reader-text"><?php esc_html_e( 'Previous page', 'woocommerce' ); ?></span>
+ <span aria-hidden="true">‹</span>
+ </button>
+ <span class="paging-input">
+ <label for="add_order_tax_current_page" class="screen-reader-text"><?php esc_html_e( 'Current page', 'woocommerce' ); ?></label>
+ <input class="current-page" id="add_order_tax_current_page" type="text" name="paged" value="1" size="1" aria-describedby="add-order-tax-table-paging" />
+ <span class="tablenav-paging-text" id="add-order-tax-table-paging">
+ <?php
+ echo wp_kses_post(
+ sprintf(
+ /* translators: %s: total number of pages. */
+ __( 'of %s', 'woocommerce' ),
+ '<span class="total-pages">1</span>'
+ )
+ );
+ ?>
+ </span>
+ </span>
+ <button type="button" class="next-page button" disabled>
+ <span class="screen-reader-text"><?php esc_html_e( 'Next page', 'woocommerce' ); ?></span>
+ <span aria-hidden="true">›</span>
+ </button>
+ <button type="button" class="last-page button" disabled>
+ <span class="screen-reader-text"><?php esc_html_e( 'Last page', 'woocommerce' ); ?></span>
+ <span aria-hidden="true">»</span>
+ </button>
+ </span>
+ </div>
+ <br class="clear" />
+ </div>
</form>
</article>
<footer>
<div class="wc-backbone-modal-buttons">
- <button id="btn-ok" class="button button-primary button-large"><?php esc_html_e( 'Add', 'woocommerce' ); ?></button>
+ <button id="btn-ok" class="button button-primary button-large" disabled><?php esc_html_e( 'Add', 'woocommerce' ); ?></button>
</div>
</footer>
</section>
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index cd8c085b96b..b76180fabfe 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -189,6 +189,7 @@ class WC_AJAX {
'add_order_note',
'delete_order_note',
'json_search_order_metakeys',
+ 'json_search_tax_rates',
'json_search_products',
'json_search_products_and_variations',
'json_search_downloadable_products_and_variations',
@@ -943,12 +944,14 @@ class WC_AJAX {
wp_die( -1 );
}
- global $post; // Set $post global so its available, like within the admin screens.
+ global $post;
+ // Set $post global so its available, like within the admin screens.
- $product_id = intval( $_POST['post_id'] );
+ $product_id = intval( $_POST['post_id'] );
$post = get_post( $product_id ); // phpcs:ignore
- $loop = intval( $_POST['loop'] );
- $product_object = wc_get_product_object( ProductType::VARIABLE, $product_id ); // Forces type to variable in case product is unsaved.
+ $loop = intval( $_POST['loop'] );
+ $product_object = wc_get_product_object( ProductType::VARIABLE, $product_id );
+ // Forces type to variable in case product is unsaved.
$variation_object = wc_get_product_object( ProductType::VARIATION );
$variation_object->set_parent_id( $product_id );
$variation_object->set_attributes( array_fill_keys( array_map( 'sanitize_title', array_keys( $product_object->get_variation_attributes() ) ), '' ) );
@@ -1271,7 +1274,8 @@ class WC_AJAX {
'notes_html' => $notes_html,
);
} catch ( Exception $e ) {
- throw $e; // Forward exception to caller.
+ throw $e;
+ // Forward exception to caller.
}
}
@@ -1839,6 +1843,201 @@ class WC_AJAX {
wp_die();
}
+ /**
+ * Search for tax rates and return json.
+ *
+ * @return void
+ */
+ public static function json_search_tax_rates() {
+ check_ajax_referer( 'search-tax-rates', 'security' );
+
+ if ( ! current_user_can( 'edit_shop_orders' ) ) {
+ wp_die( '-1' );
+ }
+
+ // phpcs:disable WordPress.Security.NonceVerification.Recommended
+ $term = '';
+ if ( isset( $_GET['term'] ) && is_scalar( $_GET['term'] ) ) {
+ $clean_term = wc_clean( wp_unslash( (string) $_GET['term'] ) );
+ $term = is_string( $clean_term ) ? $clean_term : '';
+ }
+
+ $page = ! empty( $_GET['page'] ) && is_scalar( $_GET['page'] ) ? absint( wp_unslash( (string) $_GET['page'] ) ) : 1;
+
+ /**
+ * Filters the number of results returned by the JSON search endpoints.
+ *
+ * @since 3.5.0
+ * @param int $limit Maximum number of results to return.
+ */
+ $default_per_page = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) );
+ $per_page = ! empty( $_GET['per_page'] ) && is_scalar( $_GET['per_page'] ) ? absint( wp_unslash( (string) $_GET['per_page'] ) ) : $default_per_page;
+ // phpcs:enable WordPress.Security.NonceVerification.Recommended
+
+ global $wpdb;
+
+ $page = max( 1, $page );
+ $per_page = max( 1, min( $per_page, 100 ) );
+ $tax_rates = $wpdb->prefix . 'woocommerce_tax_rates';
+ $tax_locations = $wpdb->prefix . 'woocommerce_tax_rate_locations';
+ $classes = wc_get_product_tax_class_options();
+ $found_tax_rates = array();
+ $join = '';
+ $where = '';
+ $distinct = '';
+ $count_select = 'COUNT(*)';
+
+ if ( '' !== $term ) {
+ // Locations only need joining when they are actually searched, and the join is
+ // what makes the DISTINCT necessary.
+ $join = "LEFT JOIN {$tax_locations} tax_locations ON tax_rates.tax_rate_id = tax_locations.tax_rate_id";
+ $distinct = 'DISTINCT ';
+ $count_select = 'COUNT(DISTINCT tax_rates.tax_rate_id)';
+ $conditions = array();
+ $like = '%' . $wpdb->esc_like( $term ) . '%';
+
+ $conditions[] = $wpdb->prepare(
+ 'CAST(tax_rates.tax_rate_id AS CHAR) LIKE %s
+ OR tax_rates.tax_rate_name LIKE %s
+ OR tax_rates.tax_rate_country LIKE %s
+ OR tax_rates.tax_rate_state LIKE %s
+ OR tax_rates.tax_rate_class LIKE %s
+ OR tax_locations.location_code LIKE %s',
+ $like,
+ $like,
+ $like,
+ $like,
+ $like,
+ $like
+ );
+
+ /*
+ * The rate column stores a bare decimal (8.0000) while the modal shows a formatted
+ * percentage (8%), so strip the percent sign and any spacing before matching it.
+ */
+ $rate_term = trim( str_replace( '%', '', $term ) );
+
+ if ( '' !== $rate_term ) {
+ $conditions[] = $wpdb->prepare(
+ 'tax_rates.tax_rate LIKE %s',
+ '%' . $wpdb->esc_like( $rate_term ) . '%'
+ );
+ }
+
+ /*
+ * The rate code is derived rather than stored, so rebuild it in SQL to keep
+ * WC_Tax::get_rate_code() searchable as it appears in the modal.
+ */
+ $conditions[] = $wpdb->prepare(
+ "UPPER(
+ CONCAT_WS(
+ '-',
+ NULLIF( tax_rates.tax_rate_country, '' ),
+ NULLIF( tax_rates.tax_rate_state, '' ),
+ COALESCE( NULLIF( tax_rates.tax_rate_name, '' ), 'TAX' ),
+ NULLIF( tax_rates.tax_rate_priority, 0 )
+ )
+ ) LIKE %s",
+ '%' . $wpdb->esc_like( wc_strtoupper( $term ) ) . '%'
+ );
+
+ /*
+ * Unnamed rates fall back to the store's tax or VAT label in the results table,
+ * so searching for that label needs to match them too.
+ */
+ if ( false !== stripos( WC()->countries->tax_or_vat(), $term ) ) {
+ $conditions[] = "tax_rates.tax_rate_name = ''";
+ }
+
+ $matching_classes = array();
+ $normalized_search = sanitize_title( $term );
+
+ foreach ( $classes as $class_slug => $class_label ) {
+ if (
+ false !== stripos( $class_label, $term )
+ || (
+ '' !== $normalized_search
+ && '' !== $class_slug
+ && false !== stripos( $class_slug, $normalized_search )
+ )
+ ) {
+ $matching_classes[] = $class_slug;
+ }
+ }
+
+ if ( $matching_classes ) {
+ $conditions[] = $wpdb->prepare(
+ 'tax_rates.tax_rate_class IN ( ' . implode( ', ', array_fill( 0, count( $matching_classes ), '%s' ) ) . ' )',
+ $matching_classes
+ );
+ }
+
+ $where = 'WHERE ( ' . implode( ' OR ', $conditions ) . ' )';
+ }
+
+ // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
+ $total = absint(
+ $wpdb->get_var(
+ "
+ SELECT {$count_select}
+ FROM {$tax_rates} tax_rates
+ {$join}
+ {$where}
+ "
+ )
+ );
+ $total_pages = $total ? (int) ceil( $total / $per_page ) : 1;
+ $page = min( $page, $total_pages );
+ $offset = ( $page - 1 ) * $per_page;
+
+ $rates = $wpdb->get_results(
+ $wpdb->prepare(
+ "
+ SELECT {$distinct}tax_rates.*
+ FROM {$tax_rates} tax_rates
+ {$join}
+ {$where}
+ ORDER BY tax_rates.tax_rate_name, tax_rates.tax_rate_id
+ LIMIT %d OFFSET %d
+ ",
+ $per_page,
+ $offset
+ )
+ );
+ // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
+
+ foreach ( $rates as $rate ) {
+ $tax_class = isset( $classes[ $rate->tax_rate_class ] ) ? $classes[ $rate->tax_rate_class ] : __( 'Tax', 'woocommerce' );
+
+ $found_tax_rates[] = array(
+ 'id' => absint( $rate->tax_rate_id ),
+ 'label' => wp_strip_all_tags( WC_Tax::get_rate_label( $rate ) ),
+ 'tax_class' => wp_strip_all_tags( $tax_class ),
+ 'rate_code' => wp_strip_all_tags( WC_Tax::get_rate_code( $rate ) ),
+ 'rate_percent' => wp_strip_all_tags( WC_Tax::get_rate_percent( $rate ) ),
+ );
+ }
+
+ wp_send_json(
+ array(
+ 'results' => $found_tax_rates,
+ 'pagination' => array(
+ 'page' => $page,
+ 'per_page' => $per_page,
+ 'total' => $total,
+ 'total_pages' => $total_pages,
+ 'has_prev' => $page > 1,
+ 'has_next' => $page < $total_pages,
+ 'displaying_num' => sprintf(
+ /* translators: %s: number of tax rates. */
+ _n( '%s item', '%s items', $total, 'woocommerce' ),
+ number_format_i18n( $total )
+ ),
+ ),
+ )
+ );
+ }
+
/**
* Search for products and echo json.
*
@@ -1861,6 +2060,12 @@ class WC_AJAX {
if ( ! empty( $_GET['limit'] ) ) {
$limit = absint( $_GET['limit'] );
} else {
+ /**
+ * Filters the number of results returned by the JSON search endpoints.
+ *
+ * @since 3.5.0
+ * @param int $limit Maximum number of results to return.
+ */
$limit = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) );
}
@@ -1962,6 +2167,12 @@ class WC_AJAX {
if ( ! empty( $_GET['limit'] ) ) {
$limit = absint( $_GET['limit'] );
} else {
+ /**
+ * Filters the number of results returned by the JSON search endpoints.
+ *
+ * @since 3.5.0
+ * @param int $limit Maximum number of results to return.
+ */
$limit = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) );
}
@@ -1998,9 +2209,10 @@ class WC_AJAX {
wp_die( -1 );
}
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ // phpcs:disable WordPress.Security.NonceVerification.Recommended
$term = isset( $_GET['term'] ) ? (string) wc_clean( wp_unslash( $_GET['term'] ) ) : '';
$limit = 0;
+ // phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( empty( $term ) ) {
wp_die();
@@ -2035,11 +2247,11 @@ class WC_AJAX {
$found_customers = array();
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ // phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['exclude'] ) ) {
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$ids = array_diff( $ids, array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) );
}
+ // phpcs:enable WordPress.Security.NonceVerification.Recommended
foreach ( $ids as $id ) {
$customer = new WC_Customer( $id );
@@ -4405,7 +4617,8 @@ class WC_AJAX {
private static function render_variation_html( WC_Product $product_object, WC_Product $variation_object, $loop, ?float $base_cost ) {
$variation_id = $variation_object->get_id();
$variation = get_post( $variation_id );
- $variation_data = array_merge( get_post_custom( $variation_id ), wc_get_product_variation_attributes( $variation_id ) ); // kept for BW compatibility.
+ $variation_data = array_merge( get_post_custom( $variation_id ), wc_get_product_variation_attributes( $variation_id ) );
+ // kept for BW compatibility.
include __DIR__ . '/admin/meta-boxes/views/html-variation-admin.php';
}
// phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
index 2c86d66e0bb..2cf59e79be7 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
@@ -398,6 +398,327 @@ class WC_AJAX_Test extends \WP_Ajax_UnitTestCase {
$this->assertEquals( 108, $order->get_total() );
}
+ /**
+ * @testdox Should paginate tax rate search results and find rates by location code.
+ */
+ public function test_json_search_tax_rates_supports_pagination_and_location_search(): void {
+ global $wpdb;
+
+ $this->_setRole( 'administrator' );
+
+ $first_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'US',
+ 'tax_rate_state' => 'CA',
+ 'tax_rate' => '7.2500',
+ 'tax_rate_name' => 'Pagination fixture California rate',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_compound' => 0,
+ 'tax_rate_shipping' => 1,
+ 'tax_rate_order' => 1,
+ 'tax_rate_class' => '',
+ )
+ );
+ WC_Tax::_update_tax_rate_postcodes( $first_rate_id, '90001' );
+
+ $second_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'US',
+ 'tax_rate_state' => 'NY',
+ 'tax_rate' => '8.8750',
+ 'tax_rate_name' => 'Pagination fixture New York rate',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_compound' => 0,
+ 'tax_rate_shipping' => 1,
+ 'tax_rate_order' => 2,
+ 'tax_rate_class' => '',
+ )
+ );
+ WC_Tax::_update_tax_rate_postcodes( $second_rate_id, '10001' );
+
+ try {
+ $_GET['security'] = wp_create_nonce( 'search-tax-rates' );
+ $_GET['term'] = 'Pagination fixture';
+ $_GET['page'] = 1;
+ $_GET['per_page'] = 1;
+
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+
+ $this->assertSame( 1, $response['pagination']['page'] );
+ $this->assertSame( 1, $response['pagination']['per_page'] );
+ $this->assertSame( 2, $response['pagination']['total'] );
+ $this->assertSame( 2, $response['pagination']['total_pages'] );
+ $this->assertFalse( $response['pagination']['has_prev'] );
+ $this->assertTrue( $response['pagination']['has_next'] );
+ $this->assertCount( 1, $response['results'] );
+ $this->assertSame( $first_rate_id, $response['results'][0]['id'] );
+
+ $_GET['page'] = 2;
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+
+ $this->assertSame( 2, $response['pagination']['page'] );
+ $this->assertFalse( $response['pagination']['has_next'] );
+ $this->assertTrue( $response['pagination']['has_prev'] );
+ $this->assertCount( 1, $response['results'] );
+ $this->assertSame( $second_rate_id, $response['results'][0]['id'] );
+
+ $_GET['page'] = 99;
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+
+ $this->assertSame( 2, $response['pagination']['page'] );
+ $this->assertCount( 1, $response['results'] );
+ $this->assertSame( $second_rate_id, $response['results'][0]['id'] );
+
+ $_GET['term'] = '10001';
+ $_GET['page'] = 1;
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+
+ $this->assertSame( 1, $response['pagination']['page'] );
+ $this->assertSame( 1, $response['pagination']['per_page'] );
+ $this->assertSame( 1, $response['pagination']['total'] );
+ $this->assertFalse( $response['pagination']['has_next'] );
+ $this->assertCount( 1, $response['results'] );
+ $this->assertSame( $second_rate_id, $response['results'][0]['id'] );
+ $this->assertSame( 'Pagination fixture New York rate', $response['results'][0]['label'] );
+ $this->assertSame( 'US-NY-PAGINATION FIXTURE NEW YORK RATE-1', $response['results'][0]['rate_code'] );
+ $this->assertSame( '8.875%', $response['results'][0]['rate_percent'] );
+ } finally {
+ unset( $_GET['security'], $_GET['term'], $_GET['page'], $_GET['per_page'] );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rate_locations', array( 'tax_rate_id' => $first_rate_id ) );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rate_locations', array( 'tax_rate_id' => $second_rate_id ) );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_id' => $first_rate_id ) );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_id' => $second_rate_id ) );
+ wp_set_current_user( 0 );
+ }
+ }
+
+ /**
+ * @testdox Should find tax rates by their visible tax class labels.
+ */
+ public function test_json_search_tax_rates_supports_tax_class_label_search(): void {
+ global $wpdb;
+
+ $this->_setRole( 'administrator' );
+
+ $tax_class = WC_Tax::create_tax_class( 'Reduced rate', 'reduced-rate' );
+ $created_tax_class_slug = is_wp_error( $tax_class ) ? null : $tax_class['slug'];
+
+ $standard_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'US',
+ 'tax_rate_state' => 'CA',
+ 'tax_rate' => '7.2500',
+ 'tax_rate_name' => 'California base rate',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_compound' => 0,
+ 'tax_rate_shipping' => 1,
+ 'tax_rate_order' => 1,
+ 'tax_rate_class' => '',
+ )
+ );
+
+ $reduced_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'US',
+ 'tax_rate_state' => 'NY',
+ 'tax_rate' => '4.0000',
+ 'tax_rate_name' => 'Reduced class rate',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_compound' => 0,
+ 'tax_rate_shipping' => 1,
+ 'tax_rate_order' => 2,
+ 'tax_rate_class' => 'reduced-rate',
+ )
+ );
+
+ try {
+ $_GET['security'] = wp_create_nonce( 'search-tax-rates' );
+ $_GET['page'] = 1;
+ $_GET['per_page'] = 100;
+
+ $_GET['term'] = 'Standard';
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+ $rate_ids = array_column( $response['results'], 'id' );
+
+ $this->assertContains( $standard_rate_id, $rate_ids );
+ $standard_results = array_filter(
+ $response['results'],
+ function ( $result ) use ( $standard_rate_id ) {
+ return $standard_rate_id === $result['id'];
+ }
+ );
+ $standard_result = current( $standard_results );
+ $this->assertIsArray( $standard_result );
+ $this->assertSame( 'Standard', $standard_result['tax_class'] );
+
+ $_GET['term'] = 'Reduced rate';
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+ $rate_ids = array_column( $response['results'], 'id' );
+
+ $this->assertContains( $reduced_rate_id, $rate_ids );
+ $reduced_results = array_filter(
+ $response['results'],
+ function ( $result ) use ( $reduced_rate_id ) {
+ return $reduced_rate_id === $result['id'];
+ }
+ );
+ $reduced_result = current( $reduced_results );
+ $this->assertIsArray( $reduced_result );
+ $this->assertSame( 'Reduced rate', $reduced_result['tax_class'] );
+ } finally {
+ unset( $_GET['security'], $_GET['term'], $_GET['page'], $_GET['per_page'] );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_id' => $standard_rate_id ) );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_id' => $reduced_rate_id ) );
+ if ( $created_tax_class_slug ) {
+ WC_Tax::delete_tax_class_by( 'slug', $created_tax_class_slug );
+ }
+ wp_set_current_user( 0 );
+ }
+ }
+
+ /**
+ * @testdox Should find tax rates by displayed percentages, rate codes, and fallback labels.
+ */
+ public function test_json_search_tax_rates_supports_displayed_value_search(): void {
+ global $wpdb;
+
+ $this->_setRole( 'administrator' );
+
+ $named_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'US',
+ 'tax_rate_state' => 'NY',
+ 'tax_rate' => '8.8750',
+ 'tax_rate_name' => 'Displayed value fixture rate',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_compound' => 0,
+ 'tax_rate_shipping' => 1,
+ 'tax_rate_order' => 1,
+ 'tax_rate_class' => '',
+ )
+ );
+
+ $unnamed_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'ZZ',
+ 'tax_rate_state' => 'ZZ',
+ 'tax_rate' => '3.5000',
+ 'tax_rate_name' => '',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_compound' => 0,
+ 'tax_rate_shipping' => 1,
+ 'tax_rate_order' => 2,
+ 'tax_rate_class' => '',
+ )
+ );
+
+ try {
+ $_GET['security'] = wp_create_nonce( 'search-tax-rates' );
+ $_GET['page'] = 1;
+ $_GET['per_page'] = 100;
+
+ // The results table shows "8.875%", so that string has to find the rate.
+ $_GET['term'] = '8.875%';
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+
+ $this->assertContains( $named_rate_id, array_column( $response['results'], 'id' ) );
+
+ // The full rate code is derived from several columns and must be searchable as shown.
+ $_GET['term'] = 'US-NY-DISPLAYED VALUE FIXTURE RATE-1';
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+
+ $this->assertSame( 1, $response['pagination']['total'] );
+ $this->assertSame( $named_rate_id, $response['results'][0]['id'] );
+
+ $_GET['term'] = 'us-ny-displayed value';
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+
+ $this->assertContains( $named_rate_id, array_column( $response['results'], 'id' ) );
+
+ // Rates without a name are shown under the store's tax or VAT label.
+ $_GET['term'] = WC()->countries->tax_or_vat();
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+
+ $this->assertContains( $unnamed_rate_id, array_column( $response['results'], 'id' ) );
+
+ $_GET['term'] = 'ZZ-ZZ-TAX-1';
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+
+ $this->assertSame( 1, $response['pagination']['total'] );
+ $this->assertSame( $unnamed_rate_id, $response['results'][0]['id'] );
+ $this->assertSame( 'ZZ-ZZ-TAX-1', $response['results'][0]['rate_code'] );
+ } finally {
+ unset( $_GET['security'], $_GET['term'], $_GET['page'], $_GET['per_page'] );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_id' => $named_rate_id ) );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_id' => $unnamed_rate_id ) );
+ wp_set_current_user( 0 );
+ }
+ }
+
+ /**
+ * @testdox Should return each tax rate once when listing results without a search term.
+ */
+ public function test_json_search_tax_rates_without_a_term_lists_all_rates(): void {
+ global $wpdb;
+
+ $this->_setRole( 'administrator' );
+
+ $first_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'US',
+ 'tax_rate_state' => 'CA',
+ 'tax_rate' => '7.2500',
+ 'tax_rate_name' => 'Unfiltered listing fixture one',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_compound' => 0,
+ 'tax_rate_shipping' => 1,
+ 'tax_rate_order' => 1,
+ 'tax_rate_class' => '',
+ )
+ );
+ WC_Tax::_update_tax_rate_postcodes( $first_rate_id, '90001' );
+
+ $second_rate_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => 'US',
+ 'tax_rate_state' => 'NY',
+ 'tax_rate' => '8.8750',
+ 'tax_rate_name' => 'Unfiltered listing fixture two',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_compound' => 0,
+ 'tax_rate_shipping' => 1,
+ 'tax_rate_order' => 2,
+ 'tax_rate_class' => '',
+ )
+ );
+ WC_Tax::_update_tax_rate_postcodes( $second_rate_id, '10001,10002,10003' );
+
+ $expected_total = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_tax_rates" ) );
+
+ try {
+ $_GET['security'] = wp_create_nonce( 'search-tax-rates' );
+ $_GET['term'] = '';
+ $_GET['page'] = 1;
+ $_GET['per_page'] = 100;
+
+ $response = $this->do_ajax( 'woocommerce_json_search_tax_rates' );
+ $rate_ids = array_column( $response['results'], 'id' );
+
+ // A rate with several postcodes must still be counted once.
+ $this->assertSame( $expected_total, $response['pagination']['total'] );
+ $this->assertContains( $first_rate_id, $rate_ids );
+ $this->assertContains( $second_rate_id, $rate_ids );
+ $this->assertSame( count( $rate_ids ), count( array_unique( $rate_ids ) ) );
+ } finally {
+ unset( $_GET['security'], $_GET['term'], $_GET['page'], $_GET['per_page'] );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rate_locations', array( 'tax_rate_id' => $first_rate_id ) );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rate_locations', array( 'tax_rate_id' => $second_rate_id ) );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_id' => $first_rate_id ) );
+ $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_id' => $second_rate_id ) );
+ wp_set_current_user( 0 );
+ }
+ }
+
/**
* @testdox Applying a coupon in the order editor calculates the discount from a manually edited line total.
*/