2019-01-15 01:53:02 +00:00
< ? php
/**
* WC_Admin_Reports_Orders_Stats_Data_Store class file .
*
* @ package WooCommerce Admin / Classes
*/
defined ( 'ABSPATH' ) || exit ;
/**
* WC_Admin_Reports_Orders_Stats_Data_Store .
*
* @ version 3.5 . 0
*/
class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Store implements WC_Admin_Reports_Data_Store_Interface {
/**
* Table used to get the data .
*
* @ var string
*/
const TABLE_NAME = 'wc_order_stats' ;
/**
* Cron event name .
*/
const CRON_EVENT = 'wc_order_stats_update' ;
/**
* Type for each column to cast values correctly later .
*
* @ var array
*/
protected $column_types = array (
'orders_count' => 'intval' ,
'num_items_sold' => 'intval' ,
'gross_revenue' => 'floatval' ,
'coupons' => 'floatval' ,
'refunds' => 'floatval' ,
'taxes' => 'floatval' ,
'shipping' => 'floatval' ,
'net_revenue' => 'floatval' ,
'avg_items_per_order' => 'floatval' ,
'avg_order_value' => 'floatval' ,
'num_returning_customers' => 'intval' ,
'num_new_customers' => 'intval' ,
'products' => 'intval' ,
2019-01-11 10:11:23 +00:00
'segment_id' => 'intval' ,
2019-01-15 01:53:02 +00:00
);
/**
* SQL definition for each column .
*
* @ var array
*/
protected $report_columns = array (
'orders_count' => 'COUNT(*) as orders_count' ,
'num_items_sold' => 'SUM(num_items_sold) as num_items_sold' ,
'gross_revenue' => 'SUM(gross_total) AS gross_revenue' ,
'coupons' => 'SUM(coupon_total) AS coupons' ,
'refunds' => 'SUM(refund_total) AS refunds' ,
'taxes' => 'SUM(tax_total) AS taxes' ,
'shipping' => 'SUM(shipping_total) AS shipping' ,
'net_revenue' => '( SUM(net_total) - SUM(refund_total) ) AS net_revenue' ,
'avg_items_per_order' => 'AVG(num_items_sold) AS avg_items_per_order' ,
'avg_order_value' => '( SUM(net_total) - SUM(refund_total) ) / COUNT(*) AS avg_order_value' ,
2019-01-17 02:47:30 +00:00
// Count returning customers as ( total_customers - new_customers ) to get an accurate number and count customers in with both new and old statuses as new.
'num_returning_customers' => '( COUNT( DISTINCT( customer_id ) ) - COUNT( DISTINCT( CASE WHEN returning_customer = 0 THEN customer_id END ) ) ) AS num_returning_customers' ,
'num_new_customers' => 'COUNT( DISTINCT( CASE WHEN returning_customer = 0 THEN customer_id END ) ) AS num_new_customers' ,
2019-01-15 01:53:02 +00:00
);
/**
* Set up all the hooks for maintaining and populating table data .
*/
public static function init () {
add_action ( 'woocommerce_refund_deleted' , array ( __CLASS__ , 'sync_on_refund_delete' ), 10 , 2 );
2019-01-17 02:47:30 +00:00
add_action ( 'delete_post' , array ( __CLASS__ , 'delete_order' ) );
2019-01-15 01:53:02 +00:00
}
/**
* Updates the totals and intervals database queries with parameters used for Orders report : categories , coupons and order status .
*
* @ param array $query_args Query arguments supplied by the user .
* @ param array $totals_query Array of options for totals db query .
* @ param array $intervals_query Array of options for intervals db query .
*/
protected function orders_stats_sql_filter ( $query_args , & $totals_query , & $intervals_query ) {
2019-02-06 06:41:53 +00:00
// @todo Performance of all of this?
2019-01-15 01:53:02 +00:00
global $wpdb ;
$from_clause = '' ;
$orders_stats_table = $wpdb -> prefix . self :: TABLE_NAME ;
$operator = $this -> get_match_operator ( $query_args );
$where_filters = array ();
2019-02-06 06:41:53 +00:00
// @todo Maybe move the sql inside the get_included/excluded functions?
2019-01-15 01:53:02 +00:00
// Products filters.
$included_products = $this -> get_included_products ( $query_args );
$excluded_products = $this -> get_excluded_products ( $query_args );
if ( $included_products ) {
$where_filters [] = " { $orders_stats_table } .order_id IN (
SELECT
DISTINCT { $wpdb -> prefix } wc_order_product_lookup . order_id
FROM
{ $wpdb -> prefix } wc_order_product_lookup
WHERE
{ $wpdb -> prefix } wc_order_product_lookup . product_id IN ({ $included_products })
) " ;
}
if ( $excluded_products ) {
$where_filters [] = " { $orders_stats_table } .order_id NOT IN (
SELECT
DISTINCT { $wpdb -> prefix } wc_order_product_lookup . order_id
FROM
{ $wpdb -> prefix } wc_order_product_lookup
WHERE
{ $wpdb -> prefix } wc_order_product_lookup . product_id IN ({ $excluded_products })
) " ;
}
// Coupons filters.
$included_coupons = $this -> get_included_coupons ( $query_args );
$excluded_coupons = $this -> get_excluded_coupons ( $query_args );
if ( $included_coupons ) {
$where_filters [] = " { $orders_stats_table } .order_id IN (
SELECT
DISTINCT { $wpdb -> prefix } wc_order_coupon_lookup . order_id
FROM
{ $wpdb -> prefix } wc_order_coupon_lookup
WHERE
{ $wpdb -> prefix } wc_order_coupon_lookup . coupon_id IN ({ $included_coupons })
) " ;
}
if ( $excluded_coupons ) {
$where_filters [] = " { $orders_stats_table } .order_id NOT IN (
SELECT
DISTINCT { $wpdb -> prefix } wc_order_coupon_lookup . order_id
FROM
{ $wpdb -> prefix } wc_order_coupon_lookup
WHERE
{ $wpdb -> prefix } wc_order_coupon_lookup . coupon_id IN ({ $excluded_coupons })
) " ;
}
$customer_filter = $this -> get_customer_subquery ( $query_args );
if ( $customer_filter ) {
$where_filters [] = $customer_filter ;
}
$where_subclause = implode ( " $operator " , $where_filters );
2019-01-16 02:23:00 +00:00
// Append status filter after to avoid matching ANY on default statuses.
$order_status_filter = $this -> get_status_subquery ( $query_args , $operator );
if ( $order_status_filter ) {
if ( empty ( $query_args [ 'status_is' ] ) && empty ( $query_args [ 'status_is_not' ] ) ) {
$operator = 'AND' ;
}
$where_subclause = implode ( " $operator " , array_filter ( array ( $where_subclause , $order_status_filter ) ) );
}
2019-01-15 01:53:02 +00:00
// To avoid requesting the subqueries twice, the result is applied to all queries passed to the method.
if ( $where_subclause ) {
$totals_query [ 'where_clause' ] .= " AND ( $where_subclause ) " ;
$totals_query [ 'from_clause' ] .= $from_clause ;
$intervals_query [ 'where_clause' ] .= " AND ( $where_subclause ) " ;
$intervals_query [ 'from_clause' ] .= $from_clause ;
}
}
/**
* Returns the report data based on parameters supplied by the user .
*
* @ param array $query_args Query parameters .
* @ return stdClass | WP_Error Data .
*/
public function get_data ( $query_args ) {
global $wpdb ;
$table_name = $wpdb -> prefix . self :: TABLE_NAME ;
// These defaults are only applied when not using REST API, as the API has its own defaults that overwrite these for most values (except before, after, etc).
$defaults = array (
'per_page' => get_option ( 'posts_per_page' ),
'page' => 1 ,
'order' => 'DESC' ,
'orderby' => 'date' ,
2019-02-10 16:07:45 +00:00
'before' => WC_Admin_Reports_Interval :: default_before (),
'after' => WC_Admin_Reports_Interval :: default_after (),
2019-01-15 01:53:02 +00:00
'interval' => 'week' ,
'fields' => '*' ,
2019-01-02 12:35:40 +00:00
'segmentby' => '' ,
2019-01-15 01:53:02 +00:00
'match' => 'all' ,
'status_is' => array (),
'status_is_not' => array (),
'product_includes' => array (),
'product_excludes' => array (),
'coupon_includes' => array (),
'coupon_excludes' => array (),
'customer' => '' ,
'categories' => array (),
);
$query_args = wp_parse_args ( $query_args , $defaults );
2019-02-10 16:16:43 +00:00
$this -> normalize_timezones ( $query_args , $defaults );
2019-01-15 01:53:02 +00:00
$cache_key = $this -> get_cache_key ( $query_args );
$data = wp_cache_get ( $cache_key , $this -> cache_group );
if ( false === $data ) {
$data = ( object ) array (
'totals' => ( object ) array (),
'intervals' => ( object ) array (),
'total' => 0 ,
'pages' => 0 ,
'page_no' => 0 ,
);
$selections = $this -> selected_columns ( $query_args );
$totals_query = $this -> get_time_period_sql_params ( $query_args , $table_name );
$intervals_query = $this -> get_intervals_sql_params ( $query_args , $table_name );
// Additional filtering for Orders report.
$this -> orders_stats_sql_filter ( $query_args , $totals_query , $intervals_query );
$totals = $wpdb -> get_results (
" SELECT
{ $selections }
FROM
{ $table_name }
{ $totals_query [ 'from_clause' ]}
WHERE
1 = 1
{ $totals_query [ 'where_time_clause' ]}
{ $totals_query [ 'where_clause' ]} " ,
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return new WP_Error ( 'woocommerce_reports_revenue_result_failed' , __ ( 'Sorry, fetching revenue data failed.' , 'wc-admin' ) );
}
$unique_products = $this -> get_unique_product_count ( $totals_query [ 'from_clause' ], $totals_query [ 'where_time_clause' ], $totals_query [ 'where_clause' ] );
$totals [ 0 ][ 'products' ] = $unique_products ;
2019-01-17 11:14:24 +00:00
$segmenting = new WC_Admin_Reports_Orders_Stats_Segmenting ( $query_args , $this -> report_columns );
$totals [ 0 ][ 'segments' ] = $segmenting -> get_totals_segments ( $totals_query , $table_name );
2019-01-11 10:11:23 +00:00
$totals = ( object ) $this -> cast_numbers ( $totals [ 0 ] );
2019-01-15 01:53:02 +00:00
$db_intervals = $wpdb -> get_col (
" SELECT
{ $intervals_query [ 'select_clause' ]} AS time_interval
FROM
{ $table_name }
{ $intervals_query [ 'from_clause' ]}
WHERE
1 = 1
{ $intervals_query [ 'where_time_clause' ]}
{ $intervals_query [ 'where_clause' ]}
GROUP BY
time_interval "
); // WPCS: cache ok, DB call ok, , unprepared SQL ok.
$db_interval_count = count ( $db_intervals );
$expected_interval_count = WC_Admin_Reports_Interval :: intervals_between ( $query_args [ 'after' ], $query_args [ 'before' ], $query_args [ 'interval' ] );
$total_pages = ( int ) ceil ( $expected_interval_count / $intervals_query [ 'per_page' ] );
if ( $query_args [ 'page' ] < 1 || $query_args [ 'page' ] > $total_pages ) {
return $data ;
}
2019-01-16 02:23:00 +00:00
$this -> update_intervals_sql_params ( $intervals_query , $query_args , $db_interval_count , $expected_interval_count , $table_name );
2019-01-15 01:53:02 +00:00
if ( '' !== $selections ) {
$selections = ', ' . $selections ;
}
$intervals = $wpdb -> get_results (
" SELECT
MAX ( date_created ) AS datetime_anchor ,
{ $intervals_query [ 'select_clause' ]} AS time_interval
{ $selections }
FROM
{ $table_name }
{ $intervals_query [ 'from_clause' ]}
WHERE
1 = 1
{ $intervals_query [ 'where_time_clause' ]}
{ $intervals_query [ 'where_clause' ]}
GROUP BY
time_interval
ORDER BY
{ $intervals_query [ 'order_by_clause' ]}
{ $intervals_query [ 'limit' ]} " ,
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return new WP_Error ( 'woocommerce_reports_revenue_result_failed' , __ ( 'Sorry, fetching revenue data failed.' , 'wc-admin' ) );
}
$data = ( object ) array (
'totals' => $totals ,
'intervals' => $intervals ,
'total' => $expected_interval_count ,
'pages' => $total_pages ,
'page_no' => ( int ) $query_args [ 'page' ],
);
if ( WC_Admin_Reports_Interval :: intervals_missing ( $expected_interval_count , $db_interval_count , $intervals_query [ 'per_page' ], $query_args [ 'page' ], $query_args [ 'order' ], $query_args [ 'orderby' ], count ( $intervals ) ) ) {
$this -> fill_in_missing_intervals ( $db_intervals , $query_args [ 'adj_after' ], $query_args [ 'adj_before' ], $query_args [ 'interval' ], $data );
$this -> sort_intervals ( $data , $query_args [ 'orderby' ], $query_args [ 'order' ] );
2019-01-23 16:18:39 +00:00
$this -> remove_extra_records ( $data , $query_args [ 'page' ], $intervals_query [ 'per_page' ], $db_interval_count , $expected_interval_count , $query_args [ 'orderby' ], $query_args [ 'order' ] );
2019-01-15 01:53:02 +00:00
} else {
$this -> update_interval_boundary_dates ( $query_args [ 'after' ], $query_args [ 'before' ], $query_args [ 'interval' ], $data -> intervals );
}
2019-01-17 11:14:24 +00:00
$segmenting -> add_intervals_segments ( $data , $intervals_query , $table_name );
2019-01-15 01:53:02 +00:00
$this -> create_interval_subtotals ( $data -> intervals );
wp_cache_set ( $cache_key , $data , $this -> cache_group );
}
return $data ;
}
/**
* Get unique products based on user time query
*
* @ param string $from_clause From clause with date query .
* @ param string $where_time_clause Where clause with date query .
* @ param string $where_clause Where clause with date query .
* @ return integer Unique product count .
*/
public function get_unique_product_count ( $from_clause , $where_time_clause , $where_clause ) {
global $wpdb ;
$table_name = $wpdb -> prefix . self :: TABLE_NAME ;
return $wpdb -> get_var (
" SELECT
COUNT ( DISTINCT { $wpdb -> prefix } wc_order_product_lookup . product_id )
FROM
{ $wpdb -> prefix } wc_order_product_lookup JOIN { $table_name } ON { $wpdb -> prefix } wc_order_product_lookup . order_id = { $table_name } . order_id
{ $from_clause }
WHERE
1 = 1
{ $where_time_clause }
{ $where_clause } "
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
}
/**
* Add order information to the lookup table when orders are created or modified .
*
* @ param int $post_id Post ID .
2019-01-29 21:36:02 +00:00
* @ return int | bool Returns - 1 if order won ' t be processed , or a boolean indicating processing success .
2019-01-15 01:53:02 +00:00
*/
public static function sync_order ( $post_id ) {
if ( 'shop_order' !== get_post_type ( $post_id ) ) {
2019-01-29 21:36:02 +00:00
return - 1 ;
2019-01-15 01:53:02 +00:00
}
$order = wc_get_order ( $post_id );
if ( ! $order ) {
2019-01-29 21:36:02 +00:00
return - 1 ;
2019-01-15 01:53:02 +00:00
}
2019-01-29 21:36:02 +00:00
return self :: update ( $order );
2019-01-15 01:53:02 +00:00
}
/**
* Syncs order information when a refund is deleted .
*
* @ param int $refund_id Refund ID .
* @ param int $order_id Order ID .
*/
public static function sync_on_refund_delete ( $refund_id , $order_id ) {
self :: sync_order ( $order_id );
}
/**
* Update the database with stats data .
*
* @ param WC_Order $order Order to update row for .
2019-01-29 21:36:02 +00:00
* @ return int | bool Returns - 1 if order won ' t be processed , or a boolean indicating processing success .
2019-01-15 01:53:02 +00:00
*/
public static function update ( $order ) {
global $wpdb ;
$table_name = $wpdb -> prefix . self :: TABLE_NAME ;
if ( ! $order -> get_id () || ! $order -> get_date_created () ) {
2019-01-29 21:36:02 +00:00
return - 1 ;
2019-01-15 01:53:02 +00:00
}
$data = array (
'order_id' => $order -> get_id (),
'date_created' => $order -> get_date_created () -> date ( 'Y-m-d H:i:s' ),
'num_items_sold' => self :: get_num_items_sold ( $order ),
'gross_total' => $order -> get_total (),
'coupon_total' => $order -> get_total_discount (),
'refund_total' => $order -> get_total_refunded (),
'tax_total' => $order -> get_total_tax (),
'shipping_total' => $order -> get_shipping_total (),
2019-02-20 01:39:42 +00:00
'net_total' => self :: get_net_total ( $order ),
2019-01-15 01:53:02 +00:00
'returning_customer' => self :: is_returning_customer ( $order ),
'status' => self :: normalize_order_status ( $order -> get_status () ),
2019-02-20 02:30:00 +00:00
'customer_id' => WC_Admin_Reports_Customers_Data_Store :: get_or_create_customer_from_order ( $order ),
2019-01-15 01:53:02 +00:00
);
$format = array (
'%d' ,
'%s' ,
'%d' ,
'%f' ,
'%f' ,
'%f' ,
'%f' ,
'%f' ,
'%f' ,
'%d' ,
'%s' ,
2019-02-20 02:30:00 +00:00
'%d' ,
2019-01-15 01:53:02 +00:00
);
// Update or add the information to the DB.
2019-01-29 21:36:02 +00:00
$result = $wpdb -> replace ( $table_name , $data , $format );
2019-02-01 17:43:24 +00:00
2019-02-01 19:02:15 +00:00
/**
* Fires when order ' s stats reports are updated .
*
* @ param int $order_id Order ID .
*/
2019-02-04 17:58:37 +00:00
do_action ( 'woocommerce_reports_update_order_stats' , $order -> get_id () );
2019-02-05 17:42:34 +00:00
2019-01-29 21:36:02 +00:00
return ( 1 === $result );
2019-01-15 01:53:02 +00:00
}
2019-01-17 02:47:30 +00:00
/**
* Deletes the order stats when an order is deleted .
*
* @ param int $post_id Post ID .
*/
public static function delete_order ( $post_id ) {
global $wpdb ;
2019-01-29 20:50:15 +00:00
$order_id = ( int ) $post_id ;
2019-01-17 02:47:30 +00:00
$table_name = $wpdb -> prefix . self :: TABLE_NAME ;
2019-01-29 20:50:15 +00:00
if ( 'shop_order' !== get_post_type ( $order_id ) ) {
2019-01-17 02:47:30 +00:00
return ;
}
$wpdb -> query (
$wpdb -> prepare (
" DELETE FROM ${ table_name } WHERE order_id = %d " ,
2019-01-29 20:50:15 +00:00
$order_id
2019-01-17 02:47:30 +00:00
)
);
2019-01-29 20:50:15 +00:00
/**
* Fires when orders stats are deleted .
*
* @ param int $order_id Order ID .
*/
2019-02-04 18:02:49 +00:00
do_action ( 'woocommerce_reports_delete_order_stats' , $order_id );
2019-01-17 02:47:30 +00:00
}
2019-01-15 01:53:02 +00:00
/**
* Calculation methods .
*/
/**
* Get number of items sold among all orders .
*
* @ param array $order WC_Order object .
* @ return int
*/
protected static function get_num_items_sold ( $order ) {
$num_items = 0 ;
$line_items = $order -> get_items ( 'line_item' );
foreach ( $line_items as $line_item ) {
$num_items += $line_item -> get_quantity ();
}
return $num_items ;
}
2019-02-20 01:39:42 +00:00
/**
* Get the net amount from an order without shipping , tax , or refunds .
*
* @ param array $order WC_Order object .
* @ return float
*/
protected static function get_net_total ( $order ) {
$net_total = $order -> get_total () - $order -> get_total_tax () - $order -> get_shipping_total ();
$refunds = $order -> get_refunds ();
foreach ( $refunds as $refund ) {
$net_total += $refund -> get_total () - $refund -> get_total_tax () - $refund -> get_shipping_total ();
}
return $net_total > 0 ? ( float ) $net_total : 0 ;
}
2019-01-15 01:53:02 +00:00
/**
* Check to see if an order ' s customer has made previous orders or not
*
* @ param array $order WC_Order object .
* @ return bool
*/
protected static function is_returning_customer ( $order ) {
2019-02-22 21:06:21 +00:00
$customer_id = WC_Admin_Reports_Customers_Data_Store :: get_existing_customer_id_from_order ( $order );
2019-01-15 01:53:02 +00:00
2019-01-17 02:47:30 +00:00
if ( ! $customer_id ) {
2019-01-15 01:53:02 +00:00
return false ;
}
2019-02-14 03:14:01 +00:00
$oldest_orders = WC_Admin_Reports_Customers_Data_Store :: get_oldest_orders ( $customer_id );
if ( empty ( $oldest_orders ) ) {
return false ;
}
2019-02-15 02:09:16 +00:00
$first_order = $oldest_orders [ 0 ];
$second_order = isset ( $oldest_orders [ 1 ] ) ? $oldest_orders [ 1 ] : false ;
$excluded_statuses = self :: get_excluded_report_order_statuses ();
2019-02-14 03:14:01 +00:00
2019-02-23 17:38:45 +00:00
$local_tz = new DateTimeZone ( wc_timezone_string () );
2019-02-14 03:14:01 +00:00
// Order is older than previous first order.
2019-02-23 17:38:45 +00:00
if ( $order -> get_date_created () < new WC_DateTime ( $first_order -> date_created , $local_tz ) &&
2019-02-15 02:09:16 +00:00
! in_array ( $order -> get_status (), $excluded_statuses , true )
) {
2019-02-14 03:14:01 +00:00
self :: set_customer_first_order ( $customer_id , $order -> get_id () );
return false ;
}
2019-02-15 02:09:16 +00:00
// The current order is the oldest known order.
$is_first_order = ( int ) $order -> get_id () === ( int ) $first_order -> order_id ;
// Order date has changed and next oldest is now the first order.
$date_change = $second_order &&
2019-02-23 17:38:45 +00:00
$order -> get_date_created () > new WC_DateTime ( $first_order -> date_created , $local_tz ) &&
new WC_DateTime ( $second_order -> date_created , $local_tz ) < $order -> get_date_created ();
2019-02-15 02:09:16 +00:00
// Status has changed to an excluded status and next oldest order is now the first order.
$status_change = $second_order &&
in_array ( $order -> get_status (), $excluded_statuses , true );
if ( $is_first_order && ( $date_change || $status_change ) ) {
2019-02-14 03:14:01 +00:00
self :: set_customer_first_order ( $customer_id , $second_order -> order_id );
return true ;
}
return ( int ) $order -> get_id () !== ( int ) $first_order -> order_id ;
}
/**
* Set a customer ' s first order and all others to returning .
*
* @ param int $customer_id Customer ID .
* @ param int $order_id Order ID .
*/
protected static function set_customer_first_order ( $customer_id , $order_id ) {
global $wpdb ;
$orders_stats_table = $wpdb -> prefix . self :: TABLE_NAME ;
$wpdb -> query (
2019-01-17 02:47:30 +00:00
$wpdb -> prepare (
2019-02-14 03:14:01 +00:00
" UPDATE ${ orders_stats_table } SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d " ,
$order_id ,
$customer_id
2019-01-15 01:53:02 +00:00
)
);
}
/**
* Returns string to be used as cache key for the data .
*
* @ param array $params Query parameters .
* @ return string
*/
protected function get_cache_key ( $params ) {
return 'woocommerce_' . self :: TABLE_NAME . '_stats_' . md5 ( wp_json_encode ( $params ) );
}
}