Skip to content

[WOOTAX-250] fix: implement itemized tax calculation for mixed carts services#2920

Open
ayushpahwa wants to merge 26 commits intotrunkfrom
fix/wootax-250/implement-itemized-tax-calculation-for-mixed-carts-services
Open

[WOOTAX-250] fix: implement itemized tax calculation for mixed carts services#2920
ayushpahwa wants to merge 26 commits intotrunkfrom
fix/wootax-250/implement-itemized-tax-calculation-for-mixed-carts-services

Conversation

@ayushpahwa
Copy link
Contributor

@ayushpahwa ayushpahwa commented Feb 4, 2026

Description

Modify calculate_totals() to support multiple tax locations in a single cart. This enables mixed carts containing both physical products (taxed at customer's shipping address) and services/bookings (taxed at store base address) by grouping items by their tax_location and making separate TaxJar API calls per group.

Key changes:

  • group_items_by_location() — Groups line items by their tax_location key (set by the woocommerce_tax_line_item_location filter from #2917)
  • calculate_taxes_by_location() — Loops over groups, makes separate API calls, merges results
  • get_taxable_address() / get_address() — Extended with $location_type parameter to resolve addresses per group
  • is_local_pickup() — Extracted from get_taxable_address() for reuse; forces all items to 'base' when local pickup is selected
  • override_item_tax_rates() — Ensures WooCommerce applies the correct per-item rates from TaxJar (not its native rate lookup)
  • aggregate_tax_totals() — Combines duplicate tax labels from different locations into single display lines (e.g., two "County Tax" entries become one)
  • Cross-state handling — When a group targets a different US state (no nexus), calculate_tax() returns empty taxes instead of false, allowing other groups to proceed. A safety net returns false if all groups produce empty taxes.

Related issue(s)

Fixes WOOTAX-250

Prerequisites

  1. Ensure automated taxes are enabled at WooCommerce > Settings > Tax (/wp-admin/admin.php?page=next-admin&p=%2Fwoocommerce%2Fsettings%2Ftaxes)
  2. Make sure the Tax rates table is empty at WooCommerce > Settings > Tax > Tax rates (/wp-admin/admin.php?page=next-admin&p=%2Fwoocommerce%2Fsettings%2Ftaxes%2Frates) — stale rates from previous sessions can cause incorrect taxes
  3. Have at least 2 simple products in the store (e.g., "Jacket" at $25 and "Hat" at $12)

Setup

  1. Check out the branch locally and on CIAB

  2. Create the test mu-plugin in your CIAB container to simulate a mixed cart (forces every other cart item to use the store's base address for tax):

docker exec <CONTAINER_ID> bash -c 'cat > /var/www/html/wp-content/mu-plugins/wootax-250-test.php << '\''PLUGIN'\''
<?php
/**
 * WOOTAX-250 Test: Force every other cart item to use base tax location.
 * Odd-indexed items (2nd, 4th, ...) use "base", even-indexed (1st, 3rd, ...) use default.
 */
add_filter( "woocommerce_tax_line_item_location", function( $location, $product ) {
	static $index = 0;
	$index++;

	$new_location = ( $index % 2 === 0 ) ? "base" : $location;
	$name         = $product ? $product->get_name() : "unknown";

	error_log( sprintf(
		"[WOOTAX-250 Test] #%d %s | original: %s | applied: %s",
		$index,
		$name,
		$location,
		$new_location
	) );

	return $new_location;
}, 10, 2 );
PLUGIN'

Replace <CONTAINER_ID> with your WordPress container ID (docker ps).

Test scenarios

Test 1: Cross-state — customer in a different US state

  1. Set store address to a US location (e.g., San Francisco, CA 94110)
  2. Add both products to cart (e.g., Jacket + Hat)
  3. Go to checkout, enter a shipping address in a different US state (e.g., New York, NY 10001)
  4. Expected: Only the "base" group item (Hat) is taxed at the store's CA rates. The "shipping" group item (Jacket) gets no tax (cross-state, no nexus). You should see tax lines like:
    • County Tax — on Hat only
    • Special Tax — on Hat only
    • State Sales Tax — on Hat only
  5. Check wp-content/debug.log — you should see:
    [WOOTAX-250 Test] #1 Jacket | original: shipping | applied: shipping
    [WOOTAX-250 Test] #2 Hat | original: shipping | applied: base
    US from_state and to_state are different. No tax applies.
    

Test 2: In-state — customer in the same US state

  1. Same store address (San Francisco, CA 94110)
  2. Same products in cart
  3. Change shipping address to a same-state location (e.g., San Jose, CA 95110)
  4. Expected: Both items are taxed — Hat at SF rates (base group), Jacket at San Jose rates (shipping group). Tax lines should show aggregated amounts from both locations:
    • County Tax — combined from both jurisdictions
    • Special Tax — combined
    • State Sales Tax — combined
  5. Total should be higher than Test 1 since all items are taxed

Test 3: Single location cart (no regression)

  1. Remove the test mu-plugin (docker exec <CONTAINER_ID> rm /var/www/html/wp-content/mu-plugins/wootax-250-test.php)
  2. Add products to cart, go to checkout with any US address
  3. Expected: Taxes calculate normally — all items grouped under one location, single API call, same behavior as before this PR

Checklist

  • unit tests
  • changelog.txt entry added
  • readme.txt entry added

@ayushpahwa ayushpahwa force-pushed the fix/wootax-250/implement-itemized-tax-calculation-for-mixed-carts-services branch from 06f31d6 to 7d85709 Compare February 4, 2026 16:13
@ayushpahwa ayushpahwa self-assigned this Feb 5, 2026
@ayushpahwa ayushpahwa requested review from a team and samnajian and removed request for a team February 5, 2026 10:45
@ayushpahwa ayushpahwa marked this pull request as ready for review February 5, 2026 10:46
@samnajian
Copy link
Contributor

@ayushpahwa numerous tests are failing, do you mind fixing them while I'm testsing?

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.

2 participants