diff --git a/changelog.txt b/changelog.txt
index f8ade2fcc..e2f1f18f6 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,8 @@
*** WooCommerce Tax Changelog ***
+= 3.4.0 - 2026-xx-xx =
+* Revamp - Tax Rate Table population logic.
+
= 3.3.1 - 2026-01-12 =
* Fix - Normalize state and country codes to uppercase in TaxJar integration.
diff --git a/classes/class-wc-connect-taxjar-integration.php b/classes/class-wc-connect-taxjar-integration.php
index ed9a11f19..d3f7731d0 100644
--- a/classes/class-wc-connect-taxjar-integration.php
+++ b/classes/class-wc-connect-taxjar-integration.php
@@ -187,47 +187,39 @@ public function init() {
$store_settings = $this->get_store_settings();
$store_country = $store_settings['country'];
- // TaxJar supports USA, Canada, Australia, and the European Union
+ // TaxJar supports USA, Canada, Australia, and the European Union.
if ( ! $this->is_supported_country( $store_country ) ) {
return;
}
- // Add toggle for automated taxes to the core settings page
+ // Add toggle for automated taxes to the core settings page.
add_filter( 'woocommerce_tax_settings', array( $this, 'add_tax_settings' ) );
- // Fix tooltip with link on older WC.
- if ( version_compare( WOOCOMMERCE_VERSION, '4.4.0', '<' ) ) {
- add_action( 'admin_enqueue_scripts', array( $this, 'fix_tooltip_keepalive' ), 11 );
- }
-
- // Settings values filter to handle the hardcoded settings
+ // Settings values filter to handle the hardcoded settings.
add_filter( 'woocommerce_admin_settings_sanitize_option', array( $this, 'sanitize_tax_option' ), 10, 2 );
- // Bow out if we're not wanted
+ // Bow out if we're not wanted.
if ( ! $this->is_enabled() ) {
return;
}
- // Scripts / Stylesheets
+ // Scripts / Stylesheets.
add_action( 'admin_enqueue_scripts', array( $this, 'load_taxjar_admin_new_order_assets' ) );
$this->configure_tax_settings();
- // Calculate Taxes at Cart / Checkout
- if ( class_exists( 'WC_Cart_Totals' ) ) { // Woo 3.2+
- add_action( 'woocommerce_after_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
- } else {
- add_action( 'woocommerce_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
- }
+ // Calculate Taxes at Cart / Checkout.
+ add_action( 'woocommerce_after_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
- // Calculate Taxes for Backend Orders (Woo 2.6+)
+ // Calculate Taxes for Backend Orders.
add_action( 'woocommerce_before_save_order_items', array( $this, 'calculate_backend_totals' ), 20 );
- // Set customer taxable location for local pickup
+ // Set customer taxable location for local pickup.
add_filter( 'woocommerce_customer_taxable_address', array( $this, 'append_base_address_to_customer_taxable_address' ), 10, 1 );
add_filter( 'woocommerce_calc_tax', array( $this, 'override_woocommerce_tax_rates' ), 10, 3 );
- add_filter( 'woocommerce_matched_rates', array( $this, 'allow_street_address_for_matched_rates' ), 10, 2 );
+ add_filter( 'woocommerce_get_tax_location', array( $this, 'get_tax_location' ), 20, 3 );
+ add_filter( 'woocommerce_order_get_tax_location', array( $this, 'get_tax_location_for_order' ), 20 );
add_filter( 'woocommerce_rate_label', array( $this, 'cleanup_tax_label' ) );
@@ -240,7 +232,7 @@ public function init() {
* @return bool
*/
public function is_enabled() {
- // Migrate automated taxes selection from the setup wizard
+ // Migrate automated taxes selection from the setup wizard.
if ( get_option( self::SETUP_WIZARD_OPTION_NAME ) ) {
update_option( self::OPTION_NAME, 'yes' );
delete_option( self::SETUP_WIZARD_OPTION_NAME );
@@ -254,7 +246,7 @@ public function is_enabled() {
/**
* Add our "automated taxes" setting to the core group.
*
- * @param array $tax_settings WooCommerce Tax Settings
+ * @param array $tax_settings WooCommerce Tax Settings.
*
* @return array
*/
@@ -293,11 +285,11 @@ public function add_tax_settings( $tax_settings ) {
),
);
- // Insert the "automated taxes" setting at the top (under the section title)
+ // Insert the "automated taxes" setting at the top (under the section title).
array_splice( $tax_settings, 1, 0, array( $automated_taxes ) );
if ( $enabled ) {
- // If the automated taxes are enabled, disable the settings that would be reverted in the original plugin
+ // If the automated taxes are enabled, disable the settings that would be reverted in the original plugin.
foreach ( $tax_settings as $index => $tax_setting ) {
if ( empty( $tax_setting['id'] ) || ! array_key_exists( $tax_setting['id'], $this->expected_options ) ) {
continue;
@@ -330,33 +322,6 @@ private function get_tax_tooltip() {
return sprintf( __( 'Your tax rates are now automatically calculated for %1$s. Automated taxes uses your store address as your "tax nexus". If you want to charge tax for any other state, you can add a %2$stax rate%3$s for that state in addition to using automated taxes. %4$sLearn more about Tax Nexus here%5$s.', 'woocommerce-services' ), $country_state, '', '', '', '' );
}
- /**
- * Hack to force keepAlive: true on tax setting tooltip.
- */
- public function fix_tooltip_keepalive() {
- global $pagenow;
- if ( 'admin.php' !== $pagenow || ! isset( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] || ! isset( $_GET['tab'] ) || 'tax' !== $_GET['tab'] || ! empty( $_GET['section'] ) ) {
- return;
- }
-
- $tooltip = $this->get_tax_tooltip();
- // Links in tooltips will not work unless keepAlive is true.
- wp_add_inline_script(
- 'woocommerce_admin',
- "jQuery( function () {
- jQuery( 'label[for=wc_connect_taxes_enabled] .woocommerce-help-tip')
- .off( 'mouseenter mouseleave' )
- .tipTip( {
- 'fadeIn': 50,
- 'fadeOut': 50,
- 'delay': 200,
- keepAlive: true,
- content: '" . $tooltip . "'
- } );
- } );"
- );
- }
-
/**
* When automated taxes are enabled, overwrite core tax settings that might break the API integration
* This is similar to the original plugin functionality where these options were reverted on page load
@@ -369,22 +334,23 @@ public function fix_tooltip_keepalive() {
public function sanitize_tax_option( $value, $option ) {
// phpcs:disable WordPress.Security.NonceVerification.Missing --- Security is taken care of by WooCommerce
if (
- // skip unrecognized option format
+ // skip unrecognized option format.
! is_array( $option )
- // skip if unexpected option format
+ // skip if unexpected option format.
|| ! isset( $option['id'] )
- // skip if not enabled or not being enabled in the current request
- || ! $this->is_enabled() && ( ! isset( $_POST[ self::OPTION_NAME ] ) || 'yes' != $_POST[ self::OPTION_NAME ] ) ) {
+ // skip if not enabled or not being enabled in the current request.
+ || ( ! $this->is_enabled() && ( ! isset( $_POST[ self::OPTION_NAME ] ) || 'yes' !== $_POST[ self::OPTION_NAME ] ) )
+ ) {
return $value;
}
- // the option is currently being enabled - backup the rates and flush the rates table
+ // the option is currently being enabled - backup the rates and flush the rates table.
if ( ! $this->is_enabled() && self::OPTION_NAME === $option['id'] && 'yes' === $value ) {
$this->backup_existing_tax_rates();
return $value;
}
- // skip if unexpected option
+ // skip if unexpected option.
if ( ! array_key_exists( $option['id'], $this->expected_options ) ) {
return $value;
}
@@ -401,7 +367,7 @@ public function sanitize_tax_option( $value, $option ) {
*/
public function configure_tax_settings() {
foreach ( $this->expected_options as $option => $value ) {
- // first check the option value - with default memory caching this should help to avoid unnecessary DB operations
+ // first check the option value - with default memory caching this should help to avoid unnecessary DB operations.
if ( get_option( $option ) !== $value ) {
update_option( $option, $value );
}
@@ -427,7 +393,7 @@ public function get_supported_countries() {
* @return bool Whether or not the country is supported by TaxJar.
*/
public function is_supported_country( $country ) {
- return in_array( $country, $this->get_supported_countries() );
+ return in_array( $country, $this->get_supported_countries(), true );
}
/**
@@ -454,7 +420,7 @@ public function get_store_settings() {
* @param $message
*/
public function _log( $message ) {
- $formatted_message = is_scalar( $message ) ? $message : json_encode( $message );
+ $formatted_message = is_scalar( $message ) ? $message : wp_json_encode( $message );
$this->logger->log( $formatted_message, 'WCS Tax' );
}
@@ -463,9 +429,9 @@ public function _log( $message ) {
* @param $message
*/
public function _error( $message ) {
- $formatted_message = is_scalar( $message ) ? $message : json_encode( $message );
+ $formatted_message = is_scalar( $message ) ? $message : wp_json_encode( $message );
- // ignore error messages caused by customer input
+ // Ignore error messages caused by customer input.
$state_zip_mismatch = false !== strpos( $formatted_message, 'to_zip' ) && false !== strpos( $formatted_message, 'is not used within to_state' );
$invalid_postcode = false !== strpos( $formatted_message, 'isn\'t a valid postal code for' );
$malformed_postcode = false !== strpos( $formatted_message, 'zip code has incorrect format' );
@@ -530,21 +496,16 @@ public function calculate_totals( $wc_cart_object ) {
return;
}
- $cart_taxes = array();
- $cart_tax_total = 0;
-
/**
* WC Coupon object.
*
* @var WC_Coupon $coupon
*/
foreach ( $wc_cart_object->coupons as $coupon ) {
- if ( method_exists( $coupon, 'get_limit_usage_to_x_items' ) ) { // Woo 3.0+.
- $limit_usage_qty = $coupon->get_limit_usage_to_x_items();
+ $limit_usage_qty = $coupon->get_limit_usage_to_x_items();
- if ( $limit_usage_qty ) {
- $coupon->set_limit_usage_to_x_items( $limit_usage_qty );
- }
+ if ( $limit_usage_qty ) {
+ $coupon->set_limit_usage_to_x_items( $limit_usage_qty );
}
}
@@ -585,31 +546,21 @@ public function calculate_totals( $wc_cart_object ) {
$product = $cart_item['data'];
$line_item_key = $product->get_id() . '-' . $cart_item_key;
if ( isset( $taxes['line_items'][ $line_item_key ] ) && ! $taxes['line_items'][ $line_item_key ]->combined_tax_rate ) {
- if ( method_exists( $product, 'set_tax_status' ) ) {
- $product->set_tax_status( 'none' ); // Woo 3.0+
- } else {
- $product->tax_status = 'none'; // Woo 2.6
- }
+ $product->set_tax_status( 'none' );
}
}
- // Recalculate shipping package rates
+ // Recalculate shipping package rates.
foreach ( $wc_cart_object->get_shipping_packages() as $package_key => $package ) {
WC()->session->set( 'shipping_for_package_' . $package_key, null );
}
- if ( class_exists( 'WC_Cart_Totals' ) ) { // Woo 3.2+
- do_action( 'woocommerce_cart_reset', $wc_cart_object, false );
- do_action( 'woocommerce_before_calculate_totals', $wc_cart_object );
- new WC_Cart_Totals( $wc_cart_object );
- remove_action( 'woocommerce_after_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
- do_action( 'woocommerce_after_calculate_totals', $wc_cart_object );
- add_action( 'woocommerce_after_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
- } else {
- remove_action( 'woocommerce_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
- $wc_cart_object->calculate_totals();
- add_action( 'woocommerce_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
- }
+ do_action( 'woocommerce_cart_reset', $wc_cart_object, false );
+ do_action( 'woocommerce_before_calculate_totals', $wc_cart_object );
+ new WC_Cart_Totals( $wc_cart_object );
+ remove_action( 'woocommerce_after_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
+ do_action( 'woocommerce_after_calculate_totals', $wc_cart_object );
+ add_action( 'woocommerce_after_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
}
/**
@@ -618,17 +569,19 @@ public function calculate_totals( $wc_cart_object ) {
* Unchanged from the TaxJar plugin.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/96b5d57/includes/class-wc-taxjar-integration.php#L557
*
+ * @param int $order_id WC Order ID.
+ *
* @return void
*/
public function calculate_backend_totals( $order_id ) {
- $order = wc_get_order( $order_id );
+ $order = wc_get_order( $order_id );
+ if ( ! $order instanceof WC_Order ) {
+ return;
+ }
$address = $this->get_backend_address();
$line_items = $this->get_backend_line_items( $order );
- if ( method_exists( $order, 'get_shipping_total' ) ) {
- $shipping = $order->get_shipping_total(); // Woo 3.0+
- } else {
- $shipping = $order->get_total_shipping(); // Woo 2.6
- }
+ $shipping = $order->get_shipping_total();
+
$taxes = $this->calculate_tax(
array(
'to_country' => $address['to_country'],
@@ -640,27 +593,28 @@ public function calculate_backend_totals( $order_id ) {
'line_items' => $line_items,
)
);
- if ( class_exists( 'WC_Order_Item_Tax' ) ) { // Add tax rates manually for Woo 3.0+
+ if ( class_exists( 'WC_Order_Item_Tax' ) ) { // Add tax rates manually for Woo 3.0+.
/**
- * @var WC_Order_Item_Product $item Product Order Item.
+ * Order Item Product.
+ *
+ * @var WC_Order_Item_Product $item Order Item Product.
*/
foreach ( $order->get_items() as $item_key => $item ) {
$product_id = $item->get_product_id();
$line_item_key = $product_id . '-' . $item_key;
if ( isset( $taxes['rate_ids'][ $line_item_key ] ) ) {
- $rate_id = $taxes['rate_ids'][ $line_item_key ];
- $item_tax = new WC_Order_Item_Tax();
- $item_tax->set_rate( $rate_id );
- $item_tax->set_order_id( $order_id );
- $item_tax->save();
+ $rate_ids = $taxes['rate_ids'][ $line_item_key ];
+ if ( ! is_array( $rate_ids ) ) {
+ continue;
+ }
+ foreach ( $rate_ids as $rate_id ) {
+ $item_tax = new WC_Order_Item_Tax();
+ $item_tax->set_rate( $rate_id );
+ $item_tax->set_order_id( $order_id );
+ $item_tax->save();
+ }
}
}
- } elseif ( class_exists( 'WC_AJAX' ) ) { // Recalculate tax for Woo 2.6 to apply new tax rates
- remove_action( 'woocommerce_before_save_order_items', array( $this, 'calculate_backend_totals' ), 20 );
- if ( check_ajax_referer( 'calc-totals', 'security', false ) ) {
- WC_AJAX::calc_line_taxes();
- }
- add_action( 'woocommerce_before_save_order_items', array( $this, 'calculate_backend_totals' ), 20 );
}
}
@@ -692,31 +646,12 @@ protected function get_address() {
}
/**
- * Allow street address to be passed when finding rates
+ * Remove jurisdictions from tax label.
*
- * @param array $matched_tax_rates
- * @param string $tax_class
- * @return array
+ * @param string $rate_name Tax rate name.
+ *
+ * @return string
*/
- public function allow_street_address_for_matched_rates( $matched_tax_rates, $tax_class = '' ) {
- $tax_class = sanitize_title( $tax_class );
- $location = WC_Tax::get_tax_location( $tax_class );
- $matched_tax_rates = array();
- if ( sizeof( $location ) >= 4 ) {
- list( $country, $state, $postcode, $city, $street ) = array_pad( $location, 5, '' );
- $matched_tax_rates = WC_Tax::find_rates(
- array(
- 'country' => $country,
- 'state' => $state,
- 'postcode' => $postcode,
- 'city' => strtoupper( $city ),
- 'tax_class' => $tax_class,
- )
- );
- }
- return $matched_tax_rates;
- }
-
public function cleanup_tax_label( $rate_name ) {
if ( ! $this->is_itemized_tax_display ) {
@@ -729,6 +664,84 @@ public function cleanup_tax_label( $rate_name ) {
return $clean_label;
}
+ /**
+ * Replace WooCommerce tax locations with TaxJar jurisdictions.
+ *
+ * @param array $location Location address.
+ * @param string $tax_class Tax class.
+ * @param WC_Customer $customer Customer.
+ *
+ * @return array
+ */
+ public function get_tax_location( $location, $tax_class, $customer ) {
+ $jurisdictions = $this->get_jurisdictions( $location, $customer );
+ $county = $jurisdictions->county ?? '';
+ $city = $jurisdictions->city ?? '';
+
+ $location[2] = '';
+ $location[3] = trim( $county . ' ' . $city );
+
+ return $location;
+ }
+
+ /**
+ * Replace WooCommerce tax locations for Order with TaxJar jurisdictions.
+ *
+ * @param array $location Location address.
+ *
+ * @return array
+ */
+ public function get_tax_location_for_order( $location ) {
+
+ if ( ! is_null( WC()->customer ) ) {
+ $jurisdictions = $this->get_jurisdictions( $location, WC()->customer );
+ $county = $jurisdictions->county ?? '';
+ $city = $jurisdictions->city ?? '';
+ $location['postcode'] = '';
+ $location['city'] = trim( $county . ' ' . $city );
+ }
+
+ return $location;
+ }
+
+ /**
+ * Get jurisdictions stored in transient.
+ *
+ * When OK response is received from TaxJar in smartcalcs_cache_request() method jurisdictions are stored in transient.
+ *
+ * @param array $location Location address.
+ * @param WC_Customer $customer Customer.
+ *
+ * @return object|false
+ */
+ private function get_jurisdictions( $location, $customer ) {
+ if ( is_null( $customer ) ) {
+ return false;
+ }
+
+ $key = 'tj_jurisdictions_' . md5( strtoupper( implode( '', $location ) . $customer->get_shipping_address() ) );
+ $jurisdictions = get_transient( $key );
+
+ return $jurisdictions;
+ }
+
+ /**
+ * Get normalized address from request body.
+ *
+ * @param array $request Request.
+ *
+ * @return array
+ */
+ private function get_normalized_location( $request ) {
+ return array(
+ 'to_country' => strtoupper( (string) ( $request['to_country'] ?? '' ) ),
+ 'to_state' => strtoupper( (string) ( $request['to_state'] ?? '' ) ),
+ 'to_zip' => strtoupper( (string) ( $request['to_zip'] ?? '' ) ),
+ 'to_city' => strtoupper( (string) ( $request['to_city'] ?? '' ) ),
+ 'to_street' => strtoupper( (string) ( $request['to_street'] ?? '' ) ),
+ );
+ }
+
/**
* Get taxable address.
*
@@ -738,13 +751,20 @@ public function get_taxable_address() {
$tax_based_on = get_option( 'woocommerce_tax_based_on' );
// Check shipping method at this point to see if we need special handling
// See WC_Customer get_taxable_address()
- // wc_get_chosen_shipping_method_ids() available since Woo 2.6.2+
- if ( function_exists( 'wc_get_chosen_shipping_method_ids' ) ) {
- if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && sizeof( array_intersect( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) {
- $tax_based_on = 'base';
- }
- } elseif ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && sizeof( array_intersect( WC()->session->get( 'chosen_shipping_methods', array() ), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) {
- $tax_based_on = 'base';
+ // wc_get_chosen_shipping_method_ids() available since Woo 2.6.2+.
+ if (
+ true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true )
+ && count(
+ array_intersect(
+ wc_get_chosen_shipping_method_ids(),
+ apply_filters(
+ 'woocommerce_local_pickup_methods',
+ array( 'legacy_local_pickup', 'local_pickup' )
+ )
+ )
+ ) > 0
+ ) {
+ $tax_based_on = 'base';
}
if ( 'base' === $tax_based_on ) {
@@ -781,11 +801,11 @@ public function get_taxable_address() {
*/
protected function get_backend_address() {
// phpcs:disable WordPress.Security.NonceVerification.Missing --- Security handled by WooCommerce
- $to_country = isset( $_POST['country'] ) ? strtoupper( wc_clean( $_POST['country'] ) ) : false;
- $to_state = isset( $_POST['state'] ) ? strtoupper( wc_clean( $_POST['state'] ) ) : false;
- $to_zip = isset( $_POST['postcode'] ) ? strtoupper( wc_clean( $_POST['postcode'] ) ) : false;
- $to_city = isset( $_POST['city'] ) ? strtoupper( wc_clean( $_POST['city'] ) ) : false;
- $to_street = isset( $_POST['street'] ) ? strtoupper( wc_clean( $_POST['street'] ) ) : false;
+ $to_country = isset( $_POST['country'] ) ? strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : false;
+ $to_state = isset( $_POST['state'] ) ? strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : false;
+ $to_zip = isset( $_POST['postcode'] ) ? strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : false;
+ $to_city = isset( $_POST['city'] ) ? strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : false;
+ $to_street = isset( $_POST['street'] ) ? strtoupper( wc_clean( wp_unslash( $_POST['street'] ) ) ) : false;
// phpcs:enable WordPress.Security.NonceVerification.Missing
return array(
@@ -809,26 +829,31 @@ protected function get_line_items( $wc_cart_object ) {
$line_items = array();
foreach ( $wc_cart_object->get_cart() as $cart_item_key => $cart_item ) {
- $product = $cart_item['data'];
- $id = $product->get_id();
- $quantity = $cart_item['quantity'];
- $unit_price = wc_format_decimal( $product->get_price() );
- $line_subtotal = wc_format_decimal( $cart_item['line_subtotal'] );
- $discount = wc_format_decimal( $cart_item['line_subtotal'] - $cart_item['line_total'] );
- $tax_class = explode( '-', $product->get_tax_class() );
- $tax_code = '';
+ $product = $cart_item['data'];
+ $id = $product->get_id();
+ $quantity = $cart_item['quantity'];
+ $unit_price = wc_format_decimal( $product->get_price() );
+ $discount = wc_format_decimal( $cart_item['line_subtotal'] - $cart_item['line_total'] );
+ $tax_class = explode( '-', $product->get_tax_class() );
+ $tax_code = '';
if ( isset( $tax_class ) && is_numeric( end( $tax_class ) ) ) {
$tax_code = end( $tax_class );
}
- if ( 'shipping' !== $product->get_tax_status() && ( ! $product->is_taxable() || 'zero-rate' == sanitize_title( $product->get_tax_class() ) ) ) {
+ if (
+ 'shipping' !== $product->get_tax_status()
+ && (
+ ! $product->is_taxable()
+ || 'zero-rate' === sanitize_title( $product->get_tax_class() )
+ )
+ ) {
$tax_code = '99999';
}
- // Get WC Subscription sign-up fees for calculations
+ // Get WC Subscription sign-up fees for calculations.
if ( class_exists( 'WC_Subscriptions_Cart' ) ) {
- if ( 'none' == WC_Subscriptions_Cart::get_calculation_type() ) {
+ if ( 'none' === WC_Subscriptions_Cart::get_calculation_type() ) {
if ( class_exists( 'WC_Subscriptions_Synchroniser' ) ) {
WC_Subscriptions_Synchroniser::maybe_set_free_trial();
}
@@ -863,25 +888,25 @@ protected function get_line_items( $wc_cart_object ) {
* @return array
*/
protected function get_backend_line_items( $order ) {
+ if ( ! $order instanceof WC_Order ) {
+ return array();
+ }
+
$line_items = array();
$this->backend_tax_classes = array();
+ /**
+ * Order Item Product.
+ *
+ * @var WC_Order_Item_Product $item Order Item Product.
+ */
foreach ( $order->get_items() as $item_key => $item ) {
- if ( is_object( $item ) ) { // Woo 3.0+
- $id = $item->get_product_id();
- $quantity = $item->get_quantity();
- $unit_price = empty( $quantity ) ? $item->get_subtotal() : wc_format_decimal( $item->get_subtotal() / $quantity );
- $discount = wc_format_decimal( $item->get_subtotal() - $item->get_total() );
- $tax_class_name = $item->get_tax_class();
- $tax_status = $item->get_tax_status();
- } else { // Woo 2.6
- $id = $item['product_id'];
- $quantity = $item['qty'];
- $unit_price = empty( $quantity ) ? $item['line_subtotal'] : wc_format_decimal( $item['line_subtotal'] / $quantity );
- $discount = wc_format_decimal( $item['line_subtotal'] - $item['line_total'] );
- $tax_class_name = $item['tax_class'];
- $product = $order->get_product_from_item( $item );
- $tax_status = $product ? $product->get_tax_status() : 'taxable';
- }
+ $id = $item->get_product_id();
+ $quantity = $item->get_quantity();
+ $unit_price = empty( $quantity ) ? $item->get_subtotal() : wc_format_decimal( $item->get_subtotal() / $quantity );
+ $discount = wc_format_decimal( $item->get_subtotal() - $item->get_total() );
+ $tax_class_name = $item->get_tax_class();
+ $tax_status = $item->get_tax_status();
+
$this->backend_tax_classes[ $id ] = $tax_class_name;
$tax_class = explode( '-', $tax_class_name );
$tax_code = '';
@@ -935,25 +960,25 @@ public function override_woocommerce_tax_rates( $taxes, $price, $rates ) {
return $taxes;
}
- // Get tax rate ID for current item
+ // Get tax rate ID for current item.
$keys = array_keys( $taxes );
$tax_rate_id = $keys[0];
$line_items = array();
- // Map line items using rate ID
+ // Map line items using rate ID.
foreach ( $this->response_rate_ids as $line_item_key => $rate_id ) {
if ( $rate_id == $tax_rate_id ) {
$line_items[] = $line_item_key;
}
}
- // Remove number precision if Woo 3.2+
+ // Remove number precision if Woo 3.2+.
if ( function_exists( 'wc_remove_number_precision' ) ) {
$price = wc_remove_number_precision( $price );
}
foreach ( $this->response_line_items as $line_item_key => $line_item ) {
- // If line item belongs to rate and matches the price, manually set the tax
+ // If line item belongs to rate and matches the price, manually set the tax.
if ( in_array( $line_item_key, $line_items ) && $price == $line_item->line_total ) {
if ( function_exists( 'wc_add_number_precision' ) ) {
$taxes[ $tax_rate_id ] = wc_add_number_precision( $line_item->tax_collectable );
@@ -975,32 +1000,32 @@ public function override_woocommerce_tax_rates( $taxes, $price, $rates ) {
* @return array
*/
public function append_base_address_to_customer_taxable_address( $address ) {
- $tax_based_on = '';
-
list( $country, $state, $postcode, $city, $street ) = array_pad( $address, 5, '' );
- // See WC_Customer get_taxable_address()
- // wc_get_chosen_shipping_method_ids() available since Woo 2.6.2+
- if ( function_exists( 'wc_get_chosen_shipping_method_ids' ) ) {
- if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && sizeof( array_intersect( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) {
- $tax_based_on = 'base';
- }
- } elseif ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && sizeof( array_intersect( WC()->session->get( 'chosen_shipping_methods', array() ), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) {
- $tax_based_on = 'base';
- }
-
- if ( 'base' == $tax_based_on ) {
+ // See WC_Customer get_taxable_address().
+ if (
+ true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true )
+ && count(
+ array_intersect(
+ wc_get_chosen_shipping_method_ids(),
+ apply_filters(
+ 'woocommerce_local_pickup_methods',
+ array( 'legacy_local_pickup', 'local_pickup' )
+ )
+ )
+ ) > 0
+ ) {
$store_settings = $this->get_store_settings();
$postcode = $store_settings['postcode'];
$city = strtoupper( $store_settings['city'] );
$street = $store_settings['street'];
}
- if ( '' != $street ) {
- return array( $country, $state, $postcode, $city, $street );
+ if ( empty( $street ) ) {
+ return array( $country, $state, $postcode, $city );
}
- return array( $country, $state, $postcode, $city );
+ return array( $country, $state, $postcode, $city, $street );
}
/**
@@ -1141,7 +1166,7 @@ private function is_nexus_address_valid( $address ): bool {
'country' => array(
'type' => 'string',
'required' => true,
- 'pattern' => '/^[A-Z]{2}$/', // two-letter ISO alpha-2 (upper-case)
+ 'pattern' => '/^[A-Z]{2}$/', // two-letter ISO alpha-2 (upper-case).
'description' => 'Two-letter ISO country code (e.g. "US").',
'max_length' => 2,
),
@@ -1154,7 +1179,7 @@ private function is_nexus_address_valid( $address ): bool {
'state' => array(
'type' => 'string',
'required' => true,
- 'pattern' => '/^[A-Z0-9\-]{1,100}$/', // typical short code like "NY", "CA", "NSW"
+ 'pattern' => '/^[A-Z0-9\-]{1,100}$/', // typical short code like "NY", "CA", "NSW".
'description' => 'Two-letter (or short) ISO state/province code where applicable.',
'max_length' => 100,
),
@@ -1195,27 +1220,21 @@ private function is_nexus_address_valid( $address ): bool {
continue;
}
- if ( ! $exists || $value === '' || $value === null ) {
+ if ( ! $exists || empty( $value ) ) {
continue;
}
- if ( isset( $rules['type'] ) ) {
- if ( $rules['type'] === 'string' && ! is_string( $value ) ) {
- $errors[] = "[$field] field must be a string";
- continue;
- }
+ if ( 'string' === $rules['type'] && ! is_string( $value ) ) {
+ $errors[] = "[$field] field must be a string";
+ continue;
}
- if ( isset( $rules['max_length'] ) && is_string( $value ) ) {
- if ( strlen( $value ) > $rules['max_length'] ) {
- $errors[] = "[$field] field exceeds maximum length of {$rules['max_length']}";
- }
+ if ( isset( $rules['max_length'] ) && is_string( $value ) && strlen( $value ) > $rules['max_length'] ) {
+ $errors[] = "[$field] field exceeds maximum length of {$rules['max_length']}";
}
- if ( isset( $rules['pattern'] ) && is_string( $value ) ) {
- if ( ! preg_match( $rules['pattern'], $value ) ) {
- $errors[] = "[$field] field format is invalid";
- }
+ if ( isset( $rules['pattern'] ) && is_string( $value ) && ! preg_match( $rules['pattern'], $value ) ) {
+ $errors[] = "[$field] field format is invalid";
}
}
@@ -1278,8 +1297,6 @@ public function calculate_tax( $options = array() ) {
$from_city = $store_settings['city'];
$from_street = $store_settings['street'];
- $this->_log( ':::: TaxJar API called ::::' );
-
$body = array(
'from_country' => $from_country,
'from_state' => $from_state,
@@ -1367,7 +1384,7 @@ public function calculate_tax( $options = array() ) {
$body['line_items'] = $line_items;
}
- $response = $this->smartcalcs_cache_request( wp_json_encode( $body ), $from_state );
+ $response = $this->smartcalcs_cache_request( $body, $from_state );
// if no response, no need to keep going - bail early.
if ( ! isset( $response ) || ! $response ) {
@@ -1450,12 +1467,13 @@ private function get_itemized_tax_rates( $taxes, $taxjar_taxes, $options ): arra
'city' => $taxjar_taxes->jurisdictions->city ?? null,
);
$location = array(
- 'from_country' => $from_country,
- 'from_state' => $from_state,
- 'to_country' => $to_country,
- 'to_state' => $to_state,
- 'to_zip' => $to_zip,
- 'to_city' => $to_city,
+ 'from_country' => $from_country,
+ 'from_state' => $from_state,
+ 'to_country' => $to_country,
+ 'to_state' => $to_state,
+ 'to_zip' => $to_zip,
+ 'to_city' => $to_city,
+ 'jurisdictions' => $jurisdictions,
);
// Add line item tax rates.
@@ -1558,7 +1576,7 @@ public function create_or_update_tax_rate( $location, $rate, $tax_class = '', $f
'tax_rate_state' => $to_state,
// For the US, we're going to modify the name of the tax rate to simplify the reporting and distinguish between the tax rates at the counties level.
// I would love to do this for other locations, but it looks like that would create issues.
- // For example, for the UK it would continuously rename the rate name with an updated `state` "piece", each time a request is made
+ // For example, for the UK it would continuously rename the rate name with an updated `state` "piece", each time a request is made.
'tax_rate_name' => $tax_rate_name,
'tax_rate_priority' => $rate_priority,
'tax_rate_compound' => false,
@@ -1567,12 +1585,13 @@ public function create_or_update_tax_rate( $location, $rate, $tax_class = '', $f
'tax_rate_class' => $tax_class,
);
+ $jurisdictions = trim( implode( ' ', $location['jurisdictions'] ) );
+
$wc_rates = WC_Tax::find_rates(
array(
'country' => $location['to_country'],
'state' => str_replace( ' ', '', $to_state ),
- 'postcode' => $location['to_zip'],
- 'city' => strtoupper( $location['to_city'] ),
+ 'city' => strtoupper( $jurisdictions ),
'tax_class' => $tax_class,
)
);
@@ -1600,14 +1619,13 @@ public function create_or_update_tax_rate( $location, $rate, $tax_class = '', $f
WC_Tax::_update_tax_rate( $rate_id, $tax_rate );
}
} else {
- // Insert a rate if we did not find one
+ // Insert a rate if we did not find one.
$this->_log( ':: Adding New Tax Rate ::' );
$this->_log( $tax_rate );
$rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
// VAT is always country wide, no need to create separate entires for each zip and city.
if ( 'VAT' !== $tax_rate_name ) {
- WC_Tax::_update_tax_rate_postcodes( $rate_id, wc_normalize_postcode( wc_clean( $location['to_zip'] ) ) );
- WC_Tax::_update_tax_rate_cities( $rate_id, wc_clean( $location['to_city'] ) );
+ WC_Tax::_update_tax_rate_cities( $rate_id, wc_clean( strtoupper( $jurisdictions ) ) );
}
}
@@ -1670,62 +1688,68 @@ public function validate_taxjar_request( $json ) {
* Unchanged from the TaxJar plugin.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/4b481f5/includes/class-wc-taxjar-integration.php#L451
*
- * @param $json
- * @param $from_state
+ * @param array $request_body Request body.
+ * @param string $from_state From state.
*
* @return mixed|WP_Error
*/
- public function smartcalcs_cache_request( $json, $from_state ) {
+ public function smartcalcs_cache_request( $request_body, $from_state ) {
+ $json = wp_json_encode( $request_body );
$cache_key = 'tj_tax_' . hash( 'md5', $json );
+ $location = $this->get_normalized_location( $request_body );
+ $location_catch_key = 'tj_jurisdictions_' . hash( 'md5', $location['to_country'] . $location['to_state'] . $location['to_zip'] . $location['to_city'] . $location['to_street'] );
$zip_state_cache_key = false;
- $request = json_decode( $json );
- $to_zip = isset( $request->to_zip ) ? (string) $request->to_zip : false;
- $to_state = isset( $request->to_state ) ? strtoupper( (string) $request->to_state ) : false;
- if ( $to_zip && $to_state ) {
- $zip_state_cache_key = strtolower( 'tj_tax_' . $to_zip . '_' . $to_state );
+ if ( $location['to_zip'] && $location['to_state'] ) {
+ $zip_state_cache_key = strtolower( 'tj_tax_' . $location['to_zip'] . '_' . $location['to_state'] );
$response = get_transient( $zip_state_cache_key );
}
- $response = ! empty( $response ) ? $response : get_transient( $cache_key );
+ $response = ! empty( $response ) ? $response : get_transient( $cache_key );
+ $response_code = (int) wp_remote_retrieve_response_code( $response );
+ $response_body = wp_remote_retrieve_body( $response );
if ( $response && 'CA' !== $from_state ) {
// If $from_state is not California, we need to check for incorrect California tax nexus.
try {
- $this->check_for_incorrect_california_tax_nexus( $response['body'], true, $from_state );
+ $this->check_for_incorrect_california_tax_nexus( $response_body, true, $from_state );
} catch ( Exception $e ) {
$this->_log( 'Error checking for incorrect California tax nexus: ' . $e->getMessage() );
}
}
- $response_code = wp_remote_retrieve_response_code( $response );
$save_error_codes = array( 404, 400 );
// Clear the taxjar notices before calculating taxes or using cached response.
$this->notifier->clear_notices( 'taxjar' );
if ( false === $response ) {
- $response = $this->smartcalcs_request( $json );
- $response_code = wp_remote_retrieve_response_code( $response );
- $body = json_decode( wp_remote_retrieve_body( $response ) );
+ $response = $this->smartcalcs_request( $json );
+ $response_code = (int) wp_remote_retrieve_response_code( $response );
+ $response_body = wp_remote_retrieve_body( $response );
+ $response_body_decoded = json_decode( $response_body );
if ( 'CA' !== $from_state ) {
// If $from_state is not California, we need to check for incorrect California tax nexus.
try {
- $this->check_for_incorrect_california_tax_nexus( $body, false, $from_state );
+ $this->check_for_incorrect_california_tax_nexus( $response_body, false, $from_state );
} catch ( Exception $e ) {
$this->_log( 'Error checking for incorrect California tax nexus: ' . $e->getMessage() );
}
}
$is_zip_to_state_mismatch = (
- isset( $body->detail )
- && is_string( $body->detail )
- && $to_zip
- && $to_state
- && false !== strpos( $body->detail, 'to_zip ' . $to_zip )
- && false !== strpos( $body->detail, 'to_state ' . $to_state )
+ isset( $response_body_decoded->detail )
+ && is_string( $response_body_decoded->detail )
+ && $location['to_zip']
+ && $location['to_state']
+ && false !== strpos( $response_body_decoded->detail, 'to_zip ' . $location['to_zip'] )
+ && false !== strpos( $response_body_decoded->detail, 'to_state ' . $location['to_state'] )
);
$transient_set = false;
- if ( 200 == $response_code ) {
+ if ( 200 === $response_code ) {
set_transient( $cache_key, $response, $this->cache_time );
- } elseif ( in_array( $response_code, $save_error_codes ) ) {
- if ( 400 == $response_code
+ $jurisdictions = isset( $response_body_decoded->tax->jurisdictions ) ? $response_body_decoded->tax->jurisdictions : null;
+ if ( ! empty( $jurisdictions ) ) {
+ set_transient( $location_catch_key, $jurisdictions, $this->cache_time );
+ }
+ } elseif ( in_array( $response_code, $save_error_codes, true ) ) {
+ if ( 400 === $response_code
&& $is_zip_to_state_mismatch
&& $zip_state_cache_key
) {
@@ -1738,9 +1762,9 @@ public function smartcalcs_cache_request( $json, $from_state ) {
}
}
- if ( in_array( $response_code, $save_error_codes ) ) {
- $this->_log( 'Retrieved the error from the cache. Received (' . $response['response']['code'] . '): ' . $response['body'] );
- $this->_error( 'Error retrieving the tax rates. Received (' . $response['response']['code'] . '): ' . $response['body'] );
+ if ( in_array( $response_code, $save_error_codes, true ) ) {
+ $this->_log( 'Retrieved the error from the cache. Received (' . $response_code . '): ' . $response_body );
+ $this->_error( 'Error retrieving the tax rates. Received (' . $response_code . '): ' . $response_body );
return false;
}
@@ -1753,7 +1777,7 @@ public function smartcalcs_cache_request( $json, $from_state ) {
* Modified from TaxJar's plugin.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/82bf7c58/includes/class-wc-taxjar-integration.php#L440
*
- * @param $json
+ * @param string $json Request body.
*
* @return array|WP_Error
*/
@@ -1765,7 +1789,9 @@ public function smartcalcs_request( $json ) {
return false;
}
- $this->_log( 'Requesting: ' . $path . ' - ' . $json );
+ $this->_log( ':::: TaxJar API called ::::' );
+
+ $this->_log( 'Request: ' . $path . ' - ' . $json );
$response = $this->api_client->proxy_request(
$path,
@@ -1778,16 +1804,18 @@ public function smartcalcs_request( $json ) {
)
);
+ $response_code = (int) wp_remote_retrieve_response_code( $response );
+
if ( is_wp_error( $response ) ) {
$this->_error( 'Error retrieving the tax rates. Received (' . $response->get_error_code() . '): ' . $response->get_error_message() );
- } elseif ( 200 == $response['response']['code'] ) {
+ } elseif ( 200 === $response_code ) {
return $response;
- } elseif ( 404 == $response['response']['code'] || 400 == $response['response']['code'] ) {
- $this->_error( 'Error retrieving the tax rates. Received (' . $response['response']['code'] . '): ' . $response['body'] );
+ } elseif ( 404 === $response_code || 400 === $response_code ) {
+ $this->_error( 'Error retrieving the tax rates. Received (' . $response_code . '): ' . $response['body'] );
return $response;
} else {
- $this->_error( 'Error retrieving the tax rates. Received (' . $response['response']['code'] . '): ' . $response['body'] );
+ $this->_error( 'Error retrieving the tax rates. Received (' . $response_code . '): ' . $response['body'] );
}
}
@@ -1859,10 +1887,12 @@ public function load_taxjar_admin_new_order_assets() {
/**
* Check for incorrect California tax nexus in the TaxJar API response or cached response.
*
- * @param $response_body
- * @param $cached
+ * @param string $response_body Response body JSON.
+ * @param bool $cached Is cached response.
+ * @param string $from_state State.
*
* @return void
+ * @throws Exception Throws exceptions when One or more values are not set.
*/
private function check_for_incorrect_california_tax_nexus( $response_body, $cached, $from_state ): void {
$log_suffix = 'in TaxJar API response.';
@@ -1884,13 +1914,13 @@ private function check_for_incorrect_california_tax_nexus( $response_body, $cach
$from_state ?: 'unknown',
$to_state,
$to_country,
- json_encode( $has_nexus ),
+ wp_json_encode( $has_nexus ),
)
);
}
if ( 'not set' === $to_state || 'not set' === $to_country || null === $has_nexus ) {
- throw new Exception( sprintf( 'One or more values are not set : to_state=>%1$s, to_country=>%2$s, has_nexus=>%3$s', $to_state, $to_country, json_encode( $has_nexus ) ) );
+ throw new Exception( sprintf( 'One or more values are not set : to_state=>%1$s, to_country=>%2$s, has_nexus=>%3$s', esc_html( $to_state ), esc_html( $to_country ), wp_json_encode( $has_nexus ) ) );
}
}
}
diff --git a/readme.txt b/readme.txt
index 4f8951c1e..a585729a1 100644
--- a/readme.txt
+++ b/readme.txt
@@ -70,6 +70,9 @@ This plugin relies on the following external services:
== Changelog ==
+= 3.4.0 - 2026-xx-xx =
+* Revamp - Tax Rate Table population logic.
+
= 3.3.1 - 2026-01-12 =
* Fix - Normalize state and country codes to uppercase in TaxJar integration.