Add support for `_held_for_checkout` records to improve performance.
This will also improve transactional stability and avoid race conditions by providing a way to lock usage counts.
This commit is contained in:
parent
9c4de8f5e7
commit
72545c44b7
|
@ -979,6 +979,128 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and records coupon usage tentatively so that counts validation is correct. Display an error if coupon usage limit has been reached.
|
||||
*
|
||||
* If you are using this method, make sure to `release_held_coupons` in case an Exception is thrown.
|
||||
*
|
||||
* @throws Exception When not able to apply coupon.
|
||||
*
|
||||
* @param string $billing_email Billing email of order.
|
||||
*/
|
||||
public function hold_applied_coupons( $billing_email ) {
|
||||
$held_keys = array();
|
||||
$held_keys_for_user = array();
|
||||
$error = null;
|
||||
|
||||
try {
|
||||
foreach ( WC()->cart->get_applied_coupons() as $code ) {
|
||||
$coupon = new WC_Coupon( $code );
|
||||
if ( ! $coupon->get_data_store() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hold coupon for when global coupon usage limit is present.
|
||||
if ( 0 < $coupon->get_usage_limit() ) {
|
||||
$held_key = $this->hold_coupon( $coupon );
|
||||
if ( $held_key ) {
|
||||
$held_keys[ $coupon->get_id() ] = $held_key;
|
||||
}
|
||||
}
|
||||
|
||||
// Hold coupon for when usage limit per customer is enabled.
|
||||
if ( 0 < $coupon->get_usage_limit_per_user() ) {
|
||||
|
||||
if ( ! isset( $user_ids_and_emails ) ) {
|
||||
$user_alias = get_current_user_id() ? wp_get_current_user()->ID : sanitize_email( $billing_email );
|
||||
$user_ids_and_emails = $this->get_billing_and_current_user_aliases( $billing_email );
|
||||
}
|
||||
|
||||
$held_key_for_user = $this->hold_coupon_for_users( $coupon, $user_ids_and_emails, $user_alias );
|
||||
|
||||
if ( $held_key_for_user ) {
|
||||
$held_keys_for_user[ $coupon->get_id() ] = $held_key_for_user;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
$error = $e;
|
||||
} finally {
|
||||
// Even in case of error, we will save keys for whatever coupons that were held so our data remains accurate.
|
||||
// We save them in bulk instead of one by one for performance reasons.
|
||||
if ( 0 < count( $held_keys_for_user ) || 0 < count( $held_keys ) ) {
|
||||
$this->get_data_store()->set_coupon_held_keys( $this, $held_keys, $held_keys_for_user );
|
||||
}
|
||||
if ( $error instanceof Exception ) {
|
||||
throw $error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hold coupon if a global usage limit is defined.
|
||||
*
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
*
|
||||
* @return string Meta key which indicates held coupon.
|
||||
* @throws Exception When can't be held.
|
||||
*/
|
||||
private function hold_coupon( $coupon ) {
|
||||
$result = $coupon->get_data_store()->check_and_hold_coupon( $coupon );
|
||||
if ( false === $result ) {
|
||||
// translators: Actual coupon code.
|
||||
throw new Exception( sprintf( __( 'An unexpected error happened while applying the Coupon %s.', 'woocommerce' ), $coupon->get_code() ) );
|
||||
} elseif ( 0 === $result ) {
|
||||
// translators: Actual coupon code.
|
||||
throw new Exception( sprintf( __( 'Coupon %s was used in another transaction during this checkout, and coupon usage limit is reached. Please remove the coupon and try again.', 'woocommerce' ), $coupon->get_code() ) );
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hold coupon if usage limit per customer is defined.
|
||||
*
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
* @param array $user_ids_and_emails Array of user Id and emails to check for usage limit.
|
||||
* @param string $user_alias User ID or email to use to record current usage.
|
||||
*
|
||||
* @return string Meta key which indicates held coupon.
|
||||
* @throws Exception When coupon can't be held.
|
||||
*/
|
||||
private function hold_coupon_for_users( $coupon, $user_ids_and_emails, $user_alias ) {
|
||||
$result = $coupon->get_data_store()->check_and_hold_coupon_for_user( $coupon, $user_ids_and_emails, $user_alias );
|
||||
if ( false === $result ) {
|
||||
// translators: Actual coupon code.
|
||||
throw new Exception( sprintf( __( 'An unexpected error happened while applying the Coupon %s.', 'woocommerce' ), $coupon->get_code() ) );
|
||||
} elseif ( 0 === $result ) {
|
||||
// translators: Actual coupon code.
|
||||
throw new Exception( sprintf( __( 'You have used this coupon %s in another transaction during this checkout, and coupon usage limit is reached. Please remove the coupon and try again.', 'woocommerce' ), $coupon->get_code() ) );
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get all aliases for current user and provide billing email.
|
||||
*
|
||||
* @param string $billing_email Billing email provided in form.
|
||||
*
|
||||
* @return array Array of all aliases.
|
||||
* @throws Exception When validation fails.
|
||||
*/
|
||||
private function get_billing_and_current_user_aliases( $billing_email ) {
|
||||
$emails = array( $billing_email );
|
||||
if ( get_current_user_id() ) {
|
||||
$emails[] = wp_get_current_user()->user_email;
|
||||
}
|
||||
$emails = array_unique(
|
||||
array_map( 'strtolower', array_map( 'sanitize_email', $emails ) )
|
||||
);
|
||||
$customer_data_store = WC_Data_Store::load( 'customer' );
|
||||
$user_ids = $customer_data_store->get_user_ids_for_billing_email( $emails );
|
||||
return array_merge( $user_ids, $emails );
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a coupon to the order and recalculate totals.
|
||||
*
|
||||
|
@ -996,13 +1118,6 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
|
|||
if ( $coupon->get_code() !== $code ) {
|
||||
return new WP_Error( 'invalid_coupon', __( 'Invalid coupon code', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$discounts = new WC_Discounts( $this );
|
||||
$valid = $discounts->is_coupon_valid( $coupon );
|
||||
|
||||
if ( is_wp_error( $valid ) ) {
|
||||
return $valid;
|
||||
}
|
||||
} else {
|
||||
return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) );
|
||||
}
|
||||
|
|
|
@ -115,7 +115,7 @@ class WC_Cart extends WC_Legacy_Cart {
|
|||
add_action( 'woocommerce_cart_item_restored', array( $this, 'calculate_totals' ), 20, 0 );
|
||||
add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_items' ), 1 );
|
||||
add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_coupons' ), 1 );
|
||||
add_action( 'woocommerce_after_checkout_validation', array( $this, 'check_customer_coupons' ), 1 );
|
||||
add_action( 'woocommerce_after_checkout_validation', array( $this, 'check_customer_coupons' ), 1, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1476,47 +1476,6 @@ class WC_Cart extends WC_Legacy_Cart {
|
|||
$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED );
|
||||
$this->remove_coupon( $code );
|
||||
}
|
||||
|
||||
// Usage limits per user - check against billing and user email and user ID.
|
||||
$limit_per_user = $coupon->get_usage_limit_per_user();
|
||||
|
||||
if ( 0 < $limit_per_user ) {
|
||||
$used_by = $coupon->get_used_by();
|
||||
$usage_count = 0;
|
||||
$user_id_matches = array( get_current_user_id() );
|
||||
|
||||
// Check usage against emails.
|
||||
foreach ( $check_emails as $check_email ) {
|
||||
$usage_count += count( array_keys( $used_by, $check_email, true ) );
|
||||
$user = get_user_by( 'email', $check_email );
|
||||
$user_id_matches[] = $user ? $user->ID : 0;
|
||||
}
|
||||
|
||||
// Check against billing emails of existing users.
|
||||
$users_query = new WP_User_Query(
|
||||
array(
|
||||
'fields' => 'ID',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_billing_email',
|
||||
'value' => $check_emails,
|
||||
'compare' => 'IN',
|
||||
),
|
||||
),
|
||||
)
|
||||
); // WPCS: slow query ok.
|
||||
|
||||
$user_id_matches = array_unique( array_filter( array_merge( $user_id_matches, $users_query->get_results() ) ) );
|
||||
|
||||
foreach ( $user_id_matches as $user_id ) {
|
||||
$usage_count += count( array_keys( $used_by, (string) $user_id, true ) );
|
||||
}
|
||||
|
||||
if ( $usage_count >= $coupon->get_usage_limit_per_user() ) {
|
||||
$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED );
|
||||
$this->remove_coupon( $code );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -366,6 +366,7 @@ class WC_Checkout {
|
|||
}
|
||||
}
|
||||
|
||||
$order->hold_applied_coupons( $data['billing_email'] );
|
||||
$order->set_created_via( 'checkout' );
|
||||
$order->set_cart_hash( $cart_hash );
|
||||
$order->set_customer_id( apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ) );
|
||||
|
@ -403,6 +404,9 @@ class WC_Checkout {
|
|||
|
||||
return $order_id;
|
||||
} catch ( Exception $e ) {
|
||||
if ( $order && $order instanceof WC_Order ) {
|
||||
$order->get_data_store()->release_held_coupons( $order );
|
||||
}
|
||||
return new WP_Error( 'checkout-error', $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -774,11 +774,12 @@ class WC_Coupon extends WC_Legacy_Coupon {
|
|||
/**
|
||||
* Increase usage count for current coupon.
|
||||
*
|
||||
* @param string $used_by Either user ID or billing email.
|
||||
* @param string $used_by Either user ID or billing email.
|
||||
* @param WC_Order $order If provided, will clear the coupons held by this order.
|
||||
*/
|
||||
public function increase_usage_count( $used_by = '' ) {
|
||||
public function increase_usage_count( $used_by = '', $order = null ) {
|
||||
if ( $this->get_id() && $this->data_store ) {
|
||||
$new_count = $this->data_store->increase_usage_count( $this, $used_by );
|
||||
$new_count = $this->data_store->increase_usage_count( $this, $used_by, $order );
|
||||
|
||||
// Bypass set_prop and remove pending changes since the data store saves the count already.
|
||||
$this->data['usage_count'] = $new_count;
|
||||
|
@ -813,8 +814,7 @@ class WC_Coupon extends WC_Legacy_Coupon {
|
|||
|
||||
/**
|
||||
* Returns the error_message string.
|
||||
*
|
||||
* @access public
|
||||
|
||||
* @return string
|
||||
*/
|
||||
public function get_error_message() {
|
||||
|
|
|
@ -629,8 +629,8 @@ class WC_Discounts {
|
|||
}
|
||||
|
||||
if ( $coupon && $user_id && apply_filters( 'woocommerce_coupon_validate_user_usage_limit', $coupon->get_usage_limit_per_user() > 0, $user_id, $coupon, $this ) && $coupon->get_id() && $coupon->get_data_store() ) {
|
||||
$date_store = $coupon->get_data_store();
|
||||
$usage_count = $date_store->get_usage_by_user_id( $coupon, $user_id );
|
||||
$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 );
|
||||
}
|
||||
|
|
|
@ -284,14 +284,22 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
|
|||
* Increase usage count for current coupon.
|
||||
*
|
||||
* @since 3.0.0
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
* @param string $used_by Either user ID or billing email.
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
* @param string $used_by Either user ID or billing email.
|
||||
* @param WC_Order $order (Optional) If passed, clears the hold record associated with order.
|
||||
|
||||
* @return int New usage count.
|
||||
*/
|
||||
public function increase_usage_count( &$coupon, $used_by = '' ) {
|
||||
public function increase_usage_count( &$coupon, $used_by = '', $order = null ) {
|
||||
$coupon_held_key_for_user = '';
|
||||
if ( $order instanceof WC_Order ) {
|
||||
$coupon_held_key_for_user = $order->get_data_store()->get_coupon_held_keys_for_users( $order, $coupon->get_id() );
|
||||
}
|
||||
|
||||
$new_count = $this->update_usage_count_meta( $coupon, 'increase' );
|
||||
|
||||
if ( $used_by ) {
|
||||
add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
|
||||
$this->add_coupon_used_by( $coupon, $used_by, $coupon_held_key_for_user );
|
||||
$coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
|
||||
}
|
||||
|
||||
|
@ -300,6 +308,36 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
|
|||
return $new_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to add a `_used_by` record to track coupons used by the user.
|
||||
*
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
* @param string $used_by Either user ID or billing email.
|
||||
* @param string $coupon_held_key (Optional) Update meta key to `_used_by` instead of adding a new record.
|
||||
*/
|
||||
private function add_coupon_used_by( $coupon, $used_by, $coupon_held_key ) {
|
||||
global $wpdb;
|
||||
if ( $coupon_held_key && '' !== $coupon_held_key ) {
|
||||
// Looks like we added a tentative record for this coupon getting used.
|
||||
// Lets change the tentative record to a permanent one.
|
||||
$result = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
UPDATE $wpdb->postmeta SET meta_key = %s, meta_value = %s WHERE meta_key = %s LIMIT 1",
|
||||
'_used_by',
|
||||
$used_by,
|
||||
$coupon_held_key
|
||||
)
|
||||
);
|
||||
if ( ! $result ) {
|
||||
// If no rows were updated, then insert a `_used_by` row manually to maintain consistency.
|
||||
add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
|
||||
}
|
||||
} else {
|
||||
add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease usage count for current coupon.
|
||||
*
|
||||
|
@ -316,7 +354,13 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
|
|||
* We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes.
|
||||
* all instances where the key and value match, and we only want to delete one.
|
||||
*/
|
||||
$meta_id = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", $used_by, $coupon->get_id() ) );
|
||||
$meta_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;",
|
||||
$used_by,
|
||||
$coupon->get_id()
|
||||
)
|
||||
);
|
||||
if ( $meta_id ) {
|
||||
delete_metadata_by_mid( 'post', $meta_id );
|
||||
$coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
|
||||
|
@ -344,7 +388,7 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
|
|||
add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true );
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
"UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;",
|
||||
$id
|
||||
)
|
||||
|
@ -354,6 +398,20 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
|
|||
return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns tentative usage count for coupon.
|
||||
*
|
||||
* @param int $coupon_id Coupon ID.
|
||||
*
|
||||
* @return int Tentative usage count.
|
||||
*/
|
||||
public function get_tentative_usage_count( $coupon_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_var(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$this->get_tentative_usage_query( $coupon_id )
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Get the number of uses for a coupon by user ID.
|
||||
*
|
||||
|
@ -364,7 +422,15 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
|
|||
*/
|
||||
public function get_usage_by_user_id( &$coupon, $user_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;", $coupon->get_id(), $user_id ) );
|
||||
$usage_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;",
|
||||
$coupon->get_id(),
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
$tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) );
|
||||
return $tentative_usage_count + $usage_count;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -377,7 +443,229 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
|
|||
*/
|
||||
public function get_usage_by_email( &$coupon, $email ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;", $coupon->get_id(), $email ) );
|
||||
$usage_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;",
|
||||
$coupon->get_id(),
|
||||
$email
|
||||
)
|
||||
);
|
||||
$tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $email ) );
|
||||
return $tentative_usage_count + $usage_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tentative coupon usages for user.
|
||||
*
|
||||
* @param int $coupon_id Coupon ID.
|
||||
* @param array $user_aliases Array of user aliases to check tentative usages for.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_tentative_usages_for_user( $coupon_id, $user_aliases ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_var(
|
||||
$this->get_tentative_usage_query_for_user( $coupon_id, $user_aliases )
|
||||
); // WPCS: unprepared SQL ok.
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get held time for resources before cancelling the order. Use 60 minutes as sane default.
|
||||
* Note that the filter `woocommerce_coupon_hold_minutes` only support minutes because it's getting used elsewhere as well, however this function returns in seconds.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function get_tentative_held_time() {
|
||||
return apply_filters( 'woocommerce_coupon_hold_minutes', ( (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) ) ) * 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and records coupon usage tentatively for short period of time so that counts validation is correct. Returns early if there is no limit defined for the coupon.
|
||||
*
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
*
|
||||
* @return bool|int|string|null Returns meta key if coupon was held, null if returned early.
|
||||
*/
|
||||
public function check_and_hold_coupon( $coupon ) {
|
||||
global $wpdb;
|
||||
|
||||
$usage_limit = $coupon->get_usage_limit();
|
||||
$held_time = $this->get_tentative_held_time();
|
||||
|
||||
if ( 0 >= $usage_limit || 0 >= $held_time ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! apply_filters( 'woocommerce_coupon_hold_enabled', true ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query_for_usages = $wpdb->prepare(
|
||||
"
|
||||
SELECT meta_value from $wpdb->postmeta
|
||||
WHERE {$wpdb->postmeta}.meta_key = 'usage_count'
|
||||
AND {$wpdb->postmeta}.post_id = %d
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
",
|
||||
$coupon->get_id()
|
||||
);
|
||||
|
||||
$query_for_tentative_usages = $this->get_tentative_usage_query( $coupon->get_id() );
|
||||
|
||||
$coupon_usage_key = '_coupon_held_' . ( time() + $held_time ) . '_' . wp_generate_password( 6, false );
|
||||
|
||||
$insert_statement = $wpdb->prepare(
|
||||
"
|
||||
INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value )
|
||||
SELECT %d, %s, %s FROM DUAL
|
||||
WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d
|
||||
",
|
||||
$coupon->get_id(),
|
||||
$coupon_usage_key,
|
||||
'',
|
||||
$usage_limit
|
||||
); // WPCS: unprepared SQL ok.
|
||||
|
||||
/**
|
||||
* In some cases, specifically when there is a combined index on post_id,meta_key, the insert statement above could end up in a deadlock.
|
||||
* We will try to insert 3 times before giving up to recover from deadlock.
|
||||
*/
|
||||
for ( $count = 0; $count < 3; $count++ ) {
|
||||
$result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok.
|
||||
if ( false !== $result ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $result > 0 ? $coupon_usage_key : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate query to calculate tentative usages for the coupon.
|
||||
*
|
||||
* @param int $coupon_id Coupon ID to get tentative usage query for.
|
||||
*
|
||||
* @return string Query for tentative usages.
|
||||
*/
|
||||
private function get_tentative_usage_query( $coupon_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(meta_id) FROM $wpdb->postmeta
|
||||
WHERE {$wpdb->postmeta}.meta_key like %s
|
||||
AND {$wpdb->postmeta}.meta_key > %s
|
||||
AND {$wpdb->postmeta}.post_id = %d
|
||||
FOR UPDATE
|
||||
",
|
||||
array(
|
||||
'_coupon_held_%',
|
||||
'_coupon_held_' . time(),
|
||||
$coupon_id,
|
||||
)
|
||||
); // WPCS: unprepared SQL ok.
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and records coupon usage tentatively for passed user aliases for short period of time so that counts validation is correct. Returns early if there is no limit per user for the coupon.
|
||||
*
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
* @param array $user_aliases Emails or Ids to check for user.
|
||||
* @param string $user_alias Email/ID to use as `used_by` value.
|
||||
*
|
||||
* @return null|false|int
|
||||
*/
|
||||
public function check_and_hold_coupon_for_user( $coupon, $user_aliases, $user_alias ) {
|
||||
global $wpdb;
|
||||
$limit_per_user = $coupon->get_usage_limit_per_user();
|
||||
$held_time = $this->get_tentative_held_time();
|
||||
|
||||
if ( 0 >= $limit_per_user || 0 >= $held_time ) {
|
||||
// This coupon do not have any restriction for usage per customer. No need to check further, lets bail.
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! apply_filters( 'woocommerce_coupon_hold_enabled', true ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) );
|
||||
|
||||
$query_for_usages = $wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(*) FROM $wpdb->postmeta
|
||||
WHERE {$wpdb->postmeta}.meta_key = '_used_by'
|
||||
AND {$wpdb->postmeta}.meta_value IN ('$format')
|
||||
AND {$wpdb->postmeta}.post_id = %d
|
||||
FOR UPDATE
|
||||
",
|
||||
array_merge(
|
||||
$user_aliases,
|
||||
array( $coupon->get_id() )
|
||||
)
|
||||
); // WPCS: unprepared SQL ok.
|
||||
|
||||
$query_for_tentative_usages = $this->get_tentative_usage_query_for_user( $coupon->get_id(), $user_aliases );
|
||||
|
||||
$coupon_used_by_meta_key = '_maybe_used_by_' . ( time() + $held_time ) . '_' . wp_generate_password( 6, false );
|
||||
$insert_statement = $wpdb->prepare(
|
||||
"
|
||||
INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value )
|
||||
SELECT %d, %s, %s FROM DUAL
|
||||
WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d
|
||||
",
|
||||
$coupon->get_id(),
|
||||
$coupon_used_by_meta_key,
|
||||
$user_alias,
|
||||
$limit_per_user
|
||||
); // WPCS: unprepared SQL ok.
|
||||
|
||||
// This query can potentially be deadlocked if a combined index on post_id and meta_key is present and there is
|
||||
// high concurrency, in which case DB will abort the query which has done less work to resolve deadlock.
|
||||
// We will try up to 3 times before giving up.
|
||||
for ( $count = 0; $count < 3; $count++ ) {
|
||||
$result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok.
|
||||
if ( false !== $result ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $result > 0 ? $coupon_used_by_meta_key : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate query to calculate tentative usages for the coupon by the user.
|
||||
*
|
||||
* @param int $coupon_id Coupon ID.
|
||||
* @param array $user_aliases List of user aliases to check for usages.
|
||||
*
|
||||
* @return string Tentative usages query.
|
||||
*/
|
||||
private function get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) {
|
||||
global $wpdb;
|
||||
|
||||
$format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) );
|
||||
|
||||
// Note that if you are debugging, `_maybe_used_by_%` will be converted to `_maybe_used_by_{...very long str...}` to very long string. This is expected, and is automatically corrected while running the insert query.
|
||||
return $wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT( meta_id ) FROM $wpdb->postmeta
|
||||
WHERE {$wpdb->postmeta}.meta_key like %s
|
||||
AND {$wpdb->postmeta}.meta_key > %s
|
||||
AND {$wpdb->postmeta}.post_id = %d
|
||||
AND {$wpdb->postmeta}.meta_value IN ('$format')
|
||||
FOR UPDATE
|
||||
",
|
||||
array_merge(
|
||||
array(
|
||||
'_maybe_used_by_%',
|
||||
'_maybe_used_by_' . time(),
|
||||
$coupon_id,
|
||||
),
|
||||
$user_aliases
|
||||
)
|
||||
); // WPCS: unprepared SQL ok.
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -485,4 +485,28 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
|
|||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all user ids who have `billing_email` set to any of the email passed in array.
|
||||
*
|
||||
* @param array $emails List of emails to check against.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_user_ids_for_billing_email( $emails ) {
|
||||
$emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) );
|
||||
$users_query = new WP_User_Query(
|
||||
array(
|
||||
'fields' => 'ID',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'billing_email',
|
||||
'value' => $emails,
|
||||
'compare' => 'IN',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
return array_unique( $users_query->get_results() );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -610,6 +610,87 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
|
|||
update_post_meta( $order_id, '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of coupon_code => meta_key for coupon which have usage limit and have tentative keys.
|
||||
* Pass $coupon_id if key for only one of the coupon is needed.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param int $coupon_id If passed, will return held key for that coupon.
|
||||
*
|
||||
* @return array|string Key value pair for coupon code and meta key name. If $coupon_id is passed, returns meta_key for only that coupon.
|
||||
*/
|
||||
public function get_coupon_held_keys( $order, $coupon_id = null ) {
|
||||
$held_keys = $order->get_meta( '_coupon_held_keys' );
|
||||
if ( $coupon_id ) {
|
||||
return $held_keys[ $coupon_id ];
|
||||
}
|
||||
return $held_keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of coupon_code => meta_key for coupon which have usage limit per customer and have tentative keys.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param int $coupon_id If passed, will return held key for that coupon.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) {
|
||||
$held_keys_for_user = $order->get_meta( '_coupon_held_keys_for_users' );
|
||||
if ( $coupon_id && is_array( $held_keys_for_user ) ) {
|
||||
return $held_keys_for_user[ $coupon_id ];
|
||||
}
|
||||
return $held_keys_for_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/Update list of meta keys that are currently being used by this order to hold a coupon.
|
||||
* This is used to figure out what all meta entries we should delete when order is cancelled/completed.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $held_keys Array of coupon_code => meta_key.
|
||||
* @param array $held_keys_for_user Array of coupon_code => meta_key for held coupon for user.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) {
|
||||
if ( is_array( $held_keys ) && 0 < count( $held_keys ) ) {
|
||||
$order->update_meta_data( '_coupon_held_keys', $held_keys );
|
||||
}
|
||||
if ( is_array( $held_keys_for_user ) && 0 < count( $held_keys_for_user ) ) {
|
||||
$order->update_meta_data( '_coupon_held_keys_for_users', $held_keys_for_user );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release all coupons held by this order.
|
||||
*
|
||||
* @param WC_Order $order Current order object.
|
||||
* @param bool $save Whether to delete keys from DB right away. Could be useful to pass `false` if you are building a bulk request.
|
||||
*/
|
||||
public function release_held_coupons( $order, $save = true ) {
|
||||
$coupon_held_keys = $this->get_coupon_held_keys( $order );
|
||||
if ( is_array( $coupon_held_keys ) ) {
|
||||
foreach ( $coupon_held_keys as $coupon_id => $meta_key ) {
|
||||
delete_post_meta( $coupon_id, $meta_key );
|
||||
}
|
||||
}
|
||||
$order->delete_meta_data( '_coupon_held_keys' );
|
||||
|
||||
$coupon_held_keys_for_users = $this->get_coupon_held_keys_for_users( $order );
|
||||
if ( is_array( $coupon_held_keys_for_users ) ) {
|
||||
foreach ( $coupon_held_keys_for_users as $coupon_id => $meta_key ) {
|
||||
delete_post_meta( $coupon_id, $meta_key );
|
||||
}
|
||||
}
|
||||
$order->delete_meta_data( '_coupon_held_keys_for_users' );
|
||||
|
||||
if ( $save ) {
|
||||
$order->save_meta_data();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about whether stock was reduced.
|
||||
*
|
||||
|
|
|
@ -874,10 +874,11 @@ function wc_update_coupon_usage_counts( $order_id ) {
|
|||
$coupon->decrease_usage_count( $used_by );
|
||||
break;
|
||||
case 'increase':
|
||||
$coupon->increase_usage_count( $used_by );
|
||||
$coupon->increase_usage_count( $used_by, $order );
|
||||
break;
|
||||
}
|
||||
}
|
||||
$order->get_data_store()->release_held_coupons( $order, true );
|
||||
}
|
||||
}
|
||||
add_action( 'woocommerce_order_status_pending', 'wc_update_coupon_usage_counts' );
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
/**
|
||||
* Checkout tests.
|
||||
*
|
||||
* @package WooCommerce|Tests|Checkout
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class WC_Checkout
|
||||
*/
|
||||
class WC_Tests_Checkout extends WC_Unit_Test_Case {
|
||||
|
||||
public function tearDown() {
|
||||
parent::tearDown();
|
||||
WC()->cart->empty_cart();
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
WC()->cart->empty_cart();
|
||||
}
|
||||
|
||||
/*
|
||||
* Test if order can be created when a coupon with usage limit is applied.
|
||||
*
|
||||
* @throws Exception When unable to create order.
|
||||
*/
|
||||
public function test_create_order_with_limited_coupon() {
|
||||
$coupon_code = 'coupon4one';
|
||||
$coupon_data_store = WC_Data_Store::load( 'coupon' );
|
||||
$coupon = WC_Helper_Coupon::create_coupon(
|
||||
$coupon_code,
|
||||
array( 'usage_limit' => 1 )
|
||||
);
|
||||
$product = WC_Helper_Product::create_simple_product( true );
|
||||
WC()->cart->add_to_cart( $product->get_id(), 1 );
|
||||
WC()->cart->add_discount( $coupon->get_code() );
|
||||
$checkout = WC_Checkout::instance();
|
||||
$order_id = $checkout->create_order(
|
||||
array(
|
||||
'billing_email' => 'a@b.com',
|
||||
'payment_method' => 'dummy_payment_gateway',
|
||||
)
|
||||
);
|
||||
$this->assertNotWPError( $order_id );
|
||||
$order = new WC_Order( $order_id );
|
||||
$coupon_held_key = $order->get_data_store()->get_coupon_held_keys( $order );
|
||||
$this->assertEquals( count( $coupon_held_key ), 1 );
|
||||
$this->assertEquals( array_keys( $coupon_held_key )[0], $coupon->get_id() );
|
||||
$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',
|
||||
'payment_method' => 'dummy_payment_gateway',
|
||||
)
|
||||
);
|
||||
$this->assertWPError( $order2_id );
|
||||
$this->assertEquals( $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ), 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test when order is created with multiple coupon when usage limit for one is exhausted.
|
||||
*
|
||||
* @throws Exception When unable to create an order.
|
||||
*/
|
||||
public function test_create_order_with_multiple_limited_coupons() {
|
||||
$coupon_code1 = 'coupon1';
|
||||
$coupon_code2 = 'coupon2';
|
||||
$coupon_data_store = WC_Data_Store::load( 'coupon' );
|
||||
|
||||
$coupon1 = WC_Helper_Coupon::create_coupon(
|
||||
$coupon_code1,
|
||||
array( 'usage_limit' => 2 )
|
||||
);
|
||||
$coupon2 = WC_Helper_Coupon::create_coupon(
|
||||
$coupon_code2,
|
||||
array( 'usage_limit' => 1 )
|
||||
);
|
||||
$product = WC_Helper_Product::create_simple_product( true );
|
||||
WC()->cart->add_to_cart( $product->get_id(), 1 );
|
||||
WC()->cart->add_discount( $coupon_code1 );
|
||||
WC()->cart->add_discount( $coupon_code2 );
|
||||
$checkout = WC_Checkout::instance();
|
||||
$order_id1 = $checkout->create_order(
|
||||
array(
|
||||
'billing_email' => 'a@b.com',
|
||||
'payment_method' => 'dummy_payment_gateway',
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertNotWPError( $order_id1 );
|
||||
$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',
|
||||
'payment_method' => 'dummy_payment_gateway',
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertWPError( $order2_id );
|
||||
$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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if held coupon is released automatically after specified time.
|
||||
*
|
||||
* TODO: This test will take atleast 1s to run. Consider making this optional?
|
||||
* TODO: This test is flaky, please re-run if this fails at first.
|
||||
*
|
||||
* @throws Exception When unable to create order.
|
||||
*/
|
||||
public function test_create_order_after_coupon_expired() {
|
||||
$coupon_code = 'coupon4one';
|
||||
$coupon_data_store = WC_Data_Store::load( 'coupon' );
|
||||
$coupon = WC_Helper_Coupon::create_coupon(
|
||||
$coupon_code,
|
||||
array(
|
||||
'usage_limit' => 1,
|
||||
'usage_limit_per_user' => 1,
|
||||
)
|
||||
);
|
||||
$product = WC_Helper_Product::create_simple_product( true );
|
||||
WC()->cart->add_to_cart( $product->get_id(), 1 );
|
||||
WC()->cart->add_discount( $coupon->get_code() );
|
||||
$checkout = WC_Checkout::instance();
|
||||
|
||||
add_filter( 'woocommerce_coupon_hold_minutes', array( $this, '__return_0_01' ) );
|
||||
|
||||
$order_id = $checkout->create_order(
|
||||
array(
|
||||
'billing_email' => 'a@b.com',
|
||||
'payment_method' => 'dummy_payment_gateway',
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertNotWPError( $order_id );
|
||||
|
||||
$this->assertEquals( 1, $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ) );
|
||||
$this->assertEquals( 1, $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( 'a@b.com' ) ) );
|
||||
|
||||
sleep( 1 );
|
||||
|
||||
$this->assertEquals( 0, $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ) );
|
||||
$this->assertEquals( 0, $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( 'a@b.com' ) ) );
|
||||
|
||||
remove_filter( 'woocommerce_coupon_hold_minutes', array( $this, '__return_0_01' ) );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return 0.01.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function __return_0_01() {
|
||||
return 0.01;
|
||||
}
|
||||
|
||||
}
|
|
@ -1430,4 +1430,92 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
|
|||
// Should return nothing when searching for nonexistent term.
|
||||
$this->assertEmpty( wc_order_search( 'Nonexistent term' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if hold coupons are released when increasing usage count.
|
||||
*
|
||||
* @throws Exception When unable to create an order.
|
||||
*/
|
||||
public function test_wc_update_coupon_usage_counts() {
|
||||
$coupon_code = 'coupon1';
|
||||
$coupon_data_store = WC_Data_Store::load( 'coupon' );
|
||||
|
||||
$coupon = WC_Helper_Coupon::create_coupon(
|
||||
$coupon_code,
|
||||
array(
|
||||
'usage_limit' => 2,
|
||||
'usage_limit_per_user' => 2,
|
||||
)
|
||||
);
|
||||
|
||||
$product = WC_Helper_Product::create_simple_product( true );
|
||||
WC()->cart->add_to_cart( $product->get_id(), 1 );
|
||||
WC()->cart->add_discount( $coupon_code );
|
||||
$this->assertEquals( 0, $coupon_data_store->get_usage_by_email( $coupon, 'a@b.com' ) );
|
||||
|
||||
$order_id = WC_Checkout::instance()->create_order(
|
||||
array(
|
||||
'billing_email' => 'a@b.com',
|
||||
'payment_method' => 'dummy',
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertEquals( 0, $coupon->get_usage_count() );
|
||||
$this->assertEquals( 1, $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ) );
|
||||
$this->assertEquals( 1, $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( 'a@b.com' ) ) );
|
||||
$this->assertEquals( 1, $coupon_data_store->get_usage_by_email( $coupon, 'a@b.com' ) );
|
||||
|
||||
$order = new WC_Order( $order_id );
|
||||
$order->update_status( 'processing' );
|
||||
|
||||
$this->assertEquals( 1, get_post_meta( $coupon->get_id(), 'usage_count', true ) );
|
||||
$this->assertEquals( 0, $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ) );
|
||||
$this->assertEquals( 1, $coupon_data_store->get_usage_by_email( $coupon, 'a@b.com' ) );
|
||||
$this->assertEquals( 0, $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( 'a@b.com' ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if everything works as expected when coupon hold is disabled using filter.
|
||||
*/
|
||||
public function test_wc_update_usage_count_without_coupon_hold() {
|
||||
$coupon_code = 'coupon1';
|
||||
$coupon_data_store = WC_Data_Store::load( 'coupon' );
|
||||
|
||||
$coupon = WC_Helper_Coupon::create_coupon(
|
||||
$coupon_code,
|
||||
array(
|
||||
'usage_limit' => 2,
|
||||
'usage_limit_per_user' => 2,
|
||||
)
|
||||
);
|
||||
|
||||
$product = WC_Helper_Product::create_simple_product( true );
|
||||
WC()->cart->add_to_cart( $product->get_id(), 1 );
|
||||
WC()->cart->add_discount( $coupon_code );
|
||||
$this->assertEquals( 0, $coupon_data_store->get_usage_by_email( $coupon, 'a@b.com' ) );
|
||||
|
||||
add_filter( 'woocommerce_coupon_hold_enabled', '__return_false' );
|
||||
|
||||
$order_id = WC_Checkout::instance()->create_order(
|
||||
array(
|
||||
'billing_email' => 'a@b.com',
|
||||
'payment_method' => 'dummy',
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertEquals( 0, $coupon->get_usage_count() );
|
||||
$this->assertEquals( 0, $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ) );
|
||||
$this->assertEquals( 0, $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( 'a@b.com' ) ) );
|
||||
$this->assertEquals( 0, $coupon_data_store->get_usage_by_email( $coupon, 'a@b.com' ) );
|
||||
|
||||
$order = new WC_Order( $order_id );
|
||||
$order->update_status( 'processing' );
|
||||
|
||||
$this->assertEquals( 1, get_post_meta( $coupon->get_id(), 'usage_count', true ) );
|
||||
$this->assertEquals( 0, $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ) );
|
||||
$this->assertEquals( 1, $coupon_data_store->get_usage_by_email( $coupon, 'a@b.com' ) );
|
||||
$this->assertEquals( 0, $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( 'a@b.com' ) ) );
|
||||
|
||||
remove_filter( 'woocommerce_coupon_hold_enabled', '__return_false' );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ class WC_Tests_Totals extends WC_Unit_Test_Case {
|
|||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
WC()->cart->empty_cart();
|
||||
|
||||
$this->ids = array();
|
||||
|
||||
$tax_rate = array(
|
||||
|
|
Loading…
Reference in New Issue