Commit 028ceb6
authored
Fix - Non-taxable product no longer zeroes shared standard tax rate (WOOTAX-240) (#2979)
* 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.
* Widen non-taxable rate-row guard to cover "Shipping only" status
The guard added for the mixed-cart zeroing bug only skipped rate-row
writes for Tax Status "None". The backend order path emits the exempt
code 99999 for any status other than "taxable", so a "Shipping only"
product still returned a 0% breakdown line that overwrote the shared
Standard rate row — the same failure, on the admin order screen.
Guard on `'taxable' !== $tax_status` instead. ProductTaxStatus defines
exactly three statuses and set_tax_status() normalises empty to
"taxable", so this is precisely "none or shipping". Skipping the write
is safe for both: WC_Cart_Totals gates item tax on is_taxable() and
WC_Order_Item::calculate_taxes() on ProductTaxStatus::TAXABLE, so
neither status is taxed by core. Shipping tax is unaffected — that rate
row is written separately after this loop.
Adds coverage for both sides of the case: that the backend path sends a
"Shipping only" product as exempt, and that its 0% line leaves the
Standard row intact.
* Record taxability at request time instead of re-deriving it
The response-side guard read the raw tax status while get_line_items()
set the exempt code from is_taxable(), which is filtered through
woocommerce_product_is_taxable. When the two disagreed, the 0% TaxJar
breakdown line still overwrote the shared tax class rate row.
Record the decision where it is made and read it back by line item key,
so both halves share one predicate. The cart path records is_taxable()
and the order path records the raw status, matching how WC_Cart_Totals
and WC_Order_Item::calculate_taxes() each gate item tax. Line items sent
as exempt for the zero-rate class are not recorded and keep updating
their own rate row.
Drive the regression tests through the request side rather than
fabricating breakdown keys, which could not catch the divergence, and
move the "Shipping only" case to the backend order path where that
status is actually emitted as exempt.
* Fix - Do not record "Shipping only" cart lines as non-taxable.
The recording added on the cart path used an unconditional ! is_taxable(), which
is false for every Tax Status other than "taxable" -- including "shipping". The
exempt branch three lines above deliberately excludes that status, so a
"Shipping only" product is sent to TaxJar as genuinely taxable and its breakdown
line comes back with a real, non-zero rate. Recording it made
get_itemized_tax_rates() skip the write and throw that rate away.
This is the one case where skipping is not preventing a 0% clobber but
discarding good data. WooCommerce applies no item tax to such a product, so the
skip looks harmless, but the row it would have written carries
tax_rate_shipping: with the default woocommerce_shipping_tax_class = 'inherit',
WC_Cart::get_cart_item_tax_classes_for_shipping() includes the product (its
shipping is taxable), WooCommerce resolves the shipping tax class to that
product's class, and WC_Tax::get_shipping_tax_rates() looks the rate up there.
TaxJar's separate shipping write only ever covers the standard class, so for a
"Shipping only" product in a non-standard class nothing kept that row current.
The condition now mirrors the exempt branch, so the cart path records exactly
what it emitted. The backend order path already did: there the same
'taxable' !== $tax_status test is what sets the 99999 code, so recording on it
is correct rather than inconsistent. The divergence was between the two paths'
*recording*, not their emission -- the emission difference is pre-existing and
deliberate.
Two tests, covering both sides of the boundary:
- test_get_line_items_does_not_record_shipping_only_status_as_non_taxable --
fails against the previous condition, which recorded '10-<key>' => true.
- test_get_line_items_still_records_none_status_as_non_taxable -- passes in both
states, pinning that the exclusion narrows by exactly one status and that a
line genuinely emitted as exempt stays recorded.
The cart path had no coverage for this status, which is why it slipped through;
the only "Shipping only" tests were on the order path.
Full suite 237 tests / 527 assertions green.
* Tweak - Move the stranded docblock onto the method it documents.
The docblock for test_get_backend_line_items_sends_shipping_only_status_as_exempt()
sat above the cart-path test instead, leaving the backend test with no docblock.
PHPCS binds a docblock to the immediately following token, so the orphan became a
floating comment and Squiz.Commenting.FunctionComment.Missing fired on the method
60 lines below it.
WordPress-Docs loads globally in .phpcs.php.xml and its tests/ exclusions do not
cover Squiz.Commenting.FunctionComment, so the sniff does apply to tests/.
Measured across both changed PHP files, --report=source:
merge-base 590070b : 192 violations / 31 sources
before this commit : 193 / 31 (FunctionComment.Missing 3 -> 4)
after this commit : 192 / 31, byte-identical to baseline
* Tweak - Restore the changelog entry to the 3.6.12 section after rebase.
The rebase onto trunk placed the WOOTAX-240 entry into the released
3.6.8 section instead of 3.6.12, in both changelog.txt and readme.txt.
The three-way merge anchored on the similar "unbounded growth of
WooCommerce tax rate rows" line that 3.6.8 already carried, and the
3.6.12 header the original commit had created now came from trunk.
No test covers changelog placement, so this was invisible to CI.1 parent 1512d21 commit 028ceb6
4 files changed
Lines changed: 471 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
89 | 100 | | |
90 | 101 | | |
91 | 102 | | |
| |||
962 | 973 | | |
963 | 974 | | |
964 | 975 | | |
965 | | - | |
966 | | - | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
967 | 979 | | |
968 | 980 | | |
969 | 981 | | |
| |||
983 | 995 | | |
984 | 996 | | |
985 | 997 | | |
| 998 | + | |
| 999 | + | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
986 | 1021 | | |
987 | 1022 | | |
988 | 1023 | | |
| |||
1039 | 1074 | | |
1040 | 1075 | | |
1041 | 1076 | | |
1042 | | - | |
1043 | | - | |
1044 | | - | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
1045 | 1081 | | |
1046 | 1082 | | |
1047 | 1083 | | |
| |||
1069 | 1105 | | |
1070 | 1106 | | |
1071 | 1107 | | |
| 1108 | + | |
| 1109 | + | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
1072 | 1113 | | |
1073 | 1114 | | |
1074 | 1115 | | |
| |||
1780 | 1821 | | |
1781 | 1822 | | |
1782 | 1823 | | |
| 1824 | + | |
| 1825 | + | |
| 1826 | + | |
| 1827 | + | |
| 1828 | + | |
| 1829 | + | |
| 1830 | + | |
| 1831 | + | |
| 1832 | + | |
1783 | 1833 | | |
1784 | 1834 | | |
1785 | 1835 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| |||
0 commit comments