Plugin Directory

Changeset 3746747


Ignore:
Timestamp:
10/14/2026 03:52:29 PM (11 days ago)
Author:
SamNajian
Message:

Tagging version 3.7.3

Location:
woocommerce-services
Files:
18 added
18 deleted
10 edited
1 copied

Legend:

Unmodified
Added
Removed
  • woocommerce-services/tags/3.7.3/classes/class-wc-connect-taxjar-integration.php

    r3734320 r3746747  
    7272
    7373        /**
     74         * The TaxJar line id each cart item was sent as, keyed by cart item key. Set with
     75         * $response_rate_ids, so each cart and order line can find its own answer.
     76         *
     77         * @var array<string, string>
     78         */
     79        private $response_cart_item_keys = array();
     80
     81        /**
     82         * The cart item each order item was created from at checkout, keyed by
     83         * spl_object_id() of the order item. Only set in the request that creates the
     84         * items: an order item loaded from the database carries no cart item key.
     85         *
     86         * @var array<int, string>
     87         */
     88        private $order_item_cart_item_keys = array();
     89
     90        /**
    7491         * Rate ids TaxJar returned for an admin recalculation, keyed by order id, then by
    7592         * order item id. Kept apart from $response_rate_ids, which other code reads as
    … …  
    7794         * take_admin_recalculation_rate_ids().
    7895         *
    79          * @var array<int, array<int, int[]>>
     96         * @var array<int, array<int, array{rate_ids: int[], line: object|null}>>
    8097         */
    8198        private $admin_recalculation_rate_ids = array();
    … …  
    377394                add_filter( 'woocommerce_matched_rates', array( $this, 'allow_street_address_for_matched_rates' ), 10, 2 );
    378395                add_filter( 'woocommerce_cart_totals_get_item_tax_rates', array( $this, 'override_cart_item_tax_rates' ), 10, 3 );
     396                add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'remember_order_item_cart_item_key' ), 10, 2 );
    379397                add_action( 'woocommerce_order_item_after_calculate_taxes', array( $this, 'override_order_item_taxes' ), 10, 2 );
    380398
    … …  
    753771                }
    754772
    755                 $this->response_rate_ids   = $taxes['rate_ids'];
    756                 $this->response_line_items = $taxes['line_items'];
     773                $this->response_rate_ids       = $taxes['rate_ids'];
     774                $this->response_line_items     = $taxes['line_items'];
     775                $this->response_cart_item_keys = wp_list_pluck( $line_items, 'id' );
    757776
    758777                if ( isset( $this->response_line_items ) ) {
    … …  
    840859                                $rate_ids = $taxes['rate_ids'][ $line_item['id'] ?? '' ] ?? null;
    841860                                if ( is_array( $rate_ids ) && $rate_ids ) {
    842                                         $this->admin_recalculation_rate_ids[ (int) $order_id ][ (int) $item_key ] = $rate_ids;
     861                                        $this->admin_recalculation_rate_ids[ (int) $order_id ][ (int) $item_key ] = array(
     862                                                'rate_ids' => $rate_ids,
     863                                                'line'     => $taxes['line_items'][ $line_item['id'] ] ?? null,
     864                                        );
    843865                                }
    844866                        }
    … …  
    16151637                $product_id = $product->get_id();
    16161638
    1617                 // Find the matching line_item_key in response_rate_ids.
    1618                 // Format is "product_id-fingerprint-occurrence". The trailing "-" delimiter prevents
    1619                 // false prefix matches (e.g. product ID 1 won't match "10-xyz" because "1-" != "10").
    1620                 // First-match-wins is safe: if the same product ID appears multiple times (e.g.
    1621                 // two bookings), they share the same tax_location and thus the same tax rates.
    1622                 $matching_rate_ids = null;
    1623                 foreach ( $this->response_rate_ids as $line_item_key => $rate_ids ) {
    1624                         if ( strpos( $line_item_key, $product_id . '-' ) === 0 ) {
    1625                                 $matching_rate_ids = $rate_ids;
    1626                                 break;
    1627                         }
    1628                 }
     1639                // The line this cart item was sent as. Without a record of it, fall back to the
     1640                // first line for the product: format is "product_id-fingerprint-occurrence", and
     1641                // the trailing "-" keeps product 1 from matching "10-xyz".
     1642                $cart_item_key = isset( $item->key ) && is_scalar( $item->key ) ? (string) $item->key : '';
     1643                $line_item_key = $this->response_cart_item_keys[ $cart_item_key ] ?? null;
     1644                if ( null === $line_item_key ) {
     1645                        foreach ( array_keys( $this->response_rate_ids ) as $response_key ) {
     1646                                if ( strpos( (string) $response_key, $product_id . '-' ) === 0 ) {
     1647                                        $line_item_key = (string) $response_key;
     1648                                        break;
     1649                                }
     1650                        }
     1651                }
     1652
     1653                $matching_rate_ids = null === $line_item_key ? null : ( $this->response_rate_ids[ $line_item_key ] ?? null );
    16291654
    16301655                if ( empty( $matching_rate_ids ) || ! is_array( $matching_rate_ids ) ) {
    … …  
    16321657                }
    16331658
    1634                 // Fetch the tax rates from the database using the rate IDs.
    1635                 $tax_rates = array();
    1636                 foreach ( $matching_rate_ids as $rate_id ) {
    1637                         $rate_id = absint( $rate_id );
    1638                         if ( ! $rate_id ) {
    1639                                 continue;
    1640                         }
    1641 
    1642                         // Get rate data from WooCommerce.
    1643                         $rate_data = WC_Tax::_get_tax_rate( $rate_id );
    1644                         if ( $rate_data ) {
    1645                                 $tax_rates[ $rate_id ] = array(
    1646                                         'rate'     => (float) $rate_data['tax_rate'],
    1647                                         'label'    => $rate_data['tax_rate_name'],
    1648                                         'shipping' => 'yes' === $rate_data['tax_rate_shipping'] ? 'yes' : 'no',
    1649                                         'compound' => 'yes' === $rate_data['tax_rate_compound'] ? 'yes' : 'no',
    1650                                 );
    1651                         }
    1652                 }
     1659                // The line's own percentages, as the order uses: rows are shared by every product
     1660                // in a tax class and hold whichever line was stored last.
     1661                $tax_rates = $this->get_looked_up_rates( $matching_rate_ids, $this->response_line_items[ $line_item_key ] ?? null );
    16531662
    16541663                // Return our rates if we found any, otherwise fall back to WooCommerce's.
    … …  
    16691678        public function override_order_item_taxes( $item, $calculate_tax_for ) {
    16701679                // An admin recalculation looked this item up in calculate_backend_totals().
    1671                 $admin_rate_ids = $this->take_admin_recalculation_rate_ids( $item );
    1672                 if ( null !== $admin_rate_ids ) {
    1673                         $this->set_order_item_taxes_from_rate_ids( $item, $admin_rate_ids );
     1680                $admin_lookup = $this->take_admin_recalculation_rate_ids( $item );
     1681                if ( null !== $admin_lookup ) {
     1682                        $this->set_order_item_taxes_from_rate_ids( $item, $admin_lookup['rate_ids'], $admin_lookup['line'] );
    16741683                        return;
    16751684                }
    … …  
    16851694                }
    16861695
    1687                 $product_id = $item->get_product_id();
     1696                $line_item_key = $this->find_order_item_line_key( $item );
     1697
     1698                // No match means this item wasn't TaxJar-calculated (e.g. cross-state) — leave as-is.
     1699                if ( null === $line_item_key ) {
     1700                        return;
     1701                }
     1702
     1703                $matching_rate_ids = $this->response_rate_ids[ $line_item_key ] ?? null;
     1704                $matching_line     = $this->response_line_items[ $line_item_key ] ?? null;
     1705
     1706                if ( empty( $matching_rate_ids ) || ! is_array( $matching_rate_ids ) ) {
     1707                        // A line sent as exempt writes no rows, so WooCommerce taxed it from rows another
     1708                        // line wrote. That includes a line checkout sends as exempt on its second pass,
     1709                        // because TaxJar answered 0% for it on the first.
     1710                        if ( is_object( $matching_line ) && isset( $matching_line->combined_tax_rate ) && 0.0 === (float) $matching_line->combined_tax_rate ) {
     1711                                $item->set_taxes( false );
     1712                        }
     1713                        return;
     1714                }
     1715
     1716                $this->set_order_item_taxes_from_rate_ids( $item, $matching_rate_ids, $matching_line );
     1717        }
     1718
     1719        /**
     1720         * Remember the cart item an order item was created from.
     1721         *
     1722         * @internal Hooked to woocommerce_checkout_create_order_line_item.
     1723         *
     1724         * @param WC_Order_Item_Product $item          The order item being created.
     1725         * @param string                $cart_item_key The key of the cart item it was copied from.
     1726         *
     1727         * @since 3.7.3
     1728         */
     1729        public function remember_order_item_cart_item_key( $item, $cart_item_key ) {
     1730                if ( ! $item instanceof WC_Order_Item_Product || ! is_string( $cart_item_key ) || '' === $cart_item_key ) {
     1731                        return;
     1732                }
     1733
     1734                $this->order_item_cart_item_keys[ spl_object_id( $item ) ] = $cart_item_key;
     1735        }
     1736
     1737        /**
     1738         * Find the TaxJar line an order item was sent as at checkout.
     1739         *
     1740         * Lines are matched on the product, the variation when there is one. When the cart
     1741         * holds that product on more than one line, the order item is matched to the cart
     1742         * line it was created from: the same product can be on two lines at different
     1743         * prices, and TaxJar can answer them differently. That is the cart item key
     1744         * WooCommerce passed when it created the item, or else the cart line with the same
     1745         * quantity and amounts. Without either, a line TaxJar taxed is used.
     1746         *
     1747         * @param WC_Order_Item_Product $item Order item.
     1748         * @return string|null The line id, or null when the product was not sent.
     1749         */
     1750        private function find_order_item_line_key( $item ) {
     1751                $product_id = $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id();
    16881752                if ( ! $product_id ) {
    1689                         return;
    1690                 }
    1691 
    1692                 // Find matching rate_ids by product_id prefix (format: "product_id-fingerprint-occurrence").
    1693                 // The trailing "-" delimiter prevents false prefix matches between IDs (e.g. 1 vs 10).
    1694                 // First-match-wins is safe: same product always shares the same tax_location and rates.
    1695                 $matching_rate_ids = null;
    1696                 foreach ( $this->response_rate_ids as $line_item_key => $rate_ids ) {
     1753                        return null;
     1754                }
     1755
     1756                // An exempt line has a breakdown line but no rate ids, so look in both.
     1757                $candidates = array();
     1758                foreach ( array_merge( array_keys( (array) $this->response_line_items ), array_keys( $this->response_rate_ids ) ) as $line_item_key ) {
     1759                        $line_item_key = (string) $line_item_key;
    16971760                        if ( strpos( $line_item_key, $product_id . '-' ) === 0 ) {
    1698                                 $matching_rate_ids = $rate_ids;
    1699                                 break;
    1700                         }
    1701                 }
    1702 
    1703                 // No match means this item wasn't TaxJar-calculated (e.g. cross-state) — leave as-is.
    1704                 if ( empty( $matching_rate_ids ) || ! is_array( $matching_rate_ids ) ) {
    1705                         return;
    1706                 }
    1707 
    1708                 $this->set_order_item_taxes_from_rate_ids( $item, $matching_rate_ids );
     1761                                $candidates[ $line_item_key ] = true;
     1762                        }
     1763                }
     1764                $candidates = array_keys( $candidates );
     1765
     1766                if ( count( $candidates ) < 2 ) {
     1767                        return $candidates[0] ?? null;
     1768                }
     1769
     1770                $cart_item_key = $this->order_item_cart_item_keys[ spl_object_id( $item ) ] ?? null;
     1771                $line_item_key = null === $cart_item_key ? null : ( $this->response_cart_item_keys[ $cart_item_key ] ?? null );
     1772                if ( null !== $line_item_key && in_array( (string) $line_item_key, $candidates, true ) ) {
     1773                        return (string) $line_item_key;
     1774                }
     1775
     1776                if ( function_exists( 'WC' ) && WC()->cart instanceof WC_Cart ) {
     1777                        foreach ( $candidates as $line_item_key ) {
     1778                                $cart_item_key = array_search( $line_item_key, $this->response_cart_item_keys, true );
     1779                                $cart_item     = false === $cart_item_key ? array() : WC()->cart->get_cart_item( $cart_item_key );
     1780
     1781                                if (
     1782                                        ! empty( $cart_item )
     1783                                        && (float) $cart_item['quantity'] === (float) $item->get_quantity()
     1784                                        && abs( (float) $cart_item['line_subtotal'] - (float) $item->get_subtotal() ) < 0.01
     1785                                        && abs( (float) $cart_item['line_total'] - (float) $item->get_total() ) < 0.01
     1786                                ) {
     1787                                        return $line_item_key;
     1788                                }
     1789                        }
     1790                }
     1791
     1792                // Still no match: an exempt line would clear the tax of a taxed item, so prefer a
     1793                // line TaxJar taxed. That charges what was charged before lines were told apart.
     1794                foreach ( $candidates as $line_item_key ) {
     1795                        if ( ! empty( $this->response_rate_ids[ $line_item_key ] ) ) {
     1796                                return $line_item_key;
     1797                        }
     1798                }
     1799
     1800                return $candidates[0];
    17091801        }
    17101802
    … …  
    17161808         *
    17171809         * @param WC_Order_Item $item The order item being taxed.
    1718          * @return int[]|null Rate ids, or null when the item was not looked up.
     1810         * @return array{rate_ids: int[], line: object|null}|null Rate ids and TaxJar's answer
     1811         *                                                       line, or null when the item
     1812         *                                                       was not looked up.
    17191813         */
    17201814        private function take_admin_recalculation_rate_ids( $item ) {
    … …  
    17391833         * Tax an order item at the given rate rows, replacing what WooCommerce found.
    17401834         *
    1741          * @param WC_Order_Item_Product $item     The order item.
    1742          * @param array                 $rate_ids Tax rate ids.
    1743          */
    1744         private function set_order_item_taxes_from_rate_ids( $item, array $rate_ids ) {
    1745                 // Build tax rates array from the stored rate IDs.
    1746                 $tax_rates = array();
    1747                 foreach ( $rate_ids as $rate_id ) {
    1748                         $rate_id = absint( $rate_id );
    1749                         if ( ! $rate_id ) {
    1750                                 continue;
    1751                         }
    1752 
    1753                         $rate_data = \WC_Tax::_get_tax_rate( $rate_id );
    1754                         if ( $rate_data ) {
    1755                                 $tax_rates[ $rate_id ] = array(
    1756                                         'rate'     => (float) $rate_data['tax_rate'],
    1757                                         'label'    => $rate_data['tax_rate_name'],
    1758                                         'shipping' => 'yes' === $rate_data['tax_rate_shipping'] ? 'yes' : 'no',
    1759                                         'compound' => 'yes' === $rate_data['tax_rate_compound'] ? 'yes' : 'no',
    1760                                 );
    1761                         }
    1762                 }
     1835         * The percentages come from TaxJar's answer for the item when it is given. Rows are
     1836         * shared by every product in a tax class, so they hold whichever line was stored
     1837         * last: an item TaxJar answered as exempt would otherwise pay the rate of a taxed
     1838         * item beside it.
     1839         *
     1840         * @param WC_Order_Item_Product $item          The order item.
     1841         * @param array                 $rate_ids      Tax rate ids.
     1842         * @param object|null           $response_line TaxJar's breakdown line for the item.
     1843         */
     1844        private function set_order_item_taxes_from_rate_ids( $item, array $rate_ids, $response_line = null ) {
     1845                // Positions matter: the answer's components were stored in this order.
     1846                $tax_rates = $this->get_looked_up_rates( $rate_ids, $response_line );
    17631847
    17641848                if ( empty( $tax_rates ) ) {
  • woocommerce-services/tags/3.7.3/i18n/languages/woocommerce-services.pot

    r3734320 r3746747  
    55"Project-Id-Version: _s woocommerce-services\n"
    66"Report-Msgid-Bugs-To: \n"
    7 "POT-Creation-Date: 2026-10-08T10:46:46.612Z\n"
     7"POT-Creation-Date: 2026-10-14T15:49:53.757Z\n"
    88"MIME-Version: 1.0\n"
    99"Content-Type: text/plain; charset=UTF-8\n"
    … …  
    110110#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    111111#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    112 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     112#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    113113msgid "Create shipping label"
    114114msgid_plural "Create shipping labels"
    … …  
    117117
    118118#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    119 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     119#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    120120msgid "Track Package"
    121121msgid_plural "Track Packages"
    … …  
    124124
    125125#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    126 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     126#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    127127msgid "Create new label"
    128128msgstr ""
    129129
    130130#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    131 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     131#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    132132msgid "Connection error: unable to create label at this time"
    133133msgstr ""
    134134
    135135#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    136 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     136#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    137137msgid "%(itemCount)d item was fulfilled on {{span}}%(createdDate)s{{/span}}"
    138138msgid_plural ""
    … …  
    142142
    143143#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    144 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     144#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    145145msgid "%(itemCount)d item is ready to be fulfilled"
    146146msgid_plural "%(itemCount)d items are ready to be fulfilled"
    … …  
    810810#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    811811#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    812 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     812#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    813813msgid "Try WooCommerce Shipping"
    814814msgstr ""
    … …  
    816816#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    817817#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    818 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     818#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    819819msgid "A new WooCommerce Shipping experience is now available"
    820820msgstr ""
    … …  
    822822#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    823823#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    824 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     824#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    825825msgid ""
    826826"We'll ensure a seamless transition by allowing you to migrate all your "
    … …  
    830830#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    831831#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    832 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     832#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    833833msgid "Here's what you can expect from the new shipping experience:"
    834834msgstr ""
    … …  
    836836#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    837837#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    838 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     838#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    839839msgid "A seamless transition"
    840840msgstr ""
    … …  
    842842#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    843843#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    844 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     844#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    845845msgid ""
    846846"Automatically migrate all your compatible settings and shipment history to "
    … …  
    850850#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    851851#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    852 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     852#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    853853msgid "Print and save"
    854854msgstr ""
    … …  
    856856#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    857857#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    858 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     858#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    859859msgid ""
    860860"Speed up label creation with a streamlined process to print and save your "
    … …  
    864864#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    865865#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    866 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     866#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    867867msgid "USPS and DHL Express"
    868868msgstr ""
    … …  
    870870#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    871871#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    872 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     872#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    873873msgid ""
    874874"Ship your productss using trusted shipping carriers like USPS and DHL "
    … …  
    878878#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    879879#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    880 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     880#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    881881msgid "Enhanced label purchase flow"
    882882msgstr ""
    … …  
    884884#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    885885#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    886 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     886#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    887887msgid ""
    888888"Experience a smoother label purchasing process with our updated interface."
    … …  
    891891#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    892892#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    893 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     893#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    894894msgid "Maybe later"
    895895msgstr ""
  • woocommerce-services/tags/3.7.3/readme.txt

    r3734320 r3746747  
    88WC requires at least: 11.0
    99WC tested up to: 11.2
    10 Stable tag: 3.7.2
     10Stable tag: 3.7.3
    1111License: GPLv2 or later
    1212License URI: http://www.gnu.org/licenses/gpl-2.0.html
    … …  
    7070
    7171== Changelog ==
     72
     73= 3.7.3 - 2026-10-14 =
     74* Fix   - Stop charging tax on an item the tax service answered as tax-exempt, such as clothing under New York's price threshold, when another item in the same tax class is taxed. This affected orders placed at checkout and orders recalculated in the admin. The cart now also shows the tax the order charges when items in one tax class are taxed differently, including product variations and the same product added twice at different prices.
    7275
    7376= 3.7.2 - 2026-10-08 =
  • woocommerce-services/tags/3.7.3/vendor/composer/installed.php

    r3734320 r3746747  
    44        'pretty_version' => 'dev-trunk',
    55        'version' => 'dev-trunk',
    6         'reference' => 'da576c383c6574372e4c4d6c61b88989fc109bc6',
     6        'reference' => '84d4dd49245d73c4c107a9af4bcd1f143d15ad5c',
    77        'type' => 'library',
    88        'install_path' => __DIR__ . '/../../',
    … …  
    1414            'pretty_version' => 'dev-trunk',
    1515            'version' => 'dev-trunk',
    16             'reference' => 'da576c383c6574372e4c4d6c61b88989fc109bc6',
     16            'reference' => '84d4dd49245d73c4c107a9af4bcd1f143d15ad5c',
    1717            'type' => 'library',
    1818            'install_path' => __DIR__ . '/../../',
  • woocommerce-services/tags/3.7.3/woocommerce-services.php

    r3734320 r3746747  
    1111 * License: GPLv2 or later
    1212 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
    13  * Version: 3.7.2
     13 * Version: 3.7.3
    1414 * Requires Plugins: woocommerce
    1515 * Requires PHP: 7.4
  • woocommerce-services/trunk/classes/class-wc-connect-taxjar-integration.php

    r3734320 r3746747  
    7272
    7373        /**
     74         * The TaxJar line id each cart item was sent as, keyed by cart item key. Set with
     75         * $response_rate_ids, so each cart and order line can find its own answer.
     76         *
     77         * @var array<string, string>
     78         */
     79        private $response_cart_item_keys = array();
     80
     81        /**
     82         * The cart item each order item was created from at checkout, keyed by
     83         * spl_object_id() of the order item. Only set in the request that creates the
     84         * items: an order item loaded from the database carries no cart item key.
     85         *
     86         * @var array<int, string>
     87         */
     88        private $order_item_cart_item_keys = array();
     89
     90        /**
    7491         * Rate ids TaxJar returned for an admin recalculation, keyed by order id, then by
    7592         * order item id. Kept apart from $response_rate_ids, which other code reads as
    … …  
    7794         * take_admin_recalculation_rate_ids().
    7895         *
    79          * @var array<int, array<int, int[]>>
     96         * @var array<int, array<int, array{rate_ids: int[], line: object|null}>>
    8097         */
    8198        private $admin_recalculation_rate_ids = array();
    … …  
    377394                add_filter( 'woocommerce_matched_rates', array( $this, 'allow_street_address_for_matched_rates' ), 10, 2 );
    378395                add_filter( 'woocommerce_cart_totals_get_item_tax_rates', array( $this, 'override_cart_item_tax_rates' ), 10, 3 );
     396                add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'remember_order_item_cart_item_key' ), 10, 2 );
    379397                add_action( 'woocommerce_order_item_after_calculate_taxes', array( $this, 'override_order_item_taxes' ), 10, 2 );
    380398
    … …  
    753771                }
    754772
    755                 $this->response_rate_ids   = $taxes['rate_ids'];
    756                 $this->response_line_items = $taxes['line_items'];
     773                $this->response_rate_ids       = $taxes['rate_ids'];
     774                $this->response_line_items     = $taxes['line_items'];
     775                $this->response_cart_item_keys = wp_list_pluck( $line_items, 'id' );
    757776
    758777                if ( isset( $this->response_line_items ) ) {
    … …  
    840859                                $rate_ids = $taxes['rate_ids'][ $line_item['id'] ?? '' ] ?? null;
    841860                                if ( is_array( $rate_ids ) && $rate_ids ) {
    842                                         $this->admin_recalculation_rate_ids[ (int) $order_id ][ (int) $item_key ] = $rate_ids;
     861                                        $this->admin_recalculation_rate_ids[ (int) $order_id ][ (int) $item_key ] = array(
     862                                                'rate_ids' => $rate_ids,
     863                                                'line'     => $taxes['line_items'][ $line_item['id'] ] ?? null,
     864                                        );
    843865                                }
    844866                        }
    … …  
    16151637                $product_id = $product->get_id();
    16161638
    1617                 // Find the matching line_item_key in response_rate_ids.
    1618                 // Format is "product_id-fingerprint-occurrence". The trailing "-" delimiter prevents
    1619                 // false prefix matches (e.g. product ID 1 won't match "10-xyz" because "1-" != "10").
    1620                 // First-match-wins is safe: if the same product ID appears multiple times (e.g.
    1621                 // two bookings), they share the same tax_location and thus the same tax rates.
    1622                 $matching_rate_ids = null;
    1623                 foreach ( $this->response_rate_ids as $line_item_key => $rate_ids ) {
    1624                         if ( strpos( $line_item_key, $product_id . '-' ) === 0 ) {
    1625                                 $matching_rate_ids = $rate_ids;
    1626                                 break;
    1627                         }
    1628                 }
     1639                // The line this cart item was sent as. Without a record of it, fall back to the
     1640                // first line for the product: format is "product_id-fingerprint-occurrence", and
     1641                // the trailing "-" keeps product 1 from matching "10-xyz".
     1642                $cart_item_key = isset( $item->key ) && is_scalar( $item->key ) ? (string) $item->key : '';
     1643                $line_item_key = $this->response_cart_item_keys[ $cart_item_key ] ?? null;
     1644                if ( null === $line_item_key ) {
     1645                        foreach ( array_keys( $this->response_rate_ids ) as $response_key ) {
     1646                                if ( strpos( (string) $response_key, $product_id . '-' ) === 0 ) {
     1647                                        $line_item_key = (string) $response_key;
     1648                                        break;
     1649                                }
     1650                        }
     1651                }
     1652
     1653                $matching_rate_ids = null === $line_item_key ? null : ( $this->response_rate_ids[ $line_item_key ] ?? null );
    16291654
    16301655                if ( empty( $matching_rate_ids ) || ! is_array( $matching_rate_ids ) ) {
    … …  
    16321657                }
    16331658
    1634                 // Fetch the tax rates from the database using the rate IDs.
    1635                 $tax_rates = array();
    1636                 foreach ( $matching_rate_ids as $rate_id ) {
    1637                         $rate_id = absint( $rate_id );
    1638                         if ( ! $rate_id ) {
    1639                                 continue;
    1640                         }
    1641 
    1642                         // Get rate data from WooCommerce.
    1643                         $rate_data = WC_Tax::_get_tax_rate( $rate_id );
    1644                         if ( $rate_data ) {
    1645                                 $tax_rates[ $rate_id ] = array(
    1646                                         'rate'     => (float) $rate_data['tax_rate'],
    1647                                         'label'    => $rate_data['tax_rate_name'],
    1648                                         'shipping' => 'yes' === $rate_data['tax_rate_shipping'] ? 'yes' : 'no',
    1649                                         'compound' => 'yes' === $rate_data['tax_rate_compound'] ? 'yes' : 'no',
    1650                                 );
    1651                         }
    1652                 }
     1659                // The line's own percentages, as the order uses: rows are shared by every product
     1660                // in a tax class and hold whichever line was stored last.
     1661                $tax_rates = $this->get_looked_up_rates( $matching_rate_ids, $this->response_line_items[ $line_item_key ] ?? null );
    16531662
    16541663                // Return our rates if we found any, otherwise fall back to WooCommerce's.
    … …  
    16691678        public function override_order_item_taxes( $item, $calculate_tax_for ) {
    16701679                // An admin recalculation looked this item up in calculate_backend_totals().
    1671                 $admin_rate_ids = $this->take_admin_recalculation_rate_ids( $item );
    1672                 if ( null !== $admin_rate_ids ) {
    1673                         $this->set_order_item_taxes_from_rate_ids( $item, $admin_rate_ids );
     1680                $admin_lookup = $this->take_admin_recalculation_rate_ids( $item );
     1681                if ( null !== $admin_lookup ) {
     1682                        $this->set_order_item_taxes_from_rate_ids( $item, $admin_lookup['rate_ids'], $admin_lookup['line'] );
    16741683                        return;
    16751684                }
    … …  
    16851694                }
    16861695
    1687                 $product_id = $item->get_product_id();
     1696                $line_item_key = $this->find_order_item_line_key( $item );
     1697
     1698                // No match means this item wasn't TaxJar-calculated (e.g. cross-state) — leave as-is.
     1699                if ( null === $line_item_key ) {
     1700                        return;
     1701                }
     1702
     1703                $matching_rate_ids = $this->response_rate_ids[ $line_item_key ] ?? null;
     1704                $matching_line     = $this->response_line_items[ $line_item_key ] ?? null;
     1705
     1706                if ( empty( $matching_rate_ids ) || ! is_array( $matching_rate_ids ) ) {
     1707                        // A line sent as exempt writes no rows, so WooCommerce taxed it from rows another
     1708                        // line wrote. That includes a line checkout sends as exempt on its second pass,
     1709                        // because TaxJar answered 0% for it on the first.
     1710                        if ( is_object( $matching_line ) && isset( $matching_line->combined_tax_rate ) && 0.0 === (float) $matching_line->combined_tax_rate ) {
     1711                                $item->set_taxes( false );
     1712                        }
     1713                        return;
     1714                }
     1715
     1716                $this->set_order_item_taxes_from_rate_ids( $item, $matching_rate_ids, $matching_line );
     1717        }
     1718
     1719        /**
     1720         * Remember the cart item an order item was created from.
     1721         *
     1722         * @internal Hooked to woocommerce_checkout_create_order_line_item.
     1723         *
     1724         * @param WC_Order_Item_Product $item          The order item being created.
     1725         * @param string                $cart_item_key The key of the cart item it was copied from.
     1726         *
     1727         * @since 3.7.3
     1728         */
     1729        public function remember_order_item_cart_item_key( $item, $cart_item_key ) {
     1730                if ( ! $item instanceof WC_Order_Item_Product || ! is_string( $cart_item_key ) || '' === $cart_item_key ) {
     1731                        return;
     1732                }
     1733
     1734                $this->order_item_cart_item_keys[ spl_object_id( $item ) ] = $cart_item_key;
     1735        }
     1736
     1737        /**
     1738         * Find the TaxJar line an order item was sent as at checkout.
     1739         *
     1740         * Lines are matched on the product, the variation when there is one. When the cart
     1741         * holds that product on more than one line, the order item is matched to the cart
     1742         * line it was created from: the same product can be on two lines at different
     1743         * prices, and TaxJar can answer them differently. That is the cart item key
     1744         * WooCommerce passed when it created the item, or else the cart line with the same
     1745         * quantity and amounts. Without either, a line TaxJar taxed is used.
     1746         *
     1747         * @param WC_Order_Item_Product $item Order item.
     1748         * @return string|null The line id, or null when the product was not sent.
     1749         */
     1750        private function find_order_item_line_key( $item ) {
     1751                $product_id = $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id();
    16881752                if ( ! $product_id ) {
    1689                         return;
    1690                 }
    1691 
    1692                 // Find matching rate_ids by product_id prefix (format: "product_id-fingerprint-occurrence").
    1693                 // The trailing "-" delimiter prevents false prefix matches between IDs (e.g. 1 vs 10).
    1694                 // First-match-wins is safe: same product always shares the same tax_location and rates.
    1695                 $matching_rate_ids = null;
    1696                 foreach ( $this->response_rate_ids as $line_item_key => $rate_ids ) {
     1753                        return null;
     1754                }
     1755
     1756                // An exempt line has a breakdown line but no rate ids, so look in both.
     1757                $candidates = array();
     1758                foreach ( array_merge( array_keys( (array) $this->response_line_items ), array_keys( $this->response_rate_ids ) ) as $line_item_key ) {
     1759                        $line_item_key = (string) $line_item_key;
    16971760                        if ( strpos( $line_item_key, $product_id . '-' ) === 0 ) {
    1698                                 $matching_rate_ids = $rate_ids;
    1699                                 break;
    1700                         }
    1701                 }
    1702 
    1703                 // No match means this item wasn't TaxJar-calculated (e.g. cross-state) — leave as-is.
    1704                 if ( empty( $matching_rate_ids ) || ! is_array( $matching_rate_ids ) ) {
    1705                         return;
    1706                 }
    1707 
    1708                 $this->set_order_item_taxes_from_rate_ids( $item, $matching_rate_ids );
     1761                                $candidates[ $line_item_key ] = true;
     1762                        }
     1763                }
     1764                $candidates = array_keys( $candidates );
     1765
     1766                if ( count( $candidates ) < 2 ) {
     1767                        return $candidates[0] ?? null;
     1768                }
     1769
     1770                $cart_item_key = $this->order_item_cart_item_keys[ spl_object_id( $item ) ] ?? null;
     1771                $line_item_key = null === $cart_item_key ? null : ( $this->response_cart_item_keys[ $cart_item_key ] ?? null );
     1772                if ( null !== $line_item_key && in_array( (string) $line_item_key, $candidates, true ) ) {
     1773                        return (string) $line_item_key;
     1774                }
     1775
     1776                if ( function_exists( 'WC' ) && WC()->cart instanceof WC_Cart ) {
     1777                        foreach ( $candidates as $line_item_key ) {
     1778                                $cart_item_key = array_search( $line_item_key, $this->response_cart_item_keys, true );
     1779                                $cart_item     = false === $cart_item_key ? array() : WC()->cart->get_cart_item( $cart_item_key );
     1780
     1781                                if (
     1782                                        ! empty( $cart_item )
     1783                                        && (float) $cart_item['quantity'] === (float) $item->get_quantity()
     1784                                        && abs( (float) $cart_item['line_subtotal'] - (float) $item->get_subtotal() ) < 0.01
     1785                                        && abs( (float) $cart_item['line_total'] - (float) $item->get_total() ) < 0.01
     1786                                ) {
     1787                                        return $line_item_key;
     1788                                }
     1789                        }
     1790                }
     1791
     1792                // Still no match: an exempt line would clear the tax of a taxed item, so prefer a
     1793                // line TaxJar taxed. That charges what was charged before lines were told apart.
     1794                foreach ( $candidates as $line_item_key ) {
     1795                        if ( ! empty( $this->response_rate_ids[ $line_item_key ] ) ) {
     1796                                return $line_item_key;
     1797                        }
     1798                }
     1799
     1800                return $candidates[0];
    17091801        }
    17101802
    … …  
    17161808         *
    17171809         * @param WC_Order_Item $item The order item being taxed.
    1718          * @return int[]|null Rate ids, or null when the item was not looked up.
     1810         * @return array{rate_ids: int[], line: object|null}|null Rate ids and TaxJar's answer
     1811         *                                                       line, or null when the item
     1812         *                                                       was not looked up.
    17191813         */
    17201814        private function take_admin_recalculation_rate_ids( $item ) {
    … …  
    17391833         * Tax an order item at the given rate rows, replacing what WooCommerce found.
    17401834         *
    1741          * @param WC_Order_Item_Product $item     The order item.
    1742          * @param array                 $rate_ids Tax rate ids.
    1743          */
    1744         private function set_order_item_taxes_from_rate_ids( $item, array $rate_ids ) {
    1745                 // Build tax rates array from the stored rate IDs.
    1746                 $tax_rates = array();
    1747                 foreach ( $rate_ids as $rate_id ) {
    1748                         $rate_id = absint( $rate_id );
    1749                         if ( ! $rate_id ) {
    1750                                 continue;
    1751                         }
    1752 
    1753                         $rate_data = \WC_Tax::_get_tax_rate( $rate_id );
    1754                         if ( $rate_data ) {
    1755                                 $tax_rates[ $rate_id ] = array(
    1756                                         'rate'     => (float) $rate_data['tax_rate'],
    1757                                         'label'    => $rate_data['tax_rate_name'],
    1758                                         'shipping' => 'yes' === $rate_data['tax_rate_shipping'] ? 'yes' : 'no',
    1759                                         'compound' => 'yes' === $rate_data['tax_rate_compound'] ? 'yes' : 'no',
    1760                                 );
    1761                         }
    1762                 }
     1835         * The percentages come from TaxJar's answer for the item when it is given. Rows are
     1836         * shared by every product in a tax class, so they hold whichever line was stored
     1837         * last: an item TaxJar answered as exempt would otherwise pay the rate of a taxed
     1838         * item beside it.
     1839         *
     1840         * @param WC_Order_Item_Product $item          The order item.
     1841         * @param array                 $rate_ids      Tax rate ids.
     1842         * @param object|null           $response_line TaxJar's breakdown line for the item.
     1843         */
     1844        private function set_order_item_taxes_from_rate_ids( $item, array $rate_ids, $response_line = null ) {
     1845                // Positions matter: the answer's components were stored in this order.
     1846                $tax_rates = $this->get_looked_up_rates( $rate_ids, $response_line );
    17631847
    17641848                if ( empty( $tax_rates ) ) {
  • woocommerce-services/trunk/i18n/languages/woocommerce-services.pot

    r3734320 r3746747  
    55"Project-Id-Version: _s woocommerce-services\n"
    66"Report-Msgid-Bugs-To: \n"
    7 "POT-Creation-Date: 2026-10-08T10:46:46.612Z\n"
     7"POT-Creation-Date: 2026-10-14T15:49:53.757Z\n"
    88"MIME-Version: 1.0\n"
    99"Content-Type: text/plain; charset=UTF-8\n"
    … …  
    110110#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    111111#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    112 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     112#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    113113msgid "Create shipping label"
    114114msgid_plural "Create shipping labels"
    … …  
    117117
    118118#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    119 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     119#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    120120msgid "Track Package"
    121121msgid_plural "Track Packages"
    … …  
    124124
    125125#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    126 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     126#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    127127msgid "Create new label"
    128128msgstr ""
    129129
    130130#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    131 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     131#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    132132msgid "Connection error: unable to create label at this time"
    133133msgstr ""
    134134
    135135#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    136 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     136#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    137137msgid "%(itemCount)d item was fulfilled on {{span}}%(createdDate)s{{/span}}"
    138138msgid_plural ""
    … …  
    142142
    143143#: dist/chunks/0f0750e0dfa6ef74edc7.min.js:1
    144 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     144#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    145145msgid "%(itemCount)d item is ready to be fulfilled"
    146146msgid_plural "%(itemCount)d items are ready to be fulfilled"
    … …  
    810810#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    811811#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    812 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     812#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    813813msgid "Try WooCommerce Shipping"
    814814msgstr ""
    … …  
    816816#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    817817#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    818 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     818#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    819819msgid "A new WooCommerce Shipping experience is now available"
    820820msgstr ""
    … …  
    822822#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    823823#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    824 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     824#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    825825msgid ""
    826826"We'll ensure a seamless transition by allowing you to migrate all your "
    … …  
    830830#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    831831#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    832 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     832#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    833833msgid "Here's what you can expect from the new shipping experience:"
    834834msgstr ""
    … …  
    836836#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    837837#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    838 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     838#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    839839msgid "A seamless transition"
    840840msgstr ""
    … …  
    842842#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    843843#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    844 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     844#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    845845msgid ""
    846846"Automatically migrate all your compatible settings and shipment history to "
    … …  
    850850#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    851851#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    852 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     852#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    853853msgid "Print and save"
    854854msgstr ""
    … …  
    856856#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    857857#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    858 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     858#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    859859msgid ""
    860860"Speed up label creation with a streamlined process to print and save your "
    … …  
    864864#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    865865#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    866 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     866#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    867867msgid "USPS and DHL Express"
    868868msgstr ""
    … …  
    870870#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    871871#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    872 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     872#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    873873msgid ""
    874874"Ship your productss using trusted shipping carriers like USPS and DHL "
    … …  
    878878#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    879879#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    880 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     880#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    881881msgid "Enhanced label purchase flow"
    882882msgstr ""
    … …  
    884884#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    885885#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    886 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     886#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    887887msgid ""
    888888"Experience a smoother label purchasing process with our updated interface."
    … …  
    891891#: dist/chunks/1c4cd3dc89733fe0b6a4.min.js:1
    892892#: dist/chunks/8d185b5886cb12db43a8.min.js:1
    893 #: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.2.js:1
     893#: dist/woocommerce-services-wcshipping-migration-admin-notice-3.7.3.js:1
    894894msgid "Maybe later"
    895895msgstr ""
  • woocommerce-services/trunk/readme.txt

    r3734320 r3746747  
    88WC requires at least: 11.0
    99WC tested up to: 11.2
    10 Stable tag: 3.7.2
     10Stable tag: 3.7.3
    1111License: GPLv2 or later
    1212License URI: http://www.gnu.org/licenses/gpl-2.0.html
    … …  
    7070
    7171== Changelog ==
     72
     73= 3.7.3 - 2026-10-14 =
     74* Fix   - Stop charging tax on an item the tax service answered as tax-exempt, such as clothing under New York's price threshold, when another item in the same tax class is taxed. This affected orders placed at checkout and orders recalculated in the admin. The cart now also shows the tax the order charges when items in one tax class are taxed differently, including product variations and the same product added twice at different prices.
    7275
    7376= 3.7.2 - 2026-10-08 =
  • woocommerce-services/trunk/vendor/composer/installed.php

    r3734320 r3746747  
    44        'pretty_version' => 'dev-trunk',
    55        'version' => 'dev-trunk',
    6         'reference' => 'da576c383c6574372e4c4d6c61b88989fc109bc6',
     6        'reference' => '84d4dd49245d73c4c107a9af4bcd1f143d15ad5c',
    77        'type' => 'library',
    88        'install_path' => __DIR__ . '/../../',
    … …  
    1414            'pretty_version' => 'dev-trunk',
    1515            'version' => 'dev-trunk',
    16             'reference' => 'da576c383c6574372e4c4d6c61b88989fc109bc6',
     16            'reference' => '84d4dd49245d73c4c107a9af4bcd1f143d15ad5c',
    1717            'type' => 'library',
    1818            'install_path' => __DIR__ . '/../../',
  • woocommerce-services/trunk/woocommerce-services.php

    r3734320 r3746747  
    1111 * License: GPLv2 or later
    1212 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
    13  * Version: 3.7.2
     13 * Version: 3.7.3
    1414 * Requires Plugins: woocommerce
    1515 * Requires PHP: 7.4
Note: See TracChangeset for help on using the changeset viewer.