Merge pull request #28592 from woocommerce/fix/27673

Better error messages when coupons are stuck in a pending order.
This commit is contained in:
Roy Ho 2020-12-21 05:22:41 -08:00 committed by GitHub
commit d2cd61762a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 157 additions and 15 deletions

View File

@ -1621,12 +1621,16 @@ class WC_Cart extends WC_Legacy_Cart {
$coupon_data_store = $coupon->get_data_store();
$billing_email = strtolower( sanitize_email( $billing_email ) );
if ( $coupon_data_store && $coupon_data_store->get_usage_by_email( $coupon, $billing_email ) >= $coupon_usage_limit ) {
if ( $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $billing_email ) ) ) {
$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST );
} else {
$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED );
}
}
}
}
}
}
/**
* Checks if the given email address(es) matches the ones specified on the coupon.

View File

@ -67,6 +67,8 @@ class WC_Coupon extends WC_Legacy_Coupon {
const E_WC_COUPON_MAX_SPEND_LIMIT_MET = 112;
const E_WC_COUPON_EXCLUDED_PRODUCTS = 113;
const E_WC_COUPON_EXCLUDED_CATEGORIES = 114;
const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK = 115;
const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST = 116;
const WC_COUPON_SUCCESS = 200;
const WC_COUPON_REMOVED = 201;
@ -994,6 +996,17 @@ class WC_Coupon extends WC_Legacy_Coupon {
case self::E_WC_COUPON_NOT_APPLICABLE:
$err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' );
break;
case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK:
if ( is_user_logged_in() && wc_get_page_id( 'myaccount' ) > 0 ) {
/* translators: %s: myaccount page link. */
$err = sprintf( __( 'Coupon usage limit has been reached. If you were using this coupon just now but order was not complete, you can retry or cancel the order by going to the <a href="%s">my account page</a>.', 'woocommerce' ), wc_get_endpoint_url( 'orders', '', wc_get_page_permalink( 'myaccount' ) ) );
} else {
$err = $this->get_coupon_error( self::E_WC_COUPON_USAGE_LIMIT_REACHED );
}
break;
case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST:
$err = __( 'Coupon usage limit has been reached. Please try again after some time, or contact us for help.', 'woocommerce' );
break;
case self::E_WC_COUPON_EXCLUDED_PRODUCTS:
// Store excluded products that are in cart in $products.
$products = array();

View File

@ -599,12 +599,41 @@ class WC_Discounts {
* @return bool
*/
protected function validate_coupon_usage_limit( $coupon ) {
if ( $coupon->get_usage_limit() > 0 && $coupon->get_usage_count() >= $coupon->get_usage_limit() ) {
throw new Exception( __( 'Coupon usage limit has been reached.', 'woocommerce' ), 106 );
}
if ( ! $coupon->get_usage_limit() ) {
return true;
}
$usage_count = $coupon->get_usage_count();
$data_store = $coupon->get_data_store();
$tentative_usage_count = is_callable( array( $data_store, 'get_tentative_usage_count' ) ) ? $data_store->get_tentative_usage_count( $coupon->get_id() ) : 0;
if ( $usage_count + $tentative_usage_count < $coupon->get_usage_limit() ) {
// All good.
return true;
}
// Coupon usage limit is reached. Let's show as informative error message as we can.
if ( 0 === $tentative_usage_count ) {
// No held coupon, usage limit is indeed reached.
$error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED;
} elseif ( is_user_logged_in() ) {
$recent_pending_orders = wc_get_orders(
array(
'limit' => 1,
'post_status' => array( 'wc-failed', 'wc-pending' ),
'customer' => get_current_user_id(),
'return' => 'ids',
)
);
if ( count( $recent_pending_orders ) > 0 ) {
// User logged in and have a pending order, maybe they are trying to use the coupon.
$error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK;
} else {
$error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED;
}
} else {
// Maybe this user was trying to use the coupon but got stuck. We can't know for sure (performantly). Show a slightly better error message.
$error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST;
}
throw new Exception( $coupon->get_coupon_error( $error_code ), $error_code );
}
/**
* Ensure coupon user usage limit is valid or throw exception.
@ -631,7 +660,14 @@ class WC_Discounts {
$data_store = $coupon->get_data_store();
$usage_count = $data_store->get_usage_by_user_id( $coupon, $user_id );
if ( $usage_count >= $coupon->get_usage_limit_per_user() ) {
throw new Exception( __( 'Coupon usage limit has been reached.', 'woocommerce' ), 106 );
if ( $data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ) > 0 ) {
$error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK );
$error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK;
} else {
$error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED );
$error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED;
}
throw new Exception( $error_message, $error_code );
}
}

View File

@ -422,6 +422,7 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
$this->get_tentative_usage_query( $coupon_id )
);
}
/**
* Get the number of uses for a coupon by user ID.
*

View File

@ -55,9 +55,6 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
$this->assertEquals( strpos( $coupon_held_key[ $coupon->get_id() ], '_coupon_held_' ), 0 );
$this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ), 1 );
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->add_discount( $coupon->get_code() );
$order2_id = $checkout->create_order(
array(
'billing_email' => 'a@c.com',
@ -102,11 +99,6 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
$this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon1->get_id() ), 1 );
$this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon2->get_id() ), 1 );
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->add_discount( $coupon_code1 );
WC()->cart->add_discount( $coupon_code2 );
$order2_id = $checkout->create_order(
array(
'billing_email' => 'a@b.com',
@ -175,7 +167,7 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->add_discount( $coupon->get_code() );
WC()->cart->check_customer_coupons( $posted_data );
$this->assertTrue( wc_has_notice( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ), 'error' ) );
$this->assertTrue( wc_has_notice( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST ), 'error' ) );
}
/**

View File

@ -0,0 +1,96 @@
<?php
/**
* Unit tests for WC_Discounts class.
*
* @package WooCommerce\Tests.
*/
/**
* Class WC_Discounts_Tests.
*/
class WC_Discounts_Tests extends WC_Unit_Test_Case {
/**
* Helper method to create limited coupon.
*/
private function create_limited_coupon() {
update_option( 'woocommerce_hold_stock_minutes', 60 );
return WC_Helper_Coupon::create_coupon(
'coupon4one' . microtime( true ) . wp_generate_password( 6, false, false ),
array(
'usage_limit' => 1,
'usage_limit_per_user' => 1,
)
);
}
/**
* Helper method to create customer.
*/
public function create_customer() {
$username = 'testusername-' . microtime( true ) . wp_generate_password( 6, false, false );
$customer = new WC_Customer();
$customer->set_username( $username );
$customer->set_password( 'test123' );
$customer->set_email( "$username@woo.local" );
$customer->save();
return $customer;
}
/**
* Test if coupon is valid when usage limit is reached for guest
*/
public function test_is_coupon_valid_when_limit_reached_for_guest() {
$coupon = $this->create_limited_coupon();
$data_store = WC_Data_Store::load( 'coupon' );
$result = $data_store->check_and_hold_coupon( $coupon );
$this->assertNotNull( $result );
wp_set_current_user( 0 );
$valid = ( new WC_Discounts() )->is_coupon_valid( $coupon );
$this->assertWPError( $valid );
$this->assertEquals( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST ), $valid->get_error_message() );
}
/**
* Test if coupon is valid when usage limit is reached for logged in user.
*/
public function test_is_coupon_valid_when_limit_reached_for_user() {
$coupon = $this->create_limited_coupon();
$customer = $this->create_customer();
$data_store = WC_Data_Store::load( 'coupon' );
$order = wc_create_order(
array(
'status' => 'pending',
'customer_id' => $customer->get_id(),
)
);
$order->save();
$result = $data_store->check_and_hold_coupon( $coupon );
$this->assertNotNull( $result );
wp_set_current_user( $customer->get_id() );
$valid = ( new WC_Discounts() )->is_coupon_valid( $coupon );
$this->assertWPError( $valid );
$this->assertEquals( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK ), $valid->get_error_message() );
}
/**
* Test if coupon is valid when usage limit per user is reached for logged in user.
*/
public function test_is_coupon_valid_per_user_when_limit_reached_for_user() {
$coupon = $this->create_limited_coupon();
$data_store = WC_Data_Store::load( 'coupon' );
$customer = $this->create_customer();
$result = $data_store->check_and_hold_coupon_for_user( $coupon, array( $customer->get_id() ), $customer->get_id() );
$this->assertNotNull( $result );
wp_set_current_user( $customer->get_id() );
$valid = ( new WC_Discounts() )->is_coupon_valid( $coupon );
$this->assertWPError( $valid );
$this->assertEquals( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK ), $valid->get_error_message() );
}
}