Commit 4252b33a800 for woocommerce
commit 4252b33a8009117f3463103cad10de71f54e87b2
Author: Brandon Kraft <public@brandonkraft.com>
Date: Thu Jul 9 01:39:28 2026 -0500
Rename new coupon result filters to woocommerce_coupon_is_valid_for_* (#66433)
PR #64363 (merged to trunk, unreleased, @since 11.1.0) added six coupon
validation result filters under the woocommerce_coupon_validate_* prefix
with true=valid polarity. That prefix is already owned by three shipped
filters (expiry_date, minimum_amount, maximum_amount) that use the
opposite true=reject polarity, so the shared prefix was a public-API
footgun.
Rename the six new filters to woocommerce_coupon_is_valid_for_* (polarity
unchanged: true=valid). The is_* prefix mirrors the existing
woocommerce_coupon_is_valid filter and makes the true=valid semantics
self-documenting, cleanly separated from the validate_* (true=reject)
family. Docblocks now call out the polarity difference explicitly.
Because the filters have never shipped in a stable release, this is a
pre-freeze rename with no backward-compatibility impact and no
deprecation window required.
Tests updated to the new names and extended to cover the force-reject
(return false) direction for all six filters, not just two.
Bug introduced in PR #64363.
diff --git a/plugins/woocommerce/changelog/fix-coupon-validate-filter-polarity b/plugins/woocommerce/changelog/fix-coupon-validate-filter-polarity
new file mode 100644
index 00000000000..1d4d94db09d
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-coupon-validate-filter-polarity
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Rename the new coupon result filters to woocommerce_coupon_is_valid_for_* so their true=valid polarity is clear and no longer collides with the woocommerce_coupon_validate_* family.
diff --git a/plugins/woocommerce/includes/class-wc-discounts.php b/plugins/woocommerce/includes/class-wc-discounts.php
index c5be196095b..9da858cfb61 100644
--- a/plugins/woocommerce/includes/class-wc-discounts.php
+++ b/plugins/woocommerce/includes/class-wc-discounts.php
@@ -810,16 +810,19 @@ class WC_Discounts {
}
/**
- * Filter the result of coupon product_ids validation.
+ * Filter whether the coupon is valid for the cart given its product_ids restriction.
*
- * Return true to treat the coupon as valid for the cart, or false to reject it.
+ * Return true to treat the coupon as valid, or false to reject it. Mind the polarity: these
+ * woocommerce_coupon_is_valid_for_* filters follow the woocommerce_coupon_is_valid convention where
+ * true means valid. This is the opposite of the woocommerce_coupon_validate_* filters
+ * (e.g. woocommerce_coupon_validate_expiry_date), where returning true rejects the coupon.
*
* @since 11.1.0
- * @param bool $valid Whether the coupon is valid.
+ * @param bool $valid Whether the coupon is valid for the selected products.
* @param WC_Coupon $coupon Coupon data.
* @param WC_Discounts $discounts The discounts instance.
*/
- $valid = apply_filters( 'woocommerce_coupon_validate_product_ids', $valid, $coupon, $this );
+ $valid = apply_filters( 'woocommerce_coupon_is_valid_for_product_ids', $valid, $coupon, $this );
if ( ! $valid ) {
throw new Exception(
@@ -867,16 +870,19 @@ class WC_Discounts {
}
/**
- * Filter the result of coupon product_categories validation.
+ * Filter whether the coupon is valid for the cart given its product_categories restriction.
*
- * Return true to treat the coupon as valid for the cart, or false to reject it.
+ * Return true to treat the coupon as valid, or false to reject it. Mind the polarity: these
+ * woocommerce_coupon_is_valid_for_* filters follow the woocommerce_coupon_is_valid convention where
+ * true means valid. This is the opposite of the woocommerce_coupon_validate_* filters
+ * (e.g. woocommerce_coupon_validate_expiry_date), where returning true rejects the coupon.
*
* @since 11.1.0
- * @param bool $valid Whether the coupon is valid.
+ * @param bool $valid Whether the coupon is valid for the selected product categories.
* @param WC_Coupon $coupon Coupon data.
* @param WC_Discounts $discounts The discounts instance.
*/
- $valid = apply_filters( 'woocommerce_coupon_validate_product_categories', $valid, $coupon, $this );
+ $valid = apply_filters( 'woocommerce_coupon_is_valid_for_product_categories', $valid, $coupon, $this );
if ( ! $valid ) {
throw new Exception(
@@ -913,16 +919,19 @@ class WC_Discounts {
}
/**
- * Filter the result of coupon sale_items validation.
+ * Filter whether the coupon is valid for the cart given its sale_items restriction.
*
- * Return true to treat the coupon as valid for the cart, or false to reject it.
+ * Return true to treat the coupon as valid, or false to reject it. Mind the polarity: these
+ * woocommerce_coupon_is_valid_for_* filters follow the woocommerce_coupon_is_valid convention where
+ * true means valid. This is the opposite of the woocommerce_coupon_validate_* filters
+ * (e.g. woocommerce_coupon_validate_expiry_date), where returning true rejects the coupon.
*
* @since 11.1.0
- * @param bool $valid Whether the coupon is valid.
+ * @param bool $valid Whether the coupon is valid given the sale items in the cart.
* @param WC_Coupon $coupon Coupon data.
* @param WC_Discounts $discounts The discounts instance.
*/
- $valid = apply_filters( 'woocommerce_coupon_validate_sale_items', $valid, $coupon, $this );
+ $valid = apply_filters( 'woocommerce_coupon_is_valid_for_sale_items', $valid, $coupon, $this );
if ( ! $valid ) {
throw new Exception(
@@ -960,16 +969,19 @@ class WC_Discounts {
}
/**
- * Filter the result of coupon excluded_items validation.
+ * Filter whether the coupon is valid for the cart given its excluded items.
*
- * Return true to treat the coupon as valid for the cart, or false to reject it.
+ * Return true to treat the coupon as valid, or false to reject it. Mind the polarity: these
+ * woocommerce_coupon_is_valid_for_* filters follow the woocommerce_coupon_is_valid convention where
+ * true means valid. This is the opposite of the woocommerce_coupon_validate_* filters
+ * (e.g. woocommerce_coupon_validate_expiry_date), where returning true rejects the coupon.
*
* @since 11.1.0
- * @param bool $valid Whether the coupon is valid.
+ * @param bool $valid Whether the coupon is valid given the cart's exclusion rules.
* @param WC_Coupon $coupon Coupon data.
* @param WC_Discounts $discounts The discounts instance.
*/
- $valid = apply_filters( 'woocommerce_coupon_validate_excluded_items', $valid, $coupon, $this );
+ $valid = apply_filters( 'woocommerce_coupon_is_valid_for_excluded_items', $valid, $coupon, $this );
if ( ! $valid ) {
throw new Exception(
@@ -1026,18 +1038,21 @@ class WC_Discounts {
$valid = empty( $products );
/**
- * Filter the result of coupon excluded_product_ids validation.
+ * Filter whether the coupon is valid for the cart given its excluded_product_ids restriction.
*
- * Return true to treat the coupon as valid for the cart, or false to reject it. When rejecting,
- * use the woocommerce_coupon_error filter (error code WC_Coupon::E_WC_COUPON_EXCLUDED_PRODUCTS)
- * to customize the message shown to the shopper.
+ * Return true to treat the coupon as valid, or false to reject it. Mind the polarity: these
+ * woocommerce_coupon_is_valid_for_* filters follow the woocommerce_coupon_is_valid convention where
+ * true means valid. This is the opposite of the woocommerce_coupon_validate_* filters
+ * (e.g. woocommerce_coupon_validate_expiry_date), where returning true rejects the coupon. When
+ * rejecting, use the woocommerce_coupon_error filter (error code
+ * WC_Coupon::E_WC_COUPON_EXCLUDED_PRODUCTS) to customize the message shown to the shopper.
*
* @since 11.1.0
- * @param bool $valid Whether the coupon is valid.
+ * @param bool $valid Whether the coupon is valid given the excluded products.
* @param WC_Coupon $coupon Coupon data.
* @param WC_Discounts $discounts The discounts instance.
*/
- $valid = apply_filters( 'woocommerce_coupon_validate_excluded_product_ids', $valid, $coupon, $this );
+ $valid = apply_filters( 'woocommerce_coupon_is_valid_for_excluded_product_ids', $valid, $coupon, $this );
if ( ! $valid ) {
// When a filter forces rejection but no cart item matched, fall back to the generic message.
@@ -1097,18 +1112,21 @@ class WC_Discounts {
$valid = empty( $categories );
/**
- * Filter the result of coupon excluded_product_categories validation.
+ * Filter whether the coupon is valid for the cart given its excluded_product_categories restriction.
*
- * Return true to treat the coupon as valid for the cart, or false to reject it. When rejecting,
- * use the woocommerce_coupon_error filter (error code WC_Coupon::E_WC_COUPON_EXCLUDED_CATEGORIES)
- * to customize the message shown to the shopper.
+ * Return true to treat the coupon as valid, or false to reject it. Mind the polarity: these
+ * woocommerce_coupon_is_valid_for_* filters follow the woocommerce_coupon_is_valid convention where
+ * true means valid. This is the opposite of the woocommerce_coupon_validate_* filters
+ * (e.g. woocommerce_coupon_validate_expiry_date), where returning true rejects the coupon. When
+ * rejecting, use the woocommerce_coupon_error filter (error code
+ * WC_Coupon::E_WC_COUPON_EXCLUDED_CATEGORIES) to customize the message shown to the shopper.
*
* @since 11.1.0
- * @param bool $valid Whether the coupon is valid.
+ * @param bool $valid Whether the coupon is valid given the excluded categories.
* @param WC_Coupon $coupon Coupon data.
* @param WC_Discounts $discounts The discounts instance.
*/
- $valid = apply_filters( 'woocommerce_coupon_validate_excluded_product_categories', $valid, $coupon, $this );
+ $valid = apply_filters( 'woocommerce_coupon_is_valid_for_excluded_product_categories', $valid, $coupon, $this );
if ( ! $valid ) {
// When a filter forces rejection but no cart item matched, fall back to the generic message.
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php b/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php
index 03f4bd611f5..c00168278c5 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php
@@ -241,7 +241,7 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
}
/**
- * @testdox The woocommerce_coupon_validate_product_ids filter can bypass or force product ID validation.
+ * @testdox The woocommerce_coupon_is_valid_for_product_ids filter can bypass or force product ID validation.
*/
public function test_product_ids_validation_filter() {
$in_cart = WC_Helper_Product::create_simple_product();
@@ -263,9 +263,9 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
$this->assertWPError( $result, 'A coupon restricted to a product not in the cart should be invalid.' );
$this->assertStringContainsString( 'is not applicable to selected products', $result->get_error_message(), 'The failure should come from the product_ids check.' );
- add_filter( 'woocommerce_coupon_validate_product_ids', '__return_true' );
+ add_filter( 'woocommerce_coupon_is_valid_for_product_ids', '__return_true' );
$this->assertTrue( $discounts->is_coupon_valid( $coupon ), 'Returning true from the filter should bypass the product ID restriction.' );
- remove_filter( 'woocommerce_coupon_validate_product_ids', '__return_true' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_product_ids', '__return_true' );
$this->assertWPError( $discounts->is_coupon_valid( $coupon ), 'Normal validation should resume once the filter is removed.' );
@@ -280,15 +280,15 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
$valid_coupon->save();
$this->assertTrue( $discounts->is_coupon_valid( $valid_coupon ), 'A coupon restricted to a product in the cart should be valid.' );
- add_filter( 'woocommerce_coupon_validate_product_ids', '__return_false' );
+ add_filter( 'woocommerce_coupon_is_valid_for_product_ids', '__return_false' );
$result = $discounts->is_coupon_valid( $valid_coupon );
$this->assertWPError( $result, 'Returning false from the filter should force rejection.' );
$this->assertStringContainsString( 'is not applicable to selected products', $result->get_error_message(), 'Forced rejection should surface the product_ids error.' );
- remove_filter( 'woocommerce_coupon_validate_product_ids', '__return_false' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_product_ids', '__return_false' );
}
/**
- * @testdox The woocommerce_coupon_validate_product_categories filter can bypass category validation.
+ * @testdox The woocommerce_coupon_is_valid_for_product_categories filter can bypass or force category validation.
*/
public function test_product_categories_validation_filter() {
$product = WC_Helper_Product::create_simple_product();
@@ -310,13 +310,38 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
$this->assertWPError( $result, 'A coupon restricted to a category none of the cart items belong to should be invalid.' );
$this->assertStringContainsString( 'is not applicable to selected products', $result->get_error_message(), 'The failure should come from the product_categories check.' );
- add_filter( 'woocommerce_coupon_validate_product_categories', '__return_true' );
+ add_filter( 'woocommerce_coupon_is_valid_for_product_categories', '__return_true' );
$this->assertTrue( $discounts->is_coupon_valid( $coupon ), 'Returning true from the filter should bypass the category restriction.' );
- remove_filter( 'woocommerce_coupon_validate_product_categories', '__return_true' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_product_categories', '__return_true' );
+
+ // Force-reject a coupon that is otherwise valid for the cart's categories.
+ $stocked_term = wp_insert_term( 'coupon-cat-stocked-' . uniqid(), 'product_cat' );
+ $categorized = WC_Helper_Product::create_simple_product();
+ $categorized->set_category_ids( array( $stocked_term['term_id'] ) );
+ $categorized->save();
+ WC()->cart->add_to_cart( $categorized->get_id(), 1 );
+ $discounts = new WC_Discounts( WC()->cart );
+
+ $valid_coupon = new WC_Coupon();
+ $valid_coupon->set_props(
+ array(
+ 'discount_type' => 'fixed_cart',
+ 'amount' => 10,
+ 'product_categories' => array( $stocked_term['term_id'] ),
+ )
+ );
+ $valid_coupon->save();
+
+ $this->assertTrue( $discounts->is_coupon_valid( $valid_coupon ), 'A coupon whose category is in the cart should be valid.' );
+ add_filter( 'woocommerce_coupon_is_valid_for_product_categories', '__return_false' );
+ $result = $discounts->is_coupon_valid( $valid_coupon );
+ $this->assertWPError( $result, 'Returning false from the filter should force rejection.' );
+ $this->assertStringContainsString( 'is not applicable to selected products', $result->get_error_message(), 'Forced rejection should surface the product_categories error.' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_product_categories', '__return_false' );
}
/**
- * @testdox The woocommerce_coupon_validate_excluded_items filter can bypass excluded item validation.
+ * @testdox The woocommerce_coupon_is_valid_for_excluded_items filter can bypass or force excluded item validation.
*/
public function test_excluded_items_validation_filter() {
$product = WC_Helper_Product::create_simple_product();
@@ -338,13 +363,30 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
$this->assertWPError( $result, 'A product coupon whose only cart item is excluded should be invalid.' );
$this->assertStringContainsString( 'is not applicable to selected products', $result->get_error_message(), 'The failure should come from the excluded_items check.' );
- add_filter( 'woocommerce_coupon_validate_excluded_items', '__return_true' );
+ add_filter( 'woocommerce_coupon_is_valid_for_excluded_items', '__return_true' );
$this->assertTrue( $discounts->is_coupon_valid( $coupon ), 'Returning true from the filter should bypass the excluded item restriction.' );
- remove_filter( 'woocommerce_coupon_validate_excluded_items', '__return_true' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_excluded_items', '__return_true' );
+
+ // Force-reject an unrestricted product coupon that is otherwise valid for the cart items.
+ $valid_coupon = new WC_Coupon();
+ $valid_coupon->set_props(
+ array(
+ 'discount_type' => 'percent',
+ 'amount' => 10,
+ )
+ );
+ $valid_coupon->save();
+
+ $this->assertTrue( $discounts->is_coupon_valid( $valid_coupon ), 'An unrestricted product coupon should be valid for the cart.' );
+ add_filter( 'woocommerce_coupon_is_valid_for_excluded_items', '__return_false' );
+ $result = $discounts->is_coupon_valid( $valid_coupon );
+ $this->assertWPError( $result, 'Returning false from the filter should force rejection.' );
+ $this->assertStringContainsString( 'is not applicable to selected products', $result->get_error_message(), 'Forced rejection should surface the excluded_items error.' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_excluded_items', '__return_false' );
}
/**
- * @testdox The woocommerce_coupon_validate_sale_items filter can bypass sale item validation.
+ * @testdox The woocommerce_coupon_is_valid_for_sale_items filter can bypass or force sale item validation.
*/
public function test_sale_items_validation_filter() {
$product = WC_Helper_Product::create_simple_product();
@@ -369,13 +411,38 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
$this->assertWPError( $result, 'A coupon excluding sale items should be invalid when the cart holds a sale item.' );
$this->assertStringContainsString( 'is not valid for sale items', $result->get_error_message(), 'The failure should come from the sale_items check.' );
- add_filter( 'woocommerce_coupon_validate_sale_items', '__return_true' );
+ add_filter( 'woocommerce_coupon_is_valid_for_sale_items', '__return_true' );
$this->assertTrue( $discounts->is_coupon_valid( $coupon ), 'Returning true from the filter should bypass the sale item restriction.' );
- remove_filter( 'woocommerce_coupon_validate_sale_items', '__return_true' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_sale_items', '__return_true' );
+
+ // Force-reject a sale-exclusion coupon that is otherwise valid (cart holds no sale items).
+ WC()->cart->empty_cart();
+ $regular = WC_Helper_Product::create_simple_product();
+ $regular->set_regular_price( 10 );
+ $regular->save();
+ WC()->cart->add_to_cart( $regular->get_id(), 1 );
+ $discounts = new WC_Discounts( WC()->cart );
+
+ $valid_coupon = new WC_Coupon();
+ $valid_coupon->set_props(
+ array(
+ 'discount_type' => 'fixed_cart',
+ 'amount' => 10,
+ 'exclude_sale_items' => true,
+ )
+ );
+ $valid_coupon->save();
+
+ $this->assertTrue( $discounts->is_coupon_valid( $valid_coupon ), 'A sale-exclusion coupon should be valid when the cart has no sale items.' );
+ add_filter( 'woocommerce_coupon_is_valid_for_sale_items', '__return_false' );
+ $result = $discounts->is_coupon_valid( $valid_coupon );
+ $this->assertWPError( $result, 'Returning false from the filter should force rejection.' );
+ $this->assertStringContainsString( 'is not valid for sale items', $result->get_error_message(), 'Forced rejection should surface the sale_items error.' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_sale_items', '__return_false' );
}
/**
- * @testdox The woocommerce_coupon_validate_excluded_product_ids filter can bypass excluded product ID validation.
+ * @testdox The woocommerce_coupon_is_valid_for_excluded_product_ids filter can bypass excluded product ID validation.
*/
public function test_excluded_product_ids_validation_filter() {
$product = WC_Helper_Product::create_simple_product();
@@ -397,9 +464,9 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
$this->assertWPError( $result, 'A coupon excluding the only cart product should be invalid.' );
$this->assertStringContainsString( 'is not applicable to the products', $result->get_error_message(), 'The failure should come from the excluded_product_ids check.' );
- add_filter( 'woocommerce_coupon_validate_excluded_product_ids', '__return_true' );
+ add_filter( 'woocommerce_coupon_is_valid_for_excluded_product_ids', '__return_true' );
$this->assertTrue( $discounts->is_coupon_valid( $coupon ), 'Returning true from the filter should bypass the excluded product restriction.' );
- remove_filter( 'woocommerce_coupon_validate_excluded_product_ids', '__return_true' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_excluded_product_ids', '__return_true' );
// Force-reject when no cart item matched: the generic message is used, not a dangling empty list.
$unmatched_coupon = new WC_Coupon();
@@ -413,16 +480,16 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
$unmatched_coupon->save();
$this->assertTrue( $discounts->is_coupon_valid( $unmatched_coupon ), 'A coupon excluding a product not in the cart is valid by default.' );
- add_filter( 'woocommerce_coupon_validate_excluded_product_ids', '__return_false' );
+ add_filter( 'woocommerce_coupon_is_valid_for_excluded_product_ids', '__return_false' );
$result = $discounts->is_coupon_valid( $unmatched_coupon );
$this->assertWPError( $result, 'Returning false should reject even when no excluded product is in the cart.' );
$this->assertStringContainsString( 'is not applicable to selected products', $result->get_error_message(), 'With no matched products the generic message is used.' );
$this->assertStringNotContainsString( 'products: .', $result->get_error_message(), 'The rejection message should not contain an empty product list.' );
- remove_filter( 'woocommerce_coupon_validate_excluded_product_ids', '__return_false' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_excluded_product_ids', '__return_false' );
}
/**
- * @testdox The woocommerce_coupon_validate_excluded_product_categories filter can bypass excluded category validation.
+ * @testdox The woocommerce_coupon_is_valid_for_excluded_product_categories filter can bypass or force excluded category validation.
*/
public function test_excluded_product_categories_validation_filter() {
$term = wp_insert_term( 'excl-cat-' . uniqid(), 'product_cat' );
@@ -447,8 +514,27 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
$this->assertWPError( $result, 'A coupon excluding the cart product\'s category should be invalid.' );
$this->assertStringContainsString( 'is not applicable to the categories', $result->get_error_message(), 'The failure should come from the excluded_product_categories check.' );
- add_filter( 'woocommerce_coupon_validate_excluded_product_categories', '__return_true' );
+ add_filter( 'woocommerce_coupon_is_valid_for_excluded_product_categories', '__return_true' );
$this->assertTrue( $discounts->is_coupon_valid( $coupon ), 'Returning true from the filter should bypass the excluded category restriction.' );
- remove_filter( 'woocommerce_coupon_validate_excluded_product_categories', '__return_true' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_excluded_product_categories', '__return_true' );
+
+ // Force-reject when no cart item is in the excluded category: the generic message is used.
+ $unused_term = wp_insert_term( 'excl-cat-unused-' . uniqid(), 'product_cat' );
+ $unmatched_coupon = new WC_Coupon();
+ $unmatched_coupon->set_props(
+ array(
+ 'discount_type' => 'fixed_cart',
+ 'amount' => 10,
+ 'excluded_product_categories' => array( $unused_term['term_id'] ),
+ )
+ );
+ $unmatched_coupon->save();
+
+ $this->assertTrue( $discounts->is_coupon_valid( $unmatched_coupon ), 'A coupon excluding a category not in the cart is valid by default.' );
+ add_filter( 'woocommerce_coupon_is_valid_for_excluded_product_categories', '__return_false' );
+ $result = $discounts->is_coupon_valid( $unmatched_coupon );
+ $this->assertWPError( $result, 'Returning false should reject even when no excluded category is in the cart.' );
+ $this->assertStringContainsString( 'is not applicable to selected products', $result->get_error_message(), 'With no matched categories the generic message is used.' );
+ remove_filter( 'woocommerce_coupon_is_valid_for_excluded_product_categories', '__return_false' );
}
}