1

Good morning everybody. I need to implement a method to subtract shipping costs from the total in case the shipping destination is included in a specific array of values. This is not a case of free shipping, because this costs will be added later for other reasons.

I cannot base my decision on the user country, for two reasons:

  1. user can be not registered
  2. user country and shipping country can be different.

I see that WooCommerce reload the order totals when I change billing/shipping country. I believe I need to intercept this kind of change an trigger an action to insert a new cart fee (a negative one, of course).

Well, how can I do that?

This is a part of my code

function delayShippingCosts(){
  global $woocommerce;
  $EUcountries = ['IT','AT','BE','BG','CY','HR','DK','EE','FI','FR','DE','GR','IE','LV','LT','LU','MT','NE','PL','PT','CZ','RO','SK','SI','ES','SE','HU'];
  return in_array( $woocommerce->customer->get_country() , $EUcountries);
}

add_action( 'woocommerce_cart_calculate_fees', 'scc_detract_shipping_costs' );
function scc_detract_shipping_costs(){
  global $woocommerce;
  
  if(delayShippingCosts()){

    $shippingCosts = WC()->cart->get_shipping_total() * -1;
    if(current_user_can('administrator')) {
       $woocommerce->cart->add_fee( 'Delayed shipping costs', $shippingCosts, true, 'standard' );
    }
  }

}

The problem is that now I'm looking to my customer data, and these are not dynamic (and void for unregisterd / unlogged users).

Any suggestions? Thanks!!

EDIT

Almost OK

I managed to retrieve the shipping country from "woocommerce_checkout_update_order_review" hook, like that:

function action_woocommerce_checkout_update_order_review($posted_data) {
  global $shipTo;
 
  $data = array();
  $vars = explode('&', $posted_data);
  foreach ($vars as $k => $value){
    $v = explode('=', urldecode($value));
    $data[$v[0]] = $v[1];
  }

  WC()->cart->calculate_shipping();
  $shipTo = $data['shipping_country'] ? $data['shipping_country'] : $data['billing_country'];

 // REMOVE ALL NOTICES, IF PRESENTS...
 wc_clear_notices();

}
add_action('woocommerce_checkout_update_order_review', 'action_woocommerce_checkout_update_order_review', 10, 1);


add_action( 'woocommerce_cart_calculate_fees', 'scc_detract_shipping_costs' );
function scc_detract_shipping_costs(){
   global $woocommerce;
   ... something ...
   if(condition) {
     wc_add_notice("info message", "error");
   }
}

My problem is that the notice is not removed when "condition" in false. I tried to call wc_remove_notices() both in woocommerce_cart_calculate_fees and woocommerce_checkout_update_order_review. No big difference! :(

Any hint?

aynber
  • 22,380
  • 8
  • 50
  • 63
Simone Conti
  • 349
  • 1
  • 17

1 Answers1

1

The delayShippingCosts() function is not needed. You can get the list of European countries via the get_european_union_countries method of the WC_Countries class (unless you want to customize the list).

Also you are getting the country with the get_country() method of the WC_Customer class.

The WC_Customer::get_country function is deprecated since version 3.0.

You can get the country like this:

  • WC()->customer->get_shipping_country() the country of the shipping address
  • WC()->customer->get_billing_country() the country of the billing address

Finally, note that to apply the standard tax class for the fee you have to set the 4th parameter as an empty string instead of 'standard'. See here for more information.


TO GET THE COUNTRY FIELD FROM A USER NOT LOGGED IN WITH THE woocommerce_cart_calculate_fees HOOK

You can send an AJAX call to send the billing and shipping country value when the respective fields change in the checkout.

To make sure that the AJAX function is executed before the woocommerce_cart_calculate_fees hook it is necessary to remove the update_totals_on_change class from the billing and shipping country fields (to avoid the AJAX call being made to update the checkout) and update the checkout only after the call AJAX has been completed.

This method may take a few extra milliseconds/second to update the checkout because you have to wait for the AJAX call to create the option to complete.

See this answer for more details on how to submit an AJAX call in Wordpress.

Add the following code inside your active theme's functions.php:

// enqueue the script for the AJAX call
add_action('wp_enqueue_scripts', 'add_js_scripts'); 
function add_js_scripts(){
   wp_enqueue_script( 'ajax-script', get_stylesheet_directory_uri().'/js/script.js', array('jquery'), '1.0', true );
   wp_localize_script( 'ajax-script', 'ajax_object', array( 'ajaxurl' =>   admin_url( 'admin-ajax.php' ) ) );
}

// update options with checkout country values
add_action( 'wp_ajax_nopriv_set_option_country', 'set_option_country' );
add_action( 'wp_ajax_set_option_country', 'set_option_country' );
function set_option_country() {

   if ( isset( $_POST ) ) {
      // get the countries valued in the checkout by the user (guest or logged in)
      $countries = $_POST['countries'];
      $billing_country = $countries['billing_country'];
      $shipping_country = $countries['shipping_country'];

      // update options
      update_option( 'guest_billing_country', $billing_country );
      update_option( 'guest_shipping_country', $shipping_country );

      // returns the output as a response to the AJAX call
      echo 'success';

   }

   // always die in functions echoing AJAX content
   die();

}

Create a script.js file and add it inside your child theme (because I used get_stylesheet_directory_uri() instead of get_template_directory_uri()) in the directory: /child-theme/js/script.js:

jQuery(function($){

    // disable AJAX update
    $('#billing_country_field').removeClass('update_totals_on_change');
    $('#shipping_country_field').removeClass('update_totals_on_change');

    // when the country fields change
    $('#billing_country, #shipping_country').change(function(){
        var countries = {
            'billing_country': $('#billing_country').val(),
            'shipping_country': $('#shipping_country').val(),
        };
        $.ajax({
            url: ajax_object.ajaxurl,
            type : 'post',
            data: {
                'action': 'set_option_country',
                'countries': countries
            },
            complete: function(){
                // update checkout via AJAX
                $(document.body).trigger('update_checkout');
            },
            success:function(data) {
                console.log(data);
            },
            error: function(errorThrown){
                console.log(errorThrown);
            }
        });  
    });
});

The code has been tested and works.

So, the correct scc_detract_shipping_costs function will be:

add_action( 'woocommerce_cart_calculate_fees', 'scc_detract_shipping_costs' );
function scc_detract_shipping_costs(){

   $countries = new WC_Countries();
   // get the list of countries of the european union
   $eu_countries = $countries->get_european_union_countries();

   // get countries from checkout
   $billing_country = get_option( 'guest_billing_country' );
   $shipping_country = get_option( 'guest_shipping_country' );

   // if the shipping country is part of the European Union
   if ( in_array( $shipping_country, $eu_countries ) ) {
      $shippingCosts = WC()->cart->get_shipping_total() * -1;
      if ( current_user_can('administrator') ) {
         WC()->cart->add_fee( 'Delayed shipping costs', $shippingCosts, true, '' );
      }
   }
}

The code has been tested and works. Add it to your active theme's functions.php.


TO GET THE COUNTRY FIELD FROM A USER NOT LOGGED IN WITH THE woocommerce_calculate_totals HOOK

add_action( 'woocommerce_calculate_totals', 'get_post_checkout_data' );
function get_post_checkout_data( $cart ) {

   // get post data
   if ( isset( $_POST['post_data'] ) ) {
      parse_str( $_POST['post_data'], $post_data );
   } else {
      $post_data = $_POST;
   }
   
   if ( ! empty( $post_data ) ) {
      $billing_country  = $post_data['billing_country'];
      $shipping_country = $post_data['shipping_country'];
      // ...
   }

}

Add the code in your active theme's functions.php.

Vincenzo Di Gaetano
  • 3,892
  • 3
  • 13
  • 32
  • Good evening @Vincenzo Di Gaetano. I will try your code as soon as possible, but I have a doubt about the information retrieved bt WC()->customer->get_shipping_country(). This is ok only for registered users, otherwise there is no customer object (in my opinion). In add, this code does not take care of the possibility that the user (maybe from Italy) could change the destination to another country (maybe Finland), I set the global variable $shipTo in order to keep track of this eventual change, while totals are updated via Ajax. – Simone Conti Apr 28 '21 at 13:01
  • @SimoneConti I have updated my answer. As a solution I thought of making an AJAX call to get the country fields always in real time and then update the checkout *(so that the values can be used with the `woocommerce_cart_calculate_fees` hook)*. **I have tested it and it works fine.** – Vincenzo Di Gaetano Apr 28 '21 at 21:38
  • @SimoneConti Yes, `WC()->customer` gets the data only when the customer is created. Alternatively you could use `WC()->session` but the values are not always reliable. – Vincenzo Di Gaetano Apr 28 '21 at 21:40
  • I'll test it asap. The code looks good to me now, but I canno test it immediately due to a small server problem. Thanks! – Simone Conti Apr 29 '21 at 13:03
  • 1
    Your code works fine! I made some little adjustments to fit my needs but it ok! Thanks again! – Simone Conti Apr 30 '21 at 16:25