Commit 78c81c18bd6 for woocommerce

commit 78c81c18bd64157b74321af6d0c150b375cc1443
Author: Andrew Matia <73502748+drewmt@users.noreply.github.com>
Date:   Fri Oct 9 16:24:52 2026 +0300

    Fix order coupon discounts for VAT-exempt orders (#69441)

    * Fix order coupon discounts for VAT-exempt orders

    * Add changelog entry for VAT-exempt order discounts

    * Format coupon regression fixture arguments

    * Move VAT-exempt coupon regression to the main test suite

    * Refactor order VAT exemption and avoid redundant recalculation

diff --git a/plugins/woocommerce/changelog/fix-vat-exempt-order-coupon-discounts b/plugins/woocommerce/changelog/fix-vat-exempt-order-coupon-discounts
new file mode 100644
index 00000000000..3edbea4fb27
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-vat-exempt-order-coupon-discounts
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Apply the full coupon discount and no discount tax to VAT-exempt orders.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index 1acff6b4ade..f24bac99c94 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -1993,15 +1993,17 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 	 */
 	protected function set_item_discount_amounts( $discounts ) {
 		$item_discounts = $discounts->get_discounts_by_item();
-		$tax_location   = $this->get_tax_location();
-		$tax_location   = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] );
+		$is_vat_exempt  = $this->is_vat_exempt();
+
+		$tax_location = $this->get_tax_location();
+		$tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] );

 		if ( $item_discounts ) {
 			foreach ( $item_discounts as $item_id => $amount ) {
 				$item = $this->get_item( $item_id, false );

 				// If the prices include tax, discounts should be taken off the tax inclusive prices like in the cart.
-				if ( $this->get_prices_include_tax() && wc_tax_enabled() && ProductTaxStatus::TAXABLE === $item->get_tax_status() ) {
+				if ( ! $is_vat_exempt && $this->get_prices_include_tax() && wc_tax_enabled() && ProductTaxStatus::TAXABLE === $item->get_tax_status() ) {
 					$taxes = WC_Tax::calc_tax( $amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), true );

 					// Use unrounded taxes so totals will be re-calculated accurately, like in cart.
@@ -2025,8 +2027,10 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 		$coupon_code_to_id = wc_list_pluck( $coupons, 'get_id', 'get_code' );
 		$all_discounts     = $discounts->get_discounts();
 		$coupon_discounts  = $discounts->get_discounts_by_coupon();
-		$tax_location      = $this->get_tax_location();
-		$tax_location      = array(
+		$is_vat_exempt     = $this->is_vat_exempt();
+
+		$tax_location = $this->get_tax_location();
+		$tax_location = array(
 			$tax_location['country'],
 			$tax_location['state'],
 			$tax_location['postcode'],
@@ -2057,7 +2061,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 				foreach ( $all_discounts[ $coupon_code ] as $item_id => $item_discount_amount ) {
 					$item = $this->get_item( $item_id, false );

-					if ( ProductTaxStatus::TAXABLE !== $item->get_tax_status() || ! wc_tax_enabled() ) {
+					if ( $is_vat_exempt || ProductTaxStatus::TAXABLE !== $item->get_tax_status() || ! wc_tax_enabled() ) {
 						continue;
 					}

@@ -2313,6 +2317,24 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 		return WC_Tax::get_rates_from_location( $tax_class, $tax_location, $customer );
 	}

+	/**
+	 * Get the filtered VAT exemption for this order.
+	 *
+	 * @return bool Whether the order is exempt from VAT.
+	 */
+	private function is_vat_exempt(): bool {
+		/**
+		 * Filters whether the order is exempt from VAT.
+		 *
+		 * @since 3.3.0
+		 * @since 11.3.0 Also applied to discounts.
+		 *
+		 * @param bool              $is_vat_exempt Whether the order is exempt from VAT.
+		 * @param WC_Abstract_Order $order         Order instance.
+		 */
+		return (bool) apply_filters( 'woocommerce_order_is_vat_exempt', 'yes' === $this->get_meta( 'is_vat_exempt' ), $this );
+	}
+
 	/**
 	 * Calculate taxes for all line items and shipping, and store the totals and tax rows.
 	 *
@@ -2340,7 +2362,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 			}
 		}

-		$is_vat_exempt = apply_filters( 'woocommerce_order_is_vat_exempt', 'yes' === $this->get_meta( 'is_vat_exempt' ), $this );
+		$is_vat_exempt = $this->is_vat_exempt();

 		// Trigger tax recalculation for all items.
 		foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) {
diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php
index 7b728aeac58..2421c84648e 100644
--- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php
+++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php
@@ -25,6 +25,89 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
 	 */
 	private $registered_order_statuses = array();

+	/**
+	 * @testdox Order coupon amounts and totals respect the filtered VAT exemption when applied and recalculated.
+	 * @testWith ["yes", "yes", null, true]
+	 *           ["no", "yes", null, true]
+	 *           ["yes", "no", true, true]
+	 *           ["no", "no", true, true]
+	 *           ["yes", "yes", false, false]
+	 *           ["no", "yes", false, false]
+	 *           ["yes", "no", null, false]
+	 *           ["no", "no", null, false]
+	 *
+	 * @param string    $prices_include_tax Whether store prices include tax.
+	 * @param string    $exemption_meta     Stored order VAT exemption.
+	 * @param bool|null $filtered_exemption Optional filter override.
+	 * @param bool      $is_exempt          Expected effective exemption.
+	 */
+	public function test_coupon_amounts_respect_order_vat_exemption( string $prices_include_tax, string $exemption_meta, ?bool $filtered_exemption, bool $is_exempt ): void {
+		update_option( 'woocommerce_prices_include_tax', $prices_include_tax );
+		update_option( 'woocommerce_calc_taxes', 'yes' );
+		update_option( 'woocommerce_tax_based_on', 'base' );
+
+		WC_Tax::_insert_tax_rate(
+			array(
+				'tax_rate_country'  => '',
+				'tax_rate_state'    => '',
+				'tax_rate'          => '20.0000',
+				'tax_rate_name'     => 'VAT',
+				'tax_rate_priority' => '1',
+				'tax_rate_compound' => '0',
+				'tax_rate_shipping' => '1',
+				'tax_rate_order'    => '1',
+				'tax_rate_class'    => '',
+			)
+		);
+
+		$product = WC_Helper_Product::create_simple_product();
+		$order   = wc_create_order();
+		$order->update_meta_data( 'is_vat_exempt', $exemption_meta );
+		$order->add_product(
+			$product,
+			1,
+			array(
+				'subtotal' => 100,
+				'total'    => 100,
+			)
+		);
+
+		if ( null !== $filtered_exemption ) {
+			add_filter(
+				'woocommerce_order_is_vat_exempt',
+				static function ( $exempt, $filtered_order ) use ( $order, $filtered_exemption ) {
+					return $filtered_order->get_id() === $order->get_id() ? $filtered_exemption : $exempt;
+				},
+				10,
+				2
+			);
+		}
+
+		$order->calculate_totals();
+		$coupon = new WC_Coupon();
+		$coupon->set_code( 'vat-exemption-coupon' );
+		$coupon->set_discount_type( 'percent' );
+		$coupon->set_amount( 20 );
+		$coupon->save();
+
+		$this->assertTrue( $order->apply_coupon( $coupon ), 'The percentage coupon should apply successfully.' );
+
+		foreach ( array( 'applied', 'recalculated' ) as $operation ) {
+			if ( 'recalculated' === $operation ) {
+				$order = wc_get_order( $order->get_id() );
+				$order->recalculate_coupons();
+			}
+
+			$coupon_items = $order->get_items( 'coupon' );
+			$coupon_item  = reset( $coupon_items );
+			$this->assertEquals( 20, $coupon_item->get_discount(), "The $operation coupon should discount the net subtotal by 20%." );
+			$this->assertEquals( $is_exempt ? 0 : 4, $coupon_item->get_discount_tax(), "The $operation coupon tax should respect the effective exemption." );
+			$this->assertEquals( 20, $order->get_discount_total(), "The $operation order should retain the complete discount." );
+			$this->assertEquals( $is_exempt ? 0 : 4, $order->get_discount_tax(), "The $operation order discount tax should match the coupon." );
+			$this->assertEquals( $is_exempt ? 80 : 96, $order->get_total(), "The $operation order should have the correct discounted total." );
+		}
+	}
+
 	/**
 	 * @testdox Order and refund defaults capture the store tax mode without pending changes.
 	 * @dataProvider provide_order_tax_defaults