automatewoo/includes/Customer.php
fei-source 47d3c9a8b6 Update to v6.2.2
Source: GrootMade/Festinger Vault
2026-03-15 08:31:15 +08:00

1062 lines
24 KiB
PHP

<?php
namespace AutomateWoo;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
defined( 'ABSPATH' ) || exit;
/**
* @class Customer
*
* This class uses direct DB queries to fetch order data for performance reason.
* The usage of WC_Order_Query is limited and won't return a set of totals.
*
* @since 3.0.0
*/
class Customer extends Abstract_Model_With_Meta_Table {
/** @var string */
public $table_id = 'customers';
/** @var string */
public $object_type = 'customer';
/**
* Customer constructor.
*
* @param bool|int $id
*/
public function __construct( $id = false ) {
if ( $id ) {
$this->get_by( 'id', $id );
}
}
/**
* Returns the ID of the model's meta table.
*
* @return string
*/
public function get_meta_table_id() {
return 'customer-meta';
}
/**
* Get the user ID.
*
* @return int
*/
public function get_user_id() {
return (int) $this->get_prop( 'user_id' );
}
/**
* Set the user ID.
*
* @param mixed $user_id
*/
public function set_user_id( $user_id ) {
$this->set_prop( 'user_id', (int) $user_id );
}
/**
* Get the guest ID.
*
* @return int
*/
public function get_guest_id() {
return (int) $this->get_prop( 'guest_id' );
}
/**
* Set the guest ID.
*
* @param mixed $guest_id
*/
public function set_guest_id( $guest_id ) {
$this->set_prop( 'guest_id', (int) $guest_id );
}
/**
* Returns a unique key that can ID the customer. This is added to the customer upon creation.
*
* @return string
*/
public function get_key() {
return Clean::string( $this->get_prop( 'id_key' ) );
}
/**
* Set a unique ID key.
*
* @param string $key
*/
public function set_key( $key ) {
$this->set_prop( 'id_key', Clean::string( $key ) );
}
/**
* Generates a new key for registered users that don't have one.
*
* @deprecated tracking keys are replaced with $this->get_key()
*
* @since 4.0
* @return string
*/
public function get_tracking_key() {
wc_deprecated_function( __METHOD__, '5.2.0', 'get_key' );
return Clean::string( $this->get_linked_prop( 'tracking_key' ) );
}
/**
* Get the creation date of the customer's last paid order.
*
* @return bool|DateTime
*/
public function get_date_last_purchased() {
return $this->get_date_column( 'last_purchased' );
}
/**
* Set the date of the customer's last paid order.
*
* @param DateTime|string $date
*/
public function set_date_last_purchased( $date ) {
$this->set_date_column( 'last_purchased', $date );
}
/**
* Get the creation date of the customer's first paid order.
*
* @since 4.4
*
* @return DateTime|bool
*/
public function get_date_first_purchased() {
if ( $this->is_registered() ) {
$first_order = aw_get_customer_first_order( $this->get_user_id() );
} else {
$first_order = aw_get_customer_first_order( $this->get_email() );
}
if ( $first_order ) {
return aw_normalize_date( $first_order->get_date_created() );
}
return false;
}
/**
* Takes into account the global optin_mode option.
*
* If the customer is unsubscribed then all workflows will still run but any emails sending to
* this customer will be rejected and marked in the logs.
*
* @return string
*/
public function is_unsubscribed() {
do_action( 'automatewoo/customer/before_is_unsubscribed', $this );
if ( Options::optin_enabled() ) {
$is_unsubscribed = ! $this->get_is_subscribed();
} else { // opt-out
$is_unsubscribed = $this->get_is_unsubscribed();
}
return apply_filters( 'automatewoo/customer/is_unsubscribed', $is_unsubscribed, $this );
}
/**
* Check if the customer is opted in.
*
* @return bool
*/
public function is_opted_in() {
return ! $this->is_unsubscribed();
}
/**
* Check if the customer is opted out.
*
* @return string
*/
public function is_opted_out() {
return $this->is_unsubscribed();
}
/**
* Mark a customer as subscribed
*/
public function opt_in() {
if ( $this->get_is_subscribed() ) {
return; // already subscribed
}
$this->set_is_subscribed( true );
$this->set_date_subscribed( new DateTime() );
$this->set_is_unsubscribed( false );
$this->save();
do_action( 'automatewoo/customer/opted_in', $this );
}
/**
* Mark a customer as unsubscribed
*/
public function opt_out() {
if ( $this->get_is_unsubscribed() ) {
return; // already unsubscribed
}
$this->set_is_unsubscribed( true );
$this->set_date_unsubscribed( new DateTime() );
$this->set_is_subscribed( false );
$this->save();
do_action( 'automatewoo/customer/opted_out', $this );
}
/**
* Set the customer as unsubscribed.
*
* @param bool $unsubscribed
*/
public function set_is_unsubscribed( $unsubscribed ) {
$this->set_prop( 'unsubscribed', aw_bool_int( $unsubscribed ) );
}
/**
* Check if the customer is unsubscribed.
*
* @return bool
*/
public function get_is_unsubscribed() {
return (bool) $this->get_prop( 'unsubscribed' );
}
/**
* Get the date the customer unsubscribed.
*
* @return bool|DateTime
*/
public function get_date_unsubscribed() {
return $this->get_date_column( 'unsubscribed_date' );
}
/**
* Set the date the customer unsubscribed.
*
* @param DateTime|string $date
*/
public function set_date_unsubscribed( $date ) {
$this->set_date_column( 'unsubscribed_date', $date );
}
/**
* Set the customer as subscribed.
*
* @param bool $subscribed
*/
public function set_is_subscribed( $subscribed ) {
$this->set_prop( 'subscribed', aw_bool_int( $subscribed ) );
}
/**
* Check if the customer is subscribed.
*
* @return bool
*/
public function get_is_subscribed() {
return (bool) $this->get_prop( 'subscribed' );
}
/**
* Get the date the customer subscribed.
*
* @return bool|DateTime
*/
public function get_date_subscribed() {
return $this->get_date_column( 'subscribed_date' );
}
/**
* Set the date the customer subscribed.
*
* @param DateTime|string $date
*/
public function set_date_subscribed( $date ) {
$this->set_date_column( 'subscribed_date', $date );
}
/**
* Get the guest object.
*
* @return Guest|false
*/
public function get_guest() {
if ( $this->is_registered() ) {
return false;
}
return Guest_Factory::get( $this->get_guest_id() );
}
/**
* Get the user object.
*
* @return \WP_User
*/
public function get_user() {
return get_userdata( $this->get_user_id() );
}
/**
* Get the cart object.
*
* @return Cart
*/
public function get_cart() {
if ( $this->is_registered() ) {
return Cart_Factory::get_by_user_id( $this->get_user_id() );
} else {
return Cart_Factory::get_by_guest_id( $this->get_guest_id() );
}
}
/**
* Deletes the customer's stored cart.
*
* @since 4.3.0
*/
public function delete_cart() {
$cart = $this->get_cart();
if ( $cart ) {
$cart->delete();
}
}
/**
* Check if the customer is registered.
*
* @return bool
*/
public function is_registered() {
return $this->get_user_id() !== 0;
}
/**
* Get the customer's email.
*
* @return string
*/
public function get_email() {
return Clean::email( $this->get_linked_prop( 'email' ) );
}
/**
* Get the customer's first name.
*
* @return string
*/
public function get_first_name() {
return $this->get_linked_prop( 'first_name' );
}
/**
* Get the customer's last name.
*
* @return string
*/
public function get_last_name() {
return $this->get_linked_prop( 'last_name' );
}
/**
* Get the customer's full name.
*
* @return string
*/
public function get_full_name() {
/* translators: 1: User First name, 2: User Last name */
return trim( sprintf( _x( '%1$s %2$s', 'full name', 'automatewoo' ), $this->get_first_name(), $this->get_last_name() ) );
}
/**
* Get the customer's billing country.
*
* @return string
*/
public function get_billing_country() {
return $this->get_linked_prop( 'billing_country' );
}
/**
* Get the customer's billing state.
*
* @return string
*/
public function get_billing_state() {
return $this->get_linked_prop( 'billing_state' );
}
/**
* Get the customer's billing phone.
*
* @return string
*/
public function get_billing_phone() {
return $this->get_linked_prop( 'billing_phone' );
}
/**
* Get the customer's billing postcode.
*
* @return string
*/
public function get_billing_postcode() {
return $this->get_linked_prop( 'billing_postcode' );
}
/**
* Get the customer's billing city.
*
* @return string
*/
public function get_billing_city() {
return $this->get_linked_prop( 'billing_city' );
}
/**
* Get the customer's billing address 1.
*
* @return string
*/
public function get_billing_address_1() {
return $this->get_linked_prop( 'billing_address_1' );
}
/**
* Get the customer's billing address 2.
*
* @return string
*/
public function get_billing_address_2() {
return $this->get_linked_prop( 'billing_address_2' );
}
/**
* Get the customer's billing company.
*
* @return string
*/
public function get_billing_company() {
return $this->get_linked_prop( 'billing_company' );
}
/**
* Get the customer's billing address.
*
* @param bool $include_name
* @return array
*/
public function get_address( $include_name = true ) {
$args = [];
if ( $include_name ) {
$args['first_name'] = $this->get_first_name();
$args['last_name'] = $this->get_last_name();
}
$args['company'] = $this->get_billing_company();
$args['address_1'] = $this->get_billing_address_1();
$args['address_2'] = $this->get_billing_address_2();
$args['city'] = $this->get_billing_city();
$args['state'] = $this->get_billing_state();
$args['postcode'] = $this->get_billing_postcode();
$args['country'] = $this->get_billing_country();
return $args;
}
/**
* Get the customer's formatted billing address.
*
* @param bool $include_name
* @return string
*/
public function get_formatted_billing_address( $include_name = true ) {
return WC()->countries->get_formatted_address( $this->get_address( $include_name ) );
}
/**
* Get meta value using legacy meta system.
*
* The legacy meta system stored data in the WP user meta table when the customer was registered
* and in the AW guest meta table when the customer was a guest.
*
* It's worth noting that guest meta does not become user meta when a guest creates an account.
*
* @param string $key
* @return mixed
*/
public function get_legacy_meta( $key ) {
if ( ! $key ) {
return false;
}
if ( $this->is_registered() ) {
return get_user_meta( $this->get_user_id(), $key, true );
} else {
$guest = $this->get_guest();
if ( $guest ) {
return $guest->get_meta( $key );
}
}
return false;
}
/**
* Update meta value using legacy meta system.
*
* @see \AutomateWoo\Customer::get_legacy_meta()
*
* @param string $key
* @param mixed $value
* @return mixed
*/
public function update_legacy_meta( $key, $value ) {
if ( ! $key ) {
return false;
}
if ( $this->is_registered() ) {
update_user_meta( $this->get_user_id(), $key, $value );
} else {
$guest = $this->get_guest();
if ( $guest ) {
$guest->update_meta( $key, $value );
}
}
}
/**
* Get count of customer's orders.
*
* Includes orders that match user ID OR billing email.
*
* @return int
*/
public function get_order_count() {
$count = $this->get_meta( 'order_count' );
if ( '' !== $count ) {
return (int) $count;
}
global $wpdb;
$statuses = array_map( 'esc_sql', aw_get_counted_order_statuses() );
if ( HPOS_Helper::is_HPOS_enabled() ) {
$table = OrdersTableDataStore::get_orders_table_name();
$query = "
SELECT COUNT(DISTINCT id)
FROM $table as orders
WHERE orders.type = 'shop_order'
AND orders.status IN ('" . implode( "','", $statuses ) . "')
AND {$this->get_customer_order_sql()}
";
} else {
$query = "
SELECT COUNT(DISTINCT ID)
FROM $wpdb->posts as posts
LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id
WHERE posts.post_type = 'shop_order'
AND posts.post_status IN ('" . implode( "','", $statuses ) . "')
AND {$this->get_customer_order_sql()}
";
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$count = (int) $wpdb->get_var( $wpdb->prepare( $query, $this->get_customer_order_sql_args() ) );
$this->update_meta( 'order_count', $count );
return $count;
}
/**
* Get total spent by the customer.
*
* Includes orders that match user ID OR billing email.
*
* @return float
*/
public function get_total_spent() {
$total = $this->get_meta( 'total_spent' );
if ( '' !== $total ) {
return (float) $total;
}
global $wpdb;
$statuses = array_map( 'aw_add_order_status_prefix', wc_get_is_paid_statuses() );
$statuses = array_map( 'esc_sql', $statuses );
if ( HPOS_Helper::is_HPOS_enabled() ) {
$table = OrdersTableDataStore::get_orders_table_name();
$query = "
SELECT SUM(total_amount)
FROM $table as orders
WHERE orders.type = 'shop_order'
AND orders.status IN ('" . implode( "','", $statuses ) . "')
AND {$this->get_customer_order_sql()}
";
} else {
$query = "
SELECT SUM(order_total) FROM (
SELECT posts.ID as order_id, meta2.meta_value as order_total
FROM $wpdb->posts as posts
LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id
LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id
WHERE posts.post_type = 'shop_order'
AND posts.post_status IN ('" . implode( "','", $statuses ) . "')
AND {$this->get_customer_order_sql()}
AND meta2.meta_key = '_order_total'
GROUP BY order_id
) AS orders_table
";
}
// Use formatting function to round the total
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$total = (float) Format::decimal( $wpdb->get_var( $wpdb->prepare( $query, $this->get_customer_order_sql_args() ) ) );
$this->update_meta( 'total_spent', $total );
return $total;
}
/**
* Get SQL used for customer order queries.
*
* Used to get orders that match user ID OR email.
*
* @since 4.6.0
*
* @return string
*/
protected function get_customer_order_sql() {
if ( HPOS_Helper::is_HPOS_enabled() ) {
$sql = '( orders.billing_email = %s )';
if ( $this->is_registered() ) {
$sql .= 'OR ( orders.customer_id = %s )';
}
} else {
$sql = "( meta.meta_key = '_billing_email' AND meta.meta_value = %s )";
if ( $this->is_registered() ) {
$sql .= "OR ( meta.meta_key = '_customer_user' AND meta.meta_value = %s )";
}
}
if ( $this->is_registered() ) {
return "(( {$sql} ))";
}
return $sql;
}
/**
* Get SQL query args used for customer order queries.
*
* @since 4.6.0
*
* @return array
*/
protected function get_customer_order_sql_args() {
$args = [ $this->get_email() ];
if ( $this->is_registered() ) {
$args[] = $this->get_user_id();
}
return $args;
}
/**
* Get the customer's role.
*
* @return string
*/
public function get_role() {
if ( $this->is_registered() ) {
$user = $this->get_user();
if ( $user ) {
return current( $user->roles );
}
}
return 'guest';
}
/**
* Get the customer's language if site is multilingual.
*
* @return string
*/
public function get_language() {
$lang = '';
if ( Language::is_multilingual() ) {
if ( $this->is_registered() ) {
$lang = Language::get_user_language( $this->get_user() );
} else {
$lang = Language::get_guest_language( $this->get_guest() );
}
}
return apply_filters( 'automatewoo/customer/get_language', $lang, $this );
}
/**
* Gets the user registered date, if the user is registered.
*
* @since 4.4
*
* @return DateTime|bool
*/
public function get_date_registered() {
$user = $this->get_user();
if ( $user ) {
// user_registered is saved in UTC
return aw_normalize_date( $user->user_registered );
}
return false;
}
/**
* No need to save after using this method
*
* @param string $language
*/
public function update_language( $language ) {
if ( ! Language::is_multilingual() || ! $language ) {
return;
}
if ( $this->is_registered() ) {
$user_lang = get_user_meta( $this->get_user_id(), '_aw_persistent_language', true );
if ( $user_lang !== $language ) {
Language::set_user_language( $this->get_user_id(), $language );
}
} else {
$guest = $this->get_guest();
if ( $guest ) {
if ( $guest->get_language() !== $language ) {
$guest->set_language( $language );
$guest->save();
}
}
}
}
/**
* Get product and variation ids of all the customers purchased products
*
* @return array
*/
public function get_purchased_products() {
global $wpdb;
$transient_name = 'aw_cpp_' . md5( $this->get_id() . \WC_Cache_Helper::get_transient_version( 'orders' ) );
$products = get_transient( $transient_name );
if ( $products === false ) {
$statuses = array_map( 'esc_sql', aw_get_counted_order_statuses( true ) );
if ( HPOS_Helper::is_HPOS_enabled() ) {
$orders_table = OrdersTableDataStore::get_orders_table_name();
$query = "
SELECT im.meta_value FROM {$orders_table} as orders
INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON orders.id = i.order_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id
WHERE orders.type = 'shop_order'
AND orders.status IN ('" . implode( "','", $statuses ) . "')
AND im.meta_key IN ( '_product_id', '_variation_id' )
AND im.meta_value != 0
AND {$this->get_customer_order_sql()}
";
} else {
$query = "
SELECT im.meta_value FROM {$wpdb->posts} AS p
INNER JOIN {$wpdb->postmeta} AS meta ON p.ID = meta.post_id
INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id
WHERE p.post_status IN ( '" . implode( "','", $statuses ) . "' )
AND im.meta_key IN ( '_product_id', '_variation_id' )
AND im.meta_value != 0
AND {$this->get_customer_order_sql()}
";
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$result = $wpdb->get_col( $wpdb->prepare( $query, $this->get_customer_order_sql_args() ) );
$products = array_unique( array_map( 'absint', $result ) );
set_transient( $transient_name, $products, DAY_IN_SECONDS * 30 );
}
return $products;
}
/**
* Get the customer's linked properties.
*
* @param string $prop
* @return mixed
*/
public function get_linked_prop( $prop ) {
$guest = false;
$user = false;
if ( $this->is_registered() ) {
$user = $this->get_user();
if ( ! $user ) {
return false;
}
} else {
$guest = $this->get_guest();
if ( ! $guest ) {
return false;
}
}
switch ( $prop ) {
case 'email':
return $this->is_registered() ? $user->user_email : $guest->get_email();
case 'first_name':
return $this->is_registered() ? $user->first_name : $guest->get_first_name();
case 'last_name':
return $this->is_registered() ? $user->last_name : $guest->get_last_name();
case 'billing_country':
return $this->is_registered() ? $user->billing_country : $guest->get_country();
case 'billing_state':
return $this->is_registered() ? $user->billing_state : $guest->get_state();
case 'billing_phone':
return $this->is_registered() ? $user->billing_phone : $guest->get_phone();
case 'billing_company':
return $this->is_registered() ? $user->billing_company : $guest->get_company();
case 'billing_address_1':
return $this->is_registered() ? $user->billing_address_1 : $guest->get_address_1();
case 'billing_address_2':
return $this->is_registered() ? $user->billing_address_2 : $guest->get_address_2();
case 'billing_postcode':
return $this->is_registered() ? $user->billing_postcode : $guest->get_postcode();
case 'billing_city':
return $this->is_registered() ? $user->billing_city : $guest->get_city();
case 'tracking_key':
if ( $this->is_registered() ) {
// not every registered user will have a key
$key = get_user_meta( $this->get_user_id(), 'automatewoo_visitor_key', true );
if ( ! $key ) {
$key = aw_generate_key( 32 );
update_user_meta( $this->get_user_id(), 'automatewoo_visitor_key', $key );
}
return $key;
} else {
// guests are always created with a key
return $guest->get_key();
}
break;
}
}
/**
* Get reviews for our customer.
*
* @since 4.4
*
* @param array $args Arguments array.
*
* @return array|int
*/
public function get_reviews( $args = [] ) {
$query_args = wp_parse_args(
$args,
[
'status' => 'approve',
'count' => false,
'type' => 'review',
'parent' => 0,
]
);
if ( $this->is_registered() ) {
$query_args['user_id'] = $this->get_user_id();
} else {
$query_args['author_email'] = $this->get_email();
}
return get_comments( $query_args );
}
/**
* Get the customer's review count.
*
* NOTE: This excludes multiple reviews of the same product.
*
* @return int
*/
public function get_review_count() {
$cache_group = 'customer_review_count';
if ( Cache::exists( $this->get_id(), $cache_group ) ) {
$count = (int) Cache::get( $this->get_id(), $cache_group );
} else {
$count = $this->calculate_unique_product_review_count();
Cache::set( $this->get_id(), $count, $cache_group );
}
return $count;
}
/**
* Calculate the customer's review count excluding multiple reviews on the same product.
*
* @since 4.5
*
* @return int
*/
public function calculate_unique_product_review_count() {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT comment_post_ID) FROM {$wpdb->comments}
WHERE comment_parent = 0
AND comment_approved = 1
AND comment_type = 'review'
AND (user_ID = %d OR comment_author_email = %s)",
$this->get_user_id(),
$this->get_email()
)
);
}
/**
* Clear customer review count cache.
*
* @since 4.5
*/
public function clear_review_count_cache() {
Cache::delete( $this->get_id(), 'customer_review_count' );
}
/**
* Gets the last review date for the user.
*
* @since 4.4
*
* @return DateTime|bool
*/
public function get_last_review_date() {
$cache_key = 'customer_last_review_date';
$last_comment_date = false;
if ( Temporary_Data::exists( $cache_key, $this->get_id() ) ) {
$last_comment_date = Temporary_Data::get( $cache_key, $this->get_id() );
} else {
$comments = $this->get_reviews( [ 'number' => 1 ] );
if ( ! empty( $comments ) ) {
$last_comment_date = $comments[0]->comment_date_gmt;
Temporary_Data::set( $cache_key, $this->get_id(), $last_comment_date );
}
}
if ( ! $last_comment_date ) {
return false;
}
return new DateTime( $last_comment_date );
}
/**
* Get the date that a workflow last run for the customer.
*
* @since 4.4
*
* @param int|array|Workflow $workflow Workflow object, ID or array of IDs.
*
* @return DateTime|bool
*/
public function get_workflow_last_run_date( $workflow ) {
if ( ! $workflow ) {
return false;
}
$query = new Log_Query();
$query->where_workflow( $workflow );
$query->where_customer_or_legacy_user( $this, true );
$query->set_limit( 1 );
$query->set_ordering( 'date', 'DESC' );
$results = $query->get_results();
if ( $results ) {
return current( $results )->get_date();
}
return false;
}
/**
* Get customer's nth last paid order.
*
* @param int $n
*
* @return \WC_Order|bool
*
* @since 4.8.0
*/
public function get_nth_last_paid_order( $n ) {
$query_args = [
'type' => 'shop_order',
'limit' => 1,
'offset' => $n - 1,
'status' => wc_get_is_paid_statuses(),
'orderby' => 'date',
'order' => 'DESC',
];
if ( $this->is_registered() ) {
$query_args['customer'] = [ $this->get_user_id(), $this->get_email() ];
} else {
$query_args['customer'] = $this->get_email();
}
return current( wc_get_orders( $query_args ) );
}
/**
* Clear cached customer data.
*
* @since 6.1.8
*/
public function clear_cached_data() {
// Clear cached dashboard counts.
Cache::flush_group( 'dashboard' );
}
}