Commit 33da01922b for woocommerce
commit 33da01922bad5709a62c1dd1fe7b61491aad989a
Author: Néstor Soriano <konamiman@konamiman.com>
Date: Wed Sep 3 11:31:29 2025 +0200
Fix error when a custom coupon discount type is "unregistered" (#60712)
diff --git a/plugins/woocommerce/changelog/pr-60712 b/plugins/woocommerce/changelog/pr-60712
new file mode 100644
index 0000000000..cf59238829
--- /dev/null
+++ b/plugins/woocommerce/changelog/pr-60712
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix error when a custom coupon discount type is unregistered and an order using such a coupon is updated
diff --git a/plugins/woocommerce/includes/class-wc-coupon.php b/plugins/woocommerce/includes/class-wc-coupon.php
index 40d1089c7c..0380021fff 100644
--- a/plugins/woocommerce/includes/class-wc-coupon.php
+++ b/plugins/woocommerce/includes/class-wc-coupon.php
@@ -543,10 +543,21 @@ class WC_Coupon extends WC_Legacy_Coupon {
* @param string $discount_type Discount type.
*/
public function set_discount_type( $discount_type ) {
+ $this->set_discount_type_core( $discount_type, true );
+ }
+
+ /**
+ * Set discount type, optionally disabling the type verification.
+ *
+ * @since 10.3.0
+ * @param string $discount_type Discount type.
+ * @param bool $verify_discount_type Whether to verify if the discount type is valid.
+ */
+ private function set_discount_type_core( $discount_type, bool $verify_discount_type ) {
if ( 'percent_product' === $discount_type ) {
$discount_type = 'percent'; // Backwards compatibility.
}
- if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ), true ) ) {
+ if ( $verify_discount_type && ! in_array( $discount_type, array_keys( wc_get_coupon_types() ), true ) ) {
$this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type.', 'woocommerce' ) );
}
$this->set_prop( 'discount_type', $discount_type );
@@ -1267,7 +1278,7 @@ class WC_Coupon extends WC_Legacy_Coupon {
$this->set_id( $info[0] ?? 0 );
$this->set_code( $info[1] ?? '' );
- $this->set_discount_type( $info[2] ?? 'fixed_cart' );
+ $this->set_discount_type_core( $info[2] ?? 'fixed_cart', false );
$this->set_amount( $info[3] ?? 0 );
$this->set_free_shipping( $info[4] ?? false );
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/order/coupons.php b/plugins/woocommerce/tests/legacy/unit-tests/order/coupons.php
index 48df53f77b..178fc831a5 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/order/coupons.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/order/coupons.php
@@ -450,4 +450,99 @@ class WC_Tests_Order_Coupons extends WC_Unit_Test_Case {
$this->assertEquals( '1.73', $order->get_total_tax() );
$this->assertEquals( '1.69', $order->get_discount_total() );
}
+
+ /**
+ * @testdox No error is thrown when coupons are recalculated if an applied coupon with custom discount type is deleted and the code that defined the discount type has disappeared.
+ */
+ public function test_custom_discount_type_removed_and_coupon_trashed() {
+ add_filter( 'woocommerce_coupon_discount_types', array( $this, 'handle_woocommerce_coupon_discount_types' ) );
+ add_filter( 'woocommerce_coupon_is_valid', array( $this, 'handle_woocommerce_coupon_is_valid' ), 999, 3 );
+ add_filter( 'woocommerce_coupon_get_discount_amount', array( $this, 'handle_woocommerce_coupon_get_discount_amount' ), 999, 5 );
+ add_filter( 'woocommerce_coupon_is_valid_for_cart', array( $this, 'handle_woocommerce_coupon_is_valid_for_cart' ), 999, 2 );
+
+ update_option( 'woocommerce_prices_include_tax', 'no' );
+ update_option( 'woocommerce_calc_taxes', 'no' );
+
+ $this->init_test();
+
+ $coupon = new WC_Coupon();
+ $coupon->set_code( 'test-coupon-3' );
+ $coupon->set_amount( 2.00 );
+ $coupon->set_discount_type( 'foobar' );
+ $coupon->save();
+
+ $order_id = $this->objects['order']->get_id();
+ $order = wc_get_order( $order_id );
+
+ $order->remove_coupon( 'test-coupon-1' );
+ $order->remove_coupon( 'this-is-a-virtal-coupon' );
+
+ $order->apply_coupon( 'test-coupon-3' );
+
+ $order->calculate_totals( true );
+ $order->save();
+
+ $this->assertEquals( '998.00', $order->get_total() );
+
+ // Remove the custom discount type handler, trash the coupon, recalculate: should not throw.
+
+ $coupon->delete( true );
+
+ remove_filter( 'woocommerce_coupon_discount_types', array( $this, 'handle_woocommerce_coupon_discount_types' ), 999 );
+ remove_filter( 'woocommerce_coupon_is_valid', array( $this, 'handle_woocommerce_coupon_is_valid' ), 999 );
+ remove_filter( 'woocommerce_coupon_get_discount_amount', array( $this, 'handle_woocommerce_coupon_get_discount_amount' ), 999 );
+ remove_filter( 'woocommerce_coupon_is_valid_for_cart', array( $this, 'handle_woocommerce_coupon_is_valid_for_cart' ), 999 );
+
+ $order->recalculate_coupons();
+ $order->save();
+
+ $this->assertEquals( '1000.00', $order->get_total() );
+ }
+
+ /**
+ * Handler for the woocommerce_coupon_discount_types filter.
+ *
+ * @param array $types Discount types.
+ */
+ public function handle_woocommerce_coupon_discount_types( $types ) {
+ $types['foobar'] = 'Alternative fixed discount';
+ return $types;
+ }
+
+ /**
+ * Handler for the woocommerce_coupon_is_valid filter.
+ *
+ * @param bool $valid Whether the coupon is initially considered valid.
+ * @param WC_Coupon $coupon The coupon to check.
+ * @param WC_Discounts $discounts Discounts object.
+ * @return bool
+ */
+ public function handle_woocommerce_coupon_is_valid( $valid, $coupon, $discounts = null ) {
+ return $valid || ( $coupon->get_discount_type() === 'foobar' );
+ }
+
+ /**
+ * Handler for the woocommerce_coupon_get_discount_amount filter.
+ *
+ * @param float $discount Initial discount amount.
+ * @param float $discounting_amount Amount from which the discount is to be substracted.
+ * @param object $cart_item Cart item.
+ * @param bool $single Always false.
+ * @param WC_Coupon $coupon The coupon to check.
+ * @return float
+ */
+ public function handle_woocommerce_coupon_get_discount_amount( $discount, $discounting_amount, $cart_item, $single, $coupon ) {
+ return $coupon->get_discount_type() === 'foobar' ? $coupon->get_amount() : $discount;
+ }
+
+ /**
+ * Handler for the woocommerce_coupon_is_valid_for_Cart filter.
+ *
+ * @param bool $valid Whether the coupon is initially considered valid for the cart.
+ * @param WC_Coupon $coupon The coupon to check.
+ * @return bool
+ */
+ public function handle_woocommerce_coupon_is_valid_for_cart( $valid, $coupon ) {
+ return $valid || ( $coupon->get_discount_type() === 'foobar' );
+ }
}