Update 2: It's much more complicated to make it work as it requires also Ajax and much more additional code…
So the following will allow to change cart items tax class on checkout page, depending on:
- (optional) The chosen payment gateway (Here it's disabled with an empty array).
- The chosen radio button value (from custom radio buttons).
As people don't use your WooCommerce checkout Add-ons commercial plugin, the code below displays some radio buttons on checkout page.
To make the code more dynamic, we start with a custom function that will handle all required settings:
// Custom function that handle your settings
function change_tax_class_settings(){
return array(
'payment_ids' => array(), // (optional) Your targeted payment method Id(s) | Leave an empty array to disable.
'tax_class' => 'Reduced rate', // The desired tax rate
'field_id' => 'additonal_services', // the Field Id (from property name ="?????")
'field_value' => 'no-dinstallation', // The field targetted option key (value)
// The below lines are optional (used for the radio buttons field display)
'field_type' => 'radio', // Field type
'field_title' => __('Additional services', 'woocommerce'),
'field_default' => 'basic-installation', // The field targetted option key (value)
'field_options' => array(
'basic-installation' => __('Basic Installation', 'woocommerce'),
'premium-installation' => __('Premium Installation', 'woocommerce'),
'no-dinstallation' => __('No Installation', 'woocommerce'),
),
);
}
Now we can load that settings on any function where it's required.
Then the radio buttons displayed before payment methods checkout section:
// Display radio buttons field (optional)
add_action( 'woocommerce_review_order_before_payment', 'installation_custom_radio_field' );
function installation_custom_radio_field() {
extract( change_tax_class_settings() ); // Load settings and convert them in variables
echo "<style>.$field_id-wrapper{padding:1em 1.41575em;background-color:#f5f5f5;margin-bottom:24px;}
.form-row.$field_id-$field_type span label{display:inline-block;margin:0 18px 0 6px;}
.$field_id-wrapper h3{font-weight:bold;}</style>";
echo '<div class="'.$field_id.'-wrapper">
<h3>'.$field_title.'</h3>';
// Get WC Session variable value
$value = WC()->session->get($field_id);
woocommerce_form_field( $field_id, array(
'type' => $field_type,
'label' => '',
'class' => array('form-row-wide ' . $field_id . '-' . $field_type ),
'options' => $field_options,
'default' => $field_default,
'required' => true,
), empty($value) ? WC()->checkout->get_value('_'.$field_id) : $value );
echo '</div>';
}

The Ajax Part (jQuery Ajax and PHP Admin Wordpress Ajax sender and receiver + WC Session variable):
// jQuery code (client side) - Ajax sender
add_action('wp_footer', 'installation_checkout_js_script');
function installation_checkout_js_script() {
if( is_checkout() && ! is_wc_endpoint_url() ) :
// Load settings and convert them in variables
extract( change_tax_class_settings() );
// jQuery Ajax code
?>
<script type="text/javascript">
jQuery( function($){
if (typeof wc_checkout_params === 'undefined')
return false;
var field = '#<?php echo $field_id; ?>_field input', fchecked = field+':checked';
// Function that sen the Ajax request
function sendAjaxRequest( value ) {
$.ajax({
type: 'POST',
url: wc_checkout_params.ajax_url,
data: {
'action': '<?php echo $field_id; ?>',
'value': value
},
success: function (result) {
$(document.body).trigger('update_checkout'); // Refresh checkout
}
});
}
// On ready (DOM loaded)
sendAjaxRequest( $(fchecked).val() );
// On change event
$(document.body).on( 'change', field, function(){
sendAjaxRequest( $(fchecked).val() );
});
// Refresh checkout on payment method change
$( 'form.checkout' ).on('change', 'input[name="payment_method"]', function() {
$(document.body).trigger('update_checkout'); // Refresh checkout
});
});
</script>
<?php
endif;
}
// The Wordpress Ajax PHP receiver
add_action( 'wp_ajax_additonal_services', 'get_additonal_services' );
add_action( 'wp_ajax_nopriv_additonal_services', 'get_additonal_services' );
function get_additonal_services() {
if ( isset($_POST['value']) ){
// Load settings and convert them in variables
extract( change_tax_class_settings() );
// Update session variable
WC()->session->set($field_id, esc_attr($_POST['value']));
// Send back the data to javascript (json encoded)
echo $_POST['value']; // optional
die();
}
}
Then the function that change cart item tax class conditionally depending on customer choices:
// Change the tax class conditionally
add_action( 'woocommerce_before_calculate_totals', 'change_tax_class_conditionally', 1000 );
function change_tax_class_conditionally( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) )
return;
if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 )
return;
extract( change_tax_class_settings() ); // Load settings and convert them in variables
// Only for a specific defined payment methods (can be disabled in the settings, with an empty array)
if ( ! empty($payment_ids) && ! in_array( WC()->session->get('chosen_payment_method'), $payment_ids ) )
return;
$choice = WC()->session->get($field_id);
// Loop through cart items
foreach( $cart->get_cart() as $cart_item ){
if( $choice === $field_value ) {
$cart_item['data']->set_tax_class($tax_class);
}
}
}
Addition: Saving the customer choice to the order and displaying that everywhere on orders in front end, admin and on email notifications:
// Save custom field as order meta data
add_action( 'woocommerce_checkout_create_order', 'save_additonal_services_as_order_meta' );
function save_additonal_services_as_order_meta( $order ) {
// Load settings and convert them in variables
extract( change_tax_class_settings() );
$choice = WC()->session->get($field_id);
if( ! empty( $choice ) ) {
$order->update_meta_data( '_'.$field_id, $choice );
}
}
// Display additonal services choice before payment method everywhere (orders and emails)
add_filter( 'woocommerce_get_order_item_totals', 'display_additonal_services_on_order_item_totals', 1000, 3 );
function display_additonal_services_on_order_item_totals( $total_rows, $order, $tax_display ){
// Load settings and convert them in variables
extract( change_tax_class_settings() );
$choice = $order->get_meta( '_'.$field_id ); // Get additonal services choice
if( ! empty($choice) ) {
$new_total_rows = [];
// Loop through order total rows
foreach( $total_rows as $key => $values ) {
// Inserting the pickp store under shipping method
if( $key === 'payment_method' ) {
$new_total_rows[$field_id] = array(
'label' => $field_title,
'value' => esc_html($field_options[$choice]),
);
}
$new_total_rows[$key] = $values;
}
return $new_total_rows;
}
return $total_rows;
}
// Display additonal services choice in Admin order pages
add_action( 'woocommerce_admin_order_data_after_billing_address', 'admin_order_display_additonal_services', 1000 );
function admin_order_display_additonal_services( $order ) {
// Load settings and convert them in variables
extract( change_tax_class_settings() );
$choice = $order->get_meta( '_'.$field_id ); // Get additonal services choice
if( ! empty($choice) ) {
// Display
echo '<p><strong>' . $field_title . '</strong>: ' . $field_options[$choice] . '</p>';
}
}
All code goes on functions.php file of your active child theme (or theme). Tested and works.
Displayed choice on Orders and Email notifications (here on Order received page)

On Admin Single Orders pages:
