Commit b786163a6c3 for woocommerce

commit b786163a6c30b277d8268fd218b2463077b761c3
Author: Daniel Kam <hey@danielk.am>
Date:   Wed Oct 14 23:12:48 2026 +0800

    Use filterable coupon-removed message when removing a coupon via AJAX (#69271)

    WC_AJAX::remove_coupon() hardcoded "Coupon has been removed.", bypassing
    WC_Coupon::get_coupon_message() and the woocommerce_coupon_message filter.
    It now uses WC_Coupon::add_coupon_message( WC_COUPON_REMOVED ).

    Fixes #31198

    Co-authored-by: Daniel Kam <danielkam@Daniels-MacBook-Pro.local>
    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/31198-filterable-coupon-removed-message b/plugins/woocommerce/changelog/31198-filterable-coupon-removed-message
new file mode 100644
index 00000000000..aa79184d14d
--- /dev/null
+++ b/plugins/woocommerce/changelog/31198-filterable-coupon-removed-message
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Use the filterable coupon-removed message from `WC_Coupon::get_coupon_message()` when removing a coupon via AJAX.
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index e54da4f5c7f..95fb13f26b3 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -322,7 +322,7 @@ class WC_AJAX {
 			wc_add_notice( __( 'Sorry there was a problem removing this coupon.', 'woocommerce' ), 'error' );
 		} else {
 			WC()->cart->remove_coupon( $coupon );
-			wc_add_notice( __( 'Coupon has been removed.', 'woocommerce' ) );
+			( new WC_Coupon( $coupon ) )->add_coupon_message( WC_Coupon::WC_COUPON_REMOVED );
 		}

 		wc_print_notices();
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
index b7536ca0986..4c18626fad9 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
@@ -146,6 +146,71 @@ class WC_AJAX_Test extends \WP_Ajax_UnitTestCase {
 		$this->assertEquals( $response['data']['message'], 'There was an error generating your API Key.' );
 	}

+	/**
+	 * @testdox Removing a coupon via AJAX shows the coupon-removed message from WC_Coupon::get_coupon_message().
+	 */
+	public function test_remove_coupon_uses_coupon_removed_message(): void {
+		$coupon = WC_Helper_Coupon::create_coupon( 'ajaxremove' );
+		WC()->cart->empty_cart();
+		WC()->cart->add_to_cart( WC_Helper_Product::create_simple_product()->get_id() );
+		WC()->cart->apply_coupon( $coupon->get_code() );
+		wc_clear_notices();
+
+		$response = $this->do_remove_coupon_request( $coupon->get_code() );
+
+		$this->assertStringContainsString( 'Coupon code removed successfully.', $response, 'The default coupon-removed message should be shown.' );
+		$this->assertStringNotContainsString( 'Coupon has been removed.', $response, 'The old hardcoded message should no longer be shown.' );
+		$this->assertNotContains( $coupon->get_code(), WC()->cart->get_applied_coupons(), 'The coupon should be removed from the cart.' );
+	}
+
+	/**
+	 * @testdox Removing a coupon via AJAX respects the woocommerce_coupon_message filter.
+	 */
+	public function test_remove_coupon_message_is_filterable(): void {
+		$coupon = WC_Helper_Coupon::create_coupon( 'ajaxremovefiltered' );
+		WC()->cart->empty_cart();
+		WC()->cart->add_to_cart( WC_Helper_Product::create_simple_product()->get_id() );
+		WC()->cart->apply_coupon( $coupon->get_code() );
+		wc_clear_notices();
+
+		$filter_args = array();
+		$filter      = function ( $msg, $msg_code, $filtered_coupon ) use ( &$filter_args ) {
+			$filter_args = array( $msg_code, $filtered_coupon->get_code() );
+			return WC_Coupon::WC_COUPON_REMOVED === $msg_code ? 'Custom coupon removed message.' : $msg;
+		};
+		add_filter( 'woocommerce_coupon_message', $filter, 10, 3 );
+
+		try {
+			$response = $this->do_remove_coupon_request( $coupon->get_code() );
+		} finally {
+			remove_filter( 'woocommerce_coupon_message', $filter, 10 );
+		}
+
+		$this->assertStringContainsString( 'Custom coupon removed message.', $response, 'The filtered message should be shown.' );
+		$this->assertSame( array( WC_Coupon::WC_COUPON_REMOVED, $coupon->get_code() ), $filter_args, 'The filter should receive the removed code and the removed coupon.' );
+	}
+
+	/**
+	 * Runs the woocommerce_remove_coupon AJAX action and returns its output.
+	 *
+	 * @param string $coupon_code Coupon code to remove.
+	 * @return string
+	 */
+	private function do_remove_coupon_request( string $coupon_code ): string {
+		$_POST['security'] = wp_create_nonce( 'remove-coupon' );
+		$_POST['coupon']   = $coupon_code;
+
+		try {
+			$this->_handleAjax( 'woocommerce_remove_coupon' );
+		} catch ( WPAjaxDieContinueException $e ) {
+			unset( $e );
+		} finally {
+			unset( $_POST['security'], $_POST['coupon'] );
+		}
+
+		return (string) $this->_last_response;
+	}
+
 	/**
 	 * @testdox Saving a new shipping class with a blank slug generates and returns its persisted slug.
 	 */