Skip to content
PN Scripts

Development

Cash on delivery in WooCommerce: a fair fee and sensible limits

  • PN Scripts
  • 8 min read

In Bulgaria, and in much of Eastern Europe, cash on delivery is still how a large share of online orders are paid. Customers like it because they pay only when the parcel is in their hands. Shops pay for it: the courier charges for collecting the cash and transferring it, and a refused parcel comes back with two shipping costs and no sale. WooCommerce has a Cash on delivery payment method, but it has no fee and no limits. This guide shows how to decide on a fee, calculate it without rounding surprises, limit cash on delivery where it does not make sense, and show all of it to the customer before they order.

#What WooCommerce gives you out of the box

WooCommerce's own Cash on delivery method has a title, a description, instructions, and two rules: "Enable for shipping methods" (offer it only with certain delivery methods) and "Accept for virtual orders". That is all. There is no fee, no minimum or maximum order value, and no way to exclude a product. Shops that need more usually end up with a fee hidden in the shipping price, which customers who pay by card also pay, or with a note in the terms that nobody reads.

#Decide what the fee should cover

Start from your courier contract, not from a competitor's checkout. Couriers usually charge for cash on delivery as a fixed amount, a percentage of the collected sum, or both, sometimes with a minimum. Your fee can mirror that: a fixed part plus a percentage of the order value, with a floor and a ceiling. Many shops also waive the fee above an order value, because the margin on a large order covers it and the waiver makes customers add one more item.

Two choices are easy to overlook:

  • The base of the percentage. Is it the items only, or the items and the shipping? The courier collects the whole sum, so most shops use both. Either way, use the value after discounts: a coupon that cuts the order in half should not leave the fee at full size.
  • Rounding. A percentage gives amounts like 2.8745. Round the fee to a step that looks deliberate, 0.05 or 0.10, and always the same way.

Whether you may charge such a fee, and how it is taxed, depends on your situation; ask your accountant before you switch it on. The arithmetic is the same either way.

#The arithmetic, without float errors

Money in floating point numbers goes wrong in small, embarrassing ways. In PHP, 87.45 * 0.01 + 2 is 2.8745000000000003, and rounding 2.675 down with floor(2.675 * 100) / 100 gives 2.67. The safe way is to turn the amount into whole cents once and do the rounding in integers:

function cod_fee(float $orderValue, array $rule): float
{
    if ($rule['free_from'] !== null && $orderValue >= $rule['free_from']) {
        return 0.0;
    }
    $fee = $rule['fixed'] + $orderValue * $rule['percent'] / 100;
    $fee = max($fee, $rule['min_fee'] ?? 0);
    if ($rule['max_fee'] !== null) {
        $fee = min($fee, $rule['max_fee']);
    }
    // Round half up to the step, in whole cents, so 2.875 never turns into 2.87.
    $cents = (int) round($fee * 100);
    $step = (int) round($rule['step'] * 100);

    return intdiv($cents + intdiv($step, 2), $step) * $step / 100;
}

$rule = ['fixed' => 2.00, 'percent' => 1.0, 'min_fee' => null, 'max_fee' => 6.00, 'step' => 0.05, 'free_from' => 200.00];

With that rule (a fixed 2.00 plus 1 % of the order, at most 6.00, rounded to 0.05, no fee from 200) we ran it on a few orders:

  • an order of 24.90 gives a fee of 2.25;
  • 87.45 gives 2.85;
  • 150.00 gives 3.50;
  • 199.99 gives 4.00;
  • 200.00 and anything above gives no fee;
  • without the "no fee from" rule, an order of 480.00 would hit the ceiling of 6.00.

The 199.99 line is worth showing in the checkout: a customer who sees "no cash on delivery fee from 200" will often add a small item.

#Adding the fee in WooCommerce

A fee belongs in the cart totals, as its own line, calculated only when Cash on delivery is the chosen payment method. WooCommerce calls woocommerce_cart_calculate_fees every time it recalculates the cart, after items and shipping:

add_action('woocommerce_cart_calculate_fees', function (WC_Cart $cart): void {
    if (WC()->session === null || WC()->session->get('chosen_payment_method') !== 'cod') {
        return;
    }
    $value = $cart->get_cart_contents_total() + $cart->get_cart_contents_tax()
        + $cart->get_shipping_total() + $cart->get_shipping_tax();
    $fee = cod_fee($value, COD_RULE);
    if ($fee > 0) {
        $cart->add_fee('Cash on delivery fee', $fee);
    }
});

The catch is the moment of recalculation. The classic checkout does not recalculate when the customer switches payment method, so the fee would appear only after something else changes. A few lines of JavaScript fix that by triggering WooCommerce's own update_checkout event when the payment method changes. Current versions of the block checkout recalculate through the Store API by themselves, but when the order is placed, the totals are calculated before the posted payment method is stored, so the method from the request has to be put into the session first (the woocommerce_store_api_checkout_update_customer_from_request action is the place). Test both checkouts: it is easy to have a fee that shows in one and not in the other.

#Limits: when cash on delivery should not be offered

A fee is half the story. Some orders should not be paid on delivery at all:

  • Large orders. A refused parcel of 1,500 hurts far more than one of 40. A maximum order value for cash on delivery limits the risk.
  • Very small orders. When the courier's fee eats the margin, a minimum order value makes sense.
  • Some delivery methods. Pickup in the shop is paid at the counter; there is nothing to collect on delivery.
  • Some products. Made to order, engraved or cut to size: if the customer refuses, nobody else will buy it. These are better paid in advance.

The filter woocommerce_available_payment_gateways removes Cash on delivery from the list when a rule says no. Do not stop there. A payment method that silently disappears looks like a bug, so tell the customer why: "Cash on delivery is available for orders up to 500.00" or "Cash on delivery is not available for made-to-order items". And make sure an order posted with Cash on delivery anyway, through the Store API or an old open tab, is refused on the server.

#Show the fee before the customer orders

The fee should never be a surprise on the order confirmation. Show it as a separate line in the totals the moment Cash on delivery is picked, name it plainly, mention it in the method's description, and write the same rule in your delivery terms. If there is a "no fee from" amount, say so. A visible, fair fee is accepted; a hidden one becomes a refused parcel. For the rest of the checkout, our checklist for a shop with Econt and Speedy covers delivery, card payments and the legal pages.

#A plugin that does this

We built these rules into Nalozhen, a WooCommerce plugin that adds a fee and limits to WooCommerce's own Cash on delivery method without replacing it. The fee can be fixed, a percentage of the order or the items, or both, with a minimum, a maximum, rounding to 0.01 up to 1.00 (worked out in cents) and a "no fee from" value. Cash on delivery can be limited by order value, switched off or given its own fee for each delivery method, including courier plugins' methods, and switched off for products and categories. The fee appears in the totals as soon as Cash on delivery is picked, in the block checkout and in the classic one, and the customer sees what it costs or why it is missing. It is tested with 19 unit tests, an end-to-end run on WordPress 7.1.3 and WooCommerce 11.2.1 and a browser test of both checkouts, and listed as Coming soon.

#Frequently asked questions

Can I add the fee to the shipping price instead?

You can, but then customers who pay by card pay it too, and you cannot waive it for large orders without touching every shipping rate. A separate fee line is clearer for everyone.

Should the percentage include shipping?

Usually yes, because the courier collects the whole amount, shipping included. Use the value after discounts in either case.

Why does the fee not appear when I switch to Cash on delivery?

In the classic checkout, the totals refresh only when WooCommerce is told to update the checkout. Trigger update_checkout when the payment method changes.

How do I stop cash on delivery for made-to-order products?

Mark those products or their category and remove Cash on delivery from the available payment methods while one of them is in the cart, with a short note that says why.

#Sources

Keep reading

Keep reading

Comments

Comments

Be the first to leave a comment.

Leave a comment

Next step

pnscripts.com/contact

Talk to the team

Ask about an article, or tell us about a project you want built. We reply within one business day.

Write to us

The PN Scripts family

Other PN Scripts sites

Hosting, games and the blog each have their own site, run by the same company.

  • pnscripts.com

    PN Scripts

    Software engineering

    Custom web, mobile, API and game development, plus our open-source products and plugins.

  • games.pnscripts.com

    Games

    Games and game servers

    The home for PN Scripts games: free browser games you can play right now, with game servers coming soon.

  • hosting.pnscripts.com

    Hosting

    Hosting and infrastructure

    Shared hosting, KVM VPS, dedicated servers and domains, from the same company that builds your project.

  • blog.pnscripts.com

    Blog

    Articles and field notes

    Practical articles on software development, hosting, open source and games, written by the people who build them.

    You are here