Skip to content

fix(woocommerce): subtract tax from price filter bounds - #4338

Merged
felipeelia merged 6 commits into
10up:developfrom
faisalahammad:fix/4332-woo-price-filter-tax
Aug 7, 2026
Merged

fix(woocommerce): subtract tax from price filter bounds#4338
felipeelia merged 6 commits into
10up:developfrom
faisalahammad:fix/4332-woo-price-filter-tax

Conversation

@faisalahammad

Copy link
Copy Markdown
Contributor

Summary

On stores with WooCommerce tax enabled, "Prices entered with tax: NO" and "Display prices in the shop: Including tax", the Filter by Price widget returned wrong results. ElasticPress indexed the excluding-tax _price but compared the user's including-tax min_price/max_price bounds directly against it, with no tax conversion on either side. Disabling ElasticPress fixed it because WooCommerce core's native filter converts the bounds.

This fix mirrors WooCommerce core (WC_Query::price_filter_post_clauses): when the shop displays including-tax prices but prices are entered excluding tax, the inclusive tax is subtracted from the bounds before the Elasticsearch range query so they line up with the excluding-tax indexed price.

Fixes #4332

Changes

includes/classes/Feature/WooCommerce/Products.php

Before:

$min_price = ! empty( $_GET['min_price'] ) ? sanitize_text_field( wp_unslash( $_GET['min_price'] ) ) : null;
$max_price = ! empty( $_GET['max_price'] ) ? sanitize_text_field( wp_unslash( $_GET['max_price'] ) ) : null;
// phpcs:enable WordPress.Security.NonceVerification

if ( $query->is_search() ) {

After:

$min_price = ! empty( $_GET['min_price'] ) ? sanitize_text_field( wp_unslash( $_GET['min_price'] ) ) : null;
$max_price = ! empty( $_GET['max_price'] ) ? sanitize_text_field( wp_unslash( $_GET['max_price'] ) ) : null;
// phpcs:enable WordPress.Security.NonceVerification

// Align bounds with the excluding-tax price Elasticsearch indexes when the
// shop shows including-tax prices, matching WooCommerce core.
if ( null !== $min_price ) {
    $min_price = $this->get_price_filter_tax_adjustment( (float) $min_price );
}
if ( null !== $max_price ) {
    $max_price = $this->get_price_filter_tax_adjustment( (float) $max_price );
}

if ( $query->is_search() ) {

Why: A new get_price_filter_tax_adjustment() helper subtracts the inclusive tax from each bound, applied once after the bounds are read so both the search and shop code paths reuse the converted values. No-op whenever tax is off, no tax rates are configured, prices are entered including tax, or the shop displays excluding tax, so unaffected stores behave exactly as before. No mapping or index change, so no reindex is needed.

Testing

Test 1: Reproduce the bug fix (unit)

  1. composer run setup-local-tests if not already set up (needs MySQL + Elasticsearch).
  2. vendor/bin/phpunit --filter testPriceFilterWithTax tests/php/features/WooCommerce/TestWooCommerceProduct.php
  3. The test seeds a 20% tax rate, sends min_price=120&max_price=120, and asserts the Elasticsearch range bound is reduced to 100.0 (the excluding-tax price).
    Result: passes.

Test 2: Existing price filters still green

  1. vendor/bin/phpunit --filter testPriceFilter tests/php/features/WooCommerce/TestWooCommerceProduct.php
  2. vendor/bin/phpunit --filter testPriceFilterWithSearchQuery tests/php/features/WooCommerce/TestWooCommerceProduct.php
    Result: both pass (tax adjustment is a no-op when tax is off).

Test 3: Manual repro (issue #4332)

  1. WooCommerce > Settings > Tax: enable tax, prices entered excluding tax, display including tax.
  2. Add a 20% standard rate for the base location.
  3. Create a product priced 100 (excl tax); it shows 120 (incl tax).
  4. Filter ?min_price=120&max_price=120.
    Result: product appears (before fix it did not).

The Filter by Price widget compared including-tax bounds against the
excluding-tax price indexed in Elasticsearch, so products were dropped.
Mirror WooCommerce core by subtracting inclusive tax from min/max bounds
when prices are entered excluding tax but the shop shows including tax.

Fixes 10up#4332
@felipeelia felipeelia added this to the 5.3.4 milestone Jul 23, 2026
@Sidsector9

Copy link
Copy Markdown
Member

Thanks for the PR @faisalahammad and for the clear description.

The approach is correct and it fixes the case in the issue.

There is one problem to solve before we can merge this. The price filter compares prices using meta._price.long. This field stores the price as a whole number, so a product priced 100.99 is stored as 100. When the tax is subtracted, the filter value is usually not a whole number. Because of this, a product with a price that is not a whole number may not appear in the results.

You can see this yourself. Set a product price to 100.99. The shop will show 121.19. Then open /shop?min_price=121&max_price=200. The product should appear, but it does not. Could you please look into this?

It would be great if you could add a test that uses a price with decimal numbers. The current test uses 100 and 120. Both are whole numbers, so the test passes even when this rounding problem exists.

Let us know if you have any questions.

@Sidsector9
Sidsector9 self-requested a review July 28, 2026 06:27
The price filter range queries targeted meta._price.long, which stores
prices as whole numbers via intval(). Decimal prices like 100.99 lost
their decimal portion, and tax-adjusted bounds became fractional
numbers that no longer matched the truncated stored value, excluding
products from results.

Switch the range query to meta._price.double, which is already indexed
alongside long by prepare_meta_value_types() and used by InstantResults.
No reindex required, no mapping change.

The test now seeds a decimal product price (100.99) and an
incl-tax bound of 121.188 to reproduce the rounding bug directly.

Also update testPriceFilterWithoutTax's assertion, which was still
expecting meta._price.long.

Addresses PR 10up#4338 feedback.

Fixes 10up#4332
@faisalahammad

Copy link
Copy Markdown
Contributor Author

Thanks for flagging the rounding issue. Fixed in 44f4ae5.

The range query now targets meta._price.double (already indexed alongside long). meta._price.long truncates via intval() — 100.99 stored as 100, so tax-adjusted fractional bounds like 100.83 missed it. meta._price.double keeps the full decimal.

Test updated: seeds regular_price 100.99 and min_price=121.188 (the exact incl-tax WC computes for that price), so the test now reproduces the rounding bug directly. Also updated testPriceFilterWithoutTax's stale meta._price.long assertion.

No reindex needed.

Forces WC_Tax::get_rates('') to look up against shop base country (GB),
where the seeded 20% tax rate row lives. Without this, a leftover session
or prior-test setting routes WC_Customer::get_taxable_address() to a
non-GB tuple, returns [] from get_rates, and skips the inclusive-tax
subtraction in Products::get_price_filter_tax_adjustment() -- causing
gte/lte = 121.188 instead of 100.99.

PHP 7.4+ compatible. References PR 10up#4338 (Fixes CI PHPUnit matrix).

Refs 10up#4338
@faisalahammad

Copy link
Copy Markdown
Contributor Author

CI Fix Summary — 1 PHPUnit failure resolved

# File Error Fix
1 tests/php/features/WooCommerce/TestWooCommerceProduct.php:739 ElasticPressTest\TestWooCommerceProduct::testPriceFilterWithTax — expected gte/lte = 100.99, got 121.188 Pin woocommerce_tax_based_on = "base" in test setup so WC_Tax::get_rates("") resolves to the shop base country (GB) where the seeded 20% rate is seeded. Without it, WC_Customer::get_taxable_address() routes to a non-GB tuple, get_rates returns empty, and Products::get_price_filter_tax_adjustment() skips the incl→excl subtraction.

Why the helper itself is unchanged

get_price_filter_tax_adjustment() is correct in production — real HTTP requests populate the customer country via session or geo-IP. The test was just not replicating that state.

Cleanup

The test option-snapshot loop already restores all captured options in finally. Adding woocommerce_tax_based_on to the captured-keys array covers rollback automatically.

Verification

  • Resolves 6/6 failing PHPUnit matrix jobs: Single Site + Multisite × ES 7.10.1 / 8.12.2 / 9.1.5.
  • No regression in testPriceFilter or testPriceFilterWithSearchQuery (both have taxes disabled, never reach WC_Tax::get_rates).
  • PHPCS clean on changed file.
  • CodeRabbit: no findings.
  • PHP 7.4+ compatible (test definitions match CI matrix PHP 8.2).

Out of scope

@paidPlugins E2E jobs already failed on develop for unrelated reasons — fork PRs do not have access to paid-plugin secrets. Not addressed here.

@felipeelia felipeelia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @faisalahammad! Any chance you can include these @since tags so we can finally merge this PR? Thanks!

Comment thread includes/classes/Feature/WooCommerce/Products.php
Comment thread tests/php/features/WooCommerce/TestWooCommerceProduct.php
@faisalahammad

Copy link
Copy Markdown
Contributor Author

@felipeelia Done — added the @since 5.3.4 tags in both suggested spots. Both threads resolved. Ready for re-review when you have a moment.

@felipeelia
felipeelia merged commit 9001816 into 10up:develop Aug 7, 2026
faisalahammad added a commit to faisalahammad/ElasticPress that referenced this pull request Aug 7, 2026
- merge origin/develop into fix/4305-hide-subscription-token (PR 10up#4338 price filter tax fix)
- auto-fix: run phpcbf on includes/classes/ElementorUtils.php:111 equals-align warning
- tests: wrap IS_EPIO_ENVIRONMENT in try/finally for isolation in 3 Settings tests

Errors fixed:
- PHPCS: equals sign not aligned correctly; expected 1 space but found 6 spaces
- PHPUnit: testPriceFilterWithTax float drift (100.99000000000001 vs 100.99)

Refs 10up#4324
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Woo showing wrong results when filtering by price and tax is enabled

3 participants