WooCommerce tax calculation mismatch: Inclusive vs exclusive tax on coupon discounts

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:

  1. 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_Discounts applies 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.
  2. Rounding happens at different stages. Discounts are calculated in cents (wc_get_rounding_precision() and integer-based allocation in WC_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.
  3. 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.
See also  Resolving WordPress REST API error 401 unauthorized during application password sync

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.

ModeItem priceCoupon (10%)TaxTotal
Entered inclusive100.00 (incl.)-10.0015.00 / 6 = 1.50 backed out of 90.00 = 15.0090.00
Entered exclusive100.00 (excl.)-10.0018.00 on 90.00108.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.

See also  How to bypass WordPress memory_limit hardcoded by server php.ini limits

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_on set to billing or shipping, 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_price or woocommerce_cart_calculate_fees and 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 flush and wp 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. Check wp plugin get woocommerce --field=version and 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() and wc_remove_number_precision() for calculations that must match WooCommerce internals.
See also  Rank Math SEO Schema Warning: Missing Field "hasCourseInstance" on Custom Post Type

Leave a Comment