2022-11-27 15:03:07 +00:00
< ? php
/**
* Order Stats class .
*
* @ package EDD
* @ subpackage Orders
* @ copyright Copyright ( c ) 2018 , Easy Digital Downloads , LLC
* @ license http :// opensource . org / licenses / gpl - 2.0 . php GNU Public License
* @ since 3.0
*/
namespace EDD ;
use EDD\Reports as Reports ;
// Exit if accessed directly
defined ( 'ABSPATH' ) || exit ;
/**
* Stats Class .
*
* @ since 3.0
*/
class Stats {
/**
* Parsed query vars .
*
* @ since 3.0
* @ access protected
* @ var array
*/
protected $query_vars = array ();
/**
* Query var originals . These hold query vars passed to the constructor .
*
* @ since 3.0
* @ access protected
* @ var array
*/
protected $query_var_originals = array ();
/**
* Date ranges .
*
* @ since 3.0
* @ access protected
* @ var array
*/
protected $date_ranges = array ();
/**
* Date ranges used when calculating percentage difference .
*
* @ since 3.0
* @ access protected
* @ var array
*/
protected $relative_date_ranges = array ();
/**
* Constructor .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class . Some methods will not allow parameters to be overridden as it could lead to inaccurate calculations .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* @ type string $function SQL function . Certain methods will only accept certain functions . See each method for
* a list of accepted SQL functions .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*/
public function __construct ( $query = array () ) {
// Start the Reports API.
new Reports\Init ();
// Set date ranges.
$this -> set_date_ranges ();
// Maybe parse query.
if ( ! empty ( $query ) ) {
$this -> parse_query ( $query );
$this -> query_var_originals = $this -> query_vars ;
// Set defaults.
} else {
$this -> query_var_originals = $this -> query_vars = array (
'start' => '' ,
'end' => '' ,
'range' => '' ,
'exclude_taxes' => false ,
'currency' => false ,
'currency_sql' => '' ,
'status' => array (),
'status_sql' => '' ,
'type' => array (),
'type_sql' => '' ,
'where_sql' => '' ,
'date_query_sql' => '' ,
'date_query_column' => '' ,
'column' => '' ,
'table' => '' ,
'function' => 'SUM' ,
'output' => 'raw' ,
'relative' => false ,
'relative_start' => '' ,
'relative_end' => '' ,
'grouped' => false ,
'product_id' => '' ,
'price_id' => null ,
'revenue_type' => 'gross' ,
);
}
}
/**
* Builds a fully qualified amount column and function , given the currency settings ,
* tax settings , and accepted functions .
*
* @ param array $args {
* Optional arguments .
*
* @ type string $column_prefix Column prefix ( table alias or name ) .
* @ type array $accepted_function Accepted functions for this query .
* }
*
* @ return string Example : `SUM( total / rate )`
* @ throws \InvalidArgumentException
*/
private function get_amount_column_and_function ( $args = array () ) {
$args = wp_parse_args ( $args , array (
'column_prefix' => '' ,
'accepted_functions' => array (),
2023-01-18 16:39:57 +00:00
'requested_function' => false ,
'rate' => true ,
2022-11-27 15:03:07 +00:00
) );
$column = $this -> query_vars [ 'column' ];
$column_prefix = '' ;
if ( ! empty ( $args [ 'column_prefix' ] ) ) {
$column_prefix = $args [ 'column_prefix' ] . '.' ;
}
if ( empty ( $column ) ) {
$column = true === $this -> query_vars [ 'exclude_taxes' ] ? " { $column_prefix } total - { $column_prefix } tax " : $column_prefix . 'total' ;
} else {
$column = $column_prefix . $column ;
}
$default_function = is_array ( $args [ 'accepted_functions' ] ) && isset ( $args [ 'accepted_functions' ][ 0 ] ) ? $args [ 'accepted_functions' ][ 0 ] : false ;
$function = ! empty ( $this -> query_vars [ 'function' ] ) ? $this -> query_vars [ 'function' ] : $default_function ;
2023-01-18 16:39:57 +00:00
if ( ! empty ( $args [ 'requested_function' ] ) ) {
$function = $args [ 'requested_function' ];
}
2022-11-27 15:03:07 +00:00
if ( empty ( $function ) ) {
throw new \InvalidArgumentException ( 'Missing select function.' );
}
2023-01-18 16:39:57 +00:00
if ( ! empty ( $args [ 'accepted_functions' ] ) && ! in_array ( strtoupper ( $function ), $args [ 'accepted_functions' ], true ) ) {
2022-11-27 15:03:07 +00:00
if ( ! empty ( $default_function ) ) {
$function = $default_function ;
} else {
throw new \InvalidArgumentException ( sprintf ( 'Invalid function "%s". Must be one of: %s' , $this -> query_vars [ 'function' ], json_encode ( $args [ 'accepted_functions' ] ) ) );
}
}
2023-01-18 16:39:57 +00:00
$function = strtoupper ( $function );
2022-11-27 15:03:07 +00:00
// Multiply by rate if currency conversion is enabled.
if (
! empty ( $args [ 'rate' ] ) &&
in_array ( $function , array ( 'SUM' , 'AVG' ), true ) &&
( empty ( $this -> query_vars [ 'currency' ] ) || 'convert' === $this -> query_vars [ 'currency' ] ) &&
( false !== strpos ( $column , 'total' ) || false !== strpos ( $column , 'tax' ) )
) {
$column = sprintf ( '(%s) / %s' , $column , $column_prefix . 'rate' );
}
return sprintf ( '%s(%s)' , $function , $column );
}
/** Calculation Methods ***************************************************/
/** Orders ***************************************************************/
/**
* Calculate order earnings .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* @ type string $function SQL function . Accepts `SUM` and `AVG` . Default `SUM` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return string Formatted order earnings .
*/
public function get_order_earnings ( $query = array () ) {
$this -> parse_query ( $query );
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
if ( empty ( $this -> query_vars [ 'function' ] ) ) {
$this -> query_vars [ 'function' ] = 'SUM' ;
}
/*
* By default we ' re checking sales only and excluding refunds . This gives us gross order earnings .
* This may be overridden in $query parameters that get passed through .
*/
$this -> query_vars [ 'type' ] = $this -> get_revenue_type_order_types ();
$this -> query_vars [ 'status' ] = edd_get_gross_order_statuses ();
/**
* Filters Order statuses that should be included when calculating stats .
*
* @ since 2.7
*
* @ param array $statuses Order statuses to include when generating stats .
*/
$this -> query_vars [ 'status' ] = array_unique ( apply_filters ( 'edd_payment_stats_post_statuses' , $this -> query_vars [ 'status' ] ) );
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'SUM' , 'AVG' )
) );
$initial_query = " SELECT { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1
{ $this -> query_vars [ 'status_sql' ]}
{ $this -> query_vars [ 'type_sql' ]}
{ $this -> query_vars [ 'currency_sql' ]}
{ $this -> query_vars [ 'where_sql' ]}
{ $this -> query_vars [ 'date_query_sql' ]} " ;
$initial_result = $this -> get_db () -> get_row ( $initial_query );
if ( true === $this -> query_vars [ 'relative' ] ) {
$relative_date_query_sql = $this -> generate_relative_date_query_sql ();
$relative_query = " SELECT { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1
{ $this -> query_vars [ 'status_sql' ]}
{ $this -> query_vars [ 'type_sql' ]}
{ $this -> query_vars [ 'currency_sql' ]}
{ $this -> query_vars [ 'where_sql' ]}
{ $relative_date_query_sql } " ;
$relative_result = $this -> get_db () -> get_row ( $relative_query );
}
$total = null === $initial_result -> total
? 0.00
: ( float ) $initial_result -> total ;
if ( 'array' === $this -> query_vars [ 'output' ] ) {
$output = array (
'value' => $total ,
'relative_data' => ( true === $this -> query_vars [ 'relative' ] ) ? $this -> generate_relative_data ( floatval ( $total ), floatval ( $relative_result -> total ) ) : array (),
);
} else {
if ( true === $this -> query_vars [ 'relative' ] ) {
$output = $this -> generate_relative_markup ( floatval ( $total ), floatval ( $relative_result -> total ) );
} else {
$output = $this -> maybe_format ( $total );
}
}
// Reset query vars.
$this -> post_query ();
return $output ;
}
/**
* Calculate the number of orders .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `COUNT` and `AVG` . Default `COUNT` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return int Number of orders .
*/
public function get_order_count ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
/*
* By default we ' re checking sales only and excluding refunds . This gives us gross order counts .
* This may be overridden in $query parameters that get passed through .
*/
$this -> query_vars [ 'type' ] = 'sale' ;
$this -> query_vars [ 'status' ] = $this -> get_revenue_type_statuses ();
/**
* Filters Order statuses that should be included when calculating stats .
*
* @ since 2.7
*
* @ param array $statuses Order statuses to include when generating stats .
*/
$this -> query_vars [ 'status' ] = apply_filters ( 'edd_payment_stats_post_statuses' , $this -> query_vars [ 'status' ] );
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'COUNT' , 'AVG' )
) );
// First get the 'current' date filter's results.
$initial_query = " SELECT COUNT(id) AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1
{ $this -> query_vars [ 'status_sql' ]}
{ $this -> query_vars [ 'type_sql' ]}
{ $this -> query_vars [ 'currency_sql' ]}
{ $this -> query_vars [ 'where_sql' ]}
{ $this -> query_vars [ 'date_query_sql' ]} " ;
$initial_result = $this -> get_db () -> get_row ( $initial_query );
if ( true === $this -> query_vars [ 'relative' ] ) {
// Now get the relative data.
$relative_date_query_sql = $this -> generate_relative_date_query_sql ();
$relative_query = " SELECT COUNT(id) AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1
{ $this -> query_vars [ 'status_sql' ]}
{ $this -> query_vars [ 'type_sql' ]}
{ $this -> query_vars [ 'currency_sql' ]}
{ $this -> query_vars [ 'where_sql' ]}
{ $relative_date_query_sql } " ;
$relative_result = $this -> get_db () -> get_row ( $relative_query );
}
$total = null === $initial_result
? 0
: absint ( $initial_result -> total );
if ( true === $this -> query_vars [ 'relative' ] ) {
$total = $this -> generate_relative_markup ( absint ( $total ), absint ( $relative_result -> total ) );
}
// Reset query vars.
$this -> post_query ();
return $total ;
}
/**
* Calculate the busiest day of the week for stores .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return string Busiest day of the week .
*/
public function get_busiest_day ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$sql = " SELECT DAYOFWEEK(date_created) AS day, COUNT( { $this -> query_vars [ 'column' ] } ) as total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'currency_sql' ]} { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY day
ORDER BY total DESC
LIMIT 1 " ;
$result = $this -> get_db () -> get_row ( $sql );
$days = array (
__ ( 'Sunday' , 'easy-digital-downloads' ),
__ ( 'Monday' , 'easy-digital-downloads' ),
__ ( 'Tuesday' , 'easy-digital-downloads' ),
__ ( 'Wednesday' , 'easy-digital-downloads' ),
__ ( 'Thursday' , 'easy-digital-downloads' ),
__ ( 'Friday' , 'easy-digital-downloads' ),
__ ( 'Saturday' , 'easy-digital-downloads' ),
);
$day = null === $result
? ''
: $days [ $result -> day - 1 ];
// Reset query vars.
$this -> post_query ();
return $day ;
}
/**
* Calculate number of refunded orders .
*
* @ since 3.0
*
* @ see \EDD\Stats :: get_order_count ()
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `COUNT` and `AVG` . Default `COUNT` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return int Number of refunded orders .
*/
public function get_order_refund_count ( $query = array () ) {
$query [ 'status' ] = isset ( $query [ 'status' ] )
? $query [ 'status' ]
: array ( 'complete' );
if ( ! array ( $query [ 'status' ] ) ) {
$query [ 'status' ] = array ( $query [ 'status' ] );
}
$query [ 'type' ] = array ( 'refund' );
return $this -> get_order_count ( $query );
}
/**
* Calculate number of refunded order items .
*
* @ since 3.0
*
* @ see \EDD\Stats :: get_order_item_count ()
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `COUNT` and `AVG` . Default `COUNT` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return int Number of refunded orders .
*/
public function get_order_item_refund_count ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_order_items ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
if ( empty ( $this -> query_vars [ 'function' ] ) ) {
$this -> query_vars [ 'function' ] = 'COUNT' ;
}
// Base value for status.
$query [ 'status' ] = isset ( $query [ 'status' ] )
? $query [ 'status' ]
: array ( 'refunded' );
/*
* The type should be `sale` because we ' re querying for fully refunded order items only .
* That means we look in `type` = `sale` and `status` = `refunded` .
*/
$this -> query_vars [ 'where_sql' ] .= " AND { $this -> get_db () -> edd_orders } .type = 'sale' " ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'column_prefix' => $this -> query_vars [ 'table' ],
'accepted_functions' => array ( 'COUNT' , 'AVG' )
) );
$product_id = ! empty ( $this -> query_vars [ 'product_id' ] )
? $this -> get_db () -> prepare ( 'AND product_id = %d' , absint ( $this -> query_vars [ 'product_id' ] ) )
: '' ;
$price_id = $this -> generate_price_id_query_sql ();
$currency_sql = str_replace ( $this -> get_db () -> edd_order_items , $this -> get_db () -> edd_orders , $this -> query_vars [ 'currency_sql' ] );
// Calculating an average requires a subquery.
if ( 'AVG' === $this -> query_vars [ 'function' ] ) {
$sql = " SELECT AVG(id) AS total
FROM (
SELECT COUNT ({ $this -> query_vars [ 'table' ]} . id ) AS id
FROM { $this -> query_vars [ 'table' ]}
INNER JOIN { $this -> get_db () -> edd_orders } ON ( { $this -> get_db () -> edd_orders } . id = { $this -> query_vars [ 'table' ]} . order_id )
WHERE 1 = 1 { $product_id } { $price_id } { $this -> query_vars [ 'status_sql' ]} { $currency_sql } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY order_id
) AS counts " ;
} elseif ( true === $this -> query_vars [ 'grouped' ] ) {
$sql = " SELECT { $this -> query_vars [ 'table' ] } .product_id, { $this -> query_vars [ 'table' ] } .price_id, { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
INNER JOIN { $this -> get_db () -> edd_orders } ON ( { $this -> get_db () -> edd_orders } . id = { $this -> query_vars [ 'table' ]} . order_id )
WHERE 1 = 1 { $product_id } { $price_id } { $this -> query_vars [ 'status_sql' ]} { $currency_sql } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY product_id , price_id
ORDER BY total DESC " ;
} else {
$sql = " SELECT { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
INNER JOIN { $this -> get_db () -> edd_orders } ON ( { $this -> get_db () -> edd_orders } . id = { $this -> query_vars [ 'table' ]} . order_id )
WHERE 1 = 1 { $product_id } { $price_id } { $this -> query_vars [ 'status_sql' ]} { $currency_sql } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
}
$result = $this -> get_db () -> get_results ( $sql );
if ( true === $this -> query_vars [ 'grouped' ] ) {
array_walk ( $result , function ( & $value ) {
// Format resultant object.
$value -> product_id = absint ( $value -> product_id );
$value -> price_id = is_numeric ( $value -> price_id ) ? absint ( $value -> price_id ) : null ;
$value -> total = absint ( $value -> total );
// Add instance of EDD_Download to resultant object.
$value -> object = edd_get_download ( $value -> product_id );
} );
} else {
$result = null === $result [ 0 ] -> total
? 0.00
: absint ( $result [ 0 ] -> total );
}
// Reset query vars.
$this -> post_query ();
return $result ;
return $this -> get_order_item_count ( $query );
}
/**
* Calculate total amount for refunded orders .
*
* @ since 3.0
*
* @ see \EDD\Stats :: get_order_earnings ()
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* @ type string $function SQL function . Default `SUM` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return string Formatted amount from refunded orders .
*/
public function get_order_refund_amount ( $query = array () ) {
$this -> parse_query ( $query );
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
if ( empty ( $this -> query_vars [ 'function' ] ) ) {
$this -> query_vars [ 'function' ] = 'SUM' ;
}
/*
* By default we ' re checking refunds only and excluding any other types . This gives us gross refund amounts .
* This may be overridden in $query parameters that get passed through .
*/
$this -> query_vars [ 'type' ] = 'refund' ;
$this -> query_vars [ 'status' ] = array ( 'complete' );
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'SUM' , 'AVG' )
) );
$initial_query = " SELECT { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1
{ $this -> query_vars [ 'status_sql' ]}
{ $this -> query_vars [ 'type_sql' ]}
{ $this -> query_vars [ 'currency_sql' ]}
{ $this -> query_vars [ 'where_sql' ]}
{ $this -> query_vars [ 'date_query_sql' ]} " ;
$initial_result = $this -> get_db () -> get_row ( $initial_query );
if ( true === $this -> query_vars [ 'relative' ] ) {
$relative_date_query_sql = $this -> generate_relative_date_query_sql ();
$relative_query = " SELECT { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1
{ $this -> query_vars [ 'status_sql' ]}
{ $this -> query_vars [ 'type_sql' ]}
{ $this -> query_vars [ 'currency_sql' ]}
{ $this -> query_vars [ 'where_sql' ]}
{ $relative_date_query_sql } " ;
$relative_result = $this -> get_db () -> get_row ( $relative_query );
}
$total = null === $initial_result -> total
? 0.00
: ( float ) $initial_result -> total ;
if ( true === $this -> query_vars [ 'relative' ] ) {
$total = - ( floatval ( $initial_result -> total ) );
$relative = - ( floatval ( $relative_result -> total ) );
$total = $this -> generate_relative_markup ( $total , $relative , true );
} else {
$total = $this -> maybe_format ( - ( $total ) );
}
// Reset query vars.
$this -> post_query ();
return $total ;
}
/**
* Calculate average time for an order to be refunded .
*
* @ since 3.0
*
* @ see \EDD\Stats :: get_order_earnings ()
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `AVG` only . Default `AVG` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return string Average time for an order to be refunded in human readable format .
*/
public function get_average_refund_time ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'column' ] = 'date_completed' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
$type_sql = $this -> get_db () -> prepare ( 'AND o2.type = %s' , esc_sql ( 'refund' ) );
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$sql = " SELECT AVG( TIMESTAMPDIFF( SECOND, { $this -> query_vars [ 'table' ] } . { $this -> query_vars [ 'column' ] } , o2.date_created ) ) AS time_to_refund
FROM { $this -> query_vars [ 'table' ]}
INNER JOIN { $this -> query_vars [ 'table' ]} o2 ON { $this -> query_vars [ 'table' ]} . id = o2 . parent
WHERE 1 = 1 { $type_sql } { $this -> query_vars [ 'currency_sql' ]} { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
$result = $this -> get_db () -> get_var ( $sql );
$time_to_refund = null === $result
? ''
: $result ;
// Beginning of time.
$base = strtotime ( '1970-01-01 00:00:00' );
if ( ! empty ( $time_to_refund ) ) {
$time_to_refund = absint ( $time_to_refund );
$intervals = array ( 'year' , 'month' , 'day' , 'hour' , 'minute' , 'second' );
$diffs = array ();
foreach ( $intervals as $interval ) {
$time = strtotime ( '+1 ' . $interval , $base );
$add = 1 ;
$looped = 0 ;
while ( $time_to_refund >= $time ) {
$add ++ ;
$time = strtotime ( '+' . $add . ' ' . $interval , $base );
$looped ++ ;
}
$base = strtotime ( '+' . $looped . ' ' . $interval , $base );
$diffs [ $interval ] = $looped ;
}
$count = 0 ;
$times = array ();
foreach ( $diffs as $interval => $value ) {
// Keep precision to 2.
if ( $count >= 2 ) {
break ;
}
// Add value and interval if value is bigger than 0.
if ( $value > 0 ) {
$interval = substr ( $interval , 0 , 1 );
// Add value and interval to times array.
$times [] = $value . $interval ;
$count ++ ;
}
}
}
// Reset query vars.
$this -> post_query ();
return empty ( $time_to_refund )
? ''
: implode ( ' ' , $times );
}
/**
* Calculate refund rate .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return float | int Rate of refunded orders .
*/
public function get_refund_rate ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$status_sql = $this -> get_db () -> prepare ( " AND status = %s AND type = '%s' " , esc_sql ( 'complete' ), esc_sql ( 'refund' ) );
$ignore_free = $this -> get_db () -> prepare ( " AND { $this -> query_vars [ 'table' ] } .total > %d " , 0 );
$sql = " SELECT COUNT(id ) / o.number_orders * 100 AS `refund_rate`
FROM { $this -> query_vars [ 'table' ]}
CROSS JOIN (
SELECT COUNT ( id ) AS number_orders
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'currency_sql' ]} { $ignore_free } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
) o
WHERE 1 = 1 { $status_sql } { $this -> query_vars [ 'currency_sql' ]} { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
$result = $this -> get_db () -> get_var ( $sql );
$total = null === $result
? 0
: round ( $result , 2 );
if ( 'formatted' === $this -> query_vars [ 'output' ] ) {
$total .= '%' ;
$total = esc_html ( $total );
}
// Reset query vars.
$this -> post_query ();
return $total ;
}
/** Order Items **********************************************************/
/**
* Calculate order item earnings .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* @ type string $function SQL function . Default `SUM` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type int $product_id Product ID . If empty , an aggregation of the values in the `total` column in the
* `edd_order_items` table will be returned .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return array | float | int Formatted order item earnings .
*/
public function get_order_item_earnings ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_order_items ;
$this -> query_vars [ 'column' ] = true === $this -> query_vars [ 'exclude_taxes' ] ? 'total - tax' : 'total' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
2023-01-18 16:39:57 +00:00
$this -> query_vars [ 'status' ] = edd_get_gross_order_statuses ();
2022-11-27 15:03:07 +00:00
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$product_id = ! empty ( $this -> query_vars [ 'product_id' ] )
2023-01-18 16:39:57 +00:00
? $this -> get_db () -> prepare ( " AND { $this -> query_vars [ 'table' ] } .product_id = %d " , absint ( $this -> query_vars [ 'product_id' ] ) )
2022-11-27 15:03:07 +00:00
: '' ;
$price_id = $this -> generate_price_id_query_sql ();
$region = ! empty ( $this -> query_vars [ 'region' ] )
? $this -> get_db () -> prepare ( 'AND edd_oa.region = %s' , esc_sql ( $this -> query_vars [ 'region' ] ) )
: '' ;
$country = ! empty ( $this -> query_vars [ 'country' ] )
? $this -> get_db () -> prepare ( 'AND edd_oa.country = %s' , esc_sql ( $this -> query_vars [ 'country' ] ) )
: '' ;
2023-01-18 16:39:57 +00:00
$status = ! empty ( $this -> query_vars [ 'status' ] )
? " AND { $this -> query_vars [ 'table' ] } .status IN (' " . implode ( " ', ' " , $this -> query_vars [ 'status' ] ) . " ') "
: '' ;
2022-11-27 15:03:07 +00:00
$join = $currency = '' ;
if ( ! empty ( $country ) || ! empty ( $region ) ) {
$join .= " INNER JOIN { $this -> get_db () -> edd_order_addresses } edd_oa ON { $this -> query_vars [ 'table' ] } .order_id = edd_oa.order_id " ;
}
2023-01-18 16:39:57 +00:00
$join .= " INNER JOIN { $this -> get_db () -> edd_orders } edd_o ON ( { $this -> query_vars [ 'table' ] } .order_id = edd_o.id) AND edd_o.status IN (' " . implode ( " ', ' " , $this -> query_vars [ 'status' ] ) . " ') " ;
2022-11-27 15:03:07 +00:00
if ( ! empty ( $this -> query_vars [ 'currency' ] ) && array_key_exists ( strtoupper ( $this -> query_vars [ 'currency' ] ), edd_get_currencies () ) ) {
$currency = $this -> get_db () -> prepare ( " AND edd_o.currency = %s " , strtoupper ( $this -> query_vars [ 'currency' ] ) );
}
2023-01-18 16:39:57 +00:00
/**
* The adjustments query needs a different order status check than the order items . This is due to the fact that
* adjustments refunded would end up being double counted , and therefore create an inaccurate revenue report .
*/
$adjustments_join = " INNER JOIN { $this -> get_db () -> edd_orders } edd_o ON ( { $this -> query_vars [ 'table' ] } .order_id = edd_o.id) AND edd_o.type = 'sale' AND edd_o.status IN (' " . implode ( " ', ' " , edd_get_net_order_statuses () ) . " ') " ;
/**
* With the addition of including fees into the calcualtion , the order_items
* and order_adjustments for the order items needs to be a SUM and then the final function
* ( SUM or AVG ) needs to be run on the final UNION Query .
*/
$order_item_function = $this -> get_amount_column_and_function ( array (
'column_prefix' => $this -> query_vars [ 'table' ],
'accepted_functions' => array ( 'SUM' , 'AVG' ),
'requested_function' => 'SUM' ,
) );
$order_adjustment_function = $this -> get_amount_column_and_function ( array (
'column_prefix' => 'oadj' ,
'accepted_functions' => array ( 'SUM' , 'AVG' ),
'requested_function' => 'SUM' ,
) );
$union_function = $this -> get_amount_column_and_function ( array (
'column_prefix' => '' ,
'accepted_functions' => array ( 'SUM' , 'AVG' ),
'rate' => false ,
) );
2022-11-27 15:03:07 +00:00
if ( true === $this -> query_vars [ 'grouped' ] ) {
2023-01-18 16:39:57 +00:00
$order_items = " SELECT
{ $this -> query_vars [ 'table' ]} . product_id ,
{ $this -> query_vars [ 'table' ]} . price_id ,
{ $order_item_function } AS total
FROM { $this -> query_vars [ 'table' ]}
{ $join }
WHERE 1 = 1
{ $product_id }
{ $price_id }
{ $region }
{ $country }
{ $currency }
{ $this -> query_vars [ 'where_sql' ]}
{ $this -> query_vars [ 'date_query_sql' ]}
GROUP BY { $this -> query_vars [ 'table' ]} . product_id , { $this -> query_vars [ 'table' ]} . price_id " ;
$order_adjustments = " SELECT
{ $this -> query_vars [ 'table' ]} . product_id as product_id ,
{ $this -> query_vars [ 'table' ]} . price_id as price_id ,
{ $order_adjustment_function } as total
FROM { $this -> get_db () -> edd_order_adjustments } oadj
INNER JOIN { $this -> query_vars [ 'table' ]} ON
({ $this -> query_vars [ 'table' ]} . id = oadj . object_id )
{ $product_id }
{ $price_id }
{ $region }
{ $country }
{ $currency }
{ $adjustments_join }
WHERE oadj . object_type = 'order_item'
AND oadj . type != 'discount'
{ $this -> query_vars [ 'date_query_sql' ]}
GROUP BY { $this -> query_vars [ 'table' ]} . product_id , { $this -> query_vars [ 'table' ]} . price_id " ;
$sql = " SELECT product_id, price_id, { $union_function } AS total
FROM ({ $order_items } UNION { $order_adjustments }) a
GROUP BY product_id , price_id
ORDER BY total DESC " ;
2022-11-27 15:03:07 +00:00
} else {
2023-01-18 16:39:57 +00:00
$order_items = " SELECT
{ $order_item_function } AS total
FROM { $this -> query_vars [ 'table' ]}
{ $join }
WHERE 1 = 1
{ $product_id }
{ $price_id }
{ $region }
{ $country }
{ $currency }
{ $this -> query_vars [ 'where_sql' ]}
{ $this -> query_vars [ 'date_query_sql' ]} " ;
$order_adjustments = " SELECT
{ $order_adjustment_function } as total
FROM { $this -> get_db () -> edd_order_adjustments } oadj
INNER JOIN { $this -> query_vars [ 'table' ]} ON
({ $this -> query_vars [ 'table' ]} . id = oadj . object_id )
{ $product_id }
{ $price_id }
{ $region }
{ $country }
{ $currency }
{ $adjustments_join }
WHERE oadj . object_type = 'order_item'
AND oadj . type != 'discount'
{ $this -> query_vars [ 'date_query_sql' ]} " ;
$sql = " SELECT { $union_function } AS total FROM ( { $order_items } UNION { $order_adjustments } )a " ;
2022-11-27 15:03:07 +00:00
}
$result = $this -> get_db () -> get_results ( $sql );
if ( true === $this -> query_vars [ 'grouped' ] ) {
array_walk ( $result , function ( & $value ) {
// Format resultant object.
$value -> product_id = absint ( $value -> product_id );
$value -> price_id = is_numeric ( $value -> price_id ) ? absint ( $value -> price_id ) : null ;
$value -> total = $this -> maybe_format ( $value -> total );
// Add instance of EDD_Download to resultant object.
$value -> object = edd_get_download ( $value -> product_id );
} );
} else {
$result = null === $result [ 0 ] -> total
? $this -> maybe_format ( 0.00 )
: $this -> maybe_format ( floatval ( $result [ 0 ] -> total ) );
}
// Reset query vars.
$this -> post_query ();
return $result ;
}
/**
* Calculate the number of times a specific item has been purchased .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `COUNT` and `AVG` . Default `COUNT` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type int $product_id Product ID . If empty , an aggregation of the values in the `total` column in the
* `edd_order_items` table will be returned .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return array | int Number of times a specific item has been purchased .
*/
public function get_order_item_count ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_order_items ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
$this -> query_vars [ 'status' ] = array ( 'complete' , 'partially_refunded' );
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'column_prefix' => $this -> query_vars [ 'table' ],
'accepted_functions' => array ( 'COUNT' , 'AVG' ),
) );
$product_id = ! empty ( $this -> query_vars [ 'product_id' ] )
? $this -> get_db () -> prepare ( 'AND product_id = %d' , absint ( $this -> query_vars [ 'product_id' ] ) )
: '' ;
$price_id = $this -> generate_price_id_query_sql ();
$region = ! empty ( $this -> query_vars [ 'region' ] )
? $this -> get_db () -> prepare ( 'AND edd_oa.region = %s' , esc_sql ( $this -> query_vars [ 'region' ] ) )
: '' ;
$country = ! empty ( $this -> query_vars [ 'country' ] )
? $this -> get_db () -> prepare ( 'AND edd_oa.country = %s' , esc_sql ( $this -> query_vars [ 'country' ] ) )
: '' ;
$statuses = edd_get_net_order_statuses ();
$status_string = $this -> get_placeholder_string ( $statuses );
$join = $this -> get_db () -> prepare (
" INNER JOIN { $this -> get_db () -> edd_orders } edd_o ON ( { $this -> query_vars [ 'table' ] } .order_id = edd_o.id) AND edd_o.status IN( { $status_string } ) AND edd_o.type = 'sale' " ,
... $statuses
);
$currency = '' ;
if ( ! empty ( $country ) || ! empty ( $region ) ) {
$join .= " INNER JOIN { $this -> get_db () -> edd_order_addresses } edd_oa ON { $this -> query_vars [ 'table' ] } .order_id = edd_oa.order_id " ;
}
if ( ! empty ( $this -> query_vars [ 'currency' ] ) && array_key_exists ( strtoupper ( $this -> query_vars [ 'currency' ] ), edd_get_currencies () ) ) {
$currency = $this -> get_db () -> prepare ( " AND edd_o.currency = %s " , strtoupper ( $this -> query_vars [ 'currency' ] ) );
}
// Calculating an average requires a subquery.
if ( 'AVG' === $this -> query_vars [ 'function' ] ) {
$sql = " SELECT AVG(id) AS total
FROM (
SELECT COUNT ({ $this -> query_vars [ 'table' ]} . id ) AS id
FROM { $this -> query_vars [ 'table' ]}
{ $join }
WHERE 1 = 1 { $product_id } { $price_id } { $region } { $country } { $currency } { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY order_id
) AS counts " ;
} elseif ( true === $this -> query_vars [ 'grouped' ] ) {
$sql = " SELECT product_id, price_id, { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
{ $join }
WHERE 1 = 1 { $product_id } { $price_id } { $region } { $country } { $currency } { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY product_id , price_id
ORDER BY total DESC " ;
} else {
$sql = " SELECT { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
{ $join }
WHERE 1 = 1 { $product_id } { $price_id } { $region } { $country } { $currency } { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
}
$result = $this -> get_db () -> get_results ( $sql );
if ( true === $this -> query_vars [ 'grouped' ] ) {
array_walk ( $result , function ( & $value ) {
// Format resultant object.
$value -> product_id = absint ( $value -> product_id );
$value -> price_id = is_numeric ( $value -> price_id ) ? absint ( $value -> price_id ) : null ;
$value -> total = absint ( $value -> total );
// Add instance of EDD_Download to resultant object.
$value -> object = edd_get_download ( $value -> product_id );
} );
} else {
$result = null === $result [ 0 ] -> total
? 0
: absint ( $result [ 0 ] -> total );
}
// Reset query vars.
$this -> post_query ();
return $result ;
}
/**
* Calculate most valuable order items .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended to the
* query .
* @ type int $number Number of order items to fetch . Default 1.
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return array Array of objects with most valuable order items . Each object has the product ID , total earnings ,
* and an instance of EDD_Download .
*/
public function get_most_valuable_order_items ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_order_items ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
$this -> query_vars [ 'exclude_taxes' ] = true ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
// By default, the most valuable customer is returned.
$number = isset ( $this -> query_vars [ 'number' ] )
? absint ( $this -> query_vars [ 'number' ] )
: 1 ;
$function = $this -> get_amount_column_and_function ( array (
'column_prefix' => $this -> query_vars [ 'table' ],
'accepted_functions' => array ( 'SUM' )
) );
$statuses = edd_get_net_order_statuses ();
$status_string = $this -> get_placeholder_string ( $statuses );
$where = $this -> get_db () -> prepare (
" AND { $this -> get_db () -> edd_order_items } .status IN('complete','partially_refunded')
AND { $this -> get_db () -> edd_orders } . status IN ({ $status_string }) " ,
... $statuses
);
if ( ! empty ( $this -> query_vars [ 'currency' ] ) && array_key_exists ( strtoupper ( $this -> query_vars [ 'currency' ] ), edd_get_currencies () ) ) {
$where .= $this -> get_db () -> prepare (
" AND { $this -> get_db () -> edd_orders } .currency = %s " ,
strtoupper ( $this -> query_vars [ 'currency' ] )
);
}
$sql = " SELECT product_id, price_id, { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
INNER JOIN { $this -> get_db () -> edd_orders } ON ({ $this -> get_db () -> edd_orders } . id = { $this -> query_vars [ 'table' ]} . order_id )
WHERE 1 = 1 { $where } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY product_id , price_id
ORDER BY total DESC
LIMIT { $number } " ;
$result = $this -> get_db () -> get_results ( $sql );
array_walk ( $result , function ( & $value ) {
// Format resultant object.
$value -> product_id = absint ( $value -> product_id );
$value -> price_id = is_numeric ( $value -> price_id ) ? absint ( $value -> price_id ) : null ;
$download_model = new \EDD\Models\Download (
$value -> product_id ,
$value -> price_id ,
array (
'start' => $this -> query_vars [ 'start' ],
'end' => $this -> query_vars [ 'end' ],
)
);
$value -> sales = absint ( $download_model -> get_net_sales () );
$value -> total = $this -> maybe_format ( $download_model -> get_net_earnings () );
// Add instance of EDD_Download to resultant object.
$value -> object = edd_get_download ( $value -> product_id );
} );
// Reset query vars.
$this -> post_query ();
return $result ;
}
/** Discounts ************************************************************/
/**
* Calculate the usage count of discount codes .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $discount_code Discount code to fetch the usage count for .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return int Number of times a discount code has been used .
*/
public function get_discount_usage_count ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_order_adjustments ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$discount_code = isset ( $this -> query_vars [ 'discount_code' ] )
? $this -> get_db () -> prepare ( 'AND type = %s AND description = %s' , 'discount' , sanitize_text_field ( $this -> query_vars [ 'discount_code' ] ) )
: $this -> get_db () -> prepare ( 'AND type = %s' , 'discount' );
$sql = " SELECT COUNT( { $this -> query_vars [ 'column' ] } )
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $discount_code } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
$result = $this -> get_db () -> get_var ( $sql );
$total = null === $result
? 0
: absint ( $result );
// Reset query vars.
$this -> post_query ();
return $total ;
}
/**
* Retrieve the most popular discount code .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $discount_code Discount code to fetch the usage count for .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return array Most popular discounts with usage count .
*/
public function get_most_popular_discounts ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_order_adjustments ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
// By default, the most valuable discount is returned.
$number = isset ( $this -> query_vars [ 'number' ] )
? absint ( $this -> query_vars [ 'number' ] )
: 1 ;
$discount = $this -> get_db () -> prepare ( 'AND type = %s' , 'discount' );
$sql = " SELECT description AS code, COUNT( { $this -> query_vars [ 'column' ] } ) AS count
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $discount } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY description
ORDER BY count DESC
LIMIT { $number } " ;
$result = $this -> get_db () -> get_results ( $sql );
array_walk ( $result , function ( & $value ) {
// Add instance of EDD_Discount to resultant object.
$value -> object = edd_get_discount_by_code ( $value -> code );
// Format resultant object.
if ( ! empty ( $value -> object ) ) {
$value -> discount_id = absint ( $value -> object -> id );
$value -> count = absint ( $value -> count );
} else {
$value -> discount_id = 0 ;
$value -> count = '—' ;
}
} );
// Reset query vars.
$this -> post_query ();
return $result ;
}
/**
* Calculate the savings from using a discount code .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $discount_code Discount code to fetch the savings amount for . Default empty . If empty , the amount
* saved from using any discount will be returned .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return float Savings from using a discount code .
*/
public function get_discount_savings ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_order_adjustments ;
$this -> query_vars [ 'column' ] = true === $this -> query_vars [ 'exclude_taxes' ] ? 'total - tax' : 'total' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'SUM' )
) );
$discount_code = ! empty ( $this -> query_vars [ 'discount_code' ] )
? $this -> get_db () -> prepare ( 'AND type = %s AND description = %s' , 'discount' , sanitize_text_field ( $this -> query_vars [ 'discount_code' ] ) )
: $this -> get_db () -> prepare ( 'AND type = %s' , 'discount' );
$sql = " SELECT { $function }
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $discount_code } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
$result = $this -> get_db () -> get_var ( $sql );
$total = null === $result
? 0.00
: floatval ( $result );
$total = $this -> maybe_format ( $total );
// Reset query vars.
$this -> post_query ();
return $total ;
}
/**
* Calculate the average discount amount applied to an order .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return float Average discount amount applied to an order .
*/
public function get_average_discount_amount ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_order_adjustments ;
$this -> query_vars [ 'column' ] = 'total' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'AVG' )
) );
$type_discount = $this -> get_db () -> prepare ( 'AND type = %s' , 'discount' );
$sql = " SELECT { $function }
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $type_discount } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
$result = $this -> get_db () -> get_var ( $sql );
$total = null === $result
? 0.00
: floatval ( $result );
$total = $this -> maybe_format ( $total );
// Reset query vars.
$this -> post_query ();
return $total ;
}
/**
* Calculate the ratio of discounted to non - discounted orders .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return string Ratio of discounted to non - discounted orders . Format is A : B where A and B are integers .
*/
public function get_ratio_of_discounted_orders ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$sql = " SELECT COUNT(id) AS total, o.discounted_orders
FROM { $this -> query_vars [ 'table' ]}
CROSS JOIN (
SELECT COUNT ( id ) AS discounted_orders
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} AND discount > 0 { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
) o
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
$result = $this -> get_db () -> get_row ( $sql );
// No need to calculate the ratio if there are no orders.
if ( 0 === ( int ) $result -> discounted_orders || 0 === ( int ) $result -> total ) {
return 0 ;
}
// Calculate GCD.
$result -> total = absint ( $result -> total );
$result -> discounted_orders = absint ( $result -> discounted_orders );
$original_result = clone $result ;
while ( 0 !== $result -> total ) {
$remainder = $result -> discounted_orders % $result -> total ;
$result -> discounted_orders = $result -> total ;
$result -> total = $remainder ;
}
$ratio = absint ( $result -> discounted_orders );
// Reset query vars.
$this -> post_query ();
// Return the formatted ratio.
return ( $original_result -> discounted_orders / $ratio ) . ':' . ( $original_result -> total / $ratio );
}
/** Gateways *************************************************************/
/**
* Perform gateway calculations based on data passed .
*
* @ internal This method must remain `private` , it exists to reduce duplicated code .
*
* @ since 3.0
* @ access private
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `COUNT` , `AVG` , and `SUM` . Default `COUNT` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $gateway Gateway name . This is checked against a list of registered payment gateways .
* If a gateway is not passed , a list of objects are returned for each gateway and the
* number of orders processed with that gateway .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return mixed array | int | float Either a list of payment gateways and counts or just a single value .
*/
private function get_gateway_data ( $query = array () ) {
$query = wp_parse_args ( $query , array (
'type' => 'sale' ,
'status' => edd_get_gross_order_statuses (),
) );
$this -> parse_query ( $query );
// Set up default values.
$gateways = edd_get_payment_gateways ();
$defaults = array ();
// Set up an object for each gateway.
foreach ( $gateways as $id => $data ) {
$object = new \stdClass ();
$object -> gateway = $id ;
$object -> total = 0 ;
$defaults [] = $object ;
}
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'COUNT' , 'AVG' , 'SUM' )
) );
$gateway = ! empty ( $this -> query_vars [ 'gateway' ] )
? $this -> get_db () -> prepare ( 'AND gateway = %s' , sanitize_text_field ( $this -> query_vars [ 'gateway' ] ) )
: '' ;
$sql = " SELECT gateway, { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'type_sql' ]} { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'currency_sql' ]} { $gateway } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY gateway " ;
$result = $this -> get_db () -> get_results ( $sql );
// Ensure count values are always valid integers if counting sales.
if ( 'COUNT' === $this -> query_vars [ 'function' ] ) {
array_walk ( $result , function ( & $value ) {
$value -> total = absint ( $value -> total );
} );
} elseif ( 'SUM' === $this -> query_vars [ 'function' ] || 'AVG' === $this -> query_vars [ 'function' ] ) {
array_walk ( $result , function ( & $value ) {
$value -> total = floatval ( abs ( $value -> total ) );
} );
}
if ( empty ( $gateway ) && true === $this -> query_vars [ 'grouped' ] ) {
$results = array ();
// Merge defaults with values returned from the database.
foreach ( $defaults as $key => $value ) {
// Filter based on gateway.
$filter = wp_filter_object_list ( $result , array ( 'gateway' => $value -> gateway ) );
$filter = ! empty ( $filter )
? array_values ( $filter )
: array ();
if ( ! empty ( $filter ) ) {
$results [] = $filter [ 0 ];
} else {
$results [] = $defaults [ $key ];
}
}
} elseif ( false === $this -> query_vars [ 'grouped' ] ) {
$total = 0 ;
array_walk ( $result , function ( $value ) use ( & $total ) {
$total += $value -> total ;
} );
$results = 'COUNT' === $this -> query_vars [ 'function' ]
? absint ( $total )
: $this -> maybe_format ( $total );
}
if ( ! empty ( $gateway ) && true === $this -> query_vars [ 'grouped' ] ) {
// Filter based on gateway if passed.
$filter = wp_filter_object_list ( $result , array ( 'gateway' => $this -> query_vars [ 'gateway' ] ) );
$results = 'COUNT' === $this -> query_vars [ 'function' ]
? absint ( $filter [ 0 ] -> total )
: $this -> maybe_format ( $filter [ 0 ] -> total );
}
// Reset query vars.
$this -> post_query ();
// Return array of objects with gateway name and count.
return $results ;
}
/**
* Calculate the number of processed by a gateway .
*
* @ since 3.0
*
* @ see \EDD\Stats :: get_gateway_data ()
*
* @ param array $query See \EDD\Stats :: get_gateway_data () .
*
* @ return int | array List of objects containing the number of sales processed either for every gateway or the gateway
* passed as a query parameter .
*/
public function get_gateway_sales ( $query = array () ) {
$query [ 'column' ] = 'id' ;
$query [ 'function' ] = 'COUNT' ;
// Dispatch to \EDD\Stats::get_gateway_data().
return $this -> get_gateway_data ( $query );
}
/**
* Calculate the total order amount of processed by a gateway .
*
* @ since 3.0
*
* @ see \EDD\Stats :: get_gateway_data ()
*
* @ param array $query See \EDD\Stats :: get_gateway_data () .
*
* @ return array List of objects containing the amount processed either for every gateway or the gateway
* passed as a query parameter .
*/
public function get_gateway_earnings ( $query = array () ) {
// Summation is required as we are returning earnings.
$query [ 'function' ] = isset ( $query [ 'function' ] )
? $query [ 'function' ]
: 'SUM' ;
// Dispatch to \EDD\Stats::get_gateway_data().
$result = $this -> get_gateway_data ( $query );
// Rename object var.
if ( is_array ( $result ) ) {
array_walk ( $result , function ( & $value ) {
$value -> earnings = $value -> total ;
$value -> earnings = $this -> maybe_format ( $value -> earnings );
unset ( $value -> total );
} );
} else {
$result = $this -> maybe_format ( $result );
}
// Reset query vars.
$this -> post_query ();
// Return array of objects with gateway name and earnings.
return $result ;
}
/**
* Calculate the amount for refunded orders processed by a gateway .
*
* @ since 3.0
*
* @ see \EDD\Stats :: get_gateway_earnings ()
*
* @ param array $query See \EDD\Stats :: get_gateway_earnings () .
*
* @ return array List of objects containing the amount for refunded orders processed either for every
* gateway or the gateway passed as a query parameter .
*/
public function get_gateway_refund_amount ( $query = array () ) {
// Ensure orders are refunded.
$this -> query_vars [ 'where_sql' ] = $this -> get_db () -> prepare ( 'AND status = %s' , 'refunded' );
// Dispatch to \EDD\Stats::get_gateway_data().
$result = $this -> get_gateway_earnings ( $query );
// Reset query vars.
$this -> post_query ();
// Return array of objects with gateway name and amount from refunded orders.
return $result ;
}
/**
* Calculate the average order amount of processed by a gateway .
*
* @ since 3.0
*
* @ see \EDD\Stats :: get_gateway_data ()
*
* @ param array $query See \EDD\Stats :: get_gateway_data () .
*
* @ return array List of objects containing the average order value processed either for every gateway
* pr the gateway passed as a query parameter .
*/
public function get_gateway_average_value ( $query = array () ) {
// Function needs to be `AVG`.
$query [ 'function' ] = 'AVG' ;
// Dispatch to \EDD\Stats::get_gateway_data().
$result = $this -> get_gateway_data ( $query );
// Rename object var.
array_walk ( $result , function ( & $value ) {
$value -> earnings = $value -> count ;
$value -> earnings = $this -> maybe_format ( $value -> earnings );
unset ( $value -> count );
} );
// Reset query vars.
$this -> post_query ();
// Return array of objects with gateway name and earnings.
return $result ;
}
/** Tax ******************************************************************/
/**
* Calculate total tax collected .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `SUM` and `AVG` . Default `SUM` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return string Formatted amount of total tax collected .
*/
public function get_tax ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'column' ] = 'tax' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'SUM' , 'AVG' )
) );
$product_id = ! empty ( $this -> query_vars [ 'download_id' ] )
? $this -> get_db () -> prepare ( 'AND product_id = %d' , absint ( $this -> query_vars [ 'download_id' ] ) )
: '' ;
$price_id = $this -> generate_price_id_query_sql ();
if ( true === $this -> query_vars [ 'relative' ] ) {
$relative_date_query_sql = $this -> generate_relative_date_query_sql ();
$sql = " SELECT IFNULL( { $function } , 0) AS total, IFNULL(relative, 0) AS relative
FROM { $this -> query_vars [ 'table' ]}
CROSS JOIN (
SELECT IFNULL ({ $function }, 0 ) AS relative
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'currency_sql' ]} { $this -> query_vars [ 'where_sql' ]} { $relative_date_query_sql }
) o
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'currency_sql' ]} { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
} elseif ( ! empty ( $product_id ) || ! empty ( $price_id ) ) {
// Regenerate SQL clauses due to alias.
$table = $this -> query_vars [ 'table' ];
$this -> query_vars [ 'table' ] = 'o' ;
$this -> pre_query ( $query );
$this -> query_vars [ 'table' ] = $table ;
$function = $this -> get_amount_column_and_function ( array (
'column_prefix' => 'oi' ,
'accepted_functions' => array ( 'SUM' , 'AVG' )
) );
$sql = " SELECT { $function } AS total
FROM { $this -> query_vars [ 'table' ]} o
INNER JOIN { $this -> get_db () -> edd_order_items } oi ON o . id = oi . order_id
WHERE 1 = 1 { $product_id } { $price_id } { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'currency_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
$this -> pre_query ( $query );
} else {
$sql = " SELECT { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'currency_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
}
$result = $this -> get_db () -> get_row ( $sql );
$total = null === $result -> total
? 0.00
: ( float ) $result -> total ;
if ( true === $this -> query_vars [ 'relative' ] ) {
$total = floatval ( $result -> total );
$relative = floatval ( $result -> relative );
$total = $this -> generate_relative_markup ( $total , $relative );
} else {
$total = $this -> maybe_format ( $total );
}
// Reset query vars.
$this -> post_query ();
return $total ;
}
/**
* Calculate total tax collected for country and state passed .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Default `COUNT` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $country Country name . Defaults to store ' s base country .
* @ type string $region Region name . Defaults to store ' s base state .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return string Formatted amount of total tax collected for country and state passed .
*/
public function get_tax_by_location ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'column' ] = 'tax' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'column_prefix' => $this -> query_vars [ 'table' ],
'accepted_functions' => array ( 'SUM' , 'AVG' )
) );
$region = ! empty ( $this -> query_vars [ 'region' ] )
? $this -> get_db () -> prepare ( 'AND oa.region = %s' , esc_sql ( $this -> query_vars [ 'region' ] ) )
: '' ;
$country = ! empty ( $this -> query_vars [ 'country' ] )
? $this -> get_db () -> prepare ( 'AND oa.country = %s' , esc_sql ( $this -> query_vars [ 'country' ] ) )
: '' ;
$product_id = ! empty ( $this -> query_vars [ 'download_id' ] )
? $this -> get_db () -> prepare ( 'AND oi.product_id = %d' , absint ( $this -> query_vars [ 'download_id' ] ) )
: '' ;
$price_id = ! is_null ( $this -> query_vars [ 'price_id' ] ) && is_numeric ( $this -> query_vars [ 'price_id' ] )
? $this -> get_db () -> prepare ( 'AND oi.price_id = %d' , absint ( $this -> query_vars [ 'price_id' ] ) )
: '' ;
$join = ! empty ( $product_id )
? " INNER JOIN { $this -> get_db () -> edd_order_items } oi ON { $this -> query_vars [ 'table' ] } .id = oi.order_id "
: '' ;
// Re-parse function to fetch tax from the order items table.
if ( ! empty ( $product_id ) && 'tax' === $this -> query_vars [ 'column' ] ) {
$function = $this -> get_amount_column_and_function ( array (
'column_prefix' => 'oi' ,
'accepted_functions' => array ( 'SUM' , 'AVG' )
) );
}
$sql = " SELECT { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
INNER JOIN { $this -> get_db () -> edd_order_addresses } oa ON { $this -> query_vars [ 'table' ]} . id = oa . order_id
{ $join }
WHERE 1 = 1 { $region } { $country } { $product_id } { $price_id } { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'currency_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
$result = $this -> get_db () -> get_row ( $sql );
$total = null === $result -> total
? 0.00
: ( float ) $result -> total ;
$total = $this -> maybe_format ( $total );
// Reset query vars.
$this -> post_query ();
return $total ;
}
/** Customers ************************************************************/
/**
* Calculate the number of customers .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return int Number of customers .
*/
public function get_customer_count ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_customers ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
if ( true === $this -> query_vars [ 'relative' ] ) {
$relative_date_query_sql = $this -> generate_relative_date_query_sql ();
$sql = " SELECT IFNULL(COUNT(id), 0) AS total, IFNULL(relative, 0) AS relative
FROM { $this -> query_vars [ 'table' ]}
CROSS JOIN (
SELECT IFNULL ( COUNT ( id ), 0 ) AS relative
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'where_sql' ]} { $relative_date_query_sql }
) o
WHERE 1 = 1 { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
} else {
$sql = " SELECT COUNT(id) AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'date_query_sql' ]} " ;
}
$result = $this -> get_db () -> get_row ( $sql );
$total = null === $result -> total
? 0
: absint ( $result -> total );
if ( 'array' === $this -> query_vars [ 'output' ] ) {
$output = array (
'value' => $total ,
'relative_data' => ( true === $this -> query_vars [ 'relative' ] ) ? $this -> generate_relative_data ( absint ( $result -> total ), absint ( $result -> relative ) ) : array (),
);
} else {
if ( true === $this -> query_vars [ 'relative' ] ) {
$output = $this -> generate_relative_markup ( absint ( $result -> total ), absint ( $result -> relative ) );
} else {
$output = $this -> maybe_format ( $total );
}
}
// Reset query vars.
$this -> post_query ();
return $output ;
}
/**
* Calculate the lifetime value of a customer .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* @ type string $function SQL function . Accepts `AVG` and `SUM` . Default `SUM` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type int $customer_id Customer ID . Default empty .
* @ type int $user_id User ID . Default empty .
* @ type string $email Email address .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return string Formatted lifetime value of a customer .
*/
public function get_customer_lifetime_value ( $query = array () ) {
$this -> parse_query ( $query );
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'column' ] = 'total' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'SUM' , 'AVG' )
) );
$user = isset ( $this -> query_vars [ 'user_id' ] )
? $this -> get_db () -> prepare ( 'AND user_id = %d' , absint ( $this -> query_vars [ 'user_id' ] ) )
: '' ;
$customer = isset ( $this -> query_vars [ 'customer' ] )
? $this -> get_db () -> prepare ( 'AND customer_id = %d' , absint ( $this -> query_vars [ 'customer' ] ) )
: '' ;
$email = isset ( $this -> query_vars [ 'email' ] )
? $this -> get_db () -> prepare ( 'AND email = %s' , absint ( $this -> query_vars [ 'email' ] ) )
: '' ;
$function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'SUM' , 'AVG' ),
'rate' => false
) );
$inner_function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'SUM' )
) );
$sql = " SELECT { $function } AS total
FROM (
SELECT { $inner_function } AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'currency_sql' ]} { $user } { $customer } { $email } { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY customer_id
) o " ;
$result = $this -> get_db () -> get_row ( $sql );
$total = null === $result -> total
? 0.00
: ( float ) $result -> total ;
$total = $this -> maybe_format ( $total );
// Reset query vars.
$this -> post_query ();
return $total ;
}
/**
* Calculate the number of orders made by a customer .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `AVG` and `SUM` . Default `SUM` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type int $customer_id Customer ID . Default empty .
* @ type int $user_id User ID . Default empty .
* @ type string $email Email address .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return int Number of orders made by a customer .
*/
public function get_customer_order_count ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$function = $this -> get_amount_column_and_function ( array (
'accepted_functions' => array ( 'COUNT' , 'AVG' )
) );
$user = isset ( $this -> query_vars [ 'user_id' ] )
? $this -> get_db () -> prepare ( 'AND user_id = %d' , absint ( $this -> query_vars [ 'user_id' ] ) )
: '' ;
$customer = isset ( $this -> query_vars [ 'customer' ] )
? $this -> get_db () -> prepare ( 'AND customer_id = %d' , absint ( $this -> query_vars [ 'customer' ] ) )
: '' ;
$email = isset ( $this -> query_vars [ 'email' ] )
? $this -> get_db () -> prepare ( 'AND email = %s' , sanitize_email ( $this -> query_vars [ 'email' ] ) )
: '' ;
if ( true === $this -> query_vars [ 'relative' ] ) {
$relative_date_query_sql = $this -> generate_relative_date_query_sql ();
if ( 'AVG(id)' === $function ) {
$sql = " SELECT COUNT(id) / COUNT(DISTINCT customer_id) AS total, IFNULL(relative, 0) AS relative
FROM { $this -> query_vars [ 'table' ]}
CROSS JOIN (
SELECT COUNT ( id ) / COUNT ( DISTINCT customer_id ) AS relative
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $user } { $customer } { $email } { $this -> query_vars [ 'where_sql' ]} { $relative_date_query_sql }
) o
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $user } { $customer } { $email } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
} else {
$sql = " SELECT COUNT(id) AS total, IFNULL(relative, 0) AS relative
FROM { $this -> query_vars [ 'table' ]}
CROSS JOIN (
SELECT COUNT ( id ), IFNULL ( relative , 0 ) AS relative
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $user } { $customer } { $email } { $this -> query_vars [ 'where_sql' ]} { $relative_date_query_sql }
) o
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $user } { $customer } { $email } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
}
} else {
if ( 'AVG(id)' === $function ) {
$sql = " SELECT COUNT(id) / COUNT(DISTINCT customer_id) AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $user } { $customer } { $email } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
} else {
$sql = " SELECT COUNT(id) as total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $user } { $customer } { $email } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
}
}
$result = $this -> get_db () -> get_row ( $sql );
$total = null === $result
? 0
: absint ( $result -> total );
if ( true === $this -> query_vars [ 'relative' ] ) {
$total = absint ( $result -> total );
$relative = absint ( $result -> relative );
$total = $this -> generate_relative_markup ( $total , $relative );
} else {
$total = $this -> maybe_format ( $total );
}
// Reset query vars.
$this -> post_query ();
return $total ;
}
/**
* Calculate the average age of a customer .
*
* @ since 3.0
*
* @ see \EDD\Stats :: get_order_count ()
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return int | float Average age of a customer .
*/
public function get_customer_age ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_customers ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$sql = " SELECT AVG(DATEDIFF(NOW(), date_created))
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'date_query_sql' ]} " ;
$result = $this -> get_db () -> get_var ( $sql );
// Reset query vars.
$this -> post_query ();
return null === $result
? 0
: round ( $result , 2 );
}
/**
* Calculate the most valuable customers .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type bool $exclude_taxes If taxes should be excluded from calculations . Default `false` .
* @ type string $function This method does not allow any SQL functions to be passed .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type int $number Number of customers to fetch . Default 1.
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return array Array of objects with most valuable customers . Each object has the customer ID , total amount spent
* by that customer and an instance of EDD_Customer .
*/
public function get_most_valuable_customers ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_orders ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
// By default, the most valuable customer is returned.
$number = isset ( $this -> query_vars [ 'number' ] )
? absint ( $this -> query_vars [ 'number' ] )
: 1 ;
$column = true === $this -> query_vars [ 'exclude_taxes' ]
? 'total - tax'
: 'total' ;
$sql = " SELECT customer_id, SUM( { $column } ) AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'status_sql' ]} { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY customer_id
ORDER BY total DESC
LIMIT { $number } " ;
$result = $this -> get_db () -> get_results ( $sql );
array_walk ( $result , function ( & $value ) {
// Format resultant object.
$value -> customer_id = absint ( $value -> customer_id );
$value -> total = $this -> maybe_format ( $value -> total );
// Add instance of EDD_Download to resultant object.
$value -> object = edd_get_customer ( $value -> customer_id );
} );
// Reset query vars.
$this -> post_query ();
return $result ;
}
/** File Downloads *******************************************************/
/**
* Calculate the number of file downloads .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `COUNT` and `AVG` . Default `COUNT` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return int Number of file downloads .
*/
public function get_file_download_count ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_logs_file_downloads ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
// Only `COUNT` and `AVG` are accepted by this method.
$accepted_functions = array ( 'COUNT' , 'AVG' );
$function = isset ( $this -> query_vars [ 'function' ] ) && in_array ( strtoupper ( $this -> query_vars [ 'function' ] ), $accepted_functions , true )
? $this -> query_vars [ 'function' ] . " ( { $this -> query_vars [ 'column' ] } ) "
: 'COUNT(id)' ;
$product_id = ! empty ( $this -> query_vars [ 'download_id' ] )
? $this -> get_db () -> prepare ( 'AND product_id = %d' , absint ( $this -> query_vars [ 'download_id' ] ) )
: '' ;
$price_id = $this -> generate_price_id_query_sql ();
if ( true === $this -> query_vars [ 'relative' ] ) {
$relative_date_query_sql = $this -> generate_relative_date_query_sql ();
$sql = " SELECT IFNULL( { $function } , 0) AS total, IFNULL(relative, 0) AS relative
FROM { $this -> query_vars [ 'table' ]}
CROSS JOIN (
SELECT IFNULL ({ $function }, 0 ) AS relative
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'where_sql' ]} { $relative_date_query_sql }
) o
WHERE 1 = 1 { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]} " ;
} else {
$sql = " SELECT { $function } AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $product_id } { $price_id } { $this -> query_vars [ 'date_query_sql' ]} " ;
}
$result = $this -> get_db () -> get_row ( $sql );
$total = null === $result -> total
? 0
: absint ( $result -> total );
if ( true === $this -> query_vars [ 'relative' ] ) {
$total = absint ( $result -> total );
$relative = absint ( $result -> relative );
$total = $this -> generate_relative_markup ( $total , $relative );
} else {
$total = $this -> maybe_format ( $total );
}
// Reset query vars.
$this -> post_query ();
return $total ;
}
/**
* Calculate most downloaded products .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `COUNT` and `AVG` . Default `COUNT` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return array Array of objects with most valuable order items . Each object has the product ID , number of downloads ,
* and an instance of EDD_Download .
*/
public function get_most_downloaded_products ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_logs_file_downloads ;
$this -> query_vars [ 'column' ] = 'id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
// By default, the most valuable customer is returned.
$number = isset ( $this -> query_vars [ 'number' ] )
? absint ( $this -> query_vars [ 'number' ] )
: 1 ;
$sql = " SELECT product_id, file_id, COUNT(id) AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY product_id
ORDER BY total DESC
LIMIT { $number } " ;
$result = $this -> get_db () -> get_results ( $sql );
array_walk ( $result , function ( & $value ) {
// Format resultant object.
$value -> product_id = absint ( $value -> product_id );
$value -> file_id = absint ( $value -> file_id );
$value -> total = absint ( $value -> total );
// Add instance of EDD_Download to resultant object.
$value -> object = edd_get_download ( $value -> product_id );
} );
// Reset query vars.
$this -> post_query ();
return $result ;
}
/**
* Calculate average number of file downloads .
*
* @ since 3.0
*
* @ param array $query {
* Optional . Array of query parameters .
* Default empty .
*
* Each method accepts query parameters to be passed . Parameters passed to methods override the ones passed in
* the constructor . This is by design to allow for multiple calculations to be executed from one instance of
* this class .
*
* @ type string $start Start day and time ( based on the beginning of the given day ) .
* @ type string $end End day and time ( based on the end of the given day ) .
* @ type string $range Date range . If a range is passed , this will override and `start` and `end`
* values passed . See \EDD\Reports\get_dates_filter_options () for valid date ranges .
* @ type string $function SQL function . Accepts `COUNT` and `AVG` . Default `COUNT` .
* @ type string $where_sql Reserved for internal use . Allows for additional WHERE clauses to be appended
* to the query .
* @ type string $output The output format of the calculation . Accepts `raw` and `formatted` . Default `raw` .
* }
*
* @ return int Average file downloads .
*/
public function get_average_file_download_count ( $query = array () ) {
// Add table and column name to query_vars to assist with date query generation.
$this -> query_vars [ 'table' ] = $this -> get_db () -> edd_logs_file_downloads ;
$this -> query_vars [ 'column' ] = 'customer_id' ;
$this -> query_vars [ 'date_query_column' ] = 'date_created' ;
// Run pre-query checks and maybe generate SQL.
$this -> pre_query ( $query );
$product_id = ! empty ( $this -> query_vars [ 'download_id' ] )
? $this -> get_db () -> prepare ( 'AND product_id = %d' , absint ( $this -> query_vars [ 'download_id' ] ) )
: '' ;
$price_id = $this -> generate_price_id_query_sql ();
$file_id = ! empty ( $this -> query_vars [ 'file_id' ] )
? $this -> get_db () -> prepare ( 'AND file_id = %d' , absint ( $this -> query_vars [ 'file_id' ] ) )
: '' ;
$sql = " SELECT AVG(total) AS total
FROM (
SELECT { $this -> query_vars [ 'column' ]}, COUNT ( id ) AS total
FROM { $this -> query_vars [ 'table' ]}
WHERE 1 = 1 { $product_id } { $price_id } { $this -> query_vars [ 'where_sql' ]} { $this -> query_vars [ 'date_query_sql' ]}
GROUP BY { $this -> query_vars [ 'column' ]}
) o " ;
$result = $this -> get_db () -> get_var ( $sql );
$result = null === $result
? 0
: absint ( $result );
// Reset query vars.
$this -> post_query ();
return $result ;
}
/** Private Methods ******************************************************/
/**
* Parse query vars to be passed to the calculation methods .
*
* @ since 3.0
* @ access private
*
* @ see \EDD\Stats :: __construct ()
*
* @ param array $query Array of arguments . See \EDD\Stats :: __construct () .
*/
private function parse_query ( $query = array () ) {
$query_var_defaults = array (
'start' => '' ,
'end' => '' ,
'range' => '' ,
'exclude_taxes' => false ,
'currency' => false ,
'currency_sql' => '' ,
'status' => array (),
'status_sql' => '' ,
'type' => array (),
'type_sql' => '' ,
'where_sql' => '' ,
'date_query_sql' => '' ,
'date_query_column' => '' ,
'column' => '' ,
'table' => '' ,
'function' => 'SUM' ,
'output' => 'raw' ,
'relative' => false ,
'relative_start' => '' ,
'relative_end' => '' ,
'grouped' => false ,
'product_id' => '' ,
'price_id' => null ,
'revenue_type' => 'gross' ,
'country' => '' ,
'region' => '' ,
);
if ( empty ( $this -> query_vars ) ) {
$this -> query_vars_defaults = $this -> query_vars = wp_parse_args ( $query , $query_var_defaults );
} else {
$this -> query_vars = wp_parse_args ( $query , $this -> query_vars );
}
// Use Carbon to set up start and end date based on range passed.
if ( ! empty ( $this -> query_vars [ 'range' ] ) && isset ( $this -> date_ranges [ $this -> query_vars [ 'range' ] ] ) ) {
if ( ! empty ( $this -> date_ranges [ $this -> query_vars [ 'range' ] ][ 'start' ] ) ) {
$this -> query_vars [ 'start' ] = $this -> date_ranges [ $this -> query_vars [ 'range' ] ][ 'start' ] -> format ( 'mysql' );
}
if ( ! empty ( $this -> date_ranges [ $this -> query_vars [ 'range' ] ][ 'end' ] ) ) {
$this -> query_vars [ 'end' ] = $this -> date_ranges [ $this -> query_vars [ 'range' ] ][ 'end' ] -> format ( 'mysql' );
}
}
// Use Carbon to set up start and end date based on range passed.
if ( true === $this -> query_vars [ 'relative' ] && ! empty ( $this -> query_vars [ 'range' ] ) && isset ( $this -> relative_date_ranges [ $this -> query_vars [ 'range' ] ] ) ) {
if ( ! empty ( $this -> relative_date_ranges [ $this -> query_vars [ 'range' ] ][ 'start' ] ) ) {
$this -> query_vars [ 'relative_start' ] = $this -> relative_date_ranges [ $this -> query_vars [ 'range' ] ][ 'start' ] -> format ( 'mysql' );
}
if ( ! empty ( $this -> relative_date_ranges [ $this -> query_vars [ 'range' ] ][ 'end' ] ) ) {
$this -> query_vars [ 'relative_end' ] = $this -> relative_date_ranges [ $this -> query_vars [ 'range' ] ][ 'end' ] -> format ( 'mysql' );
}
}
// Validate currency.
if ( empty ( $this -> query_vars [ 'currency' ] ) ) {
$this -> query_vars [ 'currency' ] = false ;
} elseif ( array_key_exists ( strtoupper ( $this -> query_vars [ 'currency' ] ), edd_get_currencies () ) ) {
$this -> query_vars [ 'currency' ] = strtoupper ( $this -> query_vars [ 'currency' ] );
} else {
$this -> query_vars [ 'currency' ] = 'convert' ;
}
// Correctly format functions and column names.
if ( ! empty ( $this -> query_vars [ 'function' ] ) ) {
$this -> query_vars [ 'function' ] = strtoupper ( $this -> query_vars [ 'function' ] );
}
if ( ! empty ( $this -> query_vars [ 'column' ] ) ) {
$this -> query_vars [ 'column' ] = strtolower ( $this -> query_vars [ 'column' ] );
}
/** Parse country ****************************************************/
$country = isset ( $this -> query_vars [ 'country' ] )
? sanitize_text_field ( $this -> query_vars [ 'country' ] )
: '' ;
if ( $country ) {
$country_list = array_filter ( edd_get_country_list () );
// Maybe convert country code to country name.
$country = in_array ( $country , array_flip ( $country_list ), true )
? $country_list [ $country ]
: $country ;
// Ensure a valid county has been passed.
$country = in_array ( $country , $country_list , true )
? $country
: null ;
// Convert back to country code for SQL query.
$country_list = array_flip ( $country_list );
$this -> query_vars [ 'country' ] = is_null ( $country )
? ''
: $country_list [ $country ];
}
/** Parse state ******************************************************/
$state = isset ( $this -> query_vars [ 'region' ] )
? sanitize_text_field ( $this -> query_vars [ 'region' ] )
: '' ;
// Only parse state if one was passed.
if ( $state ) {
$state_list = array_filter ( edd_get_shop_states ( $this -> query_vars [ 'country' ] ) );
// Maybe convert state code to state name.
$state = in_array ( $state , array_flip ( $state_list ), true )
? $state_list [ $state ]
: $state ;
// Ensure a valid state has been passed.
$state = in_array ( $state , $state_list , true )
? $state
: null ;
// Convert back to state code for SQL query.
$state_codes = array_flip ( $state_list );
$this -> query_vars [ 'region' ] = is_null ( $state )
? ''
: $state_codes [ $state ];
}
/**
* Fires after the item query vars have been parsed .
*
* @ since 3.0
*
* @ param \EDD\Stats & $this The \EDD\Stats ( passed by reference ) .
*/
do_action_ref_array ( 'edd_order_stats_parse_query' , array ( & $this ) );
}
/**
* Ensures arguments exist before going ahead and calculating statistics .
*
* @ since 3.0
* @ access private
*
* @ param array $query
*/
private function pre_query ( $query = array () ) {
// Maybe parse query.
if ( ! empty ( $query ) ) {
$this -> parse_query ( $query );
}
// Generate date query SQL if dates have been set.
if ( ! empty ( $this -> query_vars [ 'start' ] ) || ! empty ( $this -> query_vars [ 'end' ] ) ) {
$date_query_sql = ' AND ' ;
if ( ! empty ( $this -> query_vars [ 'start' ] ) ) {
2023-01-18 16:39:57 +00:00
$start_date = EDD () -> utils -> date ( $this -> query_vars [ 'start' ], edd_get_timezone_id (), false ) -> format ( 'mysql' );
2022-11-27 15:03:07 +00:00
$date_query_sql .= " { $this -> query_vars [ 'table' ] } . { $this -> query_vars [ 'date_query_column' ] } " ;
2023-01-18 16:39:57 +00:00
$date_query_sql .= $this -> get_db () -> prepare ( '>= %s' , $start_date );
2022-11-27 15:03:07 +00:00
}
// Join dates with `AND` if start and end date set.
if ( ! empty ( $this -> query_vars [ 'start' ] ) && ! empty ( $this -> query_vars [ 'end' ] ) ) {
$date_query_sql .= ' AND ' ;
}
if ( ! empty ( $this -> query_vars [ 'end' ] ) ) {
2023-01-18 16:39:57 +00:00
$end_date = EDD () -> utils -> date ( $this -> query_vars [ 'end' ], edd_get_timezone_id (), false ) -> format ( 'mysql' );
$date_query_sql .= $this -> get_db () -> prepare ( " { $this -> query_vars [ 'table' ] } . { $this -> query_vars [ 'date_query_column' ] } <= %s " , $end_date );
2022-11-27 15:03:07 +00:00
}
$this -> query_vars [ 'date_query_sql' ] = $date_query_sql ;
}
// Generate status SQL if statuses have been set.
if ( ! empty ( $this -> query_vars [ 'status' ] ) ) {
if ( 'any' === $this -> query_vars [ 'status' ] ) {
$this -> query_vars [ 'status_sql' ] = '' ;
} else {
$this -> query_vars [ 'status' ] = array_map ( 'sanitize_text_field' , $this -> query_vars [ 'status' ] );
$placeholders = $this -> get_placeholder_string ( $this -> query_vars [ 'status' ] );
$this -> query_vars [ 'status_sql' ] = $this -> get_db () -> prepare ( " AND { $this -> query_vars [ 'table' ] } .status IN ( { $placeholders } ) " , $this -> query_vars [ 'status' ] );
}
}
if ( ! empty ( $this -> query_vars [ 'type' ] ) ) {
// We always want to format this as an array, so account for a possible string.
if ( ! is_array ( $this -> query_vars [ 'type' ] ) ) {
$this -> query_vars [ 'type' ] = array ( $this -> query_vars [ 'type' ] );
}
$this -> query_vars [ 'type' ] = array_map ( 'sanitize_text_field' , $this -> query_vars [ 'type' ] );
$placeholders = $this -> get_placeholder_string ( $this -> query_vars [ 'type' ] );
$this -> query_vars [ 'type_sql' ] = $this -> get_db () -> prepare ( " AND { $this -> query_vars [ 'table' ] } .type IN ( { $placeholders } ) " , $this -> query_vars [ 'type' ] );
}
if ( ! empty ( $this -> query_vars [ 'currency' ] ) && 'convert' !== strtolower ( $this -> query_vars [ 'currency' ] ) ) {
$this -> query_vars [ 'currency_sql' ] = $this -> get_db () -> prepare ( " AND { $this -> query_vars [ 'table' ] } .currency = %s " , $this -> query_vars [ 'currency' ] );
}
}
/**
* Runs after a query . Resets query vars back to the originals passed in via the constructor .
*
* @ since 3.0
* @ access private
*/
private function post_query () {
$this -> query_vars = $this -> query_var_originals ;
}
/**
* Format the data if requested via the query parameter .
*
* @ since 3.0
* @ access private
*
* @ param mixed $data Data to format .
*
* @ return mixed Raw or formatted data depending on query parameter .
*/
private function maybe_format ( $data = null ) {
// Bail if nothing was passed.
if ( null === $data ) {
return $data ;
}
$allowed_output_formats = array ( 'raw' , 'typed' , 'formatted' );
// Output format. Default raw.
$output = isset ( $this -> query_vars [ 'output' ] ) && in_array ( $this -> query_vars [ 'output' ], $allowed_output_formats , true )
? $this -> query_vars [ 'output' ]
: 'raw' ;
// Return data as is if the format is raw.
if ( 'raw' === $output ) {
return $data ;
}
$currency = $this -> query_vars [ 'currency' ];
if ( empty ( $currency ) || 'convert' === strtolower ( $currency ) ) {
$currency = edd_get_currency ();
}
if ( is_object ( $data ) ) {
foreach ( array_keys ( get_object_vars ( $data ) ) as $field ) {
if ( is_numeric ( $data -> { $field } ) ) {
$data -> { $field } = edd_format_amount ( $data -> { $field }, true , $currency , $output );
if ( 'formatted' === $output ) {
$data -> { $field } = edd_currency_filter ( $data -> { $field }, $currency );
}
}
}
} elseif ( is_array ( $data ) ) {
foreach ( array_keys ( $data ) as $field ) {
if ( is_numeric ( $data [ $field ] ) ) {
$data [ $field ] = edd_format_amount ( $data [ $field ], true , $currency , $output );
if ( 'formatted' === $output ) {
$data [ $field ] = edd_currency_filter ( $data [ $field ], $currency );
}
}
}
} else {
if ( is_numeric ( $data ) ) {
$data = edd_format_amount ( $data , true , $currency , $output );
if ( 'formatted' === $output ) {
$data = edd_currency_filter ( $data , $currency );
}
}
}
return $data ;
}
/**
* Generate date query SQL for relative time periods .
*
* @ since 3.0
* @ access protected
*
* @ return string Date query SQL .
*/
private function generate_relative_date_query_sql () {
// Bail if relative calculation not requested.
if ( false === $this -> query_vars [ 'relative' ] ) {
return '' ;
}
// Generate date query SQL if dates have been set.
if ( ! empty ( $this -> query_vars [ 'relative_start' ] ) || ! empty ( $this -> query_vars [ 'relative_end' ] ) ) {
$date_query_sql = " AND { $this -> query_vars [ 'table' ] } . { $this -> query_vars [ 'date_query_column' ] } " ;
if ( ! empty ( $this -> query_vars [ 'relative_start' ] ) ) {
$date_query_sql .= $this -> get_db () -> prepare ( '>= %s' , $this -> query_vars [ 'relative_start' ] );
}
// Join dates with `AND` if start and end date set.
if ( ! empty ( $this -> query_vars [ 'relative_start' ] ) && ! empty ( $this -> query_vars [ 'relative_end' ] ) ) {
$date_query_sql .= ' AND ' ;
}
if ( ! empty ( $this -> query_vars [ 'relative_end' ] ) ) {
$date_query_sql .= $this -> get_db () -> prepare ( " { $this -> query_vars [ 'table' ] } . { $this -> query_vars [ 'date_query_column' ] } <= %s " , $this -> query_vars [ 'relative_end' ] );
}
return $date_query_sql ;
}
}
/**
* Generates price ID query SQL .
*
* @ since 3.0
* @ return string
*/
private function generate_price_id_query_sql () {
return ! is_null ( $this -> query_vars [ 'price_id' ] ) && is_numeric ( $this -> query_vars [ 'price_id' ] )
2023-01-18 16:39:57 +00:00
? $this -> get_db () -> prepare ( " AND { $this -> query_vars [ 'table' ] } .price_id = %d " , absint ( $this -> query_vars [ 'price_id' ] ) )
2022-11-27 15:03:07 +00:00
: '' ;
}
/** Private Getters *******************************************************/
/**
* Return the global database interface .
*
* @ since 3.0
* @ access private
* @ static
*
* @ return \wpdb | \stdClass
*/
private static function get_db () {
return isset ( $GLOBALS [ 'wpdb' ] )
? $GLOBALS [ 'wpdb' ]
: new \stdClass ();
}
/** Private Setters ******************************************************/
/**
* Set up the date ranges available .
*
* @ since 3.0
* @ access private
*/
private function set_date_ranges () {
// Retrieve the time in UTC for the date ranges to be correctly parsed.
$date = EDD () -> utils -> date ( 'now' , edd_get_timezone_id (), false );
$date_filters = Reports\get_dates_filter_options ();
$filter = Reports\get_filter_value ( 'dates' );
foreach ( $date_filters as $range => $label ) {
$this -> date_ranges [ $range ] = Reports\parse_dates_for_range ( $range );
$this -> relative_date_ranges [ $range ] = Reports\parse_relative_dates_for_range ( $range );
}
}
/**
* Based on the query_vars [ 'revenue_type' ], use gross or net statuses .
*
* @ since 3.0
*
* @ return array The statuses of orders to use for the stats generation .
*/
private function get_revenue_type_statuses () {
if ( 'net' === $this -> query_vars [ 'revenue_type' ] ) {
return edd_get_net_order_statuses ();
}
return edd_get_gross_order_statuses ();
}
/**
* Based on the query_vars [ 'revenue_type' ], use just sale or also include refunds .
*
* @ since 3.0
*
* @ return array The order types to use when generating stats .
*/
private function get_revenue_type_order_types () {
$order_types = array ( 'sale' );
if ( 'net' === $this -> query_vars [ 'revenue_type' ] ) {
$order_types [] = 'refund' ;
}
return $order_types ;
}
/**
* Calculates the relative change between two datasets
* and outputs an array of details about comparison .
*
* @ since 3.1
*
* @ param int | float $total The primary value result for the stat .
* @ param int | float $relative The value relative to the previous date range .
* @ param bool $reverse If the stat being displayed is a 'reverse' state , where lower is better .
*
* @ return array Details about the relative change between two datasets .
*/
public function generate_relative_data ( $total = 0 , $relative = 0 , $reverse = false ) {
$output = array (
'comparable' => true ,
'no_change' => false ,
'percentage_change' => false ,
'formatted_percentage_change' => false ,
'positive_change' => false ,
'total' => $total ,
'relative' => $relative ,
'reverse' => $reverse ,
);
if ( ( floatval ( 0 ) === floatval ( $total ) && floatval ( 0 ) === floatval ( $relative ) ) || ( $total === $relative ) ) {
// There is no change between datasets.
$output [ 'no_change' ] = true ;
} else if ( floatval ( 0 ) !== floatval ( $relative ) ) {
// There is a calculatable difference between datasets.
$percentage_change = ( $total - $relative ) / $relative * 100 ;
$formatted_percentage_change = absint ( $percentage_change );
$positive_change = false ;
if ( absint ( $percentage_change ) < 100 ) {
$formatted_percentage_change = number_format ( $percentage_change , 2 );
$formatted_percentage_change = $formatted_percentage_change < 1 ? $formatted_percentage_change * - 1 : $formatted_percentage_change ;
}
// Check if stat is in a 'reverse' state, where lower is better.
$positive_change = ( bool ) ! $reverse ;
if ( 0 > $percentage_change ) {
$positive_change = ( bool ) $reverse ;
}
$output [ 'percentage_change' ] = $percentage_change ;
$output [ 'formatted_percentage_change' ] = $formatted_percentage_change ;
$output [ 'positive_change' ] = $positive_change ;
} else {
// There is no data to compare.
$output [ 'comparable' ] = false ;
}
return $output ;
}
/**
* Generates output for the report tiles when a relative % change is requested .
*
* @ since 3.0
*
* @ param int | float $total The primary value result for the stat .
* @ param int | float $relative The value relative to the previous date range .
* @ param bool $reverse If the stat being displayed is a 'reverse' state , where lower is better .
*/
private function generate_relative_markup ( $total = 0 , $relative = 0 , $reverse = false ) {
$relative_data = $this -> generate_relative_data ( $total , $relative , $reverse );
$total_output = $this -> maybe_format ( $relative_data [ 'total' ] );
$relative_markup = '' ;
if ( $relative_data [ 'no_change' ] ) {
$relative_output = esc_html__ ( 'No Change' , 'easy-digital-downloads' );
} else if ( $relative_data [ 'comparable' ] ) {
if ( 0 < $relative_data [ 'percentage_change' ] ) {
$direction = $relative_data [ 'reverse' ] ? 'up reverse' : 'up' ;
$relative_output = '<span class="dashicons dashicons-arrow-' . esc_attr ( $direction ) . '"></span> ' . $relative_data [ 'formatted_percentage_change' ] . '%' ;
} else {
$direction = $relative_data [ 'reverse' ] ? 'down reverse' : 'down' ;
$relative_output = '<span class="dashicons dashicons-arrow-' . esc_attr ( $direction ) . '"></span> ' . $relative_data [ 'formatted_percentage_change' ] . '%' ;
}
} else {
$relative_output = '<span aria-hidden="true">—</span><span class="screen-reader-text">' . __ ( 'No data to compare' , 'easy-digital-downloads' ) . '</span>' ;
}
$relative_markup = $total_output ;
if ( ! empty ( $relative_output ) ) {
$relative_markup .= '<div class="tile-relative">' . $relative_output . '</div>' ;
}
return $relative_markup ;
}
/**
* Gets a placeholder string from an array .
*
* @ since 3.1
* @ param array $array
* @ return string
*/
private function get_placeholder_string ( $array ) {
return implode ( ', ' , array_fill ( 0 , count ( $array ), '%s' ) );
}
}