Debugging a WooCommerce discount that applied twice
Summary: a WooCommerce store I run was charging customers 51% off a product that should have been 30% off. The product page also showed no discount at all. Two separate bugs were hiding behind one complaint. I traced both, fixed them, verified the fix against real cart math, and checked every past order to measure the damage: one order, undercharged $75.60.
The symptom
The store is Lighthouse Laser Co., which sells custom wood signs. Products are configured with a product designer and an options plugin, and a custom plugin I wrote applies percentage discounts. Two things were wrong:
- On the product page, the discounted price never appeared. Customers saw full price until they reached the cart.
- In the cart, a 30% discount came out much larger than 30%.
Bug 1: the discount display that never loaded
The plugin’s PHP was clearly running, because the cart discount worked. But its JavaScript, which draws the “Final Price” line on the product page, never ran. In the browser’s network panel the script request “succeeded,” yet the response was the site’s 404 page, not JavaScript.
The cause was file permissions. The plugin’s folders were set to 0700. On this server, PHP runs as the site’s own user, so it could read those folders fine. Static files like JavaScript and CSS are served directly by the web server process, which could not enter a 0700 folder, so it returned the 404 page instead. Setting the folders to 0755 fixed it.
Two follow-ups made the fix stick:
- Browsers had cached the 404 response for that exact URL, so I bumped the plugin’s version number to change the asset URL.
- The script also ran too early. The options plugin builds its price total asynchronously, so the element my script attached to did not exist yet. I changed it to wait for that element before placing the discount line.
Bug 2: the discount applied twice
This was the one that cost money. The plugin applied the discount inside WooCommerce’s woocommerce_before_calculate_totals hook. That hook does not fire once per page load. It can fire several times in a single request as WooCommerce recalculates the cart.
Each pass read the item’s current price and discounted it. But after the first pass, the “current price” was already discounted. So a 30% discount became 0.7 × 0.7 = 0.49 of the price, or 51% off. A third pass would have made it worse. The code even set a “discount applied” flag, but never checked it.
Simplified, the old logic looked like this:
// Runs on every pass of woocommerce_before_calculate_totals
$price = $product->get_price(); // already discounted after pass 1
$product->set_price( $price * ( 1 - $discount ) );
The fix
I made the calculation idempotent: running it once or ten times gives the same answer. On the first pass the plugin stores the item’s original price in the cart item data. Every pass after that calculates from the stored original instead of the current price.
if ( ! isset( $cart_item['original_price'] ) ) {
$cart_item['original_price'] = $product->get_price();
}
$product->set_price( $cart_item['original_price'] * ( 1 - $discount ) );
Hook order mattered too. The options plugin sets the cart price to base price plus selected options. My hook runs at priority 9999, after it, so the discount applies to the full configured price and not just the base. I documented that so nobody “tidies up” the priority later.
Verifying it
I tested with a real configuration: a $149.99 base sign with $209.99 of options, $359.99 in total.
| Before the fix | After the fix | |
|---|---|---|
| Expected price at 30% off | $251.99 | $251.99 |
| Price charged | $176.39 (2 passes) | $251.99 (stable over 3 passes) |
| Difference | $75.60 undercharged | $0.00 |
I also searched every past order to find out how far the bug reached. Only one order was affected. An earlier order predated the discount and was charged full price.
What I’d change next
The deeper problem is that the price is calculated in two places: once in PHP for the cart, and once in JavaScript for the product page display. They can drift apart and disagree. The right long-term fix is a single source of truth: calculate the price once on the server and have the product page ask for it. I wrote that up as the next step for the plugin.
Takeaways
- WooCommerce cart hooks can run many times per request. Anything that changes a price there has to be idempotent.
- If a plugin’s PHP works but its JavaScript or CSS returns 404, check the folder permissions, not just the file permissions.
- After fixing a money bug, measure the impact on real orders. “Fixed” is not the same as “we know what it cost.”