Search code examples
phpwoocommercepluginsshipping-method

WooCommerce Per Product Shipping plugin: Charge only the higher shipping cost


I'm using WooCommerce Per Product Shipping plugin to set shipping rates by product, however, this plugin doesn't provide any built-in functionality to charge only the most expensive shipping cost for the entire order the way the core WooCommerce flat rate shipping allows you to set a calculation type.

I'd like to only charge the shipping of the product with the highest rate. I admit I used AI to generate this code (as I'm not a developer) and I've tried a few ways to implement it, but I keep getting a critical error:

/**
 * Retrieve shipping costs for a WooCommerce product.
 *
 * @param int $product_id The ID of the WooCommerce product.
 * @return float|null Shipping cost for the product, or null if not found.
 */
function get_shipping_cost_for_product( $product_id ) {
    $shipping_methods = WC()->shipping->get_packages();
    
    foreach ( $shipping_methods as $package ) {
        foreach ( $package['rates'] as $rate ) {
            // Check if the shipping rate is applicable to the product.
            if ( $rate->method_id === '11' && $rate->get_instance_id() === 0 ) {
                // Retrieve the shipping cost for the product.
                return $rate->cost;
            }
        }
    }
    
    return null; // Shipping cost not found.
}

/**
 * Custom function to calculate shipping costs for WooCommerce Per Product Shipping.
 *
 * @param array $package Shipping package.
 * @return void
 */
function custom_per_product_shipping_cost( $package ) {
    if ( ! is_array( $package['contents'] ) ) {
        return;
    }

    // Initialize variables to keep track of the highest shipping cost.
    $highest_shipping_cost = 0;
    
    // Loop through each item in the cart.
    foreach ( $package['contents'] as $item_id => $item ) {
        $product_id = $item['product_id'];
        
        // Get the shipping cost for the current product.
        $shipping_cost = get_shipping_cost_for_product( $product_id );
        
        // Compare with the highest shipping cost found so far.
        if ( $shipping_cost > $highest_shipping_cost ) {
            $highest_shipping_cost = $shipping_cost;
        }
    }
    
    // Set the calculated highest shipping cost for the entire order.
    $package['contents_cost'] = $highest_shipping_cost;
    
    // Update the package shipping costs.
    WC()->cart->shipping_total      = $highest_shipping_cost;
    WC()->cart->shipping_tax_total  = 0; // You may need to adjust tax calculation.
    WC()->cart->shipping_taxes      = array();
    WC()->cart->shipping_methods    = array();
    
    // Recalculate cart totals.
    WC()->cart->calculate_totals();
}

// Hook into the shipping calculation process.
add_action( 'woocommerce_cart_shipping_packages', 'custom_per_product_shipping_cost' );

Any help would be greatly appreciated.

Per Product Shipping settings. Tax status: taxable; nothing set for Default Product Cost, Handling Fee per product or per order, and nothing checked with regard to ignoring other methods. See screenshot. plugin settings

No other shipping methods are enabled

On a product, I have enabled per product shipping and have added three rows (Us, HI, AK) with an item cost. See screenshot product settings


Solution

  • In your product settings, the plugin requires you to make some changes:

    • First come the line with a defined postcode for a country (you don't have)
    • Then after the line with a defined state for a country
    • Then after the country alone (all other states)
    • At the end a line without specified country for all other countries (optional)

    The first empty column (icon with 3 little horizontal lines) will allow you to reorder the lines as you need. Once done, save.

    enter image description here

    Note that your shipping cost are set "by item", which cost will be multiplied by the cart quantity for this item.

    So If I take the most expensive shipping item cost, it will be the item cost that you have set, multiplied by the quantity in your case. If you want that the quantity does not increase the cost, you will have to change your product settings.

    The following code handle cost by item (affected by quantity) and cost by line item.

    Here comes the function to get the shipping cost for a product based on the customer location:

    function get_per_product_shipping_data( $product_id, $country = '', $state = '', $postcode = '' ) {
        global $wpdb;
    
        $results = $wpdb->get_results( "
        SELECT *
        FROM {$wpdb->prefix}woocommerce_per_product_shipping_rules
        WHERE product_id = {$product_id}
        ORDER BY rule_order ASC;" );
    
        $country_found = $state_found = $postcode_found = array();
    
        foreach ( $results as $key => $result ) {
            if( $country === $result->rule_country ) {
                $country_found[$key] = $result;
            }
            if( $state === $result->rule_state ) {
                $state_found[$key] = $result;
            }
            if( $postcode === $result->rule_postcode ) {
                $postcode_found[$key] = $result;
            }
        }
        if( count($postcode_found) === 1 ) {
            return reset($postcode_found);
        } elseif( count($state_found) > 0 ) {
            if ( count($state_found) === 1 )
                return reset($state_found);
            elseif ( count($state_found) > 1 ) {
                return end($state_found);
            }
        }  elseif( count($country_found) > 0 ) {
            if ( count($country_found) === 1 )
                return reset($country_found);
            elseif ( count($country_found) > 1 ) {
                return end($country_found);
            }
        } elseif( count($results) === 1 ) {
            return reset($results);
        } else {
            return array();
        }
    }
    

    Now we can get the most expensive shipping cost and set it with the following:

    add_filter( 'woocommerce_package_rates', 'custom_per_product_shipping_filtering', 10, 2 );
    function custom_per_product_shipping_filtering( $rates, $package ) {
        if ( count($package['contents']) <= 1 ) return $rates;
    
        $country  = $package['destination']['country'];
        $state    = $package['destination']['state'];
        $postcode = $package['destination']['postcode'];
    
        $method_key_id  = 'per_product'; // shipping method slug
        $item_quantity_cost = $line_item_cost = $new_cost = array();
        
        // loop through cart items in the current shipping package
        foreach( $package['contents'] as $item_key => $item ){
            $shipping = get_per_product_shipping_data($item['product_id'], $country, $state, $postcode );
            if ( ! empty($shipping) ) {
                if ( $shipping->rule_item_cost > 0 ) {
                    $item_quantity_cost[$item_key] = $shipping->rule_item_cost * $item['quantity'];
                }
                if ( $shipping->rule_cost > 0 ) {
                    $line_item_cost[$item_key] = $shipping->rule_cost;
                }
            }
        }
    
        
        if( count($item_quantity_cost) > 1 ) {
            arsort($item_quantity_cost); // Sorting cost in DESC order
            $new_cost[] = reset($item_quantity_cost); // Set the more expensive cost
        }
        if( count($line_item_cost) > 1 ) {
            arsort($line_item_cost); // Sorting cost in DESC order
            $new_cost[] = reset($line_item_cost); // Set the more expensive cost
        }
    
        // If we find the shipping per product
        if( isset($rates[$method_key_id]) && count($new_cost) > 0 ){
            if( count($new_cost) > 0 ) {
                arsort($new_cost); // Sorting cost in DESC order
            }
            $new_cost   = reset($new_cost); // Get the most expensive individual cost
            $rate       = $rates[$method_key_id];
            $conversion =  $new_cost / $rate->cost;
    
            // Set the new cost
            $rates[$method_key_id]->cost = $new_cost;
    
            // Shipping taxes 
            $has_taxes = false; // Initializing
            $taxes     = array(); // Initializing
    
            // Loop through taxes array (change taxes rate cost if enabled)
            foreach ($rate->taxes as $key => $tax){
                if( $tax > 0 ){
                    $taxes[$key] = $tax * $conversion; // Set the new tax cost in the array
                    $has_taxes   = true; // Enabling tax changes
                }
            }
            // set the new array of shipping tax cost
            if( $has_taxes ) {
                $rates[$method_key_id]->taxes = $taxes;
            }
        }
        return $rates;
    }
    

    Code goes in functions.php file of your child theme (or in a plugin). Tested and work.

    To refresh shipping cached data (after adding the code), empty your cart.