WooCommerce applies coupon discounts to line-item amounts as they are stored in the product price, and when prices are entered inclusive of tax, the discount is applied to the tax-inclusive figure and the tax is then backed out of the discounted amount, which can differ by a few cents from the tax computed on an exclusive-price basis.
Root Cause Diagnosis
Three mechanisms produce the mismatch:
- Discount base differs by price-entry mode. With WooCommerce > Settings > Tax > Prices entered with tax set to “Yes, I will enter prices inclusive of tax”,
WC_Discountsapplies the coupon to the tax-inclusive price (or to a price WooCommerce has already converted to ex-tax when the customer’s location is outside the base rate). With “No”, the coupon applies to the ex-tax price and tax is added afterward. - Rounding happens at different stages. Discounts are calculated in cents (
wc_get_rounding_precision()and integer-based allocation inWC_Discounts), tax is calculated per line or per rate depending on Round tax at subtotal level, instead of per line, and the displayed totals are rounded again for display. The remainder cents from fixed-cart coupons are distributed across line items in a way that does not map one-to-one to the tax split. - Base-rate vs customer-rate conversion. When prices include tax and the customer’s rate differs from the store base rate (for example, a EU customer outside the base country, or a tax-exempt customer), WooCommerce strips the base tax, then re-adds the customer rate. Applying the coupon before or after that conversion yields a different tax figure than the merchant expects.
Quick Fix
Make rounding deterministic and align the display mode with the entry mode.
WooCommerce > Settings > Tax
- Round tax at subtotal level, instead of per line: enable if your accounting system computes tax on the invoice subtotal; disable if it computes per line.
- Display prices in the shop and Display prices during cart and checkout: set both to the same value as Prices entered with tax.
Then verify with WP-CLI:
wp option get woocommerce_prices_include_tax
wp option get woocommerce_tax_round_at_subtotal
wp option get woocommerce_tax_display_cart
wp option get woocommerce_calc_discounts_sequentially
Step-by-Step Resolution
Step 1: Reproduce with a minimal case
Create one simple product and a 10% coupon, and record the expected figures. Use a 20% tax rate and a price of 100.00.
| Mode | Item price | Coupon (10%) | Tax | Total |
|---|---|---|---|---|
| Entered inclusive | 100.00 (incl.) | -10.00 | 15.00 / 6 = 1.50 backed out of 90.00 = 15.00 | 90.00 |
| Entered exclusive | 100.00 (excl.) | -10.00 | 18.00 on 90.00 | 108.00 |
Expected: the coupon reduces the net by the same proportion in both modes. A discrepancy appears once rounding or multiple rates are introduced.
Step 2: Confirm the stored settings
wp option list --search="woocommerce_*tax*" --format=table
wp option get woocommerce_prices_include_tax
wp option get woocommerce_tax_based_on
woocommerce_tax_based_on accepts shipping, billing, or base. If it is not base and the customer’s rate differs from the base rate, the base-to-customer conversion in cause 3 is active.
Step 3: Choose the discount sequence
WooCommerce > Settings > General > Enable the use of coupon codes is required. Then review:
wp option get woocommerce_calc_discounts_sequentially
When yes, coupons are applied one after another on the already-discounted price. When no, all coupons apply to the original price. With multiple coupons, sequential mode compounds the rounding difference, so keep this consistent with how the invoice is expected to total.
Step 4: Inspect the actual calculation
Log the cart totals after tax calculation.
add_action( 'woocommerce_after_calculate_totals', function ( $cart ) {
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
return;
}
error_log( wp_json_encode( array(
'subtotal' => $cart->get_subtotal(),
'subtotal_tax' => $cart->get_subtotal_tax(),
'discount' => $cart->get_discount_total(),
'discount_tax' => $cart->get_discount_tax(),
'total_tax' => $cart->get_total_tax(),
'total' => $cart->get_total( 'edit' ),
) ) );
}, 99 );
Compare discount + discount_tax against the coupon amount. For inclusive pricing, the gross discount should equal the coupon value, and discount_tax is the tax portion backed out of it.
Step 5: Fix fixed-cart coupon allocation
Fixed-cart coupons are split across line items proportionally, and the leftover cent is assigned by WC_Discounts::apply_coupon_remainder(). If the split yields a mismatch, switch the coupon type.
WooCommerce > Marketing > Coupons > [coupon] > General > Discount type
- Percentage discount produces proportional per-line discounts with the least rounding drift.
- Fixed product discount applies the amount to each unit and avoids remainder distribution.
- Fixed cart discount triggers remainder allocation and is the most likely to differ by one cent from an external calculation.
Step 6: Force a consistent rounding precision (if required)
Only use this if your accounting system uses a different precision from WooCommerce’s internal one.
add_filter( 'woocommerce_tax_round', function ( $in, $value ) {
return round( $value, wc_get_price_decimals() );
}, 10, 2 );
woocommerce_tax_round runs when a tax amount is rounded. Test it against several orders before deploying it, because it changes every tax figure site-wide.
Step 7: Regenerate historical totals if needed
Changing the settings does not alter existing orders. To recalculate a specific order:
wp eval '
$order = wc_get_order( 1234 );
$order->calculate_totals( true );
$order->save();
echo $order->get_total();
'
calculate_totals( true ) recalculates taxes. Run it on staging first, since it overwrites stored line item taxes.
Step 8: Verify
wp wc shop_order get 1234 --user=1 --fields=total,total_tax,discount_total,discount_tax --format=json
Check that discount_total + discount_tax equals the coupon value in inclusive mode.
Why This Fails for Some Users
- Tax-exempt customers with inclusive pricing. WooCommerce removes the base tax from the price, so the customer pays the ex-tax amount. The coupon then applies to the ex-tax value, and the discount is smaller than the displayed coupon amount suggests.
- Customer location differs from base location. With
woocommerce_tax_based_onset tobillingorshipping, the inclusive price is converted to the customer’s rate. Set Adjust prices to match the customer’s tax rate-style behavior deliberately by using a plugin or filter, and do not rely on defaults. - Multiple tax rates on one order. Compound taxes and mixed-rate carts allocate the discount across rate classes proportionally. Rounding at subtotal level versus per line gives different totals.
- Plugin overrides. Extensions that alter prices or coupons (dynamic pricing, bundles, subscriptions, multi-currency, B2B price lists) may run on
woocommerce_product_get_priceorwoocommerce_cart_calculate_feesand change the base before the discount is applied. Disable them one at a time to isolate the cause. - Multi-currency conversion. Converting the coupon amount and the product price separately introduces independent rounding steps. Define the coupon per currency, or use percentage coupons.
- Display vs stored values. Display tax totals (itemized vs as a single total) and Display prices in the shop affect only presentation. The stored order meta can be correct while the on-screen figure looks wrong.
- Third-party tax services. TaxJar, Avalara, and similar services compute tax remotely, using their own rounding and discount allocation. Settings in WooCommerce do not control their output.
- Caching layers. Object cache or page cache serving a stale cart fragment can show totals from a prior tax configuration. Flush with
wp cache flushandwp transient delete --all, then retest. - WooCommerce version differences. Discount allocation was reworked in WooCommerce 3.2 (
WC_Discounts), and the rounding behavior for coupons has changed in later releases. Checkwp plugin get woocommerce --field=versionand the changelog for the version you run. - PHP precision. Floating point arithmetic in custom code can yield off-by-one-cent results. Use
wc_add_number_precision()andwc_remove_number_precision()for calculations that must match WooCommerce internals.