Skip to content

Commit 55fa11c

Browse files
bartechclaude
andcommitted
Fix - Validate TaxJar response before applying tax rate override (WOOTAX-74)
maybe_override_taxjar_tax() assumed the TaxJar tax response was always well-formed and assigned properties on breakdown, breakdown->shipping and each line item unconditionally. When TaxJar returns an incomplete response (a null/non-object line item, or a missing breakdown/shipping member) this fataled with "Attempt to assign property ... on null" during cart/checkout tax calculation. Guard each access: skip non-object line items, only touch breakdown and shipping when present and object-typed, and default missing taxable amounts to 0. Behavior is unchanged for well-formed responses. Adds unit tests covering the well-formed override path plus regressions for the null line item, missing breakdown and missing shipping cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f0285f8 commit 55fa11c

4 files changed

Lines changed: 153 additions & 20 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 - Prevent a fatal error during cart and checkout tax calculation when TaxJar returns an incomplete tax response.
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: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,34 +1336,52 @@ public function append_base_address_to_customer_taxable_address( $address ) {
13361336
* @return object
13371337
*/
13381338
public function maybe_override_taxjar_tax( $taxjar_resp_tax, $body ) {
1339-
if ( ! isset( $taxjar_resp_tax ) ) {
1340-
return;
1339+
if ( ! isset( $taxjar_resp_tax ) || ! is_object( $taxjar_resp_tax ) ) {
1340+
return $taxjar_resp_tax;
13411341
}
13421342

1343-
$new_tax_rate = floatval( apply_filters( 'woocommerce_services_override_tax_rate', $taxjar_resp_tax->rate, $taxjar_resp_tax, $body ) );
1343+
$original_rate = isset( $taxjar_resp_tax->rate ) ? floatval( $taxjar_resp_tax->rate ) : 0.0;
1344+
$new_tax_rate = floatval( apply_filters( 'woocommerce_services_override_tax_rate', $taxjar_resp_tax->rate ?? 0, $taxjar_resp_tax, $body ) );
13441345

1345-
if ( $new_tax_rate === floatval( $taxjar_resp_tax->rate ) ) {
1346+
if ( $new_tax_rate === $original_rate ) {
13461347
return $taxjar_resp_tax;
13471348
}
13481349

1349-
if ( ! empty( $taxjar_resp_tax->breakdown->line_items ) ) {
1350-
$taxjar_resp_tax->breakdown->line_items = array_map(
1351-
function ( $line_item ) use ( $new_tax_rate ) {
1352-
$line_item->combined_tax_rate = $new_tax_rate;
1353-
$line_item->country_tax_rate = $new_tax_rate;
1354-
$line_item->country_tax_collectable = $line_item->country_taxable_amount * $new_tax_rate;
1355-
$line_item->tax_collectable = $line_item->taxable_amount * $new_tax_rate;
1350+
// Guard against malformed TaxJar responses: the breakdown and its nested
1351+
// members are not always present or well-formed, and assigning properties
1352+
// on a missing/null member (or a non-object line item) fatals. See WOOTAX-74.
1353+
if ( isset( $taxjar_resp_tax->breakdown ) && is_object( $taxjar_resp_tax->breakdown ) ) {
1354+
$breakdown = $taxjar_resp_tax->breakdown;
13561355

1357-
return $line_item;
1358-
},
1359-
$taxjar_resp_tax->breakdown->line_items
1360-
);
1361-
}
1356+
if ( ! empty( $breakdown->line_items ) && is_array( $breakdown->line_items ) ) {
1357+
$breakdown->line_items = array_map(
1358+
function ( $line_item ) use ( $new_tax_rate ) {
1359+
if ( ! is_object( $line_item ) ) {
1360+
return $line_item;
1361+
}
1362+
1363+
$country_taxable_amount = isset( $line_item->country_taxable_amount ) ? $line_item->country_taxable_amount : 0;
1364+
$taxable_amount = isset( $line_item->taxable_amount ) ? $line_item->taxable_amount : 0;
1365+
1366+
$line_item->combined_tax_rate = $new_tax_rate;
1367+
$line_item->country_tax_rate = $new_tax_rate;
1368+
$line_item->country_tax_collectable = $country_taxable_amount * $new_tax_rate;
1369+
$line_item->tax_collectable = $taxable_amount * $new_tax_rate;
13621370

1363-
$taxjar_resp_tax->breakdown->combined_tax_rate = $new_tax_rate;
1364-
$taxjar_resp_tax->breakdown->country_tax_rate = $new_tax_rate;
1365-
$taxjar_resp_tax->breakdown->shipping->combined_tax_rate = $new_tax_rate;
1366-
$taxjar_resp_tax->breakdown->shipping->country_tax_rate = $new_tax_rate;
1371+
return $line_item;
1372+
},
1373+
$breakdown->line_items
1374+
);
1375+
}
1376+
1377+
$breakdown->combined_tax_rate = $new_tax_rate;
1378+
$breakdown->country_tax_rate = $new_tax_rate;
1379+
1380+
if ( isset( $breakdown->shipping ) && is_object( $breakdown->shipping ) ) {
1381+
$breakdown->shipping->combined_tax_rate = $new_tax_rate;
1382+
$breakdown->shipping->country_tax_rate = $new_tax_rate;
1383+
}
1384+
}
13671385

13681386
$taxjar_resp_tax->rate = $new_tax_rate;
13691387

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 - Prevent a fatal error during cart and checkout tax calculation when TaxJar returns an incomplete tax response.
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: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,4 +1865,117 @@ 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+
* Build a well-formed TaxJar tax response object (as returned under
1871+
* $taxjar_response->tax) with a base rate of 0.08.
1872+
*
1873+
* @return object
1874+
*/
1875+
private function build_taxjar_tax_response() {
1876+
return (object) array(
1877+
'rate' => 0.08,
1878+
'breakdown' => (object) array(
1879+
'combined_tax_rate' => 0.08,
1880+
'country_tax_rate' => 0.0,
1881+
'shipping' => (object) array(
1882+
'combined_tax_rate' => 0.08,
1883+
'country_tax_rate' => 0.0,
1884+
),
1885+
'line_items' => array(
1886+
(object) array(
1887+
'combined_tax_rate' => 0.08,
1888+
'country_tax_rate' => 0.0,
1889+
'country_taxable_amount' => 100.0,
1890+
'taxable_amount' => 100.0,
1891+
'country_tax_collectable' => 0.0,
1892+
'tax_collectable' => 8.0,
1893+
),
1894+
),
1895+
),
1896+
);
1897+
}
1898+
1899+
/**
1900+
* The override filter should still rewrite every rate on a well-formed
1901+
* response (behavior preserved).
1902+
*/
1903+
public function test_maybe_override_taxjar_tax_applies_override_to_wellformed_response() {
1904+
add_filter( 'woocommerce_services_override_tax_rate', '__return_zero' );
1905+
1906+
$resp = $this->build_taxjar_tax_response();
1907+
$result = $this->integration->maybe_override_taxjar_tax( $resp, array() );
1908+
1909+
$this->assertSame( 0.0, $result->rate );
1910+
$this->assertSame( 0.0, $result->breakdown->combined_tax_rate );
1911+
$this->assertSame( 0.0, $result->breakdown->country_tax_rate );
1912+
$this->assertSame( 0.0, $result->breakdown->shipping->combined_tax_rate );
1913+
$this->assertSame( 0.0, $result->breakdown->shipping->country_tax_rate );
1914+
1915+
$line_item = $result->breakdown->line_items[0];
1916+
$this->assertSame( 0.0, $line_item->combined_tax_rate );
1917+
$this->assertSame( 0.0, $line_item->country_tax_rate );
1918+
$this->assertSame( 0.0, $line_item->country_tax_collectable );
1919+
$this->assertSame( 0.0, $line_item->tax_collectable );
1920+
}
1921+
1922+
/**
1923+
* A null (or otherwise non-object) line item must not fatal; it is left
1924+
* untouched while valid line items are still overridden. Regression for the
1925+
* reported "Attempt to assign property on null" fatal (WOOTAX-74).
1926+
*/
1927+
public function test_maybe_override_taxjar_tax_survives_null_line_item() {
1928+
add_filter( 'woocommerce_services_override_tax_rate', '__return_zero' );
1929+
1930+
$resp = $this->build_taxjar_tax_response();
1931+
$valid_line_item = $resp->breakdown->line_items[0];
1932+
$resp->breakdown->line_items = array( null, $valid_line_item );
1933+
1934+
$result = $this->integration->maybe_override_taxjar_tax( $resp, array() );
1935+
1936+
$this->assertSame( 0.0, $result->rate );
1937+
$this->assertNull( $result->breakdown->line_items[0] );
1938+
$this->assertSame( 0.0, $result->breakdown->line_items[1]->combined_tax_rate );
1939+
}
1940+
1941+
/**
1942+
* A response with no breakdown must not fatal. Regression for WOOTAX-74.
1943+
*/
1944+
public function test_maybe_override_taxjar_tax_survives_missing_breakdown() {
1945+
add_filter( 'woocommerce_services_override_tax_rate', '__return_zero' );
1946+
1947+
$resp = (object) array( 'rate' => 0.08 );
1948+
1949+
$result = $this->integration->maybe_override_taxjar_tax( $resp, array() );
1950+
1951+
$this->assertSame( 0.0, $result->rate );
1952+
$this->assertObjectNotHasProperty( 'breakdown', $result );
1953+
}
1954+
1955+
/**
1956+
* A breakdown missing its shipping member must not fatal. Regression for WOOTAX-74.
1957+
*/
1958+
public function test_maybe_override_taxjar_tax_survives_missing_shipping() {
1959+
add_filter( 'woocommerce_services_override_tax_rate', '__return_zero' );
1960+
1961+
$resp = $this->build_taxjar_tax_response();
1962+
unset( $resp->breakdown->shipping );
1963+
1964+
$result = $this->integration->maybe_override_taxjar_tax( $resp, array() );
1965+
1966+
$this->assertSame( 0.0, $result->rate );
1967+
$this->assertSame( 0.0, $result->breakdown->combined_tax_rate );
1968+
}
1969+
1970+
/**
1971+
* With no override filter registered the response is returned unchanged.
1972+
*/
1973+
public function test_maybe_override_taxjar_tax_returns_unchanged_without_override() {
1974+
$resp = $this->build_taxjar_tax_response();
1975+
1976+
$result = $this->integration->maybe_override_taxjar_tax( $resp, array() );
1977+
1978+
$this->assertSame( $resp, $result );
1979+
$this->assertSame( 0.08, $result->rate );
1980+
}
18681981
}

0 commit comments

Comments
 (0)