This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -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;
}
}
@@ -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;
}
}
@@ -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");
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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,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;
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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,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']]);
}
}
@@ -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();
}
),
];
}
}
@@ -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();
}
}
@@ -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 [];
}
}
@@ -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,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();
}
}
@@ -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 {
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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 {
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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);
}
}