diff --git a/plugins/woocommerce/changelog/fix-41506 b/plugins/woocommerce/changelog/fix-41506 new file mode 100644 index 00000000000..6188f3e27e2 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-41506 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixes an issue where coupon usage is not correctly recorded when HPOS is active. diff --git a/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php index d36e547d215..abe9022dfbd 100644 --- a/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php +++ b/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php @@ -347,6 +347,8 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat } else { add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) ); } + + $this->refresh_coupon_data( $coupon ); } /** @@ -375,6 +377,7 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat if ( $meta_id ) { delete_metadata_by_mid( 'post', $meta_id ); $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); + $this->refresh_coupon_data( $coupon ); } } @@ -405,6 +408,8 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat ) ); + $this->refresh_coupon_data( $coupon ); + // Get the latest value direct from the DB, instead of possibly the WP meta cache. return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) ); } @@ -556,7 +561,7 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat $result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok. if ( false !== $result ) { // Clear meta cache. - wp_cache_delete( WC_Coupon::generate_meta_cache_key( $coupon->get_id(), 'coupons' ), 'coupons' ); + $this->refresh_coupon_data( $coupon ); break; } } @@ -651,7 +656,7 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat $result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok. if ( false !== $result ) { // Clear meta cache. - wp_cache_delete( WC_Coupon::generate_meta_cache_key( $coupon->get_id(), 'coupons' ), 'coupons' ); + $this->refresh_coupon_data( $coupon ); break; } } @@ -693,6 +698,18 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat ); // WPCS: unprepared SQL ok. } + /** + * This function clears coupon data from the WP cache after certain operations which, for performance reasons, + * are done via SQL queries. + * + * @param \WC_Coupon $coupon The coupon object. + * @return void + */ + private function refresh_coupon_data( &$coupon ) { + wp_cache_delete( $coupon->get_meta_cache_key(), 'coupons' ); + wp_cache_delete( $coupon->get_id(), 'post_meta' ); + } + /** * Return a coupon code for a specific ID. * diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 37210b675f7..f2bd371cc24 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -3198,4 +3198,45 @@ class OrdersTableDataStoreTests extends HposTestCase { remove_all_actions( 'woocommerce_webhook_process_delivery' ); remove_all_actions( 'woocommerce_webhook_should_deliver' ); } + + /** + * @testDox Check that functions in the datastore correctly hold and release coupons from the order. + */ + public function test_datastore_coupon_methods() { + $this->toggle_cot_authoritative( true ); + + $coupon = new \WC_Coupon(); + $coupon->set_code( '10off' ); + $coupon->set_discount_type( 'percent' ); + $coupon->set_amount( 10.0 ); + $coupon->set_usage_limit_per_user( 2 ); + $coupon->save(); + + $product = WC_Helper_Product::create_simple_product( true ); + + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon->get_code() ); + + $this->assertEquals( 0, $coupon->get_data_store()->get_usage_by_email( $coupon, 'user@woo.test' ) ); + + $order_id = WC()->checkout->create_order( + array( + 'billing_email' => 'user@woo.test', + 'payment_method' => 'dummy', + ) + ); + + $this->assertEquals( 1, $coupon->get_data_store()->get_tentative_usages_for_user( $coupon->get_id(), array( 'user@woo.test' ) ) ); + $this->assertEquals( 1, $coupon->get_data_store()->get_usage_by_email( $coupon, 'user@woo.test' ) ); + + wc_get_order( $order_id )->payment_complete(); + + $this->assertEquals( 0, $coupon->get_data_store()->get_tentative_usages_for_user( $coupon->get_id(), array( 'user@woo.test' ) ) ); + $this->assertEquals( 1, $coupon->get_data_store()->get_usage_by_email( $coupon, 'user@woo.test' ) ); + + // Load a fresh copy of the coupon and make sure things look ok. + $coupon = new \WC_Coupon( $coupon->get_id() ); + $this->assertContains( 'user@woo.test', $coupon->get_used_by( 'edit' ) ); + } + }