rds_count, 'pages' => $total_pages, 'page_no' => (int) $query_args['page'], ); return $data; } /** * Get or create a customer from a given order. * * @param object $order WC Order. * @return int|bool */ public static function get_or_create_customer_from_order( $order ) { if ( ! $order ) { return false; } global $wpdb; if ( ! is_a( $order, 'WC_Order' ) ) { return false; } $returning_customer_id = self::get_existing_customer_id_from_order( $order ); if ( $returning_customer_id ) { return $returning_customer_id; } list($data, $format) = self::get_customer_order_data_and_format( $order ); $result = $wpdb->insert( self::get_db_table_name(), $data, $format ); $customer_id = $wpdb->insert_id; /** * Fires when a new report customer is created. * * @param int $customer_id Customer ID. * @since 4.0.0 */ do_action( 'woocommerce_analytics_new_customer', $customer_id ); return $result ? $customer_id : false; } /** * Returns a data object and format object of the customers data coming from the order. * * @param object $order WC_Order where we get customer info from. * @param object|null $customer_user WC_Customer registered customer WP user. * @return array ($data, $format) */ public static function get_customer_order_data_and_format( $order, $customer_user = null ) { $data = array( 'first_name' => $order->get_customer_first_name(), 'last_name' => $order->get_customer_last_name(), 'email' => $order->get_billing_email( 'edit' ), 'city' => $order->get_billing_city( 'edit' ), 'state' => $order->get_billing_state( 'edit' ), 'postcode' => $order->get_billing_postcode( 'edit' ), 'country' => $order->get_billing_country( 'edit' ), 'date_last_active' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), ); $format = array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', ); // Add registered customer data. if ( 0 !== $order->get_user_id() ) { $user_id = $order->get_user_id(); if ( is_null( $customer_user ) ) { $customer_user = new \WC_Customer( $user_id ); } // Set email as customer email instead of Order Billing Email if we have a customer. $data['email'] = $customer_user->get_email( 'edit' ); // Adding other relevant customer data. $data['user_id'] = $user_id; $data['username'] = $customer_user->get_username( 'edit' ); $data['date_registered'] = $customer_user->get_date_created( 'edit' ) ? $customer_user->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ) : null; $format[] = '%d'; $format[] = '%s'; $format[] = '%s'; } return array( $data, $format ); } /** * Retrieve a guest ID (when user_id is null) by email. * * @param string $email Email address. * @return false|array Customer array if found, boolean false if not. */ public static function get_guest_id_by_email( $email ) { global $wpdb; $table_name = self::get_db_table_name(); $customer_id = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT customer_id FROM {$table_name} WHERE email = %s AND user_id IS NULL LIMIT 1", $email ) ); return $customer_id ? (int) $customer_id : false; } /** * Retrieve a customer ID by email address, regardless of user registration status. * Prioritizes registered customers over guest customers when both exist. * * @param string $email Email address. * @return false|int Customer ID if found, boolean false if not. */ public static function get_customer_id_by_email( $email ) { global $wpdb; if ( empty( $email ) || ! is_email( $email ) ) { return false; } $table_name = self::get_db_table_name(); $customer_id = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT customer_id FROM {$table_name} WHERE email = %s ORDER BY user_id IS NOT NULL DESC LIMIT 1", $email ) ); return $customer_id ? (int) $customer_id : false; } /** * Retrieve a registered customer row id by user_id. * * @param string|int $user_id User ID. * @return false|int Customer ID if found, boolean false if not. */ public static function get_customer_id_by_user_id( $user_id ) { global $wpdb; $table_name = self::get_db_table_name(); $customer_id = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT customer_id FROM {$table_name} WHERE user_id = %d LIMIT 1", $user_id ) ); return $customer_id ? (int) $customer_id : false; } /** * Retrieve the last order made by a customer. * * @param int $customer_id Customer ID. * @return object WC_Order|false. */ public static function get_last_order( $customer_id ) { global $wpdb; $orders_table = $wpdb->prefix . 'wc_order_stats'; $last_order = $wpdb->get_var( $wpdb->prepare( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT order_id, date_created_gmt FROM {$orders_table} WHERE customer_id = %d ORDER BY date_created_gmt DESC, order_id DESC LIMIT 1", // phpcs:enable $customer_id ) ); if ( ! $last_order ) { return false; } return wc_get_order( absint( $last_order ) ); } /** * Retrieve the oldest orders made by a customer. * * @param int $customer_id Customer ID. * @return array Orders. */ public static function get_oldest_orders( $customer_id ) { global $wpdb; $orders_table = $wpdb->prefix . 'wc_order_stats'; $excluded_statuses = array_map( array( __CLASS__, 'normalize_order_status' ), self::get_excluded_report_order_statuses() ); $excluded_statuses_condition = ''; if ( ! empty( $excluded_statuses ) ) { $excluded_statuses_str = implode( "','", $excluded_statuses ); $excluded_statuses_condition = "AND status NOT IN ('{$excluded_statuses_str}')"; } return $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT order_id, date_created FROM {$orders_table} WHERE customer_id = %d {$excluded_statuses_condition} ORDER BY date_created, order_id ASC LIMIT 2", $customer_id ) ); } /** * Retrieve the amount of orders made by a customer. * * @param int $customer_id Customer ID. * @return int|null Amount of orders for customer or null on failure. */ public static function get_order_count( $customer_id ) { global $wpdb; $customer_id = absint( $customer_id ); if ( 0 === $customer_id ) { return null; } $result = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( order_id ) FROM {$wpdb->prefix}wc_order_stats WHERE customer_id = %d", $customer_id ) ); if ( is_null( $result ) ) { return null; } return (int) $result; } /** * Update the database with customer data. * * @param int $user_id WP User ID to update customer data for. * @return int|bool|null Number or rows modified or false on failure. */ public static function update_registered_customer( $user_id ) { global $wpdb; $customer = new \WC_Customer( $user_id ); if ( ! self::is_valid_customer( $user_id ) ) { return false; } $first_name = $customer->get_first_name(); $last_name = $customer->get_last_name(); if ( empty( $first_name ) ) { $first_name = $customer->get_billing_first_name(); } if ( empty( $last_name ) ) { $last_name = $customer->get_billing_last_name(); } $last_active = $customer->get_meta( 'wc_last_active', true, 'edit' ); $data = array( 'user_id' => $user_id, 'username' => $customer->get_username( 'edit' ), 'first_name' => $first_name, 'last_name' => $last_name, 'email' => $customer->get_email( 'edit' ), 'city' => $customer->get_billing_city( 'edit' ), 'state' => $customer->get_billing_state( 'edit' ), 'postcode' => $customer->get_billing_postcode( 'edit' ), 'country' => $customer->get_billing_country( 'edit' ), 'date_registered' => $customer->get_date_created( 'edit' ) ? $customer->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ) : null, 'date_last_active' => $last_active ? gmdate( 'Y-m-d H:i:s', $last_active ) : null, ); $format = array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', ); $customer_id = self::get_customer_id_by_user_id( $user_id ); if ( $customer_id ) { // Preserve customer_id for existing user_id. $data['customer_id'] = $customer_id; $format[] = '%d'; } $results = $wpdb->replace( self::get_db_table_name(), $data, $format ); /** * Fires when customser's reports are updated. * * @param int $customer_id Customer ID. * @since 4.0.0 */ do_action( 'woocommerce_analytics_update_customer', $customer_id ); ReportsCache::invalidate(); return $results; } /** * Update the database if the "last active" meta value was changed. * Function expects to be hooked into the `added_user_meta` and `updated_user_meta` actions. * * @param int $meta_id ID of updated metadata entry. * @param int $user_id ID of the user being updated. * @param string $meta_key Meta key being updated. */ public static function update_registered_customer_via_last_active( $meta_id, $user_id, $meta_key ) { if ( 'wc_last_active' === $meta_key ) { // Optimization note related to guarded updates in `wc_update_user_last_active`: the meta update will trigger // this method execution. We evaluated adding `! doing_action( 'wp' )` here, but the performance gain lays // in the micro-optimization area while exposing the Analytics to certain edge-cases. self::update_registered_customer( $user_id ); } } /** * Check if a user ID is a valid customer or other user role with past orders. * * @param int $user_id User ID. * @return bool */ protected static function is_valid_customer( $user_id ) { $user = new \WP_User( $user_id ); if ( (int) $user_id !== $user->ID ) { return false; } /** * Filter the customer roles, used to check if the user is a customer. * * @param array List of customer roles. * @since 4.0.0 */ $customer_roles = (array) apply_filters( 'woocommerce_analytics_customer_roles', array( 'customer' ) ); if ( empty( $user->roles ) || empty( array_intersect( $user->roles, $customer_roles ) ) ) { return false; } return true; } /** * Delete a customer lookup row. * * @param int $customer_id Customer ID. */ public static function delete_customer( $customer_id ) { global $wpdb; $customer_id = (int) $customer_id; $num_deleted = $wpdb->delete( self::get_db_table_name(), array( 'customer_id' => $customer_id ) ); if ( $num_deleted ) { /** * Fires when a customer is deleted. * * @param int $order_id Order ID. * @since 4.0.0 */ do_action( 'woocommerce_analytics_delete_customer', $customer_id ); ReportsCache::invalidate(); } } /** * Delete a customer lookup row by WordPress User ID. * * @param int $user_id WordPress User ID. */ public static function delete_customer_by_user_id( $user_id ) { global $wpdb; if ( (int) $user_id < 1 || doing_action( 'wp_uninitialize_site' ) ) { // Skip the deletion. return; } $user_id = (int) $user_id; $num_deleted = $wpdb->delete( self::get_db_table_name(), array( 'user_id' => $user_id ) ); if ( $num_deleted ) { ReportsCache::invalidate(); } } /** * Anonymize the customer data for a single order. * * @internal * @param int|WC_Order $order Order instance or ID. * @return void */ public static function anonymize_customer( $order ) { global $wpdb; if ( ! is_object( $order ) ) { $order = wc_get_order( absint( $order ) ); } $customer_id = $wpdb->get_var( $wpdb->prepare( "SELECT customer_id FROM {$wpdb->prefix}wc_order_stats WHERE order_id = %d", $order->get_id() ) ); if ( ! $customer_id ) { return; } // Long form query because $wpdb->update rejects [deleted]. $deleted_text = __( '[deleted]', 'woocommerce' ); $updated = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}wc_customer_lookup SET user_id = NULL, username = %s, first_name = %s, last_name = %s, email = %s, country = '', postcode = %s, city = %s, state = %s WHERE customer_id = %d", array( $deleted_text, $deleted_text, $deleted_text, 'deleted@site.invalid', $deleted_text, $deleted_text, $deleted_text, $customer_id, ) ) ); // If the customer row was anonymized, flush the cache. if ( $updated ) { ReportsCache::invalidate(); } } /** * Build location filter SQL clause for includes or excludes. * * @since 10.5.0 * @param string $locations_string Comma-separated list of locations (e.g., "US:CA,US:NY,GB"). * @param bool $is_include True for IN clause, false for NOT IN clause. * @return string SQL WHERE clause condition. */ protected function build_location_filter_clause( $locations_string, $is_include = true ) { $customer_lookup_table = self::get_db_table_name(); $locations_array = explode( ',', $locations_string ); $country_state_pairs = array(); $countries = array(); foreach ( $locations_array as $location ) { $location = trim( $location ); if ( empty( $location ) ) { continue; } if ( false !== strpos( $location, ':' ) ) { $parts = explode( ':', $location ); if ( 2 === count( $parts ) ) { $country_state_pairs[] = array( 'country' => esc_sql( $parts[0] ), 'state' => esc_sql( $parts[1] ), ); } } else { $countries[] = esc_sql( $location ); } } $conditions = array(); // Build country:state pair conditions. if ( ! empty( $country_state_pairs ) ) { $pair_conditions = array(); foreach ( $country_state_pairs as $pair ) { if ( $is_include ) { $pair_conditions[] = "({$customer_lookup_table}.country = '{$pair['country']}' AND {$customer_lookup_table}.state = '{$pair['state']}')"; } else { $pair_conditions[] = "({$customer_lookup_table}.country != '{$pair['country']}' OR {$customer_lookup_table}.state != '{$pair['state']}')"; } } $pair_connector = $is_include ? ' OR ' : ' AND '; $conditions[] = '(' . implode( $pair_connector, $pair_conditions ) . ')'; } // Build country-only conditions. if ( ! empty( $countries ) ) { $operator = $is_include ? 'IN' : 'NOT IN'; $conditions[] = "{$customer_lookup_table}.country {$operator} ('" . implode( "','", $countries ) . "')"; } if ( empty( $conditions ) ) { return ''; } // Combine conditions with OR for includes, AND for excludes. $connector = $is_include ? ' OR ' : ' AND '; return '(' . implode( $connector, $conditions ) . ')'; } /** * Initialize query objects. */ protected function initialize_queries() { $this->clear_all_clauses(); $table_name = self::get_db_table_name(); $this->subquery = new SqlQuery( $this->context . '_subquery' ); $this->subquery->add_sql_clause( 'from', $table_name ); $this->subquery->add_sql_clause( 'select', "{$table_name}.customer_id" ); $this->subquery->add_sql_clause( 'group_by', "{$table_name}.customer_id" ); } }