
Need full control over delivery charges in WooCommerce? This free plugin by 24siteshop adds global or state-wise delivery fees, optional COD charge, and a prepaid discount — all from one lightweight, single PHP file. It’s built for India-based WooCommerce stores but works globally, calculating fees only on checkout and updating totals automatically when the customer changes their payment method or state. The plugin also supports conditional free delivery above a minimum order amount. 100% clean, open code — no bloat, no hidden scripts.
Full Single File Code for Free WooCommerce Delivery Fee Plugin :
<?php
/**
* Plugin Name: Delivery Fee by 24siteshop
* Description: WooCommerce add-on to manage delivery fees (global & per-state), optional COD charge, prepaid discount, and conditional free delivery based on cart subtotal. Fees appear only on Checkout and auto-update via AJAX when payment method or billing state changes.
* Author: 24siteshop
* Version: 1.4.2
* Text Domain: am24-delivery-fee
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
final class AM24_Delivery_Fee_Plugin {
const OPTION_KEY = 'am24_delivery_fee_settings';
const NONCE_KEY = 'am24_delivery_fee_nonce';
private static $instance = null;
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
// Defaults on activation
register_activation_hook( __FILE__, [ __CLASS__, 'activate' ] );
// Admin menu + save handler
add_action( 'admin_menu', [ $this, 'register_menu' ] );
add_action( 'admin_init', [ $this, 'maybe_save_settings' ] );
// Frontend fees & AJAX handling
add_action( 'woocommerce_cart_calculate_fees', [ $this, 'add_fees_on_checkout' ] );
add_action( 'woocommerce_checkout_update_order_review', [ $this, 'capture_chosen_payment_method' ] );
// Trigger refresh when payment method/state changes
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_checkout_script' ] );
}
/**
* Set default options on activation.
*/
public static function activate() {
$defaults = [
'enabled' => 1,
'delivery_fee_label' => 'Delivery Fee',
'base_delivery_fee' => '0',
// Conditional Free Delivery settings
'conditional_enabled' => 0,
'conditional_min_value' => '0', // Minimum cart subtotal for FREE delivery
// Legacy textarea for quick paste (optional)
'per_state_rates' => '', // Textarea lines: STATE_CODE=FEE
// Structured state map (blank by default -> use base fee)
'state_rates' => [], // e.g. [ 'MH' => 49, ... ]
'cod_enabled' => 0,
'cod_fee_label' => 'COD Charge',
'cod_amount' => '0',
'prepaid_discount_label' => 'Prepaid Discount',
'prepaid_discount_pct' => '0',
];
$current = get_option( self::OPTION_KEY );
if ( ! is_array( $current ) ) {
update_option( self::OPTION_KEY, $defaults );
} else {
// Merge current settings with defaults to include new keys
update_option( self::OPTION_KEY, wp_parse_args( $current, $defaults ) );
}
}
/**
* Admin: Register menu page.
*/
public function register_menu() {
add_menu_page(
__( 'Delivery Fee by 24siteshop', 'am24-delivery-fee' ),
__( 'Delivery Fee by 24siteshop', 'am24-delivery-fee' ),
'manage_options',
'am24-delivery-fee',
[ $this, 'render_settings_page' ],
'dashicons-money-alt',
56
);
}
/**
* Admin: Render settings UI.
*/
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) return;
$opt = $this->get_settings();
$states = $this->get_india_states();
$structured = is_array( $opt['state_rates'] ) ? $opt['state_rates'] : [];
// Merge legacy textarea into structured (without overwriting explicit values)
$legacy = $this->parse_state_map( isset( $opt['per_state_rates'] ) ? $opt['per_state_rates'] : '' );
foreach ( $legacy as $code => $amt ) {
if ( ! isset( $structured[ $code ] ) ) {
$structured[ $code ] = $amt;
}
}
?>
<div class="wrap">
<h1><?php esc_html_e( 'Delivery Fee by 24siteshop', 'am24-delivery-fee' ); ?> <small style="font-weight:normal;color:#777">v1.4.2</small></h1>
<form method="post" action="">
<?php wp_nonce_field( self::NONCE_KEY, self::NONCE_KEY ); ?>
<h2 class="title">General Delivery Fee Settings</h2>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><?php esc_html_e( 'Enable Module', 'am24-delivery-fee' ); ?></th>
<td>
<label><input type="checkbox" name="am24[enabled]" value="1" <?php checked( $opt['enabled'], 1 ); ?>> <?php esc_html_e( 'Enable Delivery Fee module', 'am24-delivery-fee' ); ?></label>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Delivery Fee Label', 'am24-delivery-fee' ); ?></th>
<td>
<input type="text" class="regular-text" name="am24[delivery_fee_label]" value="<?php echo esc_attr( $opt['delivery_fee_label'] ); ?>">
<p class="description">Shown in order totals at Checkout.</p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Base Delivery Fee', 'am24-delivery-fee' ); ?></th>
<td>
<input type="number" step="0.01" name="am24[base_delivery_fee]" value="<?php echo esc_attr( $opt['base_delivery_fee'] ); ?>">
<p class="description">Used when a state's fee is left blank.</p>
</td>
</tr>
</tbody>
</table>
<h2 class="title">Conditional Free Delivery</h2>
<p class="description">Set a minimum cart subtotal for the delivery fee to be waived.</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><?php esc_html_e( 'Enable Conditional Free Delivery', 'am24-delivery-fee' ); ?></th>
<td>
<label><input type="checkbox" name="am24[conditional_enabled]" value="1" <?php checked( $opt['conditional_enabled'], 1 ); ?>> <?php esc_html_e( 'Enable minimum order for free delivery', 'am24-delivery-fee' ); ?></label>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Min Order Value for Free Delivery', 'am24-delivery-fee' ); ?></th>
<td>
<input type="number" step="0.01" name="am24[conditional_min_value]" value="<?php echo esc_attr( $opt['conditional_min_value'] ); ?>">
<p class="description">Delivery fee is <strong>NOT</strong> charged if the cart subtotal is equal to or higher than this value. Set to 0 to effectively disable.</p>
</td>
</tr>
</tbody>
</table>
<h2 class="title">Per-State Delivery Rates (India)</h2>
<p>Set fees per state/UT. <strong>Leave blank</strong> to fall back to the Base Delivery Fee.</p>
<p>
<button type="button" class="button" id="am24-fill-india-states">Add all India states</button>
<small class="description" style="margin-left:8px">Adds rows for all states/UTs. Amounts are left blank by default.</small>
</p>
<table class="widefat striped" id="am24-state-table" style="max-width:760px">
<thead>
<tr>
<th style="width:120px">Code</th>
<th>State / UT</th>
<th style="width:180px">Amount</th>
</tr>
</thead>
<tbody>
<?php foreach ( $states as $code => $name ) :
$val = isset( $structured[ $code ] ) ? $structured[ $code ] : '';
?>
<tr data-code="<?php echo esc_attr( $code ); ?>">
<td><code><?php echo esc_html( $code ); ?></code></td>
<td><?php echo esc_html( $name ); ?></td>
<td>
<?php $val_attr = ($val === '' || $val === null) ? '' : ' value="'.esc_attr($val).'"'; ?>
<input type="number" step="0.01" name="am24[state_rates][<?php echo esc_attr( $code ); ?>]"<?php echo $val_attr; ?> placeholder="">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p style="margin-top:12px">
<label for="am24_per_state_legacy"><strong>Legacy textarea (optional)</strong></label><br>
<textarea id="am24_per_state_legacy" name="am24[per_state_rates]" rows="6" cols="60" class="large-text code" placeholder="MH=49 GJ=59 DL=39 TN=69 KA=59 RJ=45 ..."><?php echo esc_textarea( $opt['per_state_rates'] ); ?></textarea>
<br><small class="description">Values entered here will be merged on save. Table values win for duplicate states.</small>
</p>
<h2 class="title">COD & Prepaid Charges/Discounts</h2>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><?php esc_html_e( 'COD Charge Module', 'am24-delivery-fee' ); ?></th>
<td>
<label><input type="checkbox" name="am24[cod_enabled]" value="1" <?php checked( $opt['cod_enabled'], 1 ); ?>> <?php esc_html_e( 'Enable COD charge', 'am24-delivery-fee' ); ?></label>
<p class="description">Only applies when the customer chooses Cash on Delivery.</p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'COD Fee Label', 'am24-delivery-fee' ); ?></th>
<td><input type="text" class="regular-text" name="am24[cod_fee_label]" value="<?php echo esc_attr( $opt['cod_fee_label'] ); ?>"></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'COD Amount', 'am24-delivery-fee' ); ?></th>
<td><input type="number" step="0.01" name="am24[cod_amount]" value="<?php echo esc_attr( $opt['cod_amount'] ); ?>"></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Prepaid Discount Label', 'am24-delivery-fee' ); ?></th>
<td><input type="text" class="regular-text" name="am24[prepaid_discount_label]" value="<?php echo esc_attr( $opt['prepaid_discount_label'] ); ?>"></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Prepaid Discount %', 'am24-delivery-fee' ); ?></th>
<td>
<input type="number" min="0" step="0.01" name="am24[prepaid_discount_pct]" value="<?php echo esc_attr( $opt['prepaid_discount_pct'] ); ?>">
<p class="description">Applied when a non-COD payment method is chosen. Calculated on cart subtotal.</p>
</td>
</tr>
</tbody>
</table>
<?php submit_button( __( 'Save Settings', 'am24-delivery-fee' ) ); ?>
</form>
</div>
<script>
(function(){
const btn = document.getElementById('am24-fill-india-states');
if(!btn) return;
const data = <?php echo wp_json_encode( $states ); ?>;
btn.addEventListener('click', function(){
const tbody = document.querySelector('#am24-state-table tbody');
if(!tbody) return;
Object.keys(data).forEach(function(code){
let row = tbody.querySelector('tr[data-code="'+code+'"]');
if(!row){
const tr = document.createElement('tr');
tr.setAttribute('data-code', code);
tr.innerHTML = '<td><code>'+code+'</code></td>'+
'<td>'+data[code]+'</td>'+
'<td><input type="number" step="0.01" name="am24[state_rates]['+code+']" placeholder=""></td>';
tbody.appendChild(tr);
}
});
});
})();
</script>
<?php
}
/**
* Admin: Save settings.
*/
public function maybe_save_settings() {
if ( ! isset( $_POST[self::NONCE_KEY] ) || ! wp_verify_nonce( $_POST[self::NONCE_KEY], self::NONCE_KEY ) ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$in = isset( $_POST['am24'] ) && is_array( $_POST['am24'] ) ? wp_unslash( $_POST['am24'] ) : [];
$clean = [];
$clean['enabled'] = isset( $in['enabled'] ) ? 1 : 0;
$clean['delivery_fee_label'] = isset( $in['delivery_fee_label'] ) ? sanitize_text_field( $in['delivery_fee_label'] ) : 'Delivery Fee';
$clean['base_delivery_fee'] = isset( $in['base_delivery_fee'] ) ? $this->num( $in['base_delivery_fee'] ) : '0';
// Conditional Free Delivery
$clean['conditional_enabled'] = isset( $in['conditional_enabled'] ) ? 1 : 0;
$clean['conditional_min_value'] = isset( $in['conditional_min_value'] ) ? $this->num( $in['conditional_min_value'] ) : '0';
// Legacy textarea value (optional)
$clean['per_state_rates'] = isset( $in['per_state_rates'] ) ? $this->sanitize_multiline( $in['per_state_rates'] ) : '';
// Structured state map: store only rows with a numeric value (skip blanks)
$state_rates = [];
if ( isset( $in['state_rates'] ) && is_array( $in['state_rates'] ) ) {
foreach ( $in['state_rates'] as $code => $amt ) {
$code = strtoupper( preg_replace( '/[^A-Z]/i', '', (string) $code ) );
$amt = trim( (string) $amt );
if ( $code === '' || $amt === '' ) continue; // keep blank -> fallback to base
$amt = (string) $this->num( $amt );
$state_rates[ $code ] = (float) $amt;
}
}
// Merge legacy textarea (without overwriting explicit table entries)
$legacy_map = $this->parse_state_map( $clean['per_state_rates'] );
foreach ( $legacy_map as $code => $val ) {
if ( ! isset( $state_rates[ $code ] ) ) {
$state_rates[ $code ] = $val;
}
}
$clean['state_rates'] = $state_rates;
// COD & Prepaid
$clean['cod_enabled'] = isset( $in['cod_enabled'] ) ? 1 : 0;
$clean['cod_fee_label'] = isset( $in['cod_fee_label'] ) ? sanitize_text_field( $in['cod_fee_label'] ) : 'COD Charge';
$clean['cod_amount'] = isset( $in['cod_amount'] ) ? $this->num( $in['cod_amount'] ) : '0';
$clean['prepaid_discount_label'] = isset( $in['prepaid_discount_label'] ) ? sanitize_text_field( $in['prepaid_discount_label'] ) : 'Prepaid Discount';
$clean['prepaid_discount_pct'] = isset( $in['prepaid_discount_pct'] ) ? $this->num( $in['prepaid_discount_pct'] ) : '0';
update_option( self::OPTION_KEY, $clean );
add_action( 'admin_notices', function() {
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Delivery Fee settings saved.', 'am24-delivery-fee' ) . '</p></div>';
} );
}
/**
* Frontend: Add fees (only on Checkout, never on Cart).
* Removed strict typehint to avoid fatals in edge environments.
*/
public function add_fees_on_checkout( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) return;
if ( function_exists( 'is_cart' ) && is_cart() ) return; // Never on cart page
if ( function_exists( 'is_checkout' ) && ! is_checkout() ) return; // Only on checkout
if ( ! $cart || ! method_exists( $cart, 'add_fee' ) ) return;
$opt = $this->get_settings();
if ( empty( $opt['enabled'] ) ) return;
$chosen_payment = '';
if ( function_exists( 'WC' ) && WC()->session ) {
$chosen_payment = WC()->session->get( 'chosen_payment_method' );
}
if ( empty( $chosen_payment ) && isset( $_POST['payment_method'] ) ) {
$chosen_payment = wc_clean( wp_unslash( $_POST['payment_method'] ) );
}
// ---- Delivery Fee (global or per-state) ----
$cart_subtotal = method_exists( $cart, 'get_subtotal' ) ? (float) $cart->get_subtotal() : 0.0;
$delivery_fee = 0.0;
// Conditional Free Delivery
$is_free_delivery = false;
if ( ! empty( $opt['conditional_enabled'] ) ) {
$min_value = max( 0, (float) $opt['conditional_min_value'] );
if ( $min_value > 0 && $cart_subtotal >= $min_value ) {
$is_free_delivery = true;
}
}
// Calculate fee if not free
if ( ! $is_free_delivery ) {
$state = $this->get_billing_state();
$delivery_fee = max( 0, (float) $this->get_fee_for_state( $state, $opt ) );
}
if ( $delivery_fee > 0 ) {
$cart->add_fee( $opt['delivery_fee_label'], $delivery_fee, true );
}
// ---- COD Charge (only when method is COD) ----
if ( ! empty( $opt['cod_enabled'] ) && $this->is_cod( $chosen_payment ) ) {
$cod_amount = max( 0, (float) $opt['cod_amount'] );
if ( $cod_amount > 0 ) {
$cart->add_fee( $opt['cod_fee_label'], $cod_amount, true );
}
}
// ---- Prepaid Discount (only when NOT COD) ----
$disc_pct = max( 0, (float) $opt['prepaid_discount_pct'] );
if ( $disc_pct > 0 && ! $this->is_cod( $chosen_payment ) && $cart_subtotal > 0 ) {
$discount = round( ( $disc_pct / 100 ) * $cart_subtotal, wc_get_price_decimals() );
if ( $discount > 0 ) {
$cart->add_fee( $opt['prepaid_discount_label'], -1 * $discount, true );
}
}
}
/**
* Capture chosen payment method from checkout refreshes (AJAX).
*/
public function capture_chosen_payment_method( $posted_data ) {
parse_str( $posted_data, $data );
if ( isset( $data['payment_method'] ) && function_exists( 'WC' ) && WC()->session ) {
WC()->session->set( 'chosen_payment_method', sanitize_text_field( $data['payment_method'] ) );
}
}
/**
* Enqueue a tiny script on checkout to trigger recalculation on payment method/state change.
*/
public function enqueue_checkout_script() {
if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) return;
wp_register_script( 'am24-delivery-fee-js', '', [], '1.4.2', true );
wp_enqueue_script( 'am24-delivery-fee-js' );
$inline = "jQuery(function($){
function refresh(){ $('body').trigger('update_checkout'); }
$(document.body).on('change', 'input[name=\\'payment_method\\']', refresh);
$(document.body).on('change', '#billing_state', refresh);
});";
wp_add_inline_script( 'am24-delivery-fee-js', $inline );
}
/** Helpers **/
private function get_settings() {
$opt = get_option( self::OPTION_KEY, [] );
return is_array( $opt ) ? $opt : [];
}
private function num( $val ) {
$val = is_scalar( $val ) ? (string) $val : '0';
$val = preg_replace( '/[^0-9.\-]/', '', $val );
return $val === '' ? '0' : $val;
}
private function sanitize_multiline( $text ) {
$text = (string) $text;
$lines = preg_split( '/\r?\n/', $text );
$clean = [];
foreach ( $lines as $ln ) {
$ln = trim( $ln );
if ( $ln === '' ) continue;
// Accept formats: "MH=49" or "MH : 49"
if ( preg_match( '/^([A-Za-z]{1,10})\s*[:=]\s*([0-9]+(?:\.[0-9]+)?)$/', $ln, $m ) ) {
$clean[] = strtoupper( $m[1] ) . '=' . $m[2];
}
}
return implode( "\n", $clean );
}
private function parse_state_map( $text ) {
$map = [];
$lines = preg_split( '/\r?\n/', (string) $text );
foreach ( $lines as $ln ) {
$ln = trim( $ln );
if ( $ln === '' ) continue;
if ( strpos( $ln, '=' ) !== false ) {
list( $code, $fee ) = array_map( 'trim', explode( '=', $ln, 2 ) );
$code = strtoupper( preg_replace( '/[^A-Z]/i', '', $code ) );
$fee = (float) preg_replace( '/[^0-9.\-]/', '', $fee );
if ( $code !== '' ) {
$map[ $code ] = $fee;
}
}
}
return $map;
}
private function get_billing_state() {
$state = '';
if ( function_exists( 'WC' ) && WC()->customer ) {
$state = WC()->customer->get_billing_state();
}
if ( ! $state ) {
$state = isset( $_POST['billing_state'] ) ? wc_clean( wp_unslash( $_POST['billing_state'] ) ) : $state;
}
return strtoupper( (string) $state );
}
private function get_fee_for_state( $state, $opt ) {
$map = [];
// Structured first
if ( ! empty( $opt['state_rates'] ) && is_array( $opt['state_rates'] ) ) {
foreach ( $opt['state_rates'] as $k => $v ) {
$map[ strtoupper( (string) $k ) ] = (float) $v;
}
}
// Merge legacy without overriding structured
$legacy = $this->parse_state_map( isset( $opt['per_state_rates'] ) ? $opt['per_state_rates'] : '' );
foreach ( $legacy as $k => $v ) {
if ( ! isset( $map[ $k ] ) ) {
$map[ $k ] = $v;
}
}
if ( $state && isset( $map[ $state ] ) ) {
return $map[ $state ];
}
return (float) $opt['base_delivery_fee'];
}
private function is_cod( $method ) {
// Checks for the standard WooCommerce 'cod' payment gateway ID
return (string) $method === 'cod';
}
private function get_india_states() {
$states = [];
if ( function_exists( 'WC' ) && WC()->countries ) {
$states = (array) WC()->countries->get_states( 'IN' );
}
// Fallback list if WooCommerce states are not available for some reason
if ( empty( $states ) ) {
$states = [
'AN' => 'Andaman and Nicobar Islands',
'AP' => 'Andhra Pradesh',
'AR' => 'Arunachal Pradesh',
'AS' => 'Assam',
'BR' => 'Bihar',
'CH' => 'Chandigarh',
'CT' => 'Chhattisgarh',
'DN' => 'Dadra and Nagar Haveli and Daman and Diu',
'DL' => 'Delhi',
'GA' => 'Goa',
'GJ' => 'Gujarat',
'HR' => 'Haryana',
'HP' => 'Himachal Pradesh',
'JK' => 'Jammu and Kashmir',
'JH' => 'Jharkhand',
'KA' => 'Karnataka',
'KL' => 'Kerala',
'LA' => 'Ladakh',
'LD' => 'Lakshadweep',
'MP' => 'Madhya Pradesh',
'MH' => 'Maharashtra',
'MN' => 'Manipur',
'ML' => 'Meghalaya',
'MZ' => 'Mizoram',
'NL' => 'Nagaland',
'OR' => 'Odisha',
'PY' => 'Puducherry',
'PB' => 'Punjab',
'RJ' => 'Rajasthan',
'SK' => 'Sikkim',
'TN' => 'Tamil Nadu',
'TG' => 'Telangana',
'TR' => 'Tripura',
'UP' => 'Uttar Pradesh',
'UK' => 'Uttarakhand',
'WB' => 'West Bengal',
];
}
// Normalize keys
$clean = [];
foreach ( $states as $k => $v ) {
$k = strtoupper( preg_replace( '/[^A-Z]/i','', (string) $k ) );
$clean[ $k ] = (string) $v;
}
return $clean;
}
}
// Bootstrap
AM24_Delivery_Fee_Plugin::instance();
WooCommerce Delivery Fee Plugin – Demo by 24siteshop
How the Plugin Works (Explained)
This plugin hooks directly into WooCommerce’s woocommerce_cart_calculate_fees
action, adding delivery charges dynamically during checkout.
You can define a base delivery fee, and optionally override it with state-wise rates (preloaded with all Indian states). The plugin checks the customer’s billing state and applies the matching fee automatically.
If Conditional Free Delivery is enabled, the plugin waives the delivery charge once the cart subtotal crosses your specified threshold.
When customers choose Cash on Delivery, an additional COD fee can be applied; if they pick a prepaid method, a configurable discount percentage is given instead.
Everything updates live through AJAX, so the checkout total refreshes instantly when payment method or billing state changes.
The admin can manage all settings easily under Delivery Fee by 24siteshop in the WordPress dashboard.
Installation Steps
- Download the ZIP file of the plugin.
- In WordPress admin, go to Plugins → Add New → Upload Plugin.
- Upload the ZIP file and click Install Now.
- Activate the plugin.
- Go to Delivery Fee by 24siteshop from the left admin menu.
- Enable the module, set your base fee, per-state fees, and optional COD/Prepaid settings.
- Save your settings.
- Test it by visiting the checkout page — switch between states and payment methods to see automatic updates.