414 lines
15 KiB
PHP
414 lines
15 KiB
PHP
<?php declare(strict_types = 1);
|
|
|
|
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
|
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
|
|
use DateTimeImmutable;
|
|
use DateTimeZone;
|
|
use MailPoet\Automation\Engine\Data\Field;
|
|
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
|
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
|
use WC_Customer;
|
|
use WC_Order;
|
|
use WC_Order_Item_Product;
|
|
use WC_Product;
|
|
|
|
class CustomerOrderFieldsFactory {
|
|
/** @var WooCommerce */
|
|
private $wooCommerce;
|
|
|
|
/** @var TermOptionsBuilder */
|
|
private $termOptionsBuilder;
|
|
|
|
/** @var TermParentsLoader */
|
|
private $termParentsLoader;
|
|
|
|
public function __construct(
|
|
WooCommerce $wooCommerce,
|
|
TermOptionsBuilder $termOptionsBuilder,
|
|
TermParentsLoader $termParentsLoader
|
|
) {
|
|
$this->wooCommerce = $wooCommerce;
|
|
$this->termOptionsBuilder = $termOptionsBuilder;
|
|
$this->termParentsLoader = $termParentsLoader;
|
|
}
|
|
|
|
/** @return Field[] */
|
|
public function getFields(): array {
|
|
return [
|
|
new Field(
|
|
'woocommerce:customer:spent-total',
|
|
Field::TYPE_NUMBER,
|
|
__('Total spent', 'mailpoet'),
|
|
function (CustomerPayload $payload, array $params = []) {
|
|
$customer = $payload->getCustomer();
|
|
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
|
if (!$customer) {
|
|
$order = $payload->getOrder();
|
|
return $order && $this->isInTheLastSeconds($order, $inTheLastSeconds) ? $payload->getTotalSpent() : 0.0;
|
|
}
|
|
return $inTheLastSeconds === null
|
|
? $payload->getTotalSpent()
|
|
: $this->getRecentSpentTotal($customer, $inTheLastSeconds);
|
|
},
|
|
[
|
|
'params' => ['in_the_last'],
|
|
]
|
|
),
|
|
new Field(
|
|
'woocommerce:customer:spent-average',
|
|
Field::TYPE_NUMBER,
|
|
__('Average spent', 'mailpoet'),
|
|
function (CustomerPayload $payload, array $params = []) {
|
|
$customer = $payload->getCustomer();
|
|
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
|
|
|
if (!$customer) {
|
|
$order = $payload->getOrder();
|
|
return $order && $this->isInTheLastSeconds($order, $inTheLastSeconds) ? $payload->getAverageSpent() : 0.0;
|
|
}
|
|
|
|
if ($inTheLastSeconds === null) {
|
|
return $payload->getAverageSpent();
|
|
} else {
|
|
$totalSpent = $this->getRecentSpentTotal($customer, $inTheLastSeconds);
|
|
$orderCount = $this->getRecentOrderCount($customer, $inTheLastSeconds);
|
|
return $orderCount > 0 ? ($totalSpent / $orderCount) : 0.0;
|
|
}
|
|
},
|
|
[
|
|
'params' => ['in_the_last'],
|
|
]
|
|
),
|
|
new Field(
|
|
'woocommerce:customer:order-count',
|
|
Field::TYPE_INTEGER,
|
|
__('Order count', 'mailpoet'),
|
|
function (CustomerPayload $payload, array $params = []) {
|
|
$customer = $payload->getCustomer();
|
|
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
|
if (!$customer) {
|
|
$order = $payload->getOrder();
|
|
return $order && $this->isInTheLastSeconds($order, $inTheLastSeconds) ? $payload->getOrderCount() : 0;
|
|
}
|
|
return $inTheLastSeconds === null
|
|
? $payload->getOrderCount()
|
|
: $this->getRecentOrderCount($customer, $inTheLastSeconds);
|
|
},
|
|
[
|
|
'params' => ['in_the_last'],
|
|
]
|
|
),
|
|
new Field(
|
|
'woocommerce:customer:first-paid-order-date',
|
|
Field::TYPE_DATETIME,
|
|
__('First paid order date', 'mailpoet'),
|
|
function (CustomerPayload $payload) {
|
|
$customer = $payload->getCustomer();
|
|
if (!$customer) {
|
|
$order = $payload->getOrder();
|
|
return $order && $order->is_paid() ? $order->get_date_created() : null;
|
|
}
|
|
return $this->getPaidOrderDate($customer, true);
|
|
}
|
|
),
|
|
new Field(
|
|
'woocommerce:customer:last-paid-order-date',
|
|
Field::TYPE_DATETIME,
|
|
__('Last paid order date', 'mailpoet'),
|
|
function (CustomerPayload $payload) {
|
|
$customer = $payload->getCustomer();
|
|
if (!$customer) {
|
|
$order = $payload->getOrder();
|
|
return $order && $order->is_paid() ? $order->get_date_created() : null;
|
|
}
|
|
return $this->getPaidOrderDate($customer, false);
|
|
}
|
|
),
|
|
new Field(
|
|
'woocommerce:customer:purchased-categories',
|
|
Field::TYPE_ENUM_ARRAY,
|
|
__('Purchased categories', 'mailpoet'),
|
|
function (CustomerPayload $payload, array $params = []) {
|
|
$customer = $payload->getCustomer();
|
|
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
|
if (!$customer) {
|
|
$order = $payload->getOrder();
|
|
$items = $order && $order->is_paid() && $this->isInTheLastSeconds($order, $inTheLastSeconds) ? $order->get_items() : [];
|
|
$ids = [];
|
|
foreach ($items as $item) {
|
|
$product = $item instanceof WC_Order_Item_Product ? $item->get_product() : null;
|
|
$ids = array_merge($ids, $product instanceof WC_Product ? $product->get_category_ids() : []);
|
|
}
|
|
$ids = array_unique($ids);
|
|
} else {
|
|
$ids = $this->getOrderProductTermIds($customer, 'product_cat', $inTheLastSeconds);
|
|
}
|
|
$ids = array_merge($ids, $this->termParentsLoader->getParentIds($ids));
|
|
sort($ids);
|
|
return $ids;
|
|
},
|
|
[
|
|
'options' => $this->termOptionsBuilder->getTermOptions('product_cat'),
|
|
'params' => ['in_the_last'],
|
|
]
|
|
),
|
|
new Field(
|
|
'woocommerce:customer:purchased-tags',
|
|
Field::TYPE_ENUM_ARRAY,
|
|
__('Purchased tags', 'mailpoet'),
|
|
function (CustomerPayload $payload, array $params = []) {
|
|
$customer = $payload->getCustomer();
|
|
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
|
if (!$customer) {
|
|
$order = $payload->getOrder();
|
|
$items = $order && $order->is_paid() && $this->isInTheLastSeconds($order, $inTheLastSeconds) ? $order->get_items() : [];
|
|
$ids = [];
|
|
foreach ($items as $item) {
|
|
$product = $item instanceof WC_Order_Item_Product ? $item->get_product() : null;
|
|
$ids = array_merge($ids, $product instanceof WC_Product ? $product->get_tag_ids() : []);
|
|
}
|
|
$ids = array_unique($ids);
|
|
} else {
|
|
$ids = $this->getOrderProductTermIds($customer, 'product_tag', $inTheLastSeconds);
|
|
}
|
|
sort($ids);
|
|
return $ids;
|
|
},
|
|
[
|
|
'options' => $this->termOptionsBuilder->getTermOptions('product_tag'),
|
|
'params' => ['in_the_last'],
|
|
]
|
|
),
|
|
];
|
|
}
|
|
|
|
private function getRecentSpentTotal(WC_Customer $customer, int $inTheLastSeconds): float {
|
|
global $wpdb;
|
|
$statuses = array_map(function (string $status) {
|
|
return "wc-$status";
|
|
}, $this->wooCommerce->wcGetIsPaidStatuses());
|
|
|
|
if ($this->wooCommerce->isWooCommerceCustomOrdersTableEnabled()) {
|
|
return (float)$wpdb->get_var(
|
|
$wpdb->prepare(
|
|
'
|
|
SELECT SUM(o.total_amount)
|
|
FROM %i o
|
|
WHERE o.customer_id = %d
|
|
AND o.status IN (' . implode(',', array_fill(0, count($statuses), '%s')) . ')
|
|
AND o.date_created_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)
|
|
',
|
|
array_merge(
|
|
[
|
|
$wpdb->prefix . 'wc_orders',
|
|
$customer->get_id(),
|
|
],
|
|
$statuses,
|
|
[$inTheLastSeconds]
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
return (float)$wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"
|
|
SELECT SUM(pm_total.meta_value)
|
|
FROM {$wpdb->posts} p
|
|
LEFT JOIN {$wpdb->postmeta} pm_user ON p.ID = pm_user.post_id AND pm_user.meta_key = '_customer_user'
|
|
LEFT JOIN {$wpdb->postmeta} pm_total ON p.ID = pm_total.post_id AND pm_total.meta_key = '_order_total'
|
|
WHERE p.post_type = 'shop_order'
|
|
AND p.post_status IN (" . implode(',', array_fill(0, count($statuses), '%s')) . ")
|
|
AND pm_user.meta_value = %d
|
|
AND p.post_date_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)
|
|
",
|
|
array_merge(
|
|
$statuses,
|
|
[$customer->get_id(), $inTheLastSeconds]
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
private function getRecentOrderCount(WC_Customer $customer, int $inTheLastSeconds): int {
|
|
global $wpdb;
|
|
$statuses = array_keys($this->wooCommerce->wcGetOrderStatuses());
|
|
|
|
if ($this->wooCommerce->isWooCommerceCustomOrdersTableEnabled()) {
|
|
return (int)$wpdb->get_var(
|
|
$wpdb->prepare(
|
|
'
|
|
SELECT COUNT(o.id)
|
|
FROM %i o
|
|
WHERE o.customer_id = %d
|
|
AND o.status IN (' . implode(',', array_fill(0, count($statuses), '%s')) . ')
|
|
AND o.date_created_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)
|
|
',
|
|
array_merge(
|
|
[
|
|
$wpdb->prefix . 'wc_orders',
|
|
$customer->get_id(),
|
|
],
|
|
$statuses,
|
|
[$inTheLastSeconds]
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
return (int)$wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"
|
|
SELECT COUNT(p.ID)
|
|
FROM {$wpdb->posts} p
|
|
LEFT JOIN {$wpdb->postmeta} pm_user ON p.ID = pm_user.post_id AND pm_user.meta_key = '_customer_user'
|
|
WHERE p.post_type = 'shop_order'
|
|
AND p.post_status IN (" . implode(',', array_fill(0, count($statuses), '%s')) . ")
|
|
AND pm_user.meta_value = %d
|
|
AND p.post_date_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)
|
|
",
|
|
array_merge(
|
|
$statuses,
|
|
[
|
|
$customer->get_id(),
|
|
$inTheLastSeconds,
|
|
]
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
private function getPaidOrderDate(WC_Customer $customer, bool $fetchFirst): ?DateTimeImmutable {
|
|
global $wpdb;
|
|
$sorting = $fetchFirst ? 'ASC' : 'DESC';
|
|
$statuses = array_map(function (string $status) {
|
|
return "wc-$status";
|
|
}, $this->wooCommerce->wcGetIsPaidStatuses());
|
|
|
|
if ($this->wooCommerce->isWooCommerceCustomOrdersTableEnabled()) {
|
|
$date = $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
'
|
|
SELECT o.date_created_gmt
|
|
FROM %i o
|
|
WHERE o.customer_id = %d
|
|
AND o.status IN (' . implode(',', array_fill(0, count($statuses), '%s')) . ')
|
|
AND o.total_amount > 0
|
|
ORDER BY o.date_created_gmt ' . $sorting /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The argument is safe. */ . '
|
|
LIMIT 1
|
|
',
|
|
array_merge(
|
|
[
|
|
$wpdb->prefix . 'wc_orders',
|
|
$customer->get_id(),
|
|
],
|
|
$statuses
|
|
)
|
|
)
|
|
);
|
|
} else {
|
|
$date = $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"
|
|
SELECT p.post_date_gmt
|
|
FROM {$wpdb->prefix}posts p
|
|
LEFT JOIN {$wpdb->prefix}postmeta pm_total ON p.ID = pm_total.post_id AND pm_total.meta_key = '_order_total'
|
|
LEFT JOIN {$wpdb->prefix}postmeta pm_user ON p.ID = pm_user.post_id AND pm_user.meta_key = '_customer_user'
|
|
WHERE p.post_type = 'shop_order'
|
|
AND p.post_status IN (" . implode(',', array_fill(0, count($statuses), '%s')) . ")
|
|
AND pm_user.meta_value = %d
|
|
AND pm_total.meta_value > 0
|
|
ORDER BY p.post_date_gmt " . $sorting /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The argument is safe. */ . "
|
|
LIMIT 1
|
|
",
|
|
array_merge(
|
|
$statuses,
|
|
[$customer->get_id()]
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
return $date ? new DateTimeImmutable($date, new DateTimeZone('GMT')) : null;
|
|
}
|
|
|
|
private function getOrderProductTermIds(WC_Customer $customer, string $taxonomy, int $inTheLastSeconds = null): array {
|
|
global $wpdb;
|
|
|
|
$statuses = array_map(function (string $status) {
|
|
return "wc-$status";
|
|
}, $this->wooCommerce->wcGetIsPaidStatuses());
|
|
$statusesPlaceholder = implode(',', array_fill(0, count($statuses), '%s'));
|
|
|
|
// get all product categories that the customer has purchased
|
|
if ($this->wooCommerce->isWooCommerceCustomOrdersTableEnabled()) {
|
|
$inTheLastFilter = isset($inTheLastSeconds) ? 'AND o.date_created_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)' : '';
|
|
|
|
$orderIdsSubquery = "
|
|
SELECT o.id
|
|
FROM %i o
|
|
WHERE o.status IN ($statusesPlaceholder)
|
|
AND o.customer_id = %d
|
|
$inTheLastFilter
|
|
";
|
|
$orderIdsSubqueryArgs = array_merge(
|
|
[$wpdb->prefix . 'wc_orders'],
|
|
$statuses,
|
|
[$customer->get_id()],
|
|
$inTheLastSeconds ? [$inTheLastSeconds] : []
|
|
);
|
|
} else {
|
|
$inTheLastFilter = isset($inTheLastSeconds) ? 'AND p.post_date_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)' : '';
|
|
|
|
$orderIdsSubquery = "
|
|
SELECT p.ID
|
|
FROM {$wpdb->posts} p
|
|
LEFT JOIN {$wpdb->postmeta} pm_user ON p.ID = pm_user.post_id AND pm_user.meta_key = '_customer_user'
|
|
WHERE p.post_type = 'shop_order'
|
|
AND p.post_status IN ($statusesPlaceholder)
|
|
AND pm_user.meta_value = %d
|
|
$inTheLastFilter
|
|
";
|
|
$orderIdsSubqueryArgs = array_merge(
|
|
$statuses,
|
|
[$customer->get_id()],
|
|
$inTheLastSeconds ? [$inTheLastSeconds] : []
|
|
);
|
|
}
|
|
|
|
$result = $wpdb->get_col(
|
|
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The number of replacements is dynamic.
|
|
$wpdb->prepare(
|
|
"
|
|
SELECT DISTINCT tt.term_id
|
|
FROM {$wpdb->term_taxonomy} tt
|
|
JOIN %i AS oi ON oi.order_id IN (" . $orderIdsSubquery . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The subquery uses placeholders. */ ") AND oi.order_item_type = 'line_item'
|
|
JOIN %i AS pid ON oi.order_item_id = pid.order_item_id AND pid.meta_key = '_product_id'
|
|
JOIN {$wpdb->posts} p ON pid.meta_value = p.ID
|
|
JOIN {$wpdb->term_relationships} tr ON IF(p.post_type = 'product_variation', p.post_parent, p.ID) = tr.object_id AND tr.term_taxonomy_id = tt.term_taxonomy_id
|
|
WHERE tt.taxonomy = %s
|
|
ORDER BY tt.term_id ASC
|
|
",
|
|
array_merge(
|
|
[$wpdb->prefix . 'woocommerce_order_items'],
|
|
$orderIdsSubqueryArgs,
|
|
[
|
|
$wpdb->prefix . 'woocommerce_order_itemmeta',
|
|
(string)($taxonomy),
|
|
]
|
|
)
|
|
)
|
|
);
|
|
return array_map('intval', $result);
|
|
}
|
|
|
|
private function isInTheLastSeconds(WC_Order $order, ?int $inTheLastSeconds): bool {
|
|
if ($inTheLastSeconds === null) {
|
|
return true;
|
|
}
|
|
return $order->get_date_created() >= new DateTimeImmutable("-$inTheLastSeconds seconds");
|
|
}
|
|
}
|