init
This commit is contained in:
+32
@@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class ContextFactory {
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $woocommerce;
|
||||
|
||||
public function __construct(
|
||||
WooCommerce $woocommerce
|
||||
) {
|
||||
$this->woocommerce = $woocommerce;
|
||||
}
|
||||
|
||||
/** @return mixed[] */
|
||||
public function getContextData(): array {
|
||||
|
||||
if (!$this->woocommerce->isWooCommerceActive()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$context = [
|
||||
'order_statuses' => $this->woocommerce->wcGetOrderStatuses(),
|
||||
'review_ratings_enabled' => $this->woocommerce->wcReviewRatingsEnabled(),
|
||||
];
|
||||
return $context;
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
||||
|
||||
class CustomerFieldsFactory {
|
||||
/** @var CustomerOrderFieldsFactory */
|
||||
private $customerOrderFieldsFactory;
|
||||
|
||||
/** @var CustomerReviewFieldsFactory */
|
||||
private $customerReviewFieldsFactory;
|
||||
|
||||
public function __construct(
|
||||
CustomerOrderFieldsFactory $customerOrderFieldsFactory,
|
||||
CustomerReviewFieldsFactory $customerReviewFieldsFactory
|
||||
) {
|
||||
$this->customerOrderFieldsFactory = $customerOrderFieldsFactory;
|
||||
$this->customerReviewFieldsFactory = $customerReviewFieldsFactory;
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return array_merge(
|
||||
[
|
||||
new Field(
|
||||
'woocommerce:customer:billing-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing company', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingCompany();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing phone', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingPhone();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing city', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingCity();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing postcode', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingPostcode();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing state/county', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingState();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-country',
|
||||
Field::TYPE_ENUM,
|
||||
__('Billing country', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingCountry();
|
||||
},
|
||||
[
|
||||
'options' => $this->getBillingCountryOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping company', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingCompany();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping phone', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingPhone();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping city', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingCity();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping postcode', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingPostcode();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping state/county', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingState();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-country',
|
||||
Field::TYPE_ENUM,
|
||||
__('Shipping country', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingCountry();
|
||||
},
|
||||
[
|
||||
'options' => $this->getShippingCountryOptions(),
|
||||
]
|
||||
),
|
||||
],
|
||||
$this->customerOrderFieldsFactory->getFields(),
|
||||
$this->customerReviewFieldsFactory->getFields()
|
||||
);
|
||||
}
|
||||
|
||||
private function getBillingCountryOptions(): array {
|
||||
$options = [];
|
||||
foreach (WC()->countries->get_allowed_countries() as $code => $name) {
|
||||
$options[] = ['id' => $code, 'name' => $name];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getShippingCountryOptions(): array {
|
||||
$options = [];
|
||||
foreach (WC()->countries->get_shipping_countries() as $code => $name) {
|
||||
$options[] = ['id' => $code, 'name' => $name];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
<?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");
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
||||
use WC_Customer;
|
||||
|
||||
class CustomerReviewFieldsFactory {
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wordPress
|
||||
) {
|
||||
$this->wordPress = $wordPress;
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return [
|
||||
new Field(
|
||||
'woocommerce:customer:review-count',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Review count', 'mailpoet'),
|
||||
function (CustomerPayload $payload, array $params = []) {
|
||||
$customer = $payload->getCustomer();
|
||||
if (!$customer) {
|
||||
return 0;
|
||||
}
|
||||
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
||||
return $this->getUniqueProductReviewCount($customer, $inTheLastSeconds);
|
||||
},
|
||||
[
|
||||
'params' => ['in_the_last'],
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:last-review-date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Last review date', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $this->getLastReviewDate($customer) : null;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the customer's review count excluding multiple reviews on the same product.
|
||||
* Inspired by AutomateWoo implementation.
|
||||
*/
|
||||
private function getUniqueProductReviewCount(WC_Customer $customer, int $inTheLastSeconds = null): int {
|
||||
global $wpdb;
|
||||
|
||||
$inTheLastFilter = isset($inTheLastSeconds) ? 'AND c.comment_date_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)' : '';
|
||||
|
||||
return (int)$wpdb->get_var(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The number of replacements is dynamic.
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(DISTINCT c.comment_post_ID) FROM {$wpdb->comments} c
|
||||
JOIN {$wpdb->posts} p ON c.comment_post_ID = p.ID
|
||||
WHERE p.post_type = 'product'
|
||||
AND c.comment_parent = 0
|
||||
AND c.comment_approved = 1
|
||||
AND c.comment_type = 'review'
|
||||
AND (c.user_ID = %d OR c.comment_author_email = %s)
|
||||
" . $inTheLastFilter . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The condition uses placeholders. */ "
|
||||
",
|
||||
array_merge(
|
||||
[
|
||||
$customer->get_id(),
|
||||
$customer->get_email(),
|
||||
],
|
||||
$inTheLastSeconds ? [$inTheLastSeconds] : []
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function getLastReviewDate(WC_Customer $customer): ?DateTimeImmutable {
|
||||
global $wpdb;
|
||||
|
||||
$date = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT c.comment_date
|
||||
FROM {$wpdb->comments} c
|
||||
JOIN {$wpdb->posts} p ON c.comment_post_ID = p.ID
|
||||
WHERE p.post_type = 'product'
|
||||
AND c.comment_parent = 0
|
||||
AND c.comment_approved = 1
|
||||
AND c.comment_type = 'review'
|
||||
AND (c.user_ID = %d OR c.comment_author_email = %s)
|
||||
ORDER BY c.comment_date DESC
|
||||
LIMIT 1
|
||||
",
|
||||
[$customer->get_id(), $customer->get_email()]
|
||||
)
|
||||
);
|
||||
return $date ? new DateTimeImmutable($date, $this->wordPress->wpTimezone()) : null;
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
||||
use WC_Order;
|
||||
use WC_Order_Item_Product;
|
||||
use WC_Payment_Gateway;
|
||||
use WC_Product;
|
||||
use WP_Post;
|
||||
|
||||
class OrderFieldsFactory {
|
||||
/** @var TermOptionsBuilder */
|
||||
private $termOptionsBuilder;
|
||||
|
||||
/** @var TermParentsLoader */
|
||||
private $termParentsLoader;
|
||||
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $wooCommerce;
|
||||
|
||||
public function __construct(
|
||||
TermOptionsBuilder $termOptionsBuilder,
|
||||
TermParentsLoader $termParentsLoader,
|
||||
WordPress $wordPress,
|
||||
WooCommerce $wooCommerce
|
||||
) {
|
||||
$this->termOptionsBuilder = $termOptionsBuilder;
|
||||
$this->termParentsLoader = $termParentsLoader;
|
||||
$this->wordPress = $wordPress;
|
||||
$this->wooCommerce = $wooCommerce;
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return array_merge(
|
||||
[
|
||||
new Field(
|
||||
'woocommerce:order:billing-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing company', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_company();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:billing-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing phone', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_phone();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:billing-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing city', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_city();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:billing-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing postcode', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_postcode();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:billing-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing state/county', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_state();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:billing-country',
|
||||
Field::TYPE_ENUM,
|
||||
__('Billing country', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_country();
|
||||
},
|
||||
[
|
||||
'options' => $this->getBillingCountryOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping company', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_company();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping phone', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_phone();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping city', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_city();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping postcode', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_postcode();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping state/county', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_state();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-country',
|
||||
Field::TYPE_ENUM,
|
||||
__('Shipping country', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_country();
|
||||
},
|
||||
[
|
||||
'options' => $this->getShippingCountryOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:created-date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Created date', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_date_created();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:paid-date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Paid date', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_date_paid();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:customer-note',
|
||||
Field::TYPE_STRING,
|
||||
__('Customer provided note', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_customer_note();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:payment-method',
|
||||
Field::TYPE_ENUM,
|
||||
__('Payment method', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_payment_method();
|
||||
},
|
||||
[
|
||||
'options' => $this->getOrderPaymentOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:status',
|
||||
Field::TYPE_ENUM,
|
||||
__('Status', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_status();
|
||||
},
|
||||
[
|
||||
'options' => $this->getOrderStatusOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:total',
|
||||
Field::TYPE_NUMBER,
|
||||
__('Total', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return (float)$payload->getOrder()->get_total();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:coupons',
|
||||
Field::TYPE_ENUM_ARRAY,
|
||||
__('Used coupons', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_coupon_codes();
|
||||
},
|
||||
[
|
||||
'options' => $this->getCouponOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:is-first-order',
|
||||
Field::TYPE_BOOLEAN,
|
||||
__('Is first order', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
$order = $payload->getOrder();
|
||||
return !$this->previousOrderExists($order);
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:categories',
|
||||
Field::TYPE_ENUM_ARRAY,
|
||||
__('Categories', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
$products = $this->getProducts($payload->getOrder());
|
||||
$categoryIds = [];
|
||||
foreach ($products as $product) {
|
||||
$categoryIds = array_merge($categoryIds, $product->get_category_ids());
|
||||
}
|
||||
$categoryIds = array_merge($categoryIds, $this->termParentsLoader->getParentIds($categoryIds));
|
||||
sort($categoryIds);
|
||||
return array_unique($categoryIds);
|
||||
},
|
||||
[
|
||||
'options' => $this->termOptionsBuilder->getTermOptions('product_cat'),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:tags',
|
||||
Field::TYPE_ENUM_ARRAY,
|
||||
__('Tags', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
$products = $this->getProducts($payload->getOrder());
|
||||
$tagIds = [];
|
||||
foreach ($products as $product) {
|
||||
$tagIds = array_merge($tagIds, $product->get_tag_ids());
|
||||
}
|
||||
sort($tagIds);
|
||||
return array_unique($tagIds);
|
||||
},
|
||||
[
|
||||
'options' => $this->termOptionsBuilder->getTermOptions('product_tag'),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:products',
|
||||
Field::TYPE_ENUM_ARRAY,
|
||||
__('Products', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
$products = $this->getProducts($payload->getOrder());
|
||||
return array_map(function (WC_Product $product) {
|
||||
return $product->get_id();
|
||||
}, $products);
|
||||
},
|
||||
[
|
||||
'options' => $this->getProductOptions(),
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getBillingCountryOptions(): array {
|
||||
$options = [];
|
||||
foreach (WC()->countries->get_allowed_countries() as $code => $name) {
|
||||
$options[] = ['id' => $code, 'name' => $name];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getShippingCountryOptions(): array {
|
||||
$options = [];
|
||||
foreach (WC()->countries->get_shipping_countries() as $code => $name) {
|
||||
$options[] = ['id' => $code, 'name' => $name];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getOrderPaymentOptions(): array {
|
||||
$gateways = WC()->payment_gateways()->get_available_payment_gateways();
|
||||
$options = [];
|
||||
foreach ($gateways as $gateway) {
|
||||
if ($gateway instanceof WC_Payment_Gateway && $gateway->enabled === 'yes') {
|
||||
$options[] = ['id' => $gateway->id, 'name' => $gateway->title];
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getOrderStatusOptions(): array {
|
||||
$statuses = $this->wooCommerce->wcGetOrderStatuses();
|
||||
$options = [];
|
||||
foreach ($statuses as $id => $name) {
|
||||
$options[] = [
|
||||
// WooCommerce order statuses are internally saved with 'wc-' prefix:
|
||||
// https://github.com/woocommerce/woocommerce/blob/9c58f198/plugins/woocommerce/includes/wc-order-functions.php#L98-L109
|
||||
// However, when getting the status from the order object, it doesn't have the prefix.
|
||||
// To make the status codes consistent, we remove the prefix here and only work with unprefixed statuses.
|
||||
'id' => substr($id, 0, 3) === 'wc-' ? substr($id, 3) : $id,
|
||||
'name' => $name,
|
||||
];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getCouponOptions(): array {
|
||||
$coupons = $this->wordPress->getPosts([
|
||||
'post_type' => 'shop_coupon',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'name',
|
||||
'order' => 'asc',
|
||||
]);
|
||||
|
||||
$options = [];
|
||||
foreach ($coupons as $coupon) {
|
||||
if ($coupon instanceof WP_Post) {
|
||||
// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$options[] = ['id' => $coupon->post_title, 'name' => $coupon->post_title];
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function previousOrderExists(WC_Order $order): bool {
|
||||
$dateCreated = $order->get_date_created() ?? new DateTimeImmutable('now', $this->wordPress->wpTimezone());
|
||||
$query = [
|
||||
'date_created' => '<=' . $dateCreated->getTimestamp(),
|
||||
'limit' => 2,
|
||||
'return' => 'ids',
|
||||
];
|
||||
|
||||
if ($order->get_customer_id() > 0) {
|
||||
$query['customer_id'] = $order->get_customer_id();
|
||||
} else {
|
||||
$query['billing_email'] = $order->get_billing_email();
|
||||
}
|
||||
|
||||
$orderIds = (array)$this->wooCommerce->wcGetOrders($query);
|
||||
return count($orderIds) > 1 && min($orderIds) < $order->get_id();
|
||||
}
|
||||
|
||||
/** @return WC_Product[] */
|
||||
private function getProducts(WC_Order $order): array {
|
||||
$products = [];
|
||||
foreach ($order->get_items() as $item) {
|
||||
if (!$item instanceof WC_Order_Item_Product) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$product = $item->get_product();
|
||||
if (!$product instanceof WC_Product) {
|
||||
continue;
|
||||
}
|
||||
if (!$product->is_type('variation')) {
|
||||
$products[] = $product;
|
||||
continue;
|
||||
}
|
||||
|
||||
$parentProduct = $this->wooCommerce->wcGetProduct($product->get_parent_id());
|
||||
if (!$parentProduct instanceof WC_Product) {
|
||||
continue;
|
||||
}
|
||||
$products[] = $parentProduct;
|
||||
}
|
||||
return array_unique($products);
|
||||
}
|
||||
|
||||
private function getProductOptions(): array {
|
||||
global $wpdb;
|
||||
$products = $wpdb->get_results(
|
||||
"
|
||||
SELECT ID, post_title
|
||||
FROM {$wpdb->posts}
|
||||
WHERE post_type = 'product'
|
||||
AND post_status = 'publish'
|
||||
ORDER BY post_title ASC
|
||||
",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map(function ($product) {
|
||||
/** @var array{ID:int, post_title:string} $product */
|
||||
$id = $product['ID'];
|
||||
$title = $product['post_title'];
|
||||
return ['id' => (int)$id, 'name' => "$title (#$id)"];
|
||||
}, (array)$products);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use WP_Error;
|
||||
use WP_Term;
|
||||
|
||||
class TermOptionsBuilder {
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
/** @var array<string, array<array{id: int, name: string}>> */
|
||||
private $cache = [];
|
||||
|
||||
public function __construct(
|
||||
WordPress $wordPress
|
||||
) {
|
||||
$this->wordPress = $wordPress;
|
||||
}
|
||||
|
||||
/** @return array<array{id: int, name: string}> */
|
||||
public function getTermOptions(string $taxonomy): array {
|
||||
if (!isset($this->cache[$taxonomy])) {
|
||||
$this->cache[$taxonomy] = $this->fetchTermOptions($taxonomy);
|
||||
}
|
||||
return $this->cache[$taxonomy];
|
||||
}
|
||||
|
||||
public function resetCache(): void {
|
||||
$this->cache = [];
|
||||
}
|
||||
|
||||
/** @return array<array{id: int, name: string}> */
|
||||
private function fetchTermOptions(string $taxonomy): array {
|
||||
/** @var WP_Term[]|WP_Error $terms */
|
||||
$terms = $this->wordPress->getTerms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'orderby' => 'name']);
|
||||
if ($terms instanceof WP_Error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$termsMap = [];
|
||||
foreach ($terms as $term) {
|
||||
$termsMap[$term->parent][] = $term;
|
||||
}
|
||||
return $this->buildTermsList($termsMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<WP_Term>> $termsMap
|
||||
* @return array<array{id: int, name: string}>
|
||||
*/
|
||||
private function buildTermsList(array $termsMap, int $parentId = 0): array {
|
||||
$list = [];
|
||||
foreach ($termsMap[$parentId] ?? [] as $term) {
|
||||
$id = $term->term_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$list[] = ['id' => $id, 'name' => $term->name];
|
||||
if (isset($termsMap[$id])) {
|
||||
foreach ($this->buildTermsList($termsMap, $id) as $child) {
|
||||
$list[] = ['id' => $child['id'], 'name' => "$term->name | {$child['name']}"];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class TermParentsLoader {
|
||||
/**
|
||||
* @param int[] $termIds
|
||||
* @return int[]
|
||||
*/
|
||||
public function getParentIds(array $termIds): array {
|
||||
global $wpdb;
|
||||
if (count($termIds) === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT DISTINCT tt.parent
|
||||
FROM {$wpdb->term_taxonomy} AS tt
|
||||
WHERE tt.parent != 0
|
||||
AND tt.term_id IN (" . implode(',', array_fill(0, count($termIds), '%s')) . ")
|
||||
",
|
||||
$termIds
|
||||
)
|
||||
);
|
||||
$parentIds = array_map('intval', $result);
|
||||
if (count($parentIds) === 0) {
|
||||
return [];
|
||||
}
|
||||
return array_values(
|
||||
array_unique(
|
||||
array_merge($parentIds, $this->getParentIds($parentIds))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
|
||||
class AbandonedCartPayload implements Payload {
|
||||
|
||||
/** @var \WC_Customer */
|
||||
private $customer;
|
||||
|
||||
/** @var \DateTimeImmutable */
|
||||
private $lastActivityAt;
|
||||
|
||||
/** @var int[] */
|
||||
private $productIds;
|
||||
|
||||
/**
|
||||
* @param \WC_Customer $customer
|
||||
* @param \DateTimeImmutable $lastActivityAt
|
||||
* @param int[] $productIds
|
||||
*/
|
||||
public function __construct(
|
||||
\WC_Customer $customer,
|
||||
\DateTimeImmutable $lastActivityAt,
|
||||
array $productIds
|
||||
) {
|
||||
|
||||
$this->customer = $customer;
|
||||
$this->lastActivityAt = $lastActivityAt;
|
||||
$this->productIds = $productIds;
|
||||
}
|
||||
|
||||
public function getLastActivityAt(): \DateTimeImmutable {
|
||||
return $this->lastActivityAt;
|
||||
}
|
||||
|
||||
public function getCustomer(): \WC_Customer {
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getProductIds(): array {
|
||||
return $this->productIds;
|
||||
}
|
||||
|
||||
public function getTotal(): float {
|
||||
$total = 0.0;
|
||||
foreach ($this->productIds as $productId) {
|
||||
$product = wc_get_product($productId);
|
||||
if ($product) {
|
||||
$total += (float)$product->get_price();
|
||||
}
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use WC_Customer;
|
||||
use WC_Order;
|
||||
|
||||
class CustomerPayload implements Payload {
|
||||
private ?WC_Customer $customer;
|
||||
private ?WC_Order $order;
|
||||
|
||||
public function __construct(
|
||||
WC_Customer $customer = null,
|
||||
WC_Order $order = null
|
||||
) {
|
||||
$this->customer = $customer;
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
public function getBillingCompany(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_company() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_company();
|
||||
}
|
||||
|
||||
public function getBillingPhone(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_phone() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_phone();
|
||||
}
|
||||
|
||||
public function getBillingCity(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_city() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_city();
|
||||
}
|
||||
|
||||
public function getBillingPostcode(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_postcode() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_postcode();
|
||||
}
|
||||
|
||||
public function getBillingState(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_state() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_state();
|
||||
}
|
||||
|
||||
public function getBillingCountry(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_country() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_country();
|
||||
}
|
||||
|
||||
public function getShippingCompany(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_company() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_company();
|
||||
}
|
||||
|
||||
public function getShippingPhone(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_phone() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_phone();
|
||||
}
|
||||
|
||||
public function getShippingCity(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_city() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_city();
|
||||
}
|
||||
|
||||
public function getShippingPostcode(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_postcode() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_postcode();
|
||||
}
|
||||
|
||||
public function getShippingState(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_state() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_state();
|
||||
}
|
||||
|
||||
public function getShippingCountry(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_country() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_country();
|
||||
}
|
||||
|
||||
public function getTotalSpent(): float {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order && $this->order->is_paid() ? (float)$this->order->get_total() : 0.0;
|
||||
}
|
||||
return (float)$this->customer->get_total_spent();
|
||||
}
|
||||
|
||||
public function getAverageSpent(): float {
|
||||
$totalSpent = $this->getTotalSpent();
|
||||
$orderCount = $this->getOrderCount();
|
||||
return $orderCount > 0 ? ($totalSpent / $orderCount) : 0.0;
|
||||
}
|
||||
|
||||
public function getOrderCount(): int {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? 1 : 0;
|
||||
}
|
||||
return (int)$this->customer->get_order_count();
|
||||
}
|
||||
|
||||
public function getCustomer(): ?WC_Customer {
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function getOrder(): ?WC_Order {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->customer ? $this->customer->get_id() : 0;
|
||||
}
|
||||
|
||||
/** @phpstan-assert-if-true null $this->customer */
|
||||
public function isGuest(): bool {
|
||||
return $this->customer === null;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
|
||||
class OrderPayload implements Payload {
|
||||
|
||||
/** @var \WC_Order */
|
||||
private $order;
|
||||
|
||||
public function __construct(
|
||||
\WC_Order $order
|
||||
) {
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
public function getOrder(): \WC_Order {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
public function getEmail(): string {
|
||||
return $this->order->get_billing_email();
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->order->get_id();
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
|
||||
class OrderStatusChangePayload implements Payload {
|
||||
|
||||
/** @var string */
|
||||
private $from;
|
||||
|
||||
/** @var string */
|
||||
private $to;
|
||||
|
||||
public function __construct(
|
||||
string $from,
|
||||
string $to
|
||||
) {
|
||||
$this->from = $from;
|
||||
$this->to = $to;
|
||||
}
|
||||
|
||||
public function getFrom(): string {
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
public function getTo(): string {
|
||||
return $this->to;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\SubjectTransformers;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Subject;
|
||||
use MailPoet\Automation\Engine\Integration\SubjectTransformer;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject;
|
||||
use MailPoet\Automation\Integrations\WordPress\Subjects\UserSubject;
|
||||
|
||||
class WordPressUserSubjectToWooCommerceCustomerSubjectTransformer implements SubjectTransformer {
|
||||
public function accepts(): string {
|
||||
return UserSubject::KEY;
|
||||
}
|
||||
|
||||
public function returns(): string {
|
||||
return CustomerSubject::KEY;
|
||||
}
|
||||
|
||||
public function transform(Subject $data): Subject {
|
||||
if ($this->accepts() !== $data->getKey()) {
|
||||
throw new \InvalidArgumentException('Invalid subject type');
|
||||
}
|
||||
return new Subject(CustomerSubject::KEY, ['customer_id' => $data->getArgs()['user_id']]);
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Subjects;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\AbandonedCartPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
/**
|
||||
* @implements Subject<AbandonedCartPayload>
|
||||
*/
|
||||
class AbandonedCartSubject implements Subject {
|
||||
const KEY = 'woocommerce:abandoned_cart';
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $woocommerceHelper;
|
||||
|
||||
public function __construct(
|
||||
WooCommerce $woocommerceHelper
|
||||
) {
|
||||
$this->woocommerceHelper = $woocommerceHelper;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation subject (entity entering automation) title
|
||||
return __('WooCommerce abandoned cart', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'user_id' => Builder::integer()->required(),
|
||||
'last_activity_at' => Builder::string()->required()->default(30),
|
||||
'product_ids' => Builder::array(Builder::integer())->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPayload(SubjectData $subjectData): Payload {
|
||||
if (!$this->woocommerceHelper->isWooCommerceActive()) {
|
||||
throw InvalidStateException::create()->withMessage('WooCommerce is not active');
|
||||
}
|
||||
$lastActivityAt = \DateTimeImmutable::createFromFormat(\DateTime::W3C, $subjectData->getArgs()['last_activity_at']);
|
||||
if (!$lastActivityAt) {
|
||||
throw InvalidStateException::create()->withMessage('Invalid abandoned cart time');
|
||||
}
|
||||
|
||||
$customer = new \WC_Customer($subjectData->getArgs()['user_id']);
|
||||
|
||||
return new AbandonedCartPayload($customer, $lastActivityAt, $subjectData->getArgs()['product_ids']);
|
||||
}
|
||||
|
||||
public function getFields(): array {
|
||||
return [
|
||||
new Field(
|
||||
'woocommerce:cart:cart-total',
|
||||
Field::TYPE_NUMBER,
|
||||
__('Cart total', 'mailpoet'),
|
||||
function (AbandonedCartPayload $payload) {
|
||||
return $payload->getTotal();
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Subjects;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Fields\CustomerFieldsFactory;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
||||
use MailPoet\NotFoundException;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
use WC_Customer;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* @implements Subject<CustomerPayload>
|
||||
*/
|
||||
class CustomerSubject implements Subject {
|
||||
const KEY = 'woocommerce:customer';
|
||||
|
||||
/** @var CustomerFieldsFactory */
|
||||
private $customerFieldsFactory;
|
||||
|
||||
public function __construct(
|
||||
CustomerFieldsFactory $customerFieldsFactory
|
||||
) {
|
||||
$this->customerFieldsFactory = $customerFieldsFactory;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation subject (entity entering automation) title
|
||||
return __('WooCommerce customer', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'customer_id' => Builder::integer()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPayload(SubjectData $subjectData): Payload {
|
||||
$args = $subjectData->getArgs();
|
||||
$customerId = isset($args['customer_id']) ? (int)$args['customer_id'] : null;
|
||||
$orderId = isset($args['order_id']) ? (int)$args['order_id'] : null;
|
||||
|
||||
$order = $orderId === null ? null : wc_get_order($orderId);
|
||||
$order = $order instanceof WC_Order ? $order : null;
|
||||
|
||||
if (!$customerId) {
|
||||
return new CustomerPayload(null, $order);
|
||||
}
|
||||
|
||||
$customer = new WC_Customer($customerId);
|
||||
if (!$customer->get_id()) {
|
||||
// translators: %d is the ID of the customer.
|
||||
throw NotFoundException::create()->withMessage(sprintf(__("Customer with ID '%d' not found.", 'mailpoet'), $customerId));
|
||||
}
|
||||
|
||||
return new CustomerPayload($customer, $order);
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return $this->customerFieldsFactory->getFields();
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Subjects;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderStatusChangePayload;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
/**
|
||||
* @implements Subject<OrderStatusChangePayload>
|
||||
*/
|
||||
class OrderStatusChangeSubject implements Subject {
|
||||
|
||||
const KEY = 'woocommerce:order-status-changed';
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation subject (entity entering automation) title
|
||||
return __('WooCommerce order status change', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'from' => Builder::string()->required(),
|
||||
'to' => Builder::string()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPayload(SubjectData $subjectData): Payload {
|
||||
$from = $subjectData->getArgs()['from'];
|
||||
$to = $subjectData->getArgs()['to'];
|
||||
|
||||
return new OrderStatusChangePayload($from, $to);
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getFields(): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Subjects;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Fields\OrderFieldsFactory;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
||||
use MailPoet\NotFoundException;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
/**
|
||||
* @implements Subject<OrderPayload>
|
||||
*/
|
||||
class OrderSubject implements Subject {
|
||||
|
||||
const KEY = 'woocommerce:order';
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $woocommerce;
|
||||
|
||||
/** @var OrderFieldsFactory */
|
||||
private $orderFieldsFactory;
|
||||
|
||||
public function __construct(
|
||||
OrderFieldsFactory $orderFieldsFactory,
|
||||
WooCommerce $woocommerce
|
||||
) {
|
||||
$this->woocommerce = $woocommerce;
|
||||
$this->orderFieldsFactory = $orderFieldsFactory;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation subject (entity entering automation) title
|
||||
return __('WooCommerce order', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'order_id' => Builder::integer()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPayload(SubjectData $subjectData): Payload {
|
||||
$id = $subjectData->getArgs()['order_id'];
|
||||
$order = $this->woocommerce->wcGetOrder($id);
|
||||
if (!$order instanceof \WC_Order) {
|
||||
// translators: %d is the order ID.
|
||||
throw NotFoundException::create()->withMessage(sprintf(__("Order with ID '%d' not found.", 'mailpoet'), $id));
|
||||
}
|
||||
return new OrderPayload($order);
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getFields(): array {
|
||||
return $this->orderFieldsFactory->getFields();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\AbandonedCart;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\AutomaticEmails\WooCommerce\Events\AbandonedCart;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Cron\Workers\Automations\AbandonedCartWorker;
|
||||
use MailPoet\Entities\ScheduledTaskEntity;
|
||||
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
|
||||
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
|
||||
class AbandonedCartHandler {
|
||||
|
||||
const TASK_ABANDONED_CART = 'automation_abandoned_cart';
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
/** @var ScheduledTasksRepository */
|
||||
private $tasksRepository;
|
||||
|
||||
/** @var ScheduledTaskSubscribersRepository */
|
||||
private $taskSubscribersRepository;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp,
|
||||
ScheduledTasksRepository $tasksRepository,
|
||||
ScheduledTaskSubscribersRepository $taskSubscribersRepository,
|
||||
AutomationStorage $automationStorage
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->tasksRepository = $tasksRepository;
|
||||
$this->taskSubscribersRepository = $taskSubscribersRepository;
|
||||
$this->automationStorage = $automationStorage;
|
||||
}
|
||||
|
||||
public function registerHooks(): void {
|
||||
$this->wp->addAction(
|
||||
AbandonedCart::HOOK_SCHEDULE,
|
||||
[
|
||||
$this,
|
||||
'schedule',
|
||||
],
|
||||
10,
|
||||
2
|
||||
);
|
||||
$this->wp->addAction(
|
||||
AbandonedCart::HOOK_RE_SCHEDULE,
|
||||
[
|
||||
$this,
|
||||
'reschedule',
|
||||
]
|
||||
);
|
||||
$this->wp->addAction(
|
||||
AbandonedCart::HOOK_CANCEL,
|
||||
[
|
||||
$this,
|
||||
'cancel',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SubscriberEntity $subscriber
|
||||
* @param int[] $productIds
|
||||
* @return void
|
||||
*/
|
||||
public function schedule(SubscriberEntity $subscriber, array $productIds) {
|
||||
|
||||
$abandonedCartAutomations = $this->automationStorage->getActiveAutomationsByTriggerKey(AbandonedCartTrigger::KEY);
|
||||
$this->cancel($subscriber);
|
||||
array_map(
|
||||
function (Automation $automation) use ($subscriber, $productIds) {
|
||||
$this->scheduleForSingleAutomation($subscriber, $productIds, $automation);
|
||||
},
|
||||
$abandonedCartAutomations
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SubscriberEntity $subscriber
|
||||
* @param int[] $productIds
|
||||
* @param Automation $automation
|
||||
* @return void
|
||||
* @throws InvalidStateException
|
||||
*/
|
||||
private function scheduleForSingleAutomation(SubscriberEntity $subscriber, array $productIds, Automation $automation) {
|
||||
$trigger = $automation->getTrigger(AbandonedCartTrigger::KEY);
|
||||
if (!$trigger) {
|
||||
throw new InvalidStateException(sprintf('Abandoned cart trigger is missing from automation %d', $automation->getId()));
|
||||
}
|
||||
|
||||
$wait = $trigger->getArgs()['wait'] * 60;
|
||||
$scheduledAt = Carbon::now()->millisecond(0)->addSeconds($wait);
|
||||
$task = new ScheduledTaskEntity();
|
||||
$task->setType(AbandonedCartWorker::TASK_TYPE);
|
||||
|
||||
$lastActivity = Carbon::now()->millisecond(0);
|
||||
$task->setCreatedAt($lastActivity);
|
||||
$task->setPriority(ScheduledTaskEntity::PRIORITY_MEDIUM);
|
||||
$task->setMeta([
|
||||
'product_ids' => $productIds,
|
||||
'automation_id' => $automation->getId(),
|
||||
'automation_version' => $automation->getVersionId(),
|
||||
]);
|
||||
$task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
|
||||
$task->setScheduledAt($scheduledAt);
|
||||
$this->tasksRepository->persist($task);
|
||||
$this->tasksRepository->flush();
|
||||
|
||||
$taskSubscriber = new ScheduledTaskSubscriberEntity($task, $subscriber);
|
||||
$task->getSubscribers()->add($taskSubscriber);
|
||||
$this->taskSubscribersRepository->persist($taskSubscriber);
|
||||
$this->taskSubscribersRepository->flush();
|
||||
}
|
||||
|
||||
public function reschedule(SubscriberEntity $subscriber): void {
|
||||
$tasks = $this->tasksRepository->findByTypeAndSubscriber(AbandonedCartWorker::TASK_TYPE, $subscriber);
|
||||
if (!$tasks) {
|
||||
return;
|
||||
}
|
||||
$this->cancel($subscriber);
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$meta = $task->getMeta();
|
||||
$automation = isset($meta['automation_id']) ? $this->automationStorage->getAutomation((int)$meta['automation_id']) : null;
|
||||
if (!$automation) {
|
||||
continue;
|
||||
}
|
||||
$this->scheduleForSingleAutomation($subscriber, $meta['product_ids'] ?? [], $automation);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function cancel(SubscriberEntity $subscriber): void {
|
||||
$existingTasks = $this->tasksRepository->findByTypeAndSubscriber(AbandonedCartWorker::TASK_TYPE, $subscriber);
|
||||
if (!$existingTasks) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($existingTasks as $task) {
|
||||
if ($task->getStatus() !== ScheduledTaskEntity::STATUS_SCHEDULED) {
|
||||
continue;
|
||||
}
|
||||
foreach ($task->getSubscribers() as $taskSubscriber) {
|
||||
$this->taskSubscribersRepository->remove($taskSubscriber);
|
||||
}
|
||||
$this->tasksRepository->remove($task);
|
||||
}
|
||||
|
||||
$this->tasksRepository->flush();
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\AbandonedCart;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Engine\Data\StepValidationArgs;
|
||||
use MailPoet\Automation\Engine\Data\Subject;
|
||||
use MailPoet\Automation\Engine\Hooks;
|
||||
use MailPoet\Automation\Engine\Integration\Trigger;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationRunStorage;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\MailPoet\Subjects\SegmentSubject;
|
||||
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\AbandonedCartPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\AbandonedCartSubject;
|
||||
use MailPoet\Cron\Workers\Automations\AbandonedCartWorker;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Segments\SegmentsRepository;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
|
||||
class AbandonedCartTrigger implements Trigger {
|
||||
|
||||
const KEY = 'woocommerce:abandoned-cart';
|
||||
|
||||
/** @var AbandonedCartHandler */
|
||||
private $abandonedCartHandler;
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
/** @var AutomationRunStorage */
|
||||
private $automationRunStorage;
|
||||
|
||||
public function __construct(
|
||||
AbandonedCartHandler $abandonedCartHandler,
|
||||
AutomationRunStorage $automationRunStorage,
|
||||
SegmentsRepository $segmentsRepository,
|
||||
WordPress $wp
|
||||
) {
|
||||
$this->abandonedCartHandler = $abandonedCartHandler;
|
||||
$this->automationRunStorage = $automationRunStorage;
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('User abandons cart', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getSubjectKeys(): array {
|
||||
return [
|
||||
SubscriberSubject::KEY,
|
||||
AbandonedCartSubject::KEY,
|
||||
SegmentSubject::KEY,
|
||||
];
|
||||
}
|
||||
|
||||
public function registerHooks(): void {
|
||||
$this->abandonedCartHandler->registerHooks();
|
||||
$this->wp->addAction(
|
||||
AbandonedCartWorker::ACTION,
|
||||
[
|
||||
$this,
|
||||
'handle',
|
||||
],
|
||||
10,
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SubscriberEntity $subscriber
|
||||
* @param int[] $productIds
|
||||
* @param \DateTime $lastAcivityAt
|
||||
* @return void
|
||||
*/
|
||||
public function handle(
|
||||
SubscriberEntity $subscriber,
|
||||
array $productIds,
|
||||
\DateTime $lastAcivityAt
|
||||
): void {
|
||||
|
||||
if (!$productIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wooSegment = $this->segmentsRepository->getWooCommerceSegment();
|
||||
|
||||
$subjects = [
|
||||
new Subject(AbandonedCartSubject::KEY, ['user_id' => $subscriber->getWpUserId(), 'last_activity_at' => $lastAcivityAt->format(\DateTime::W3C), 'product_ids' => $productIds]),
|
||||
new Subject(SubscriberSubject::KEY, ['subscriber_id' => $subscriber->getId()]),
|
||||
new Subject(SegmentSubject::KEY, ['segment_id' => $wooSegment->getId()]),
|
||||
];
|
||||
$this->wp->doAction(Hooks::TRIGGER, $this, $subjects);
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
$abandonedCartSubject = $args->getSingleSubjectEntryByClass(AbandonedCartSubject::class);
|
||||
$abandonedCartPayload = $args->getSinglePayloadByClass(AbandonedCartPayload::class);
|
||||
$lastActivityAt = $abandonedCartPayload->getLastActivityAt();
|
||||
|
||||
$compareDate = Carbon::now()->millisecond(0)->subMinutes($args->getStep()->getArgs()['wait']);
|
||||
if ($lastActivityAt > $compareDate) {
|
||||
return false;
|
||||
}
|
||||
$automation = $args->getAutomation();
|
||||
$existingRuns = $this->automationRunStorage->getCountByAutomationAndSubject(
|
||||
$automation,
|
||||
$abandonedCartSubject->getSubjectData()
|
||||
);
|
||||
if ($existingRuns) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'wait' => Builder::integer()->required()->minimum(1)->default(30),
|
||||
]);
|
||||
}
|
||||
|
||||
public function validate(StepValidationArgs $args): void {
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Control\FilterHandler;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter;
|
||||
use MailPoet\Automation\Engine\Data\FilterGroup;
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Engine\Data\StepValidationArgs;
|
||||
use MailPoet\Automation\Engine\Data\Subject;
|
||||
use MailPoet\Automation\Engine\Hooks;
|
||||
use MailPoet\Automation\Engine\Integration\Trigger;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationRunStorage;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumArrayFilter;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumFilter;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderStatusChangeSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
|
||||
|
||||
class BuysAProductTrigger implements Trigger {
|
||||
public const KEY = 'woocommerce:buys-a-product';
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
/** @var WooCommerceHelper */
|
||||
private $wc;
|
||||
|
||||
/** @var AutomationRunStorage */
|
||||
private $automationRunStorage;
|
||||
|
||||
/** @var FilterHandler */
|
||||
private $filterHandler;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp,
|
||||
WooCommerceHelper $wc,
|
||||
AutomationRunStorage $automationRunStorage,
|
||||
FilterHandler $filterHandler
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->wc = $wc;
|
||||
$this->automationRunStorage = $automationRunStorage;
|
||||
$this->filterHandler = $filterHandler;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Customer buys a product', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'product_ids' => Builder::array(
|
||||
Builder::integer()
|
||||
)->minItems(1)->required(),
|
||||
'to' => Builder::string()->required()->default('wc-completed'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSubjectKeys(): array {
|
||||
return [
|
||||
OrderSubject::KEY,
|
||||
OrderStatusChangeSubject::KEY,
|
||||
CustomerSubject::KEY,
|
||||
];
|
||||
}
|
||||
|
||||
public function validate(StepValidationArgs $args): void {
|
||||
}
|
||||
|
||||
public function registerHooks(): void {
|
||||
$this->wp->addAction(
|
||||
'woocommerce_order_status_changed',
|
||||
[
|
||||
$this,
|
||||
'handle',
|
||||
],
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $orderId
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @return void
|
||||
*/
|
||||
public function handle($orderId, $from, $to): void {
|
||||
$order = $this->wc->wcGetOrder($orderId);
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
$this->wp->doAction(Hooks::TRIGGER, $this, [
|
||||
new Subject(OrderSubject::KEY, ['order_id' => $orderId]),
|
||||
new Subject(CustomerSubject::KEY, ['customer_id' => $order->get_customer_id(), 'order_id' => $orderId]),
|
||||
new Subject(OrderStatusChangeSubject::KEY, ['from' => $from, 'to' => $to]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
|
||||
//Trigger the run only once.
|
||||
$orderSubjectData = $args->getSingleSubjectEntryByClass(OrderSubject::class)->getSubjectData();
|
||||
if ($this->automationRunStorage->getCountByAutomationAndSubject($args->getAutomation(), $orderSubjectData) > 0) {
|
||||
return false;
|
||||
}
|
||||
$group = new FilterGroup(
|
||||
'',
|
||||
FilterGroup::OPERATOR_AND,
|
||||
$this->getFilters($args)
|
||||
);
|
||||
return $this->filterHandler->matchesGroup($group, $args);
|
||||
}
|
||||
|
||||
protected function getFilters(StepRunArgs $args): array {
|
||||
$triggerArgs = $args->getStep()->getArgs();
|
||||
$filters = [
|
||||
Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM_ARRAY,
|
||||
'field_key' => 'woocommerce:order:products',
|
||||
'condition' => EnumArrayFilter::CONDITION_MATCHES_ANY_OF,
|
||||
'args' => [
|
||||
'value' => $triggerArgs['product_ids'] ?? [],
|
||||
],
|
||||
]),
|
||||
];
|
||||
$status = str_replace('wc-', '', $triggerArgs['to'] ?? 'completed');
|
||||
if ($status === 'any') {
|
||||
return $filters;
|
||||
}
|
||||
|
||||
$filters[] = Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM,
|
||||
'field_key' => 'woocommerce:order:status',
|
||||
'condition' => EnumFilter::IS_ANY_OF,
|
||||
'args' => [
|
||||
'value' => [$status],
|
||||
],
|
||||
]);
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter;
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumArrayFilter;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumFilter;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class BuysFromACategoryTrigger extends BuysAProductTrigger {
|
||||
const KEY = 'woocommerce:buys-from-a-category';
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'category_ids' => Builder::array(
|
||||
Builder::integer()
|
||||
)->minItems(1)->required(),
|
||||
'to' => Builder::string()->required()->default('wc-completed'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Customer buys from a category', 'mailpoet');
|
||||
}
|
||||
|
||||
protected function getFilters(StepRunArgs $args): array {
|
||||
$triggerArgs = $args->getStep()->getArgs();
|
||||
$filters = [
|
||||
Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM_ARRAY,
|
||||
'field_key' => 'woocommerce:order:categories',
|
||||
'condition' => EnumArrayFilter::CONDITION_MATCHES_ANY_OF,
|
||||
'args' => [
|
||||
'value' => $triggerArgs['category_ids'] ?? [],
|
||||
],
|
||||
]),
|
||||
];
|
||||
$status = str_replace('wc-', '', $triggerArgs['to'] ?? 'completed');
|
||||
if ($status === 'any') {
|
||||
return $filters;
|
||||
}
|
||||
|
||||
$filters[] = Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM,
|
||||
'field_key' => 'woocommerce:order:status',
|
||||
'condition' => EnumFilter::IS_ANY_OF,
|
||||
'args' => [
|
||||
'value' => [$status],
|
||||
],
|
||||
]);
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter;
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumArrayFilter;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumFilter;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class BuysFromATagTrigger extends BuysAProductTrigger {
|
||||
|
||||
|
||||
const KEY = 'woocommerce:buys-from-a-tag';
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Customer buys from a tag', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'tag_ids' => Builder::array(
|
||||
Builder::integer()
|
||||
)->minItems(1)->required(),
|
||||
'to' => Builder::string()->required()->default('wc-completed'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getFilters(StepRunArgs $args): array {
|
||||
$triggerArgs = $args->getStep()->getArgs();
|
||||
$filters = [
|
||||
Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM_ARRAY,
|
||||
'field_key' => 'woocommerce:order:tags',
|
||||
'condition' => EnumArrayFilter::CONDITION_MATCHES_ANY_OF,
|
||||
'args' => [
|
||||
'value' => $triggerArgs['tag_ids'] ?? [],
|
||||
],
|
||||
]),
|
||||
];
|
||||
$status = str_replace('wc-', '', $triggerArgs['to'] ?? 'completed');
|
||||
if ($status === 'any') {
|
||||
return $filters;
|
||||
}
|
||||
|
||||
$filters[] = Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM,
|
||||
'field_key' => 'woocommerce:order:status',
|
||||
'condition' => EnumFilter::IS_ANY_OF,
|
||||
'args' => [
|
||||
'value' => [$status],
|
||||
],
|
||||
]);
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderStatusChangePayload;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class OrderCancelledTrigger extends OrderStatusChangedTrigger {
|
||||
public function getKey(): string {
|
||||
return 'woocommerce:order-cancelled';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Order cancelled', 'mailpoet');
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
/** @var OrderStatusChangePayload $orderPayload */
|
||||
$orderPayload = $args->getSinglePayloadByClass(OrderStatusChangePayload::class);
|
||||
return $orderPayload->getTo() === 'cancelled';
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object();
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderStatusChangePayload;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class OrderCompletedTrigger extends OrderStatusChangedTrigger {
|
||||
public function getKey(): string {
|
||||
return 'woocommerce:order-completed';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Order completed', 'mailpoet');
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
$orderPayload = $args->getSinglePayloadByClass(OrderStatusChangePayload::class);
|
||||
return $orderPayload->getTo() === 'completed';
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object();
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Engine\Data\StepValidationArgs;
|
||||
use MailPoet\Automation\Engine\Data\Subject;
|
||||
use MailPoet\Automation\Engine\Hooks;
|
||||
use MailPoet\Automation\Engine\Integration\Trigger;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class OrderCreatedTrigger implements Trigger {
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
/** @var int[] */
|
||||
private $processedOrders = [];
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return 'woocommerce:order-created';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Order created', 'mailpoet');
|
||||
}
|
||||
|
||||
public function registerHooks(): void {
|
||||
$this->wp->addAction(
|
||||
'woocommerce_new_order',
|
||||
[
|
||||
$this,
|
||||
'handleCreate',
|
||||
],
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $orderId
|
||||
* @param \WC_Order $order
|
||||
* @return void
|
||||
*/
|
||||
public function handleCreate($orderId, $order) {
|
||||
|
||||
if (in_array($orderId, $this->processedOrders)) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Creating an order via wc_create_order() does not yet set crucial information like the customer's email address.
|
||||
* It just creates the order object and saves it to the database. We need therefore to wait for the order to have at least the billing address stored.
|
||||
**/
|
||||
if (!$order->get_billing_email()) {
|
||||
add_action(
|
||||
'woocommerce_after_order_object_save',
|
||||
function($order) use ($orderId) {
|
||||
if ((int)$orderId !== (int)$order->get_id()) {
|
||||
return;
|
||||
}
|
||||
$this->handleCreate($order->get_id(), $order);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
$this->processedOrders[] = $orderId;
|
||||
$this->wp->doAction(Hooks::TRIGGER, $this, [
|
||||
new Subject(OrderSubject::KEY, ['order_id' => $order->get_id()]),
|
||||
new Subject(CustomerSubject::KEY, ['customer_id' => $order->get_customer_id(), 'order_id' => $order->get_id()]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
/**
|
||||
* If we come to this point we always want to trigger the automation.
|
||||
* The evaluation whether this is a "new" order is done in the handleCreate() method.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object();
|
||||
}
|
||||
|
||||
public function getSubjectKeys(): array {
|
||||
return [
|
||||
OrderSubject::KEY,
|
||||
CustomerSubject::KEY,
|
||||
];
|
||||
}
|
||||
|
||||
public function validate(StepValidationArgs $args): void {
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Engine\Data\StepValidationArgs;
|
||||
use MailPoet\Automation\Engine\Data\Subject;
|
||||
use MailPoet\Automation\Engine\Hooks;
|
||||
use MailPoet\Automation\Engine\Integration\Trigger;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderStatusChangePayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderStatusChangeSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class OrderStatusChangedTrigger implements Trigger {
|
||||
|
||||
/** @var WordPress */
|
||||
protected $wp;
|
||||
|
||||
/** @var WooCommerce */
|
||||
protected $woocommerce;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp,
|
||||
WooCommerce $woocommerceHelper
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->woocommerce = $woocommerceHelper;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return 'woocommerce:order-status-changed';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Order status changed', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'from' => Builder::string()->required()->default('any'),
|
||||
'to' => Builder::string()->required()->default('wc-completed'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSubjectKeys(): array {
|
||||
return [
|
||||
OrderSubject::KEY,
|
||||
OrderStatusChangeSubject::KEY,
|
||||
CustomerSubject::KEY,
|
||||
];
|
||||
}
|
||||
|
||||
public function validate(StepValidationArgs $args): void {
|
||||
}
|
||||
|
||||
public function registerHooks(): void {
|
||||
$this->wp->addAction(
|
||||
'woocommerce_order_status_changed',
|
||||
[
|
||||
$this,
|
||||
'handle',
|
||||
],
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
public function handle(int $orderId, string $oldStatus, string $newStatus): void {
|
||||
$order = $this->woocommerce->wcGetOrder($orderId);
|
||||
if (!$order instanceof \WC_Order) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->wp->doAction(Hooks::TRIGGER, $this, [
|
||||
new Subject(OrderStatusChangeSubject::KEY, ['from' => $oldStatus, 'to' => $newStatus]),
|
||||
new Subject(OrderSubject::KEY, ['order_id' => $order->get_id()]),
|
||||
new Subject(CustomerSubject::KEY, ['customer_id' => $order->get_customer_id(), 'order_id' => $order->get_id()]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
/** @var OrderStatusChangePayload $orderPayload */
|
||||
$orderPayload = $args->getSinglePayloadByClass(OrderStatusChangePayload::class);
|
||||
$triggerArgs = $args->getStep()->getArgs();
|
||||
$configuredFrom = $triggerArgs['from'] ? str_replace('wc-', '', $triggerArgs['from']) : null;
|
||||
$configuredTo = $triggerArgs['to'] ? str_replace('wc-', '', $triggerArgs['to']) : null;
|
||||
if ($configuredFrom !== 'any' && $orderPayload->getFrom() !== $configuredFrom) {
|
||||
return false;
|
||||
}
|
||||
if ($configuredTo !== 'any' && $orderPayload->getTo() !== $configuredTo) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
use stdClass;
|
||||
use WC_Order;
|
||||
|
||||
class WooCommerce {
|
||||
public function isWooCommerceActive(): bool {
|
||||
return class_exists('WooCommerce');
|
||||
}
|
||||
|
||||
public function wcGetIsPaidStatuses(): array {
|
||||
return wc_get_is_paid_statuses();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function wcGetOrderStatuses(): array {
|
||||
return wc_get_order_statuses();
|
||||
}
|
||||
|
||||
public function isWooCommerceCustomOrdersTableEnabled(): bool {
|
||||
return $this->isWooCommerceActive()
|
||||
&& method_exists(OrderUtil::class, 'custom_orders_table_usage_is_enabled')
|
||||
&& OrderUtil::custom_orders_table_usage_is_enabled();
|
||||
}
|
||||
|
||||
/** @return WC_Order[]|stdClass */
|
||||
public function wcGetOrders(array $args = []) {
|
||||
return wc_get_orders($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $product
|
||||
* @return \WC_Product|null|false
|
||||
*/
|
||||
public function wcGetProduct($product) {
|
||||
return wc_get_product($product);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|bool $order
|
||||
* @return bool|\WC_Order|\WC_Order_Refund
|
||||
*/
|
||||
public function wcGetOrder($order = false) {
|
||||
return wc_get_order($order);
|
||||
}
|
||||
|
||||
public function wcGetOrderStatusName(string $status): string {
|
||||
return wc_get_order_status_name($status);
|
||||
}
|
||||
|
||||
public function wcReviewRatingsEnabled(): bool {
|
||||
return wc_review_ratings_enabled();
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\AbandonedCartSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderStatusChangeSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\SubjectTransformers\WordPressUserSubjectToWooCommerceCustomerSubjectTransformer;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\AbandonedCart\AbandonedCartTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysAProductTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromACategoryTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromATagTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCancelledTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCompletedTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCreatedTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderStatusChangedTrigger;
|
||||
|
||||
class WooCommerceIntegration {
|
||||
|
||||
/** @var OrderStatusChangedTrigger */
|
||||
private $orderStatusChangedTrigger;
|
||||
|
||||
/** @var OrderCreatedTrigger */
|
||||
private $orderCreatedTrigger;
|
||||
|
||||
/** @var OrderCompletedTrigger */
|
||||
private $orderCompletedTrigger;
|
||||
|
||||
private $orderCancelledTrigger;
|
||||
|
||||
/** @var AbandonedCartTrigger */
|
||||
private $abandonedCartTrigger;
|
||||
|
||||
/** @var BuysAProductTrigger */
|
||||
private $buysAProductTrigger;
|
||||
|
||||
/** @var BuysFromATagTrigger */
|
||||
private $buysFromATagTrigger;
|
||||
|
||||
/** @var BuysFromACategoryTrigger */
|
||||
private $buysFromACategoryTrigger;
|
||||
|
||||
/** @var AbandonedCartSubject */
|
||||
private $abandonedCartSubject;
|
||||
|
||||
/** @var OrderStatusChangeSubject */
|
||||
private $orderStatusChangeSubject;
|
||||
|
||||
/** @var OrderSubject */
|
||||
private $orderSubject;
|
||||
|
||||
/** @var CustomerSubject */
|
||||
private $customerSubject;
|
||||
|
||||
/** @var ContextFactory */
|
||||
private $contextFactory;
|
||||
|
||||
/** @var WordPressUserSubjectToWooCommerceCustomerSubjectTransformer */
|
||||
private $wordPressUserToWooCommerceCustomerTransformer;
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $wooCommerce;
|
||||
|
||||
public function __construct(
|
||||
OrderStatusChangedTrigger $orderStatusChangedTrigger,
|
||||
OrderCreatedTrigger $orderCreatedTrigger,
|
||||
OrderCompletedTrigger $orderCompletedTrigger,
|
||||
OrderCancelledTrigger $orderCancelledTrigger,
|
||||
AbandonedCartTrigger $abandonedCartTrigger,
|
||||
BuysAProductTrigger $buysAProductTrigger,
|
||||
BuysFromACategoryTrigger $buysFromACategoryTrigger,
|
||||
BuysFromATagTrigger $buysFromATagTrigger,
|
||||
AbandonedCartSubject $abandonedCartSubject,
|
||||
OrderStatusChangeSubject $orderStatusChangeSubject,
|
||||
OrderSubject $orderSubject,
|
||||
CustomerSubject $customerSubject,
|
||||
ContextFactory $contextFactory,
|
||||
WordPressUserSubjectToWooCommerceCustomerSubjectTransformer $wordPressUserToWooCommerceCustomerTransformer,
|
||||
WooCommerce $wooCommerce
|
||||
) {
|
||||
$this->orderStatusChangedTrigger = $orderStatusChangedTrigger;
|
||||
$this->orderCreatedTrigger = $orderCreatedTrigger;
|
||||
$this->orderCompletedTrigger = $orderCompletedTrigger;
|
||||
$this->orderCancelledTrigger = $orderCancelledTrigger;
|
||||
$this->abandonedCartTrigger = $abandonedCartTrigger;
|
||||
$this->buysAProductTrigger = $buysAProductTrigger;
|
||||
$this->buysFromACategoryTrigger = $buysFromACategoryTrigger;
|
||||
$this->buysFromATagTrigger = $buysFromATagTrigger;
|
||||
$this->abandonedCartSubject = $abandonedCartSubject;
|
||||
$this->orderStatusChangeSubject = $orderStatusChangeSubject;
|
||||
$this->orderSubject = $orderSubject;
|
||||
$this->customerSubject = $customerSubject;
|
||||
$this->contextFactory = $contextFactory;
|
||||
$this->wordPressUserToWooCommerceCustomerTransformer = $wordPressUserToWooCommerceCustomerTransformer;
|
||||
$this->wooCommerce = $wooCommerce;
|
||||
}
|
||||
|
||||
public function register(Registry $registry): void {
|
||||
if (!$this->wooCommerce->isWooCommerceActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$registry->addContextFactory('woocommerce', function () {
|
||||
return $this->contextFactory->getContextData();
|
||||
});
|
||||
|
||||
$registry->addSubject($this->abandonedCartSubject);
|
||||
$registry->addSubject($this->orderSubject);
|
||||
$registry->addSubject($this->orderStatusChangeSubject);
|
||||
$registry->addSubject($this->customerSubject);
|
||||
$registry->addTrigger($this->orderStatusChangedTrigger);
|
||||
$registry->addTrigger($this->orderCreatedTrigger);
|
||||
$registry->addTrigger($this->orderCompletedTrigger);
|
||||
$registry->addTrigger($this->orderCancelledTrigger);
|
||||
$registry->addTrigger($this->abandonedCartTrigger);
|
||||
$registry->addTrigger($this->buysAProductTrigger);
|
||||
$registry->addTrigger($this->buysFromACategoryTrigger);
|
||||
$registry->addTrigger($this->buysFromATagTrigger);
|
||||
$registry->addSubjectTransformer($this->wordPressUserToWooCommerceCustomerTransformer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user