Skip to content

Commit 4f399e0

Browse files
bartechclaude
andcommitted
Fix - Non-taxable product no longer zeroes shared standard tax rate (WOOTAX-240)
In a cart mixing a taxable product and a non-taxable one (Tax Status = "None", Tax Class = "Standard"), taxes stopped calculating for the whole cart. get_itemized_tax_rates() writes one WooCommerce tax rate row per TaxJar breakdown line item, keyed by the product's Tax Class. The non-taxable product is sent to TaxJar as exempt (code 99999) so its breakdown line comes back at 0%, but its Tax Class is still Standard — so create_or_update_tax_rate() overwrote the shared Standard rate row (already populated by the taxable product) with 0%, zeroing tax for every standard item. The zeroed row persisted in wp_woocommerce_tax_rates, which is why adding another taxable product did not restore it. Skip rate-row writes for line items whose product has Tax Status "None". WooCommerce already applies no tax to such products, and this leaves the shared class rate intact for genuinely taxable items. Zero-rate *class* products are unaffected (they own a separate rate row). Note: the tax status check in get_line_items() is not the cause — a tax_status "none" product is already correctly sent as code 99999. The defect was downstream, in how the itemized rate rows are written. Adds a regression test driving get_itemized_tax_rates() with a mixed taxable/exempt breakdown; it fails on trunk (exempt line writes/zeroes the shared row) and passes with this fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f0285f8 commit 4f399e0

4 files changed

Lines changed: 97 additions & 0 deletions

File tree

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Fix - TaxJar tax lines wiped when REST API order update includes address change.
55
* Fix - Prevent fatal error on sites running WooCommerce versions without StoreApi support.
66
* Fix - Prevent unbounded growth of WooCommerce tax rate rows when checkout city contains a semicolon.
7+
* Fix - Calculate tax correctly for carts mixing taxable and non-taxable products, so a non-taxable product no longer resets the standard tax rate to zero.
78

89
= 3.6.7 - 2026-07-06 =
910
* Fix - Prevent fatal error on Atomic sites caused by incorrect path resolution when loading the API client class.

classes/class-wc-connect-taxjar-integration.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,6 +1735,17 @@ private function get_itemized_tax_rates( $taxes, $taxjar_taxes, $options ): arra
17351735
$tax_class = $this->backend_tax_classes[ $product_id ];
17361736
}
17371737

1738+
// A product with Tax Status = "None" is exempt from tax but can still
1739+
// carry the standard Tax Class. TaxJar returns a 0% breakdown line for
1740+
// it, and because tax rate rows are keyed by tax class, writing that 0%
1741+
// overwrites the shared class rate used by genuinely taxable products in
1742+
// the same class — zeroing tax for the whole cart. Skip rate-row writes
1743+
// for non-taxable products; WooCommerce already applies no tax to them.
1744+
// See WOOTAX-240.
1745+
if ( $product && 'none' === $product->get_tax_status() ) {
1746+
continue;
1747+
}
1748+
17381749
$_tax_rates = (array) $line_item;
17391750
$priority = 1;
17401751
foreach ( $_tax_rates as $tax_rate_name => $tax_rate ) {

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ This plugin relies on the following external services:
7474
* Fix - TaxJar tax lines wiped when REST API order update includes address change.
7575
* Fix - Prevent fatal error on sites running WooCommerce versions without StoreApi support.
7676
* Fix - Prevent unbounded growth of WooCommerce tax rate rows when checkout city contains a semicolon.
77+
* Fix - Calculate tax correctly for carts mixing taxable and non-taxable products, so a non-taxable product no longer resets the standard tax rate to zero.
7778

7879
= 3.6.7 - 2026-07-06 =
7980
* Fix - Prevent fatal error on Atomic sites caused by incorrect path resolution when loading the API client class.

tests/php/test-class-wc-connect-taxjar-integration.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,4 +1865,88 @@ public function test_create_or_update_tax_rate_does_not_duplicate_rows_for_semic
18651865
$this->assertStringNotContainsString( ';', $city, 'Tax rate city stored with a semicolon — `_update_tax_rate_cities()` will split it and break find_rates() on subsequent lookups.' );
18661866
}
18671867
}
1868+
1869+
/**
1870+
* A non-taxable product (Tax Status = "None") that shares the standard Tax
1871+
* Class with a taxable product must not zero out the shared Standard rate row.
1872+
*
1873+
* TaxJar returns a 0% breakdown line for the exempt product (sent as code
1874+
* 99999). Because rate rows are keyed by tax class — and Tax Status "None"
1875+
* does not change the Tax Class — that 0% would overwrite the same Standard
1876+
* row the taxable product just populated, zeroing tax for the whole cart.
1877+
* Regression for WOOTAX-240.
1878+
*/
1879+
public function test_get_itemized_tax_rates_non_taxable_line_does_not_zero_standard_rate() {
1880+
$taxable_product = WC_Helper_Product::create_simple_product();
1881+
$taxable_product->set_tax_status( 'taxable' );
1882+
$taxable_product->set_tax_class( '' );
1883+
$taxable_product->save();
1884+
1885+
$exempt_product = WC_Helper_Product::create_simple_product();
1886+
$exempt_product->set_tax_status( 'none' );
1887+
$exempt_product->set_tax_class( '' );
1888+
$exempt_product->save();
1889+
1890+
$taxable_id = $taxable_product->get_id();
1891+
$exempt_id = $exempt_product->get_id();
1892+
$taxable_key = $taxable_id . '-taxable_item';
1893+
$exempt_key = $exempt_id . '-exempt_item';
1894+
1895+
// Taxable line is listed first, so without the fix the exempt line would
1896+
// overwrite the shared Standard rate row to 0% afterwards.
1897+
$taxjar_taxes = (object) array(
1898+
'freight_taxable' => 0,
1899+
'has_nexus' => 1,
1900+
'rate' => 0.0725,
1901+
'jurisdictions' => (object) array(
1902+
'county' => '',
1903+
'city' => '',
1904+
),
1905+
'breakdown' => (object) array(
1906+
'line_items' => array(
1907+
(object) array(
1908+
'id' => $taxable_key,
1909+
'combined_tax_rate' => 0.0725,
1910+
'state_tax_rate' => 0.0725,
1911+
),
1912+
(object) array(
1913+
'id' => $exempt_key,
1914+
'combined_tax_rate' => 0.0,
1915+
'state_tax_rate' => 0.0,
1916+
),
1917+
),
1918+
),
1919+
);
1920+
1921+
$options = array(
1922+
'to_country' => 'US',
1923+
'to_state' => 'CA',
1924+
'to_zip' => '90210',
1925+
'to_city' => 'Beverly Hills',
1926+
);
1927+
1928+
$result = $this->invoke_protected_method(
1929+
'get_itemized_tax_rates',
1930+
array(
1931+
array(
1932+
'rate_ids' => array(),
1933+
'line_items' => array(),
1934+
),
1935+
$taxjar_taxes,
1936+
$options,
1937+
)
1938+
);
1939+
1940+
// The exempt line must not create or update any tax rate row.
1941+
$this->assertArrayNotHasKey( $exempt_key, $result['rate_ids'], 'Non-taxable line item should not write a tax rate row.' );
1942+
1943+
// The taxable line's Standard rate row must remain at 7.25%, not clobbered to 0.
1944+
$this->assertArrayHasKey( $taxable_key, $result['rate_ids'] );
1945+
$taxable_rate_id = $result['rate_ids'][ $taxable_key ][0];
1946+
$rate_data = WC_Tax::_get_tax_rate( $taxable_rate_id );
1947+
$this->assertSame( 7.25, (float) $rate_data['tax_rate'], 'Standard rate row was zeroed by the non-taxable line item (WOOTAX-240).' );
1948+
1949+
WC_Helper_Product::delete_product( $taxable_id );
1950+
WC_Helper_Product::delete_product( $exempt_id );
1951+
}
18681952
}

0 commit comments

Comments
 (0)