Auto-apply coupon: how 209 lines of code replaced ~13,000

Before BFCM last year, on WooCommerce.com, we were running a third-party plugin to apply tiered discounts. That plugin worked, but it came with a lot of overhead.

So we investigated replacing it for BFCM.

The idea was simple: just use WooCommerce’s built-in coupon system with a small plugin that auto-applies the right coupon silently on cart recalculation. All the actual validation like usage limits, product restrictions, and expiry happens in WooCommerce, so we don’t reimplement anything.

Final result: +209 lines, -12,888 lines, and a simpler codebase 🙂

What the plugin does

  • Adds a checkbox to the coupon edit screen: “Apply coupon automatically”
  • On cart recalculation, fetches coupons marked with _auto_apply = yes (cached 12 hours, this helps with high-volume stores like WooCommerce.com), checks each via WC_Coupon::is_valid(), and applies or removes it silently (no notices shown to the customer)
  • Hides the coupon remove button in both block cart and classic checkout, to enforce BFCM discount
  • WooCommerce.com-specific: prevents BFCM coupons from applying to subscription renewals and other selected flows
// Get all auto-apply coupon codes.
$auto_apply_codes = $this->get_auto_apply_coupon_codes();

foreach ( $auto_apply_codes as $code ) {
	$coupon = new WC_Coupon( $code );

	// Skip if coupon doesn't exist or isn't marked for auto-apply.
	if ( ! $coupon->get_id() || ! $coupon->get_auto_apply() ) {
		continue;
	}

	$is_valid = $coupon->is_valid();

	// Remove if applied but no longer valid.
	if ( $this->has_discount( $code ) && ! $is_valid ) {
		$this->remove_coupon( $code );
	}

	// Apply if not applied but now valid.
	if ( ! $this->has_discount( $code ) && $is_valid ) {
		$this->apply_coupon( $code );
	}
}

The only tricky bit for the WooCommerce.com-specific implementation was the re-entrancy guard: applying a coupon triggers woocommerce_after_calculate_totals again, so we have a static $running flag to avoid the loop. Without that there was infinite recursion, so that one was fun to debug 🙂

BFCM 2025

The plugin got its first real test during BFCM 2025 (Nov 19 – Dec 2). Tens of thousands of completed orders across tens of thousands of unique customers, all three discount tiers (20/30/40%), and we had no coupon bugs nor any outages.

Working with WooCommerce coupons had other benefits as well, for example, multi-currency was handled out of the box. The numbers came out well too: refund rates dropped from 13.1% to 7.8%, and net cash per customer increased +25% YoY.

If you want to pull the same breakdown for a future sale (adjust or remove the coupon names):

SELECT
    oi.order_item_name                  AS coupon_code,
    COUNT(DISTINCT oi.order_id)         AS orders_with_coupon,
    COUNT(DISTINCT o.customer_id)       AS unique_customers
FROM wp_woocommerce_order_items oi
JOIN wp_wc_orders o ON oi.order_id = o.id
WHERE oi.order_item_type = 'coupon'
  AND oi.order_item_name IN ('sale-20%', 'sale-30%', 'sale-40%')
  AND o.date_created_gmt BETWEEN '2025-11-19 14:00:00' AND '2025-12-02 23:59:59'
  AND o.status IN ('wc-completed', 'wc-processing')
GROUP BY oi.order_item_name
ORDER BY orders_with_coupon DESC;

Open source

I filed a draft PR upstream: https://github.com/woocommerce/woocommerce/pull/61958. The WooCommerce.com-specific restrictions aren’t there, but the core mechanic _auto_apply (meta + silent apply/remove on cart recalculation) feels like something WooCommerce could support natively.

So, we’ve prioritized this in WooCommerce core, and it will ship in a future release. In the meantime, feel free to use the PR as a reference or as a starting point for your own mini-plugin.

What’s left

A few rough edges worth noting:

  • The _auto_apply meta lives in wp_postmeta. When coupon HPOS migration happens, this will need revisiting (see query in WC_Cart::get_auto_apply_coupon_codes)
  • WooCommerce.com specific implementation
    • The JS hides remove buttons by targeting specific block class names that could drift across WooCommerce versions
    • The BFCM coupon names are hardcoded with a filter, which is fine for now, but worth cleaning up if the plugin gets more general use

Moral of the story

Less code, less headaches.

WooCommerce’s coupon system is actually pretty powerful once you get out of its way. We had a whole plugin doing what a checkbox and 200 lines of PHP can do more reliably. The trade-off was clearly worth it.


4 responses to “Auto-apply coupon: how 209 lines of code replaced ~13,000”

  1. Wait… What? Why wasn’t this the standard approach into he first place? This seems incredibly obvious and I’m amazed a plugin was used for such functionality when you have developers in-house. Am I reading this correctly that it was implemented on WooCommerce.com?

  2. Brian Avatar

    Would love to see this feature in core!

  3. “doing what a checkbox and 200 lines of PHP can do more reliably” … that’s not necessarily true though, is it?

    This PR is simplistic and it works specifically for your use case.

    There’s no conditions around when/if a coupon should be added/removed, it doesn’t handle quantity changes, what happens if there’s multiple auto applied coupons, there’s no trigger on page load/cart hydration, and it also recalcs more than is required so is currently worse for performance (opposite of what the post claims with no evidence mind you).

    Additionally, the only testing this has had is on Woo’s own website.

    Sorry but that’s not representative of the millions of WooCommerce stores out there.

    Simplifying it with a nice bow at the end of the post like that comment is just wrong and hurtful.

    Imho, a feature like this belongs in a plugin. It is not something every store needs. To me, that should be the litmus test of what belongs in core.

    Sure, I cop that I might be biased here. After all, I am the owner of one of said plugins (those who know me know which one). But I’m not upset you’re considering putting one feature of it in core. Far from it actually, I love it when core is proactive like this. It was the way it was worded and presented as a done deal and superior to any other possible solution with zero evidence or comparison.

    For what it’s worth, our solution is actually battle tested. It’s had almost 10 years of huge promotions, BFCM and otherwise, across thousands and thousands of stores (nearly $1b of discounted orders processed in the last 12 months alone). And I know there’s other similar plugins in the space that are doing the same or better.

    Anyway, this, and similar moves in recent weeks, feel like Woo core is reaching ever more into the realm of 3rd party devs when they should be focusing on bigger things like UCP/A2P, improving Store API,… Guys… not every store needs these things, they don’t belong in core. That’s actually the power of having a robust ecosystem of 3rd party devs, we have the luxury of going after smaller segments of the market than Woo core, we’re committed to continually developing and maintaining smaller add-on features that not every store needs. Let us support you so you can do the most important work to ensure Woo’s survival!

    Presenting this as a replacement and painting the plugin it replaces as evil… well all that’s doing is tar and feathering people’s good work in the ecosystem. And it’s pushing the false narrative of “all plugins baddddd, slap it all in core and all will be better!”

    I’m sorry but that’s is a dangerous ideology for an ecosystem that largely relies on 3rd party devs to grow.

    1. Boro Sitnikovski Avatar
      Boro Sitnikovski

      Hi Josh, thanks for the detailed comment!

      This PR is simplistic and it works specifically for your use case.

      Correct, we were not trying to build a generic auto-coupon solution, but focused on the exact use case we had for BFCM.

      The plugin we had used solved a broader problem and did not rely on coupons at all, but on WooCommerce price hooks, with its engine applied across the store. For our use case, we only needed a much narrower behavior, so moving back to native coupon validation gave us a smaller and more concrete solution.

      Presenting this as a replacement and painting the plugin it replaces as evil

      I’m sorry about the way this came across, that was definitely not the intention.

      The conclusion was based on our experience with this specific plugin and use case. That said, I can see how “less code, less headaches” and “more reliably” read as a broader takeaway about established plugins.

      The main story was about trade-offs, and that sometimes a concrete solution may be a better fit than a highly generic one.

      The technical concerns you raised around quantity changes, multiple coupons, hydration and recalculations are valid considerations. Some were outside the scope of the specific behavior we needed, but the post could have been clearer about that boundary.

Leave a Reply

Your email address will not be published. Required fields are marked *