2016-11-09 12:21:18 +00:00
< ? php
2018-03-08 19:27:31 +00:00
/**
* Class WC_Coupon_Data_Store_CPT file .
*
2020-08-05 20:49:10 +00:00
* @ package WooCommerce\DataStores
2018-03-08 19:27:31 +00:00
*/
2016-11-09 12:21:18 +00:00
if ( ! defined ( 'ABSPATH' ) ) {
exit ;
}
/**
* WC Coupon Data Store : Custom Post Type .
*
2017-03-15 16:36:53 +00:00
* @ version 3.0 . 0
2016-11-09 12:21:18 +00:00
*/
2016-11-22 13:54:51 +00:00
class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Data_Store_Interface , WC_Object_Data_Store_Interface {
2016-11-21 23:48:49 +00:00
/**
* Internal meta type used to store coupon data .
2018-03-07 19:16:01 +00:00
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2016-11-21 23:48:49 +00:00
* @ var string
*/
protected $meta_type = 'post' ;
/**
* Data stored in meta keys , but not considered " meta " for a coupon .
2018-03-07 19:16:01 +00:00
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2016-11-21 23:48:49 +00:00
* @ var array
*/
protected $internal_meta_keys = array (
'discount_type' ,
'coupon_amount' ,
'expiry_date' ,
2017-03-15 14:38:57 +00:00
'date_expires' ,
2016-11-21 23:48:49 +00:00
'usage_count' ,
'individual_use' ,
'product_ids' ,
'exclude_product_ids' ,
'usage_limit' ,
'usage_limit_per_user' ,
'limit_usage_to_x_items' ,
'free_shipping' ,
'product_categories' ,
'exclude_product_categories' ,
'exclude_sale_items' ,
'minimum_amount' ,
'maximum_amount' ,
'customer_email' ,
'_used_by' ,
'_edit_lock' ,
'_edit_last' ,
);
2016-11-09 12:21:18 +00:00
2020-02-23 13:07:18 +00:00
/**
2020-02-25 15:53:40 +00:00
* The updated coupon properties
*
2020-03-04 18:57:17 +00:00
* @ since 4.1 . 0
2020-02-23 13:07:18 +00:00
* @ var array
*/
protected $updated_props = array ();
2016-11-09 12:21:18 +00:00
/**
* Method to create a new coupon in the database .
2016-11-14 10:29:25 +00:00
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2018-03-08 19:27:31 +00:00
* @ param WC_Coupon $coupon Coupon object .
2016-11-09 12:21:18 +00:00
*/
public function create ( & $coupon ) {
2020-05-05 19:35:10 +00:00
if ( ! $coupon -> get_date_created ( 'edit' ) ) {
$coupon -> set_date_created ( time () );
}
2016-11-09 12:21:18 +00:00
2018-03-07 19:16:01 +00:00
$coupon_id = wp_insert_post (
apply_filters (
'woocommerce_new_coupon_data' ,
array (
'post_type' => 'shop_coupon' ,
'post_status' => 'publish' ,
'post_author' => get_current_user_id (),
'post_title' => $coupon -> get_code ( 'edit' ),
'post_content' => '' ,
'post_excerpt' => $coupon -> get_description ( 'edit' ),
'post_date' => gmdate ( 'Y-m-d H:i:s' , $coupon -> get_date_created () -> getOffsetTimestamp () ),
'post_date_gmt' => gmdate ( 'Y-m-d H:i:s' , $coupon -> get_date_created () -> getTimestamp () ),
)
2018-11-23 14:57:51 +00:00
),
true
2018-03-07 19:16:01 +00:00
);
2016-11-09 12:21:18 +00:00
if ( $coupon_id ) {
$coupon -> set_id ( $coupon_id );
2017-01-23 20:20:29 +00:00
$this -> update_post_meta ( $coupon );
2016-11-09 12:21:18 +00:00
$coupon -> save_meta_data ();
2016-11-15 23:25:36 +00:00
$coupon -> apply_changes ();
2018-09-09 00:04:13 +00:00
delete_transient ( 'rest_api_coupons_type_count' );
2019-04-17 11:50:46 +00:00
do_action ( 'woocommerce_new_coupon' , $coupon_id , $coupon );
2016-11-09 12:21:18 +00:00
}
}
/**
* Method to read a coupon .
2016-11-14 10:29:25 +00:00
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2017-05-12 08:52:26 +00:00
*
2018-03-08 19:27:31 +00:00
* @ param WC_Coupon $coupon Coupon object .
2017-05-12 08:52:26 +00:00
*
2018-03-08 19:27:31 +00:00
* @ throws Exception If invalid coupon .
2016-11-09 12:21:18 +00:00
*/
public function read ( & $coupon ) {
$coupon -> set_defaults ();
2018-03-08 19:27:31 +00:00
$post_object = get_post ( $coupon -> get_id () );
if ( ! $coupon -> get_id () || ! $post_object || 'shop_coupon' !== $post_object -> post_type ) {
2016-11-09 12:21:18 +00:00
throw new Exception ( __ ( 'Invalid coupon.' , 'woocommerce' ) );
}
$coupon_id = $coupon -> get_id ();
2018-03-07 19:16:01 +00:00
$coupon -> set_props (
array (
'code' => $post_object -> post_title ,
'description' => $post_object -> post_excerpt ,
2020-10-22 07:21:15 +00:00
'date_created' => $this -> string_to_timestamp ( $post_object -> post_date_gmt ),
'date_modified' => $this -> string_to_timestamp ( $post_object -> post_modified_gmt ),
2019-05-15 09:23:41 +00:00
'date_expires' => metadata_exists ( 'post' , $coupon_id , 'date_expires' ) ? get_post_meta ( $coupon_id , 'date_expires' , true ) : get_post_meta ( $coupon_id , 'expiry_date' , true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine.
2018-03-07 19:16:01 +00:00
'discount_type' => get_post_meta ( $coupon_id , 'discount_type' , true ),
'amount' => get_post_meta ( $coupon_id , 'coupon_amount' , true ),
'usage_count' => get_post_meta ( $coupon_id , 'usage_count' , true ),
'individual_use' => 'yes' === get_post_meta ( $coupon_id , 'individual_use' , true ),
'product_ids' => array_filter ( ( array ) explode ( ',' , get_post_meta ( $coupon_id , 'product_ids' , true ) ) ),
'excluded_product_ids' => array_filter ( ( array ) explode ( ',' , get_post_meta ( $coupon_id , 'exclude_product_ids' , true ) ) ),
'usage_limit' => get_post_meta ( $coupon_id , 'usage_limit' , true ),
'usage_limit_per_user' => get_post_meta ( $coupon_id , 'usage_limit_per_user' , true ),
'limit_usage_to_x_items' => 0 < get_post_meta ( $coupon_id , 'limit_usage_to_x_items' , true ) ? get_post_meta ( $coupon_id , 'limit_usage_to_x_items' , true ) : null ,
'free_shipping' => 'yes' === get_post_meta ( $coupon_id , 'free_shipping' , true ),
'product_categories' => array_filter ( ( array ) get_post_meta ( $coupon_id , 'product_categories' , true ) ),
'excluded_product_categories' => array_filter ( ( array ) get_post_meta ( $coupon_id , 'exclude_product_categories' , true ) ),
'exclude_sale_items' => 'yes' === get_post_meta ( $coupon_id , 'exclude_sale_items' , true ),
'minimum_amount' => get_post_meta ( $coupon_id , 'minimum_amount' , true ),
'maximum_amount' => get_post_meta ( $coupon_id , 'maximum_amount' , true ),
'email_restrictions' => array_filter ( ( array ) get_post_meta ( $coupon_id , 'customer_email' , true ) ),
'used_by' => array_filter ( ( array ) get_post_meta ( $coupon_id , '_used_by' ) ),
)
);
2016-11-09 12:21:18 +00:00
$coupon -> read_meta_data ();
$coupon -> set_object_read ( true );
do_action ( 'woocommerce_coupon_loaded' , $coupon );
}
/**
* Updates a coupon in the database .
2016-11-14 10:29:25 +00:00
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2018-03-08 19:27:31 +00:00
* @ param WC_Coupon $coupon Coupon object .
2016-11-09 12:21:18 +00:00
*/
public function update ( & $coupon ) {
2017-04-05 21:39:41 +00:00
$coupon -> save_meta_data ();
2017-04-15 20:18:24 +00:00
$changes = $coupon -> get_changes ();
2017-04-05 21:39:41 +00:00
2017-04-15 20:48:22 +00:00
if ( array_intersect ( array ( 'code' , 'description' , 'date_created' , 'date_modified' ), array_keys ( $changes ) ) ) {
2017-04-15 20:18:24 +00:00
$post_data = array (
2017-04-15 20:48:22 +00:00
'post_title' => $coupon -> get_code ( 'edit' ),
'post_excerpt' => $coupon -> get_description ( 'edit' ),
2017-04-15 20:18:24 +00:00
'post_date' => gmdate ( 'Y-m-d H:i:s' , $coupon -> get_date_created ( 'edit' ) -> getOffsetTimestamp () ),
'post_date_gmt' => gmdate ( 'Y-m-d H:i:s' , $coupon -> get_date_created ( 'edit' ) -> getTimestamp () ),
'post_modified' => isset ( $changes [ 'date_modified' ] ) ? gmdate ( 'Y-m-d H:i:s' , $coupon -> get_date_modified ( 'edit' ) -> getOffsetTimestamp () ) : current_time ( 'mysql' ),
2018-03-07 19:16:01 +00:00
'post_modified_gmt' => isset ( $changes [ 'date_modified' ] ) ? gmdate ( 'Y-m-d H:i:s' , $coupon -> get_date_modified ( 'edit' ) -> getTimestamp () ) : current_time ( 'mysql' , 1 ),
2017-04-15 20:18:24 +00:00
);
/**
* When updating this object , to prevent infinite loops , use $wpdb
* to update data , since wp_update_post spawns more calls to the
* save_post action .
*
* This ensures hooks are fired by either WP itself ( admin screen save ),
* or an update purely from CRUD .
*/
if ( doing_action ( 'save_post' ) ) {
$GLOBALS [ 'wpdb' ] -> update ( $GLOBALS [ 'wpdb' ] -> posts , $post_data , array ( 'ID' => $coupon -> get_id () ) );
clean_post_cache ( $coupon -> get_id () );
} else {
wp_update_post ( array_merge ( array ( 'ID' => $coupon -> get_id () ), $post_data ) );
}
$coupon -> read_meta_data ( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
}
2016-11-09 12:21:18 +00:00
$this -> update_post_meta ( $coupon );
2016-11-15 23:25:36 +00:00
$coupon -> apply_changes ();
2018-09-09 00:04:13 +00:00
delete_transient ( 'rest_api_coupons_type_count' );
2019-04-17 11:50:46 +00:00
do_action ( 'woocommerce_update_coupon' , $coupon -> get_id (), $coupon );
2016-11-09 12:21:18 +00:00
}
/**
* Deletes a coupon from the database .
2016-11-14 10:29:25 +00:00
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2017-05-12 08:48:46 +00:00
*
2018-03-08 19:27:31 +00:00
* @ param WC_Coupon $coupon Coupon object .
2017-05-12 08:48:46 +00:00
* @ param array $args Array of args to pass to the delete method .
2016-11-09 12:21:18 +00:00
*/
2016-11-15 18:11:25 +00:00
public function delete ( & $coupon , $args = array () ) {
2018-03-07 19:16:01 +00:00
$args = wp_parse_args (
2018-11-23 14:57:51 +00:00
$args ,
array (
2018-03-07 19:16:01 +00:00
'force_delete' => false ,
)
);
2016-11-15 18:11:25 +00:00
2016-11-09 12:21:18 +00:00
$id = $coupon -> get_id ();
2016-11-15 18:11:25 +00:00
2017-05-30 13:44:28 +00:00
if ( ! $id ) {
return ;
}
2016-11-15 18:11:25 +00:00
if ( $args [ 'force_delete' ] ) {
2017-05-30 13:44:28 +00:00
wp_delete_post ( $id );
2017-09-10 02:47:25 +00:00
wp_cache_delete ( WC_Cache_Helper :: get_cache_prefix ( 'coupons' ) . 'coupon_id_from_code_' . $coupon -> get_code (), 'coupons' );
2016-11-09 12:21:18 +00:00
$coupon -> set_id ( 0 );
2016-12-29 22:22:12 +00:00
do_action ( 'woocommerce_delete_coupon' , $id );
2016-11-09 12:21:18 +00:00
} else {
2017-05-30 13:44:28 +00:00
wp_trash_post ( $id );
2016-12-29 22:22:12 +00:00
do_action ( 'woocommerce_trash_coupon' , $id );
2016-11-09 12:21:18 +00:00
}
}
/**
* Helper method that updates all the post meta for a coupon based on it ' s settings in the WC_Coupon class .
2016-11-14 10:29:25 +00:00
*
2018-03-08 19:27:31 +00:00
* @ param WC_Coupon $coupon Coupon object .
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2016-11-09 12:21:18 +00:00
*/
2017-01-23 20:20:29 +00:00
private function update_post_meta ( & $coupon ) {
2016-11-09 12:21:18 +00:00
$meta_key_to_props = array (
'discount_type' => 'discount_type' ,
'coupon_amount' => 'amount' ,
'individual_use' => 'individual_use' ,
'product_ids' => 'product_ids' ,
'exclude_product_ids' => 'excluded_product_ids' ,
'usage_limit' => 'usage_limit' ,
'usage_limit_per_user' => 'usage_limit_per_user' ,
'limit_usage_to_x_items' => 'limit_usage_to_x_items' ,
'usage_count' => 'usage_count' ,
2018-03-07 19:16:01 +00:00
'date_expires' => 'date_expires' ,
2016-11-09 12:21:18 +00:00
'free_shipping' => 'free_shipping' ,
'product_categories' => 'product_categories' ,
'exclude_product_categories' => 'excluded_product_categories' ,
'exclude_sale_items' => 'exclude_sale_items' ,
'minimum_amount' => 'minimum_amount' ,
'maximum_amount' => 'maximum_amount' ,
'customer_email' => 'email_restrictions' ,
);
2017-01-23 20:20:29 +00:00
$props_to_update = $this -> get_props_to_update ( $coupon , $meta_key_to_props );
foreach ( $props_to_update as $meta_key => $prop ) {
2016-11-09 12:21:18 +00:00
$value = $coupon -> { " get_ $prop " }( 'edit' );
2019-02-19 15:35:05 +00:00
$value = is_string ( $value ) ? wp_slash ( $value ) : $value ;
2016-11-09 12:21:18 +00:00
switch ( $prop ) {
2018-03-07 19:16:01 +00:00
case 'individual_use' :
case 'free_shipping' :
case 'exclude_sale_items' :
2019-02-19 15:35:05 +00:00
$value = wc_bool_to_string ( $value );
2016-11-09 12:21:18 +00:00
break ;
2018-03-07 19:16:01 +00:00
case 'product_ids' :
case 'excluded_product_ids' :
2019-02-19 15:35:05 +00:00
$value = implode ( ',' , array_filter ( array_map ( 'intval' , $value ) ) );
2016-12-12 15:29:53 +00:00
break ;
2018-03-07 19:16:01 +00:00
case 'product_categories' :
case 'excluded_product_categories' :
2019-02-19 15:35:05 +00:00
$value = array_filter ( array_map ( 'intval' , $value ) );
2016-11-09 12:21:18 +00:00
break ;
2018-03-07 19:16:01 +00:00
case 'email_restrictions' :
2019-02-19 15:35:05 +00:00
$value = array_filter ( array_map ( 'sanitize_email' , $value ) );
2016-11-09 12:21:18 +00:00
break ;
2018-03-07 19:16:01 +00:00
case 'date_expires' :
2019-02-19 15:35:05 +00:00
$value = $value ? $value -> getTimestamp () : null ;
2016-11-09 12:21:18 +00:00
break ;
}
2019-02-19 15:35:05 +00:00
$updated = $this -> update_or_delete_post_meta ( $coupon , $meta_key , $value );
2016-11-09 12:21:18 +00:00
if ( $updated ) {
2019-02-19 15:35:05 +00:00
$this -> updated_props [] = $prop ;
2016-11-09 12:21:18 +00:00
}
}
2017-01-24 21:38:02 +00:00
2019-11-09 21:07:39 +00:00
do_action ( 'woocommerce_coupon_object_updated_props' , $coupon , $this -> updated_props );
2016-11-09 12:21:18 +00:00
}
/**
* Increase usage count for current coupon .
2016-11-14 10:29:25 +00:00
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2019-07-09 21:07:44 +00:00
* @ 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 .
2018-03-08 19:27:31 +00:00
* @ return int New usage count .
2016-11-09 12:21:18 +00:00
*/
2019-07-09 21:07:44 +00:00
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 () );
}
2017-03-10 17:18:14 +00:00
$new_count = $this -> update_usage_count_meta ( $coupon , 'increase' );
2019-07-09 21:07:44 +00:00
2016-11-09 12:21:18 +00:00
if ( $used_by ) {
2019-07-09 21:07:44 +00:00
$this -> add_coupon_used_by ( $coupon , $used_by , $coupon_held_key_for_user );
2016-11-09 12:21:18 +00:00
$coupon -> set_used_by ( ( array ) get_post_meta ( $coupon -> get_id (), '_used_by' ) );
}
2018-05-06 11:09:59 +00:00
do_action ( 'woocommerce_increase_coupon_usage_count' , $coupon , $new_count , $used_by );
2017-03-08 23:27:37 +00:00
return $new_count ;
2016-11-09 12:21:18 +00:00
}
2019-07-09 21:07:44 +00:00
/**
* 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 ) );
}
}
2016-11-09 12:21:18 +00:00
/**
* Decrease usage count for current coupon .
2016-11-14 10:29:25 +00:00
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2018-03-08 19:27:31 +00:00
* @ param WC_Coupon $coupon Coupon object .
* @ param string $used_by Either user ID or billing email .
* @ return int New usage count .
2016-11-09 12:21:18 +00:00
*/
public function decrease_usage_count ( & $coupon , $used_by = '' ) {
global $wpdb ;
2017-03-10 17:18:14 +00:00
$new_count = $this -> update_usage_count_meta ( $coupon , 'decrease' );
2016-11-09 12:21:18 +00:00
if ( $used_by ) {
/**
* 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 .
*/
2019-07-09 21:07:44 +00:00
$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 ()
)
);
2016-11-09 12:21:18 +00:00
if ( $meta_id ) {
delete_metadata_by_mid ( 'post' , $meta_id );
$coupon -> set_used_by ( ( array ) get_post_meta ( $coupon -> get_id (), '_used_by' ) );
}
}
2018-05-06 11:09:59 +00:00
do_action ( 'woocommerce_decrease_coupon_usage_count' , $coupon , $new_count , $used_by );
2017-03-08 23:27:37 +00:00
return $new_count ;
}
/**
2017-03-09 16:51:47 +00:00
* Increase or decrease the usage count for a coupon by 1.
2017-03-08 23:27:37 +00:00
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2018-03-08 19:27:31 +00:00
* @ param WC_Coupon $coupon Coupon object .
* @ param string $operation 'increase' or 'decrease' .
2017-03-08 23:27:37 +00:00
* @ return int New usage count
*/
2017-03-10 17:18:14 +00:00
private function update_usage_count_meta ( & $coupon , $operation = 'increase' ) {
2017-03-08 23:27:37 +00:00
global $wpdb ;
2018-03-07 19:16:01 +00:00
$id = $coupon -> get_id ();
2017-03-09 16:51:47 +00:00
$operator = ( 'increase' === $operation ) ? '+' : '-' ;
2017-03-08 23:27:37 +00:00
2017-03-10 18:28:56 +00:00
add_post_meta ( $id , 'usage_count' , $coupon -> get_usage_count ( 'edit' ), true );
2018-03-08 19:27:31 +00:00
$wpdb -> query (
$wpdb -> prepare (
2019-07-09 21:07:44 +00:00
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
2018-11-23 14:57:51 +00:00
" UPDATE $wpdb->postmeta SET meta_value = meta_value { $operator } 1 WHERE meta_key = 'usage_count' AND post_id = %d; " ,
2018-03-08 19:27:31 +00:00
$id
)
);
2017-03-08 23:27:37 +00:00
2017-03-10 18:28:56 +00:00
// 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 ) );
2016-11-09 12:21:18 +00:00
}
2019-07-09 21:07:44 +00:00
/**
* 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 )
);
}
2020-12-16 09:49:40 +00:00
2016-11-09 12:21:18 +00:00
/**
* Get the number of uses for a coupon by user ID .
2016-11-14 10:29:25 +00:00
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2018-03-08 19:27:31 +00:00
* @ param WC_Coupon $coupon Coupon object .
* @ param id $user_id User ID .
2016-11-09 12:21:18 +00:00
* @ return int
*/
public function get_usage_by_user_id ( & $coupon , $user_id ) {
global $wpdb ;
2019-07-09 21:07:44 +00:00
$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 ;
2016-11-09 12:21:18 +00:00
}
2019-05-27 14:45:29 +00:00
/**
* Get the number of uses for a coupon by email address
*
* @ since 3.6 . 4
* @ param WC_Coupon $coupon Coupon object .
* @ param string $email Email address .
* @ return int
*/
public function get_usage_by_email ( & $coupon , $email ) {
global $wpdb ;
2019-07-09 21:07:44 +00:00
$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 ;
}
2019-12-13 10:03:54 +00:00
if ( ! apply_filters ( 'woocommerce_hold_stock_for_checkout' , true ) ) {
2019-07-09 21:07:44 +00:00
return null ;
}
2020-03-11 15:14:52 +00:00
// Make sure we have usage_count meta key for this coupon because its required for `$query_for_usages`.
// We are not directly modifying `$query_for_usages` to allow for `usage_count` not present only keep that query simple.
if ( ! metadata_exists ( 'post' , $coupon -> get_id (), 'usage_count' ) ) {
$coupon -> set_usage_count ( $coupon -> get_usage_count () ); // Use `get_usage_count` here to write default value, which may changed by a filter.
$coupon -> save ();
}
2019-07-09 21:07:44 +00:00
$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 () );
2019-12-13 10:41:58 +00:00
$db_timestamp = $wpdb -> get_var ( 'SELECT UNIX_TIMESTAMP() FROM DUAL' );
2019-07-09 21:07:44 +00:00
2019-12-13 10:41:58 +00:00
$coupon_usage_key = '_coupon_held_' . ( ( int ) $db_timestamp + $held_time ) . '_' . wp_generate_password ( 6 , false );
2019-07-09 21:07:44 +00:00
$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 ;
}
2019-12-13 10:03:54 +00:00
if ( ! apply_filters ( 'woocommerce_hold_stock_for_checkout' , true ) ) {
2019-07-09 21:07:44 +00:00
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 );
2019-12-13 10:41:58 +00:00
$db_timestamp = $wpdb -> get_var ( 'SELECT UNIX_TIMESTAMP() FROM DUAL' );
2019-07-09 21:07:44 +00:00
2019-12-13 10:41:58 +00:00
$coupon_used_by_meta_key = '_maybe_used_by_' . ( ( int ) $db_timestamp + $held_time ) . '_' . wp_generate_password ( 6 , false );
2019-07-09 21:07:44 +00:00
$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.
2019-05-27 14:45:29 +00:00
}
2016-11-14 10:29:25 +00:00
/**
* Return a coupon code for a specific ID .
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2018-03-08 19:27:31 +00:00
* @ param int $id Coupon ID .
2016-11-14 10:29:25 +00:00
* @ return string Coupon Code
*/
public function get_code_by_id ( $id ) {
global $wpdb ;
2018-03-07 19:16:01 +00:00
return $wpdb -> get_var (
$wpdb -> prepare (
" SELECT post_title
FROM $wpdb -> posts
WHERE ID = % d
AND post_type = 'shop_coupon'
AND post_status = 'publish' " ,
$id
)
);
2016-11-14 10:29:25 +00:00
}
/**
* Return an array of IDs for for a specific coupon code .
* Can return multiple to check for existence .
*
2017-03-15 16:36:53 +00:00
* @ since 3.0 . 0
2018-03-08 19:27:31 +00:00
* @ param string $code Coupon code .
2016-11-14 10:29:25 +00:00
* @ return array Array of IDs .
*/
public function get_ids_by_code ( $code ) {
global $wpdb ;
2018-03-07 19:16:01 +00:00
return $wpdb -> get_col (
$wpdb -> prepare (
" SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC " ,
2020-07-27 20:33:15 +00:00
wc_sanitize_coupon_code ( $code )
2018-03-07 19:16:01 +00:00
)
);
2016-11-14 10:29:25 +00:00
}
2016-11-09 12:21:18 +00:00
}