Skip to content

Instantly share code, notes, and snippets.

@TanvirHasan19
Last active January 13, 2026 03:13
Show Gist options
  • Select an option

  • Save TanvirHasan19/029016c2fb19ff2f8f0ab8c8bac19995 to your computer and use it in GitHub Desktop.

Select an option

Save TanvirHasan19/029016c2fb19ff2f8f0ab8c8bac19995 to your computer and use it in GitHub Desktop.
Standalone plugin (recommended approach)
<?php
/**
* Plugin Name: Product Feed - Table Rate Shipping Cheapest Rate Fix (Final Solution)
* Description: Filters shipping data to only include the cheapest shipping rate per country/region for products without shipping labels. This fixes the issue where Table Rate Shipping returns multiple rates and Google Merchant Center picks the most expensive one.
* Version: 3.0.0
* Author: AdTribes Support
*
* FINAL SOLUTION - Addresses all issues:
* - Works for both Product Feed Pro and Product Feed Elite
* - Handles all price formats correctly (including edge cases)
* - More robust grouping logic that handles empty/null values
* - Better error handling and validation
* - Comprehensive debugging mode
* - Ensures filter runs with proper priority
* - Handles all feed types (not just GMC)
*
* This solution addresses the issue where Table Rate Shipping returns multiple matching rates
* (fast, fast-tracked, standard) for products without shipping labels, and the feed
* incorrectly includes all rates, causing Google Merchant Center to potentially select
* the most expensive one.
*
* The filter ensures only the cheapest rate per country/region/postal_code combination
* is included in the feed.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Filter shipping data to only include the cheapest shipping rate per country/region/postal_code
*
* This fixes the issue where Table Rate Shipping returns multiple rates (fast, fast tracked, standard)
* and the feed picks up the most expensive one instead of the cheapest.
*
* @param array $shipping_data The shipping data array.
* @param object $product The product object.
* @param object $feed The feed object.
* @return array Filtered shipping data with only cheapest rates.
*/
function adt_filter_cheapest_shipping_rate_final( $shipping_data, $product, $feed ) {
// Early return if shipping data is empty or not an array
if ( empty( $shipping_data ) || ! is_array( $shipping_data ) ) {
return $shipping_data;
}
// If there's only one shipping rate, no need to filter
if ( count( $shipping_data ) <= 1 ) {
return $shipping_data;
}
// Enable debugging by setting this constant: define( 'ADT_SHIPPING_FILTER_DEBUG', true );
$debug = defined( 'ADT_SHIPPING_FILTER_DEBUG' ) && ADT_SHIPPING_FILTER_DEBUG;
if ( $debug ) {
$product_id = is_object( $product ) && method_exists( $product, 'get_id' ) ? $product->get_id() : 'unknown';
error_log( sprintf(
'[ADT Shipping Filter] Processing %d shipping rates for product ID: %s',
count( $shipping_data ),
$product_id
) );
}
// Group shipping rates by country, region, and postal_code combination
$grouped_rates = array();
foreach ( $shipping_data as $index => $shipping ) {
// Skip if not an array
if ( ! is_array( $shipping ) ) {
if ( $debug ) {
error_log( sprintf( '[ADT Shipping Filter] Skipping non-array shipping data at index %d', $index ) );
}
continue;
}
// Get country, region, and postal_code - handle various key formats
$country = '';
$region = '';
$postal_code = '';
// Try different possible key names for country
if ( isset( $shipping['country'] ) ) {
$country = trim( (string) $shipping['country'] );
}
// Try different possible key names for region/state
if ( isset( $shipping['region'] ) ) {
$region = trim( (string) $shipping['region'] );
} elseif ( isset( $shipping['state'] ) ) {
$region = trim( (string) $shipping['state'] );
}
// Try different possible key names for postal code
if ( isset( $shipping['postal_code'] ) ) {
$postal_code = trim( (string) $shipping['postal_code'] );
} elseif ( isset( $shipping['postcode'] ) ) {
$postal_code = trim( (string) $shipping['postcode'] );
}
// Create a unique key for country/region/postal_code combination
// Use empty string for missing values to ensure proper grouping
$key = $country . '|' . $region . '|' . $postal_code;
// Extract numeric price from the price string
$price_str = isset( $shipping['price'] ) ? $shipping['price'] : '';
$price = adt_extract_numeric_price_final( $price_str );
if ( $debug ) {
error_log( sprintf(
'[ADT Shipping Filter] Rate #%d - Country: "%s", Region: "%s", Postal: "%s", Price String: "%s", Price Numeric: %s',
$index,
$country,
$region,
$postal_code,
$price_str,
$price
) );
}
// Validate price extraction
if ( $price === PHP_FLOAT_MAX ) {
if ( $debug ) {
error_log( sprintf(
'[ADT Shipping Filter] WARNING: Could not extract valid price from "%s" for rate #%d. Skipping.',
$price_str,
$index
) );
}
continue; // Skip rates with invalid prices
}
// If we haven't seen this country/region/postal_code combo, or this rate is cheaper
// Free shipping (price = 0) is always considered cheapest
if ( ! isset( $grouped_rates[ $key ] ) ) {
// First rate for this combination
$grouped_rates[ $key ] = array(
'shipping' => $shipping,
'price_numeric' => $price,
'index' => $index,
);
if ( $debug ) {
error_log( sprintf(
'[ADT Shipping Filter] First rate for key "%s" - Price: %s',
$key,
$price
) );
}
} elseif ( $price < $grouped_rates[ $key ]['price_numeric'] ) {
// This rate is cheaper, replace the existing one
if ( $debug ) {
error_log( sprintf(
'[ADT Shipping Filter] Replacing rate for key "%s" - Old price: %s, New price: %s',
$key,
$grouped_rates[ $key ]['price_numeric'],
$price
) );
}
$grouped_rates[ $key ] = array(
'shipping' => $shipping,
'price_numeric' => $price,
'index' => $index,
);
} elseif ( $debug ) {
error_log( sprintf(
'[ADT Shipping Filter] Keeping existing rate for key "%s" - Existing price: %s, New price: %s',
$key,
$grouped_rates[ $key ]['price_numeric'],
$price
) );
}
}
// Rebuild the shipping data array with only the cheapest rates
$filtered_shipping_data = array();
foreach ( $grouped_rates as $key => $grouped_rate ) {
$filtered_shipping_data[] = $grouped_rate['shipping'];
if ( $debug ) {
error_log( sprintf(
'[ADT Shipping Filter] Added cheapest rate for key "%s" - Price: %s',
$key,
$grouped_rate['price_numeric']
) );
}
}
if ( $debug ) {
$product_id = is_object( $product ) && method_exists( $product, 'get_id' ) ? $product->get_id() : 'unknown';
error_log( sprintf(
'[ADT Shipping Filter] Filtered from %d rates to %d rates for product ID: %s',
count( $shipping_data ),
count( $filtered_shipping_data ),
$product_id
) );
}
return $filtered_shipping_data;
}
/**
* Extract numeric price from price string - FINAL VERSION
* Handles various formats like:
* - "GBP 5.99" or "5.99 GBP"
* - "5.99" (numeric only)
* - "EUR 10,50" or "10,50 EUR" (European format)
* - "1.000,50 EUR" (European with thousand separator)
* - "1,000.50 USD" (US format with thousand separator)
* - Empty strings or invalid formats
*
* @param string|float|int $price_str The price string or numeric value.
* @return float The numeric price value, or PHP_FLOAT_MAX if extraction fails.
*/
function adt_extract_numeric_price_final( $price_str ) {
// Handle numeric values directly
if ( is_numeric( $price_str ) ) {
$price = (float) $price_str;
return $price >= 0 ? $price : PHP_FLOAT_MAX;
}
// Handle empty or non-string values
if ( empty( $price_str ) || ! is_string( $price_str ) ) {
return PHP_FLOAT_MAX; // Return max float if no price found
}
// Trim whitespace
$price_str = trim( $price_str );
if ( empty( $price_str ) ) {
return PHP_FLOAT_MAX;
}
// Remove currency symbols and text, keep only numbers and decimal separators (.,)
// This handles formats like "GBP 5.99", "5.99 GBP", "EUR 10,50", etc.
$cleaned = preg_replace( '/[^0-9.,\-+]/', '', $price_str );
if ( empty( $cleaned ) ) {
return PHP_FLOAT_MAX;
}
// Handle negative prices (though unlikely for shipping)
$is_negative = false;
if ( strpos( $cleaned, '-' ) !== false ) {
$is_negative = true;
$cleaned = str_replace( '-', '', $cleaned );
}
// Handle comma as decimal separator (European format)
// Check if comma is used as decimal separator (e.g., "10,50" vs "1,000.50")
if ( strpos( $cleaned, ',' ) !== false && strpos( $cleaned, '.' ) !== false ) {
// Both comma and dot present - determine which is decimal separator
$comma_pos = strrpos( $cleaned, ',' );
$dot_pos = strrpos( $cleaned, '.' );
if ( $comma_pos > $dot_pos ) {
// Comma is decimal separator (e.g., "1.000,50")
$cleaned = str_replace( '.', '', $cleaned );
$cleaned = str_replace( ',', '.', $cleaned );
} else {
// Dot is decimal separator (e.g., "1,000.50")
$cleaned = str_replace( ',', '', $cleaned );
}
} elseif ( strpos( $cleaned, ',' ) !== false ) {
// Only comma present - could be decimal separator or thousand separator
// If there are 3+ digits after comma, it's likely a thousand separator
$parts = explode( ',', $cleaned );
if ( count( $parts ) === 2 && strlen( $parts[1] ) <= 2 ) {
// Comma is decimal separator (e.g., "10,50")
$cleaned = str_replace( ',', '.', $cleaned );
} else {
// Comma is thousand separator, remove it
$cleaned = str_replace( ',', '', $cleaned );
}
}
// Extract the numeric value
if ( ! is_numeric( $cleaned ) ) {
return PHP_FLOAT_MAX;
}
$price = (float) $cleaned;
// Apply negative sign if needed
if ( $is_negative ) {
$price = -$price;
}
// Return the price (including 0 for free shipping)
// Only return max float if the price is negative (invalid for shipping)
if ( $price < 0 ) {
return PHP_FLOAT_MAX;
}
return $price;
}
// Hook into the shipping data filter
// Priority 20 to run after other filters, but before final output
// Use late priority to ensure it runs after all other modifications
add_filter( 'adt_product_feed_shipping_data', 'adt_filter_cheapest_shipping_rate_final', 20, 3 );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment