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.
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
In your product settings, the plugin requires you to make some changes:
The first empty column (icon with 3 little horizontal lines) will allow you to reorder the lines as you need. Once done, save.
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.