Commit 45380b04ad7 for woocommerce

commit 45380b04ad74fd8b66ab7df80ed79d829b1feef0
Author: Ceili Cornelison <ceili@ceilicornelison.com>
Date:   Mon Oct 12 07:53:21 2026 -0500

    Fix/30922 apply coupon customerless orders (#69625)

    * Failing tests to cover customerless order case throwing 500

    * Gotta cover no billing email error as well, calling the same stuff

    * Update the way we check for guest checkouts to look for customer id

    * Ensure we have get_billing_email() available to call before we try and use it

    * A refund should have the same taxable location as its parent order

    * Add test requested by @opr (https://github.com/woocommerce/woocommerce/pull/67495#pullrequestreview-5066696966)

    * Move around phpcs comments to satisfy linter

    * Update changelog with fix

    * Remove phpcs comments and add docblocs to satisfy docstring coverage warning on pr

    * Failing test to handle per-email limited coupon codes

    * Handle order types without a customer ID; tidy up long line a lil

    * Responding to codex/coderabbit suggestions, a refund should never consume a coupon use -- failing tests

    * Coupons cannot be applied to refunds -- never an issue before because the system would 500 instead

    * Update tests to handle coupon usage on refund

    * How would you apply a $5 off coupon to a refund? You cannot apply coupons to refunds.

    * Only order data stores can record coupon usage.

    * Unneeded now that refunds return early from `apply_coupon()`

    * A refund without a parent order uses the store base address as its tax location.

    * Add CustomerlessTestOrder to DRY up class-wc-abstract-order-test.php

    * Address phpstan issues & regenerate

diff --git a/plugins/woocommerce/changelog/fix-30922-apply-coupon-on-refunds-and-custom-order-types b/plugins/woocommerce/changelog/fix-30922-apply-coupon-on-refunds-and-custom-order-types
new file mode 100644
index 00000000000..5ebafe543f5
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-30922-apply-coupon-on-refunds-and-custom-order-types
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix a fatal error when applying a coupon to a refund or to a custom order type that has no customer ID or billing email.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index f24bac99c94..98dab092d03 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -1701,7 +1701,9 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 		$data_store = $coupon->get_data_store();

 		// Check specific for guest checkouts here as well since WC_Cart handles that separately in check_customer_coupons.
-		if ( $data_store && 0 === $this->get_customer_id() ) {
+		// Order types without a customer ID are treated as guests.
+		$customer_id = is_callable( array( $this, 'get_customer_id' ) ) ? $this->get_customer_id() : 0;
+		if ( $data_store && 0 === $customer_id && is_callable( array( $this, 'get_billing_email' ) ) ) {
 			$usage_count = $data_store->get_usage_by_email( $coupon, $this->get_billing_email() );
 			if ( 0 < $coupon->get_usage_limit_per_user() && $usage_count >= $coupon->get_usage_limit_per_user() ) {
 				return new WP_Error(
@@ -1733,12 +1735,18 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 		// Record usage so counts and validation is correct.
 		$used_by = $this->get_user_id();

-		if ( ! $used_by ) {
+		if ( ! $used_by && is_callable( array( $this, 'get_billing_email' ) ) ) {
 			$used_by = $this->get_billing_email();
 		}

+		/**
+		 * Data store wrapper.
+		 *
+		 * @var WC_Data_Store $order_data_store
+		 */
 		$order_data_store = $this->get_data_store();
-		if ( $order_data_store->get_recorded_coupon_usage_counts( $this ) ) {
+		// @phpstan-ignore-next-line method.notFound (Guarded by has_callable() and called via __call() on the underlying order data store instance.)
+		if ( $order_data_store->has_callable( 'get_recorded_coupon_usage_counts' ) && $order_data_store->get_recorded_coupon_usage_counts( $this ) ) {
 			$coupon->increase_usage_count( $used_by );
 		}

diff --git a/plugins/woocommerce/includes/class-wc-coupon.php b/plugins/woocommerce/includes/class-wc-coupon.php
index 5cdc77398b0..153c1260670 100644
--- a/plugins/woocommerce/includes/class-wc-coupon.php
+++ b/plugins/woocommerce/includes/class-wc-coupon.php
@@ -924,8 +924,8 @@ class WC_Coupon extends WC_Legacy_Coupon {
 	/**
 	 * Increase usage count for current coupon.
 	 *
-	 * @param string   $used_by  Either user ID or billing email.
-	 * @param WC_Order $order  If provided, will clear the coupons held by this order.
+	 * @param int|string $used_by  Either user ID or billing email.
+	 * @param WC_Order   $order  If provided, will clear the coupons held by this order.
 	 * @return void
 	 */
 	public function increase_usage_count( $used_by = '', $order = null ) {
@@ -943,7 +943,7 @@ class WC_Coupon extends WC_Legacy_Coupon {
 	/**
 	 * Decrease usage count for current coupon.
 	 *
-	 * @param string $used_by Either user ID or billing email.
+	 * @param int|string $used_by Either user ID or billing email.
 	 * @return void
 	 */
 	public function decrease_usage_count( $used_by = '' ) {
diff --git a/plugins/woocommerce/includes/class-wc-order-refund.php b/plugins/woocommerce/includes/class-wc-order-refund.php
index 0db69d6fcfe..c00fe93b7b1 100644
--- a/plugins/woocommerce/includes/class-wc-order-refund.php
+++ b/plugins/woocommerce/includes/class-wc-order-refund.php
@@ -129,6 +129,43 @@ class WC_Order_Refund extends WC_Abstract_Order {
 		return $this->get_prop( 'refunded_payment', $context );
 	}

+	/**
+	 * Get the tax location of the parent order, since refunds have no address of their own.
+	 * In case of no parent order taxable location, return the store base location.
+	 *
+	 * @since 11.3.0
+	 * @param array $args Override the location.
+	 * @return array
+	 */
+	protected function get_tax_location( $args = array() ) {
+		$parent_order = wc_get_order( $this->get_parent_id() );
+
+		if ( $parent_order ) {
+			return $parent_order->get_taxable_location( $args );
+		}
+
+		return wp_parse_args(
+			$args,
+			array(
+				'country'  => WC()->countries->get_base_country(),
+				'state'    => WC()->countries->get_base_state(),
+				'postcode' => WC()->countries->get_base_postcode(),
+				'city'     => WC()->countries->get_base_city(),
+			)
+		);
+	}
+
+	/**
+	 * Coupons cannot be applied to refunds, so a refund never records coupon usage.
+	 *
+	 * @since 11.3.0
+	 * @param string|WC_Coupon $raw_coupon Coupon code or object.
+	 * @return WP_Error
+	 */
+	public function apply_coupon( $raw_coupon ) {
+		return new WP_Error( 'invalid_coupon', __( 'Coupons cannot be applied to refunds.', 'woocommerce' ) );
+	}
+
 	/**
 	 * Get formatted refunded amount.
 	 *
diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php
index ad787ec3d50..1741d3473b6 100644
--- a/plugins/woocommerce/includes/wc-order-functions.php
+++ b/plugins/woocommerce/includes/wc-order-functions.php
@@ -1020,7 +1020,20 @@ add_action( 'woocommerce_before_delete_order', 'wc_update_total_sales_counts' );
 function wc_update_coupon_usage_counts( $order_id ) {
 	$order = wc_get_order( $order_id );

-	if ( ! $order ) {
+	// Refunds never consume coupon usage.
+	if ( ! $order || $order instanceof WC_Order_Refund ) {
+		return;
+	}
+
+	/**
+	 * Data store wrapper.
+	 *
+	 * @var WC_Data_Store $data_store
+	 */
+	$data_store = $order->get_data_store();
+
+	// Only order data stores can record coupon usage.
+	if ( ! $data_store->has_callable( 'get_recorded_coupon_usage_counts' ) ) {
 		return;
 	}

@@ -1061,7 +1074,7 @@ function wc_update_coupon_usage_counts( $order_id ) {
 			$coupon  = new WC_Coupon( $code );
 			$used_by = $order->get_user_id();

-			if ( ! $used_by ) {
+			if ( ! $used_by && is_callable( array( $order, 'get_billing_email' ) ) ) {
 				$used_by = $order->get_billing_email();
 			}

diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 49c0a8ab8ca..fbf318cdfe8 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -366,12 +366,6 @@ parameters:
 			count: 1
 			path: includes/abstracts/abstract-wc-order.php

-		-
-			message: '#^Call to an undefined method WC_Abstract_Order\:\:get_billing_email\(\)\.$#'
-			identifier: method.notFound
-			count: 2
-			path: includes/abstracts/abstract-wc-order.php
-
 		-
 			message: '#^Call to an undefined method WC_Abstract_Order\:\:get_billing_postcode\(\)\.$#'
 			identifier: method.notFound
@@ -384,12 +378,6 @@ parameters:
 			count: 1
 			path: includes/abstracts/abstract-wc-order.php

-		-
-			message: '#^Call to an undefined method WC_Abstract_Order\:\:get_customer_id\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: includes/abstracts/abstract-wc-order.php
-
 		-
 			message: '#^Call to an undefined method WC_Abstract_Order\:\:get_shipping_city\(\)\.$#'
 			identifier: method.notFound
@@ -546,12 +534,6 @@ parameters:
 			count: 6
 			path: includes/abstracts/abstract-wc-order.php

-		-
-			message: '#^Call to an undefined method object\:\:get_recorded_coupon_usage_counts\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: includes/abstracts/abstract-wc-order.php
-
 		-
 			message: '#^Call to an undefined method object\:\:get_subtotal_tax\(\)\.$#'
 			identifier: method.notFound
@@ -756,12 +738,6 @@ parameters:
 			count: 1
 			path: includes/abstracts/abstract-wc-order.php

-		-
-			message: '#^Parameter \#1 \$used_by of method WC_Coupon\:\:decrease_usage_count\(\) expects string, int given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/abstracts/abstract-wc-order.php
-
 		-
 			message: '#^Parameter \#1 \$value of method WC_Abstract_Order\:\:set_cart_tax\(\) expects string, float\|int given\.$#'
 			identifier: argument.type
@@ -33927,12 +33903,6 @@ parameters:
 			count: 1
 			path: includes/wc-order-functions.php

-		-
-			message: '#^Call to an undefined method WC_Order\|WC_Order_Refund\:\:get_billing_email\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: includes/wc-order-functions.php
-
 		-
 			message: '#^Call to an undefined method WC_Order\|WC_Order_Refund\:\:get_remaining_refund_amount\(\)\.$#'
 			identifier: method.notFound
@@ -34131,12 +34101,6 @@ parameters:
 			count: 1
 			path: includes/wc-order-functions.php

-		-
-			message: '#^Parameter \#1 \$order of function wc_release_coupons_for_order expects int\|WC_Order, WC_Order\|WC_Order_Refund given\.$#'
-			identifier: argument.type
-			count: 2
-			path: includes/wc-order-functions.php
-
 		-
 			message: '#^Parameter \#1 \$order of function wc_restock_refunded_items expects WC_Order, WC_Order\|WC_Order_Refund given\.$#'
 			identifier: argument.type
@@ -34149,18 +34113,6 @@ parameters:
 			count: 1
 			path: includes/wc-order-functions.php

-		-
-			message: '#^Parameter \#1 \$used_by of method WC_Coupon\:\:decrease_usage_count\(\) expects string, int\<min, \-1\>\|int\<1, max\>\|string given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/wc-order-functions.php
-
-		-
-			message: '#^Parameter \#1 \$used_by of method WC_Coupon\:\:increase_usage_count\(\) expects string, int\<min, \-1\>\|int\<1, max\>\|string given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/wc-order-functions.php
-
 		-
 			message: '#^Parameter \#1 \$value of method WC_Abstract_Order\:\:set_total\(\) expects string, \(float\|int\) given\.$#'
 			identifier: argument.type
@@ -34191,12 +34143,6 @@ parameters:
 			count: 1
 			path: includes/wc-order-functions.php

-		-
-			message: '#^Parameter \#2 \$order of method WC_Coupon\:\:increase_usage_count\(\) expects WC_Order\|null, WC_Order\|WC_Order_Refund given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/wc-order-functions.php
-
 		-
 			message: '#^Parameter \#2 \$value of method WC_Data\:\:update_meta_data\(\) expects array\|string, \(float\|int\) given\.$#'
 			identifier: argument.type
diff --git a/plugins/woocommerce/tests/php/helpers/CustomerlessTestOrder.php b/plugins/woocommerce/tests/php/helpers/CustomerlessTestOrder.php
new file mode 100644
index 00000000000..7df5d9f7447
--- /dev/null
+++ b/plugins/woocommerce/tests/php/helpers/CustomerlessTestOrder.php
@@ -0,0 +1,45 @@
+<?php
+
+declare( strict_types = 1 );
+
+/**
+ * A custom order type built only on WC_Abstract_Order, so it has no get_customer_id() or get_billing_email().
+ *
+ * It uses the 'test-custom-order' data store, which tests must register through the 'woocommerce_data_stores' filter.
+ */
+class CustomerlessTestOrder extends WC_Abstract_Order {
+
+	/**
+	 * Data store registered by the test.
+	 *
+	 * @var string
+	 */
+	protected $data_store_name = 'test-custom-order';
+
+	/**
+	 * Get the order type.
+	 *
+	 * @return string
+	 */
+	public function get_type() {
+		return 'test_custom_order';
+	}
+
+	/**
+	 * Return an empty tax location, as this order type has no billing or shipping getters.
+	 *
+	 * @param array $args Override the location.
+	 * @return array
+	 */
+	protected function get_tax_location( $args = array() ) {
+		return wp_parse_args(
+			$args,
+			array(
+				'country'  => '',
+				'state'    => '',
+				'postcode' => '',
+				'city'     => '',
+			)
+		);
+	}
+}
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 2421c84648e..962c488bc20 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
@@ -537,6 +537,175 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
 		$this->assertEquals( $coupon_code, $coupon_info[1] );
 	}

+	/**
+	 * Data provider for refund coupon tests.
+	 *
+	 * @return array
+	 */
+	public function provide_refund_parent_customer_ids(): array {
+		return array(
+			'guest parent order'      => array( 0 ),
+			'registered parent order' => array( 1 ),
+		);
+	}
+
+	/**
+	 * Create a refund for one line item of a new order.
+	 *
+	 * @param int $customer_id Customer ID of the parent order.
+	 * @return WC_Order_Refund
+	 */
+	private function create_line_item_refund( int $customer_id ): WC_Order_Refund {
+		$order = WC_Helper_Order::create_order( $customer_id );
+		$item  = current( $order->get_items() );
+
+		return wc_create_refund(
+			array(
+				'order_id'   => $order->get_id(),
+				'amount'     => $item->get_total(),
+				'line_items' => array(
+					$item->get_id() => array(
+						'qty'          => $item->get_quantity(),
+						'refund_total' => $item->get_total(),
+					),
+				),
+			)
+		);
+	}
+
+	/**
+	 * @testdox Applying a coupon to a refund returns an error and records no coupon usage.
+	 * @dataProvider provide_refund_parent_customer_ids
+	 *
+	 * @param int $customer_id Customer ID of the parent order.
+	 */
+	public function test_apply_coupon_on_refund_returns_error( int $customer_id ) {
+		$coupon_code = 'coupon_test_refund_' . $customer_id;
+		WC_Helper_Coupon::create_coupon( $coupon_code );
+		$refund = $this->create_line_item_refund( $customer_id );
+
+		$result = $refund->apply_coupon( $coupon_code );
+
+		$this->assertWPError( $result );
+		$this->assertSame( 'invalid_coupon', $result->get_error_code() );
+		$this->assertCount( 0, $refund->get_items( 'coupon' ) );
+		$this->assertSame( 0, ( new WC_Coupon( $coupon_code ) )->get_usage_count() );
+	}
+
+	/**
+	 * @testdox A refund uses the tax location of its parent order.
+	 */
+	public function test_refund_get_taxable_location_returns_parent_tax_location() {
+		$refund       = $this->create_line_item_refund( 1 );
+		$parent_order = wc_get_order( $refund->get_parent_id() );
+
+		$this->assertSame( 'US', $parent_order->get_taxable_location()['country'] );
+		$this->assertSame( $parent_order->get_taxable_location(), $refund->get_taxable_location() );
+	}
+
+	/**
+	 * @testdox A refund without a parent order uses the store base address as its tax location.
+	 */
+	public function test_refund_get_taxable_location_without_parent_returns_base_address() {
+		update_option( 'woocommerce_default_country', 'US:CA' );
+		update_option( 'woocommerce_store_city', 'San Francisco' );
+		update_option( 'woocommerce_store_postcode', '94110' );
+
+		$refund = new WC_Order_Refund();
+
+		$this->assertSame(
+			array(
+				'country'  => 'US',
+				'state'    => 'CA',
+				'postcode' => '94110',
+				'city'     => 'San Francisco',
+			),
+			$refund->get_taxable_location()
+		);
+	}
+
+	/**
+	 * @testdox A coupon can be applied to a custom order type that has no customer ID or billing email.
+	 */
+	public function test_apply_coupon_on_custom_order_type_without_customer() {
+		$coupon_code = 'coupon_test_custom_order_type';
+		WC_Helper_Coupon::create_coupon( $coupon_code );
+		$order = $this->create_custom_order();
+
+		$this->assertFalse( is_callable( array( $order, 'get_customer_id' ) ) );
+		$this->assertFalse( is_callable( array( $order, 'get_billing_email' ) ) );
+
+		$result = $order->apply_coupon( $coupon_code );
+
+		$this->assertTrue( $result );
+		$this->assertCount( 1, $order->get_items( 'coupon' ) );
+	}
+
+	/**
+	 * Create and save a CustomerlessTestOrder with one line item.
+	 *
+	 * @param string|null $billing_email When set, the order type also has get_billing_email() returning this.
+	 * @return CustomerlessTestOrder
+	 */
+	private function create_custom_order( ?string $billing_email = null ): CustomerlessTestOrder {
+		// A custom order type registers its own data store, as WC_Order's store expects WC_Order methods.
+		$data_store = new class() extends Abstract_WC_Order_Data_Store_CPT {
+		};
+		add_filter(
+			'woocommerce_data_stores',
+			function ( $stores ) use ( $data_store ) {
+				$stores['test-custom-order'] = $data_store;
+				return $stores;
+			}
+		);
+
+		if ( null === $billing_email ) {
+			$order = new CustomerlessTestOrder();
+		} else {
+			$order = new class() extends CustomerlessTestOrder {
+				/**
+				 * Billing email returned by get_billing_email().
+				 *
+				 * @var string
+				 */
+				public $test_billing_email = '';
+
+				/**
+				 * Get the billing email.
+				 *
+				 * @return string
+				 */
+				public function get_billing_email() {
+					return $this->test_billing_email;
+				}
+			};
+
+			$order->test_billing_email = $billing_email;
+		}
+		$order->add_product( WC_Helper_Product::create_simple_product(), 1 );
+		$order->save();
+
+		return $order;
+	}
+
+	/**
+	 * @testdox The per-email coupon usage limit applies to a custom order type that has a billing email but no customer ID.
+	 */
+	public function test_apply_coupon_enforces_email_usage_limit_on_custom_order_type_without_customer_id() {
+		$coupon_code = 'coupon_test_custom_order_type_email_limit';
+		$coupon      = WC_Helper_Coupon::create_coupon( $coupon_code, array( 'usage_limit_per_user' => '1' ) );
+		$coupon->increase_usage_count( 'guest@example.com' );
+		$order = $this->create_custom_order( 'guest@example.com' );
+
+		$this->assertFalse( is_callable( array( $order, 'get_customer_id' ) ) );
+
+		$result = $order->apply_coupon( $coupon_code );
+
+		$this->assertWPError( $result );
+		$this->assertSame( $coupon->get_coupon_error( 106 ), $result->get_error_message() );
+		$this->assertCount( 0, $order->get_items( 'coupon' ) );
+	}
+
 	/**
 	 * Test remove_coupon fires woocommerce_order_removed_coupon hook with WC_Coupon object.
 	 */
diff --git a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
index 3a79a6cdc6b..5f0ee75078d 100644
--- a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
@@ -5,6 +5,7 @@
  * @package WooCommerce\Tests\Order.
  */

+use Automattic\WooCommerce\Caches\OrderCache;
 use Automattic\WooCommerce\Enums\OrderInternalStatus;
 use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\WooCommerce\Internal\Utilities\Users;
@@ -178,6 +179,75 @@ class WC_Order_Functions_Test extends \WC_Unit_Test_Case {
 		$this->assertEquals( 1, ( new WC_Coupon( $coupon ) )->get_usage_count() );
 	}

+	/**
+	 * @testdox wc_update_coupon_usage_counts() does not count coupon usage for a refund.
+	 */
+	public function test_wc_update_coupon_usage_counts_ignores_refunds() {
+		$coupon = WC_Helper_Coupon::create_coupon( 'test_usage_on_refund' );
+		$refund = wc_create_refund(
+			array(
+				'order_id' => WC_Helper_Order::create_order( 0 )->get_id(),
+				'amount'   => 1,
+			)
+		);
+		$item   = new WC_Order_Item_Coupon();
+		$item->set_code( $coupon->get_code() );
+		$refund->add_item( $item );
+		$refund->save();
+
+		wc_update_coupon_usage_counts( $refund->get_id() );
+
+		$coupon = new WC_Coupon( $coupon->get_code() );
+		$this->assertSame( 0, $coupon->get_usage_count() );
+		$this->assertSame( array(), $coupon->get_used_by() );
+	}
+
+	/**
+	 * @testdox wc_update_coupon_usage_counts() does not count coupon usage for an order whose data store cannot record it.
+	 */
+	public function test_wc_update_coupon_usage_counts_skips_orders_whose_data_store_cannot_record_usage() {
+		$coupon = WC_Helper_Coupon::create_coupon( 'test_usage_without_recording_support' );
+		$order  = WC_Helper_Order::create_order();
+		$item   = new WC_Order_Item_Coupon();
+		$item->set_code( $coupon->get_code() );
+		$order->add_item( $item );
+		$order->save();
+
+		// Load the order with a data store that lacks the coupon usage methods of WC_Order_Data_Store_Interface.
+		add_filter(
+			'woocommerce_data_stores',
+			function ( $stores ) {
+				$stores['test-basic-order'] = new class() extends Abstract_WC_Order_Data_Store_CPT {
+				};
+				return $stores;
+			}
+		);
+		$order_class = get_class(
+			new class() extends WC_Order {
+				/**
+				 * Get a data store without the coupon usage methods.
+				 *
+				 * @return WC_Data_Store
+				 */
+				public function get_data_store() {
+					return WC_Data_Store::load( 'test-basic-order' );
+				}
+			}
+		);
+		add_filter(
+			'woocommerce_order_class',
+			function () use ( $order_class ) {
+				return $order_class;
+			}
+		);
+		wc_get_container()->get( OrderCache::class )->remove( $order->get_id() );
+
+		wc_update_coupon_usage_counts( $order->get_id() );
+		wc_update_coupon_usage_counts( $order->get_id() );
+
+		$this->assertSame( 0, ( new WC_Coupon( $coupon->get_code() ) )->get_usage_count() );
+	}
+
 	/**
 	 * Test getting total refunded for an item with and without refunds.
 	 */