__( 'Customer', 'woocommerce-subscriptions' ), 'plural' => __( 'Customers', 'woocommerce-subscriptions' ), 'ajax' => false, ) ); } /** * No subscription products found text. */ public function no_items() { esc_html_e( 'No customers found.', 'woocommerce-subscriptions' ); } /** * Output the report. */ public function output_report() { $this->prepare_items(); echo '
'; echo '
'; echo '

' . esc_html__( 'Customer Totals', 'woocommerce-subscriptions' ) . '

'; echo '

' . esc_html__( 'Total Subscribers', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->total_customers ) . wc_help_tip( __( 'The number of unique customers with a subscription of any status other than pending or trashed.', 'woocommerce-subscriptions' ) ) . '
'; echo ' ' . esc_html__( 'Active Subscriptions', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->active_subscriptions ) . wc_help_tip( __( 'The total number of subscriptions with a status of active or pending cancellation.', 'woocommerce-subscriptions' ) ) . '
'; echo ' ' . esc_html__( 'Total Subscriptions', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->total_subscriptions ) . wc_help_tip( __( 'The total number of subscriptions with a status other than pending or trashed.', 'woocommerce-subscriptions' ) ) . '
'; echo ' ' . esc_html__( 'Total Subscription Orders', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->initial_order_count + $this->totals->renewal_switch_count ) . wc_help_tip( __( 'The total number of sign-up, switch and renewal orders placed with your store with a paid status (i.e. processing or complete).', 'woocommerce-subscriptions' ) ) . '
'; echo ' ' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . ': '; echo wp_kses_post( wc_price( $this->totals->total_customers > 0 ? ( ( $this->totals->initial_order_total + $this->totals->renewal_switch_total ) / $this->totals->total_customers ) : 0 ) ); echo wc_help_tip( __( 'The average value of all customers\' sign-up, switch and renewal orders.', 'woocommerce-subscriptions' ) ) . '

'; echo '
'; $this->display(); echo '
'; } /** * Get column value. * * @param WP_User $user * @param string $column_name * @return string */ public function column_default( $user, $column_name ) { global $wpdb; switch ( $column_name ) { case 'customer_name': $user_info = get_userdata( $user->customer_id ); return '' . $user_info->user_email . ''; case 'active_subscription_count': return $user->active_subscriptions; case 'total_subscription_count': return sprintf( '%d', admin_url( 'edit.php?post_type=shop_subscription&_customer_user=' ), $user->customer_id, $user->total_subscriptions ); case 'total_subscription_order_count': return sprintf( '%d', admin_url( 'edit.php?post_type=shop_order&_paid_subscription_orders_for_customer_user=' ), $user->customer_id, $user->initial_order_count + $user->renewal_switch_count ); case 'customer_lifetime_value': return wc_price( $user->initial_order_total + $user->renewal_switch_total ); } return ''; } /** * Get columns. * * @return array */ public function get_columns() { $columns = array( 'customer_name' => __( 'Customer', 'woocommerce-subscriptions' ), // translators: %s: help tip. 'active_subscription_count' => sprintf( __( 'Active Subscriptions %s', 'woocommerce-subscriptions' ), wc_help_tip( __( 'The number of subscriptions this customer has with a status of active or pending cancellation.', 'woocommerce-subscriptions' ) ) ), // translators: %s: help tip. 'total_subscription_count' => sprintf( __( 'Total Subscriptions %s', 'woocommerce-subscriptions' ), wc_help_tip( __( 'The number of subscriptions this customer has with a status other than pending or trashed.', 'woocommerce-subscriptions' ) ) ), // translators: %s: help tip. 'total_subscription_order_count' => sprintf( __( 'Total Subscription Orders %s', 'woocommerce-subscriptions' ), wc_help_tip( __( 'The number of sign-up, switch and renewal orders this customer has placed with your store with a paid status (i.e. processing or complete).', 'woocommerce-subscriptions' ) ) ), // translators: %s: help tip. 'customer_lifetime_value' => sprintf( __( 'Lifetime Value from Subscriptions %s', 'woocommerce-subscriptions' ), wc_help_tip( __( 'The total value of this customer\'s sign-up, switch and renewal orders.', 'woocommerce-subscriptions' ) ) ), ); return $columns; } /** * Prepare subscription list items. */ public function prepare_items() { global $wpdb; $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); $current_page = absint( $this->get_pagenum() ); $per_page = absint( apply_filters( 'wcs_reports_customers_per_page', 20 ) ); $offset = absint( ( $current_page - 1 ) * $per_page ); $this->totals = self::get_data(); $customer_query = apply_filters( 'wcs_reports_current_customer_query', "SELECT customer_ids.meta_value as customer_id, COUNT(subscription_posts.ID) as total_subscriptions, COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total, COUNT(DISTINCT parent_order.ID) as initial_order_count, SUM(CASE WHEN subscription_posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'wcs_reports_active_statuses', array( 'active', 'pending-cancel' ) ) ) . "' ) THEN 1 ELSE 0 END) AS active_subscriptions FROM {$wpdb->posts} subscription_posts INNER JOIN {$wpdb->postmeta} customer_ids ON customer_ids.post_id = subscription_posts.ID AND customer_ids.meta_key = '_customer_user' LEFT JOIN {$wpdb->posts} parent_order ON parent_order.ID = subscription_posts.post_parent AND parent_order.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ) ) . "' ) LEFT JOIN {$wpdb->postmeta} parent_total ON parent_total.post_id = parent_order.ID AND parent_total.meta_key = '_order_total' WHERE subscription_posts.post_type = 'shop_subscription' AND subscription_posts.post_status NOT IN ('wc-pending', 'trash') GROUP BY customer_ids.meta_value ORDER BY customer_id DESC LIMIT {$offset}, {$per_page}" ); $this->items = $wpdb->get_results( $customer_query ); // Now get each customer's renewal and switch total $customer_renewal_switch_total_query = apply_filters( 'wcs_reports_current_customer_renewal_switch_total_query', "SELECT customer_ids.meta_value as customer_id, COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total, COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count FROM {$wpdb->postmeta} renewal_order_ids INNER JOIN {$wpdb->posts} subscription_posts ON renewal_order_ids.meta_value = subscription_posts.ID AND subscription_posts.post_type = 'shop_subscription' AND subscription_posts.post_status NOT IN ('wc-pending', 'trash') INNER JOIN {$wpdb->postmeta} customer_ids ON renewal_order_ids.meta_value = customer_ids.post_id AND customer_ids.meta_key = '_customer_user' AND customer_ids.meta_value IN ('" . implode( "','", wp_list_pluck( $this->items, 'customer_id' ) ) . "' ) INNER JOIN {$wpdb->posts} renewal_order_posts ON renewal_order_ids.post_id = renewal_order_posts.ID AND renewal_order_posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ) ) . "' ) LEFT JOIN {$wpdb->postmeta} renewal_switch_totals ON renewal_switch_totals.post_id = renewal_order_ids.post_id AND renewal_switch_totals.meta_key = '_order_total' WHERE renewal_order_ids.meta_key = '_subscription_renewal' OR renewal_order_ids.meta_key = '_subscription_switch' GROUP BY customer_id ORDER BY customer_id" ); $customer_renewal_switch_totals = $wpdb->get_results( $customer_renewal_switch_total_query, OBJECT_K ); foreach ( $this->items as $index => $item ) { if ( isset( $customer_renewal_switch_totals[ $item->customer_id ] ) ) { $this->items[ $index ]->renewal_switch_total = $customer_renewal_switch_totals[ $item->customer_id ]->renewal_switch_total; $this->items[ $index ]->renewal_switch_count = $customer_renewal_switch_totals[ $item->customer_id ]->renewal_switch_count; } else { $this->items[ $index ]->renewal_switch_total = $this->items[ $index ]->renewal_switch_count = 0; } } /** * Pagination. */ $this->set_pagination_args( array( 'total_items' => $this->totals->total_customers, 'per_page' => $per_page, 'total_pages' => ceil( $this->totals->total_customers / $per_page ), ) ); } /** * Gather totals for customers */ public static function get_data( $args = array() ) { global $wpdb; $default_args = array( 'no_cache' => false, 'order_status' => apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ), ); $args = apply_filters( 'wcs_reports_customer_total_args', $args ); $args = wp_parse_args( $args, $default_args ); $total_query = apply_filters( 'wcs_reports_customer_total_query', "SELECT COUNT( DISTINCT customer_ids.meta_value) as total_customers, COUNT(subscription_posts.ID) as total_subscriptions, COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total, COUNT(DISTINCT parent_order.ID) as initial_order_count, COALESCE(SUM(CASE WHEN subscription_posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'wcs_reports_active_statuses', array( 'active', 'pending-cancel' ) ) ) . "' ) THEN 1 ELSE 0 END), 0) AS active_subscriptions FROM {$wpdb->posts} subscription_posts INNER JOIN {$wpdb->postmeta} customer_ids ON customer_ids.post_id = subscription_posts.ID AND customer_ids.meta_key = '_customer_user' LEFT JOIN {$wpdb->posts} parent_order ON parent_order.ID = subscription_posts.post_parent AND parent_order.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' ) LEFT JOIN {$wpdb->postmeta} parent_total ON parent_total.post_id = parent_order.ID AND parent_total.meta_key = '_order_total' WHERE subscription_posts.post_type = 'shop_subscription' AND subscription_posts.post_status NOT IN ('wc-pending', 'trash') "); $cached_results = get_transient( strtolower( __CLASS__ ) ); $query_hash = md5( $total_query ); // Set a default value for cached results for PHP 8.2+ compatibility. if ( empty( $cached_results ) ) { $cached_results = []; } if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { // Enable big selects for reports $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_data', $wpdb->get_row( $total_query ) ); set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS ); } $customer_totals = $cached_results[ $query_hash ]; $renewal_switch_total_query = apply_filters( 'wcs_reports_customer_total_renewal_switch_query', "SELECT COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total, COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count FROM {$wpdb->postmeta} renewal_order_ids INNER JOIN {$wpdb->posts} subscription_posts ON renewal_order_ids.meta_value = subscription_posts.ID AND subscription_posts.post_type = 'shop_subscription' AND subscription_posts.post_status NOT IN ('wc-pending', 'trash') INNER JOIN {$wpdb->posts} renewal_order_posts ON renewal_order_ids.post_id = renewal_order_posts.ID AND renewal_order_posts.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' ) LEFT JOIN {$wpdb->postmeta} renewal_switch_totals ON renewal_switch_totals.post_id = renewal_order_ids.post_id AND renewal_switch_totals.meta_key = '_order_total' WHERE renewal_order_ids.meta_key = '_subscription_renewal' OR renewal_order_ids.meta_key = '_subscription_switch'" ); $query_hash = md5( $renewal_switch_total_query ); if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) { // Enable big selects for reports $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_renewal_switch_data', $wpdb->get_row( $renewal_switch_total_query ) ); set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS ); } $customer_totals->renewal_switch_total = $cached_results[ $query_hash ]->renewal_switch_total; $customer_totals->renewal_switch_count = $cached_results[ $query_hash ]->renewal_switch_count; return $customer_totals; } /** * Clears the cached report data. * * @since 3.0.10 */ public static function clear_cache() { delete_transient( strtolower( __CLASS__ ) ); } }