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(), 'rate' => true ) ); $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; if ( empty( $function ) ) { throw new \InvalidArgumentException( 'Missing select function.' ); } if ( ! empty( $args['accepted_functions'] ) && ! in_array( strtoupper( $this->query_vars['function'] ), $args['accepted_functions'], true ) ) { 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'] ) ) ); } } $function = $this->query_vars['function'] = strtoupper( $function ); // 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'; $this->query_vars['status'] = array( 'complete', 'refunded', '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( 'SUM', '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'] ) ) : ''; $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 "; } if ( ! empty( $this->query_vars['currency'] ) && array_key_exists( strtoupper( $this->query_vars['currency'] ), edd_get_currencies() ) ) { $join .= " INNER JOIN {$this->get_db()->edd_orders} edd_o ON ({$this->query_vars['table']}.order_id = edd_o.id) "; $currency = $this->get_db()->prepare( "AND edd_o.currency = %s", strtoupper( $this->query_vars['currency'] ) ); } if ( 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['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['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 = $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'] ) ) { $date_query_sql .= "{$this->query_vars['table']}.{$this->query_vars['date_query_column']} "; $date_query_sql .= $this->get_db()->prepare( '>= %s', $this->query_vars['start'] ); } // 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'] ) ) { $date_query_sql .= $this->get_db()->prepare( "{$this->query_vars['table']}.{$this->query_vars['date_query_column']} <= %s", $this->query_vars['end'] ); } $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'] ) ? $this->get_db()->prepare( 'AND price_id = %d', absint( $this->query_vars['price_id'] ) ) : ''; } /** 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 = ' ' . $relative_data['formatted_percentage_change'] . '%'; } else { $direction = $relative_data['reverse'] ? 'down reverse' : 'down'; $relative_output = ' ' . $relative_data['formatted_percentage_change'] . '%'; } } else { $relative_output = '' . __( 'No data to compare', 'easy-digital-downloads' ) . ''; } $relative_markup = $total_output; if ( ! empty( $relative_output ) ) { $relative_markup .= '
' . $relative_output . '
'; } 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' ) ); } }