Commit 5b95a0ba269 for woocommerce

commit 5b95a0ba269084d1200a1033087fac73fa54c181
Author: SH Sajal Chowdhury <72102985+shsajalchowdhury@users.noreply.github.com>
Date:   Wed Sep 2 20:04:28 2026 +0600

    Fix: Add woocommerce_removed_coupon action hook to order class (#64361)

    * Fix: Add woocommerce_removed_coupon action hook to order class

    Add do_action('woocommerce_removed_coupon') in WC_Order::remove_coupon()
    for parity with WC_Cart::remove_coupon() which already has this hook.

    The order version also passes the order instance as a second parameter
    for additional context, which the cart version does not have.

    Fixes #36114

    * Fix: Use order-specific hook name to avoid cart listener cross-talk

    Rename woocommerce_removed_coupon to woocommerce_order_removed_coupon
    to match the pattern used by apply_coupon() which uses
    woocommerce_order_applied_coupon. This prevents cart-scoped listeners
    (like WC_Cart_Session::set_session) from firing during order coupon
    removal.

    Per CodeRabbit review feedback.

    * Add changelog entry

    * Address review feedback: pass WC_Coupon object and add test

    - Change hook first parameter from $code (string) to $coupon_object (WC_Coupon)
      for consistency with woocommerce_order_applied_coupon hook
    - Update docblock to match apply_coupon hook format (param alignment, @since placement)
    - Add test_remove_coupon_fires_order_removed_coupon_hook verifying:
      - Hook fires when coupon is removed from order
      - First parameter is WC_Coupon instance with correct code
      - Second parameter is the same WC_Order instance
      - Coupon is actually removed from order items
    - Update changelog entry with correct hook name

    * Wrap test assertions in try/finally to prevent hook leak on failure

    Ensures remove_all_actions('woocommerce_order_removed_coupon') always
    runs even if an assertion fails, preventing hook leakage into other tests.

    * Fix lint

    * Fix PHPStan

    * Fix Lint

    ---------

    Co-authored-by: Tom Cafferkey <tjcafferkey@gmail.com>

diff --git a/plugins/woocommerce/changelog/pr-64361 b/plugins/woocommerce/changelog/pr-64361
new file mode 100644
index 00000000000..d9a11366752
--- /dev/null
+++ b/plugins/woocommerce/changelog/pr-64361
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add woocommerce_order_removed_coupon action hook to WC_Order::remove_coupon() for parity with WC_Cart and the existing woocommerce_order_applied_coupon hook.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index ad074f6836c..102fafe9e0f 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -1874,6 +1874,16 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 				$coupon_object->decrease_usage_count( $this->get_user_id() );
 				$this->recalculate_coupons();

+				/**
+				 * Action hook fired when a coupon is removed from an order.
+				 *
+				 * @param  WC_Coupon $coupon_object The removed coupon object.
+				 * @param  WC_Order  $order         The current order object.
+				 *
+				 * @since 10.8.0
+				 */
+				do_action( 'woocommerce_order_removed_coupon', $coupon_object, $this );
+
 				return true;
 			}
 		}
@@ -3163,7 +3173,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 		 *
 		 * @param string   $total_html The formatted total COGS HTML.
 		 * @param float    $total      The total COGS value.
-		 * @param WC_Order $order      The order object.
+		 * @param WC_Abstract_Order $order The order object.
 		 */
 		return apply_filters(
 			'woocommerce_order_cogs_total_value_html',
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 d50e0419526..53b78cab8a9 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
@@ -121,7 +121,8 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
 			)
 		);

-		update_user_meta( $admin_id, 'billing_country', 'MV' ); // Different than customer's address and base location.
+		update_user_meta( $admin_id, 'billing_country', 'MV' );
+		// Different than customer's address and base location.
 		wp_set_current_user( $admin_id );
 		WC()->customer = null;
 		WC()->initialize_cart();
@@ -319,6 +320,48 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
 		$this->assertEquals( $coupon_code, $coupon_info[1] );
 	}

+	/**
+	 * Test remove_coupon fires woocommerce_order_removed_coupon hook with WC_Coupon object.
+	 */
+	public function test_remove_coupon_fires_order_removed_coupon_hook() {
+		$coupon_code = 'remove_hook_test';
+		$coupon      = WC_Helper_Coupon::create_coupon( $coupon_code );
+		$order       = WC_Helper_Order::create_order();
+		$order->set_status( OrderStatus::PROCESSING );
+		$order->save();
+
+		$order->apply_coupon( $coupon_code );
+		$this->assertCount( 1, $order->get_items( 'coupon' ) );
+
+		$hook_fired  = false;
+		$hook_coupon = null;
+		$hook_order  = null;
+
+		add_action(
+			'woocommerce_order_removed_coupon',
+			function ( $coupon_obj, $order_obj ) use ( &$hook_fired, &$hook_coupon, &$hook_order ) {
+				$hook_fired  = true;
+				$hook_coupon = $coupon_obj;
+				$hook_order  = $order_obj;
+			},
+			10,
+			2
+		);
+
+		try {
+			$result = $order->remove_coupon( $coupon_code );
+
+			$this->assertTrue( $result );
+			$this->assertTrue( $hook_fired, 'woocommerce_order_removed_coupon hook did not fire.' );
+			$this->assertInstanceOf( WC_Coupon::class, $hook_coupon, 'First parameter should be a WC_Coupon instance.' );
+			$this->assertEquals( $coupon->get_code(), $hook_coupon->get_code(), 'Hook coupon code should match.' );
+			$this->assertSame( $order, $hook_order, 'Second parameter should be the same WC_Order instance.' );
+			$this->assertCount( 0, $order->get_items( 'coupon' ) );
+		} finally {
+			remove_all_actions( 'woocommerce_order_removed_coupon' );
+		}
+	}
+
 	/**
 	 * Create a pending order with one $100 product whose line total was manually edited to $50.
 	 *
@@ -766,7 +809,8 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
 		$this->add_product_with_cogs_to_order( $order, 12.34, 2 );
 		$this->add_product_with_cogs_to_order( $order, 56.78, 3 );

-		$fee = new WC_Order_Item_Fee(); // Example of line item without COGS.
+		$fee = new WC_Order_Item_Fee();
+		// Example of line item without COGS.
 		$order->add_item( $fee );

 		$calculated_value = $order->calculate_cogs_total_value();