phpajaxwordpresswoocommercehook-woocommerce

Woocommerce apply discount to cart's total price with ajax


My Goal : Apply a 10% discount on total cart's price using AJAX.

The context : This discount must be applied only for specific users coming from a given website. The idea is to get their membership number in an input field I added in cart's page. With an API I will check this number and apply the discount or not (considering the issue explained further, I won't detail this checking part here and focus only with the problem).

The idea :

  1. Create a plugin which will add an input field in cart's page with a validation button.
  2. Make an AJAX call when the button is clicked
  3. Apply discount if the user membership number is good (I won't deal with API checking here since it's not the problem)

The issue : So I'm using AJAX to validate this number and then apply a 10% discount on the total cart's price. The problem is I can't find how to update the total.

My JS:

(function($) {
    $("body").on("click", ".updateCart", function(e) {
        e.preventDefault();
        var form = $('#cartAjaxTcs');
        var value = form.serialize();
        $.ajax({
            type:'POST',
            data: {
                action: 'test',
                number: value
            },
            url: ajaxurl,
            success: function(value) {
                jQuery("[name='update_cart']").removeAttr('disabled');
                jQuery("[name='update_cart']").trigger("click");
                console.log(value);
            },
            error:function(){
                console.log('error');
            }
        });
    });
})( jQuery );

My PHP:


<?php

/*
  Plugin Name: discount Plugin
  Description: Apply 10% discount on cart's price
  Author: Aurélien
  Version: 1.0.0
 */

/**
* Check if WooCommerce is active
**/
if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {

    class Discount_Plugin{

        public static function init() {

            add_action('wp_enqueue_scripts',  __CLASS__ .'::callback_for_setting_up_scripts');

            //Add input field
            add_action('woocommerce_cart_collaterals', __CLASS__.'::order_comments_custom_cart_field');

        }

        public static function callback_for_setting_up_scripts() {
            $path = '/wp-content/plugins/discount-plugin/assets/css/style.css';
            wp_register_style( 'discountStyle', $path );
            wp_enqueue_style( 'discountStyle' );
            wp_enqueue_script( 'ajax-script', plugins_url( 'assets/js/discount-plugin.js', __FILE__ ), array('jquery'), '1.0', true );
            wp_localize_script( 'ajax-script', 'ajaxurl', admin_url( 'admin-ajax.php' ) );
        }

        public static function order_comments_custom_cart_field() {
            ?>
            <form id="cartAjaxTcs" class="discount_input_wrapper" method="post" action="/wp-admin/admin-post.php">
                <div class="discount_form_field">
                    <input type="text" name="number" id="number" placeholder="<?php echo 'My number, ie : 4GG524G42';?>">
                    <label for="number"><?php echo 'Membership number';?></label>
                </div>
                <button class="updateCart" type="submit"><?php echo 'Update cart';?></button>
            </form>
            <?php

        }

 

    Discount_Plugin::init();
    add_action( 'wp_ajax_test', 'test' );
    add_action( 'wp_ajax_nopriv_test', 'test' );

    function test()
    {
      //Solution 1 or solution 2
    }

}

My 1st solution : I tried to directly reduce total price like this. It changes the total price but not display it, even in backend cart's report where the total price is still the same than before discount.

function test()
{
    //This is displayed in console
    echo 'test-init';

    if (!DOING_AJAX){
        return;
    }

    $total = (int) WC()->cart->get_totals()['total']; 
    $total *= 0.9;
    WC()->cart->set_total($total);

    wp_die();
}


My 2nd solution: I tried to trigger the "update cart" button (already present on the page) and use woocommerce_before_calculate_totals hook to update the price but it is not even called.

Obviously, no hook or filter are exectuted inside my AJAX's action function. I have tested with a simple filter to add a class to the body and it doesn't work inside my action function but works perfectly outside. I put some flags to see if my function was called by AJAX and it is!

//This works well! If I put the same filter in my 'test' function it doesn't work
add_filter( 'body_class', function( $classes ) {
    return array_merge( $classes, array( 'test-class' ) );
} );

function test()
{
    //This is displayed in console
    echo'test-init';

    if (!DOING_AJAX){
        return;
    }
    
    //This does't work
    add_filter( 'body_class', function( $classes ) {
      return array_merge( $classes, array( 'test-class2' ) );
    } );
    
    //The callback function isn't called
    add_action( 'woocommerce_before_calculate_totals', 'update_price' );

    //This is displayed in console
    echo 'test';

    wp_die();
}

//Not called
function update_price(){
    echo 'update';
}

EDIT :

I re-wrote my explanations to make them a bit more understandable with additional code:

Here are some tries with a negative fee try using woocommerce_cart_calculate_fees hook:

The callback function of woocommerce_cart_calculate_fees hook works perfectly if I use add_action outside of my AJAX action function like this :

add_action( 'wp_ajax_test', 'test' );
add_action( 'wp_ajax_nopriv_test', 'test' );

add_action( 'woocommerce_cart_calculate_fees','woocommerce_custom_surcharge' );
function woocommerce_custom_surcharge() {
    global $woocommerce;

    if ( ( is_admin() && ! defined( 'DOING_AJAX' )))
        return;

    $percentage = 0.1;
    $surcharge = ( $woocommerce->cart->cart_contents_total + $woocommerce->cart->shipping_total ) * $percentage;
    $woocommerce->cart->add_fee( 'discount', -$surcharge, true, '' );

}

function test(){
   
   //My AJAX action function

}

So this work, the negatif fee is applied but when the page is loaded and that's not what I would like since I want to apply the negative fee when I trigger update cart button, so the idea is to integrate the add_action inside my AJAX action function like this :

add_action( 'wp_ajax_test', 'test' );
add_action( 'wp_ajax_nopriv_test', 'test' );

function woocommerce_custom_surcharge() {
    global $woocommerce;

    if ( ( is_admin() && ! defined( 'DOING_AJAX' )))
        return;

    $percentage = 0.1;
    $surcharge = ( $woocommerce->cart->cart_contents_total + $woocommerce->cart->shipping_total ) * $percentage;
    $woocommerce->cart->add_fee( 'discount', -$surcharge, true, '' );

}

//My AJAX action function
function test(){

   echo 'test1'; //flag 1
   add_action( 'woocommerce_cart_calculate_fees','woocommerce_custom_surcharge' );
   echo 'test2'; //flag 2
}

The callback function woocommerce_custom_surcharge isn't called at all. Both my flags are displayed in chrome console so it means my action function is correctly called. So my question is : how to make this add_action work inside my action function?


Solution

  • You missed the usage a WC Session variable to set some data and use it in your discount function… The First function below handle your settings and it's loaded everywhere on other functions.

    // Settings
    function get_membership_settings(){
        $discount_percentage = 1; // the discount percentage: 1% here
    
        return array(
            'percentage'       => $discount_percentage,
            'field_key'        => 'membership_number', // Field "name" key (or id)
            'field_type'       => 'text',
            'field_label'      =>  __('Membership number', 'woocommerce'),
            'button_text'      =>  __('Apply membership number', 'woocommerce'),
            'discount_text'    =>  sprintf( __('Membership discount%s', 'woocommerce'), ' ('.$discount_percentage.' %)' ), // for negative fee
            'valid_message'    => __('Number is valid (text message).', 'woocommerce'),
            'unvalid_message'  => __('Number not valid (text message).', 'woocommerce'),
            'empty_field_text' => __('Please enter your membership number.', 'woocommerce'),
         );
    }
    
    // Settings + Membership number
    function get_membership_data(){
        $settings      = get_membership_settings();// Load settings
        $field_key     = $settings['field_key']; // The field Id
    
        $user_value    = get_user_meta( get_current_user_id(), $field_key, true ); // Get "membership number" from user data
        $session_value = WC()->session->get($field_key); // Get "membership number" from session variable
    
        // Set "membership number" in the array
        $settings['field_value'] = empty($session_value) ? $user_value : $session_value;
    
        return $settings;
    }
    

    The displayed field on cart (see the screenshot at the end):

    // Display a text input field on cart (+ javascript)
    add_action('woocommerce_cart_contents', 'display_field_membership_number', 100 );
    function display_field_membership_number(){
        extract(get_membership_data()); // Load data and settings
    
        echo '<tr><td colspan="6" class="membership" style="padding:0;border-top:16px solid #FFF;">
            <style>.message.off,label.hidden{display:none}.membership .message{margin-left:20px}</style>
            <div id="'.$field_key.'-wrapper">
                <label for="'.$field_key.'" class="hidden"> '.$field_label.'&nbsp;<abbr class="required" title="required">*</abbr></label>
                <input type="'.$field_type.'" class="input-'.$field_type.'" name="'.$field_key.'" id="'.$field_key.'" placeholder="'.$field_label.'" value="'.$field_value.'">
                <button type="button" class="button">'.$button_text.'</button>
                <span class="message off"></span>
            </div>
        </td></tr>
        <tr><td colspan="6" style="padding:0"></td></tr>';
    }
    

    The jQuery / Ajax code:

    // Function that send the Ajax request | jQuery + Ajax
    add_action('wp_footer', 'membership_number_js_script');
    function membership_number_js_script() {
        if( ! is_cart() ) return; // Only on cart
    
        $field_key = get_membership_settings()['field_key']; // Load field key id
    
        // jQuery Ajax code
        ?>
        <script type="text/javascript">
        jQuery( function($){
            if (typeof woocommerce_params === 'undefined')
                return false;
    
            var s = '#<?php echo $field_key; ?>-wrapper';
    
            $(s+' button').click( function(){
                var value = $(s+' input').val();
    
                // Function that handle the display of the message
                function handleDisplayMessage( selector, response, error = false ) {
                    if ( ! error ) {
                        $.each( $.parseJSON(response), function(index, value){
                            displayMessage( selector, value, index );
                        });
                    } else {
                        displayMessage( selector, response, 0 );
                    }
                }
    
                // Function that display a message
                function displayMessage( selector, response, type ) {
                    $(selector).hide('0').removeClass('off').html(response).css('color', (type == 1 ? '#03C03C' : '#DC143C')).show();
                    setTimeout(function() {
                        $(selector).hide();
                    }, 3000 );
                }
    
                $.ajax({
                    type: 'POST',
                    url: wc_cart_params.ajax_url,
                    data: {
                        'action': '<?php echo $field_key; ?>',
                        '<?php echo $field_key; ?>': value,
                    },
                    success: function (response) {
                        handleDisplayMessage( (s+' .message'), response );
                        $(document.body).trigger("added_to_cart"); // refresh cart
                    },
                    error:function(error){
                        handleDisplayMessage( (s+' .message'), ('A problem occured (error: '+error+')'), true );
                    }
                });
            });
        });
        </script>
        <?php
    }
    

    The PHP WordPress Ajax receiver function:

    // Get the ajax request and set value to WC session (and the field validation)
    add_action( 'wp_ajax_membership_number', 'set_membership_number_and_validation' );
    add_action( 'wp_ajax_nopriv_membership_number', 'set_membership_number_and_validation' );
    function set_membership_number_and_validation() {
        extract(get_membership_settings()); // Load and extract settings
    
        if( isset($_POST[$field_key]) && ! empty($_POST[$field_key]) ) {
    
            ## HERE BELOW, SET YOUR CODE (that checks membership number for validation)
    
            $validation = true; // "true" when validated (or false if not)
    
            // Set membership number to a WC session variable
            WC()->session->set($field_key, $_POST[$field_key]);
    
            // Send response back
            echo json_encode( ($validation ? [1 => $valid_message] : [0 => $unvalid_message]) );
        } else {
            // Send response back
            echo json_encode( [0 => $empty_field_text] );
        }
        die();
    }
    

    The discount part:

    there are multiple ways to make a discount (here are two of them, using different hooks):

    1. Discount based cart subtotal + shipping tolal (negative fee):

    // 1. Percentage discount for Membership (with a negative fee)
    add_action( 'woocommerce_cart_calculate_fees', 'add_membership_discount' );
    function add_membership_discount( $cart ) {
        if ( is_admin() && ! defined( 'DOING_AJAX' ) )
            return;
    
        extract(get_membership_data()); // Load and extract settings + membership numver
    
        if( ! empty($field_value) ) {
            // Calculation
            $discount = ( $cart->get_cart_contents_total() + $cart->get_shipping_total() ) * $percentage / 100;
    
            $cart->add_fee( $discount_text, -$discount ); // Add a discount
        }
    }
    

    2. Discount on cart item price:

    // 2. Percentage discount for Membership (on cart items price)
    add_action( 'woocommerce_before_calculate_totals', 'add_membership_cart_item_discount' );
    function add_membership_cart_item_discount( $cart ) {
        if ( is_admin() && ! defined( 'DOING_AJAX' ) )
            return;
    
        if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 )
            return;
    
        extract(get_membership_data()); // Load and extract settings + membership numver
    
        if( ! empty($field_value) ) {
    
            // Loop through cart items
            foreach( $cart->get_cart() as $cart_item ){
                // Get the real WC_Product Object
                $product = wc_get_product($cart_item['data']->get_id());
    
                $price = $product->get_price(); // The real product price
    
                $discounted_price = $price * ( 1 - ( $percentage / 100 ) ); // Calculation
    
                $cart_item['data']->set_price($discounted_price); // Set the discounted price
            }
        }
    }
    

    All code goes in functions.php file of your active child theme (or active theme). Tested and works.

    enter image description here


    Similar threads: