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,242 @@
<?php declare(strict_types = 1);
namespace MailPoet\AutomaticEmails\WooCommerce\Events;
if (!defined('ABSPATH')) exit;
use MailPoet\AutomaticEmails\WooCommerce\WooCommerce as WooCommerceEmail;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Newsletter\Scheduler\AutomaticEmailScheduler;
use MailPoet\Statistics\Track\SubscriberActivityTracker;
use MailPoet\Statistics\Track\SubscriberCookie;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
use MailPoet\WP\Functions as WPFunctions;
class AbandonedCart {
const SLUG = 'woocommerce_abandoned_shopping_cart';
const TASK_META_NAME = 'cart_product_ids';
const HOOK_SCHEDULE = 'mailpoet_abandoned_cart_schedule';
const HOOK_RE_SCHEDULE = 'mailpoet_abandoned_cart_reschedule';
const HOOK_CANCEL = 'mailpoet_abandoned_cart_cancel';
/** @var WPFunctions */
private $wp;
/** @var WooCommerceHelper */
private $wooCommerceHelper;
/** @var SubscriberCookie */
private $subscriberCookie;
/** @var AutomaticEmailScheduler */
private $scheduler;
/** @var SubscriberActivityTracker */
private $subscriberActivityTracker;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var bool */
private $loadSavedCartAfterLogin;
public function __construct(
WPFunctions $wp,
WooCommerceHelper $wooCommerceHelper,
SubscriberCookie $subscriberCookie,
SubscriberActivityTracker $subscriberActivityTracker,
AutomaticEmailScheduler $scheduler,
SubscribersRepository $subscribersRepository
) {
$this->wp = $wp;
$this->wooCommerceHelper = $wooCommerceHelper;
$this->subscriberCookie = $subscriberCookie;
$this->subscriberActivityTracker = $subscriberActivityTracker;
$this->scheduler = $scheduler;
$this->subscribersRepository = $subscribersRepository;
}
public function getEventDetails() {
return [
'slug' => self::SLUG,
'title' => _x('Abandoned Shopping Cart', 'This is the name of a type of automatic email for ecommerce. Those emails are sent automatically when a customer adds product to his shopping cart but never complete the checkout process.', 'mailpoet'),
'description' => __('Send an email to logged-in visitors who have items in their shopping carts but left your website without checking out. Can convert up to 5% of abandoned carts.', 'mailpoet'),
'listingScheduleDisplayText' => _x('Send the email when a customer abandons their cart.', 'Description of Abandoned Shopping Cart email', 'mailpoet'),
'afterDelayText' => __('after abandoning the cart', 'mailpoet'),
'badge' => [
'text' => __('Must-have', 'mailpoet'),
'style' => 'red',
],
'timeDelayValues' => [
'minutes' => [
'text' => __('minute(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'hours' => [
'text' => __('hour(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'days' => [
'text' => __('day(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'weeks' => [
'text' => __('week(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
],
'defaultAfterTimeType' => 'minutes',
'schedulingReadMoreLink' => [
'link' => 'https://www.mailpoet.com/blog/abandoned-cart-woocommerce',
'text' => __('We recommend setting up 3 abandoned cart emails. Heres why.', 'mailpoet'),
],
];
}
public function init() {
if (!$this->wooCommerceHelper->isWooCommerceActive()) {
return;
}
// item added to cart (not fired on quantity changes)
$this->wp->addAction(
'woocommerce_add_to_cart',
[$this, 'handleCartChange'],
10
);
// item removed from cart (not fired on quantity changes, not even change to zero)
$this->wp->addAction(
'woocommerce_cart_item_removed',
[$this, 'handleCartChange'],
10
);
// item quantity updated (not fired when quantity updated to zero)
$this->wp->addAction(
'woocommerce_after_cart_item_quantity_update',
[$this, 'handleCartChange'],
10
);
// item quantity set to zero
$this->wp->addAction(
'woocommerce_remove_cart_item',
[$this, 'handleCartChange'],
10
);
// cart emptied (not called when all items removed)
$this->wp->addAction(
'woocommerce_cart_emptied',
[$this, 'handleCartChange'],
10
);
// undo removal of item from cart or cart emptying (does not fire any other cart change hook)
$this->wp->addAction(
'woocommerce_cart_item_restored',
[$this, 'handleCartChange'],
10
);
// we should handle loading cart from session if user logs in
$this->wp->addAction(
'woocommerce_load_cart_from_session',
[$this, 'handleUserLogin'],
10
);
// cart loaded from session (only processed after login, not on every page load)
$this->wp->addAction(
'woocommerce_cart_loaded_from_session',
[$this, 'handleCartChangeOnLogin'],
10
);
$this->subscriberActivityTracker->registerCallback(
'mailpoet_abandoned_cart',
[$this, 'handleSubscriberActivity']
);
}
public function handleCartChange() {
$cart = $this->wooCommerceHelper->WC()->cart;
$currentAction = current_action();
if ($currentAction !== 'woocommerce_cart_emptied' && $cart && !$cart->is_empty()) {
$this->scheduleAbandonedCartEmail($this->getCartProductIds($cart));
} else {
$this->cancelAbandonedCartEmail();
}
}
public function handleUserLogin() {
$wpUserId = $this->wp->getCurrentUserId();
if (!$wpUserId) {
return false;
}
$this->loadSavedCartAfterLogin = (bool)$this->wp->getUserMeta($wpUserId, '_woocommerce_load_saved_cart_after_login', true);
}
public function handleCartChangeOnLogin() {
if (!$this->loadSavedCartAfterLogin) {
return false;
}
$this->handleCartChange();
}
public function handleSubscriberActivity(SubscriberEntity $subscriber) {
// on subscriber activity on site reschedule all currently scheduled (not yet sent) emails for given subscriber
// (it tracks at most once per minute to avoid processing many calls at the same time, i.e. AJAX)
$this->rescheduleAbandonedCartEmail($subscriber);
}
private function getCartProductIds($cart) {
$cartItems = $cart->get_cart() ?: [];
return array_column($cartItems, 'product_id');
}
private function scheduleAbandonedCartEmail(array $cartProductIds = []) {
$subscriber = $this->getSubscriber();
if (!$subscriber) {
return;
}
$this->wp->doAction(self::HOOK_SCHEDULE, $subscriber, $cartProductIds);
$meta = [self::TASK_META_NAME => $cartProductIds];
$this->scheduler->scheduleOrRescheduleAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber, $meta);
}
private function rescheduleAbandonedCartEmail(SubscriberEntity $subscriber) {
$this->wp->doAction(self::HOOK_RE_SCHEDULE, $subscriber);
$this->scheduler->rescheduleAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber);
}
private function cancelAbandonedCartEmail() {
$subscriber = $this->getSubscriber();
if (!$subscriber) {
return;
}
$this->wp->doAction(self::HOOK_CANCEL, $subscriber);
$this->scheduler->cancelAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber);
}
private function getSubscriber(): ?SubscriberEntity {
$wpUser = $this->wp->wpGetCurrentUser();
if ($wpUser->exists()) {
return $this->subscribersRepository->findOneBy(['wpUserId' => $wpUser->ID]);
}
// if user not logged in, try to find subscriber by cookie
$subscriberId = $this->subscriberCookie->getSubscriberId();
if ($subscriberId) {
return $this->subscribersRepository->findOneById($subscriberId);
}
return null;
}
}
@@ -0,0 +1,247 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\AutomaticEmails\WooCommerce\Events;
if (!defined('ABSPATH')) exit;
use MailPoet\AutomaticEmails\WooCommerce\WooCommerce;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Newsletter\AutomaticEmailsRepository;
use MailPoet\Newsletter\Scheduler\AutomaticEmailScheduler;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\WooCommerce\Helper as WCHelper;
use MailPoet\WP\Functions as WPFunctions;
class FirstPurchase {
const SLUG = 'woocommerce_first_purchase';
const ORDER_TOTAL_SHORTCODE = '[woocommerce:order_total]';
const ORDER_DATE_SHORTCODE = '[woocommerce:order_date]';
/**
* @var \MailPoet\WooCommerce\Helper
*/
private $helper;
/** @var AutomaticEmailScheduler */
private $scheduler;
/** @var LoggerFactory */
private $loggerFactory;
/** @var AutomaticEmailsRepository */
private $automaticEmailsRepository;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
WCHelper $helper = null
) {
if ($helper === null) {
$helper = ContainerWrapper::getInstance()->get(WCHelper::class);
}
$this->helper = $helper;
$this->scheduler = ContainerWrapper::getInstance()->get(AutomaticEmailScheduler::class);
$this->loggerFactory = LoggerFactory::getInstance();
$this->automaticEmailsRepository = ContainerWrapper::getInstance()->get(AutomaticEmailsRepository::class);
$this->subscribersRepository = ContainerWrapper::getInstance()->get(SubscribersRepository::class);
}
public function init() {
WPFunctions::get()->addFilter('mailpoet_newsletter_shortcode', [
$this,
'handleOrderTotalShortcode',
], 10, 4);
WPFunctions::get()->addFilter('mailpoet_newsletter_shortcode', [
$this,
'handleOrderDateShortcode',
], 10, 4);
// We have to use a set of states because an order state after checkout differs for different payment methods
$acceptedOrderStates = WPFunctions::get()->applyFilters('mailpoet_first_purchase_order_states', ['completed', 'processing']);
foreach ($acceptedOrderStates as $state) {
WPFunctions::get()->addAction('woocommerce_order_status_' . $state, [
$this,
'scheduleEmailWhenOrderIsPlaced',
], 10, 1);
}
}
public function getEventDetails() {
return [
'slug' => self::SLUG,
'title' => __('First Purchase', 'mailpoet'),
'description' => __('Let MailPoet send an email to customers who make their first purchase.', 'mailpoet'),
'listingScheduleDisplayText' => __('Email sent when a customer makes their first purchase.', 'mailpoet'),
'afterDelayText' => __('after the first purchase', 'mailpoet'),
'badge' => [
'text' => __('Must-have', 'mailpoet'),
'style' => 'red',
],
'timeDelayValues' => [
'immediate' => [
'text' => __('immediately', 'mailpoet'),
'displayAfterTimeNumberField' => false,
],
'minutes' => [
'text' => __('minute(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'hours' => [
'text' => __('hour(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'days' => [
'text' => __('day(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'weeks' => [
'text' => __('week(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
],
'shortcodes' => [
[
'text' => __('Order amount', 'mailpoet'),
'shortcode' => self::ORDER_TOTAL_SHORTCODE,
],
[
'text' => __('Order date', 'mailpoet'),
'shortcode' => self::ORDER_DATE_SHORTCODE,
],
],
];
}
public function handleOrderDateShortcode($shortcode, $newsletter, $subscriber, $queue) {
$result = $shortcode;
if ($shortcode === self::ORDER_DATE_SHORTCODE) {
$defaultValue = WPFunctions::get()->dateI18n(get_option('date_format'));
if (!$queue) {
$result = $defaultValue;
} else {
$meta = $queue->getMeta();
$result = (!empty($meta['order_date'])) ? WPFunctions::get()->dateI18n(get_option('date_format'), $meta['order_date']) : $defaultValue;
}
}
$this->loggerFactory->getLogger(self::SLUG)->info(
'handleOrderDateShortcode called',
[
'newsletter_id' => ($newsletter instanceof NewsletterEntity) ? $newsletter->getId() : null,
'subscriber_id' => ($subscriber instanceof SubscriberEntity) ? $subscriber->getId() : null,
'task_id' => ($queue instanceof SendingQueueEntity) ? (($task = $queue->getTask()) ? $task->getId() : null) : null,
'shortcode' => $shortcode,
'result' => $result,
]
);
return $result;
}
public function handleOrderTotalShortcode($shortcode, $newsletter, $subscriber, $queue) {
$result = $shortcode;
if ($shortcode === self::ORDER_TOTAL_SHORTCODE) {
$defaultValue = $this->helper->wcPrice(0);
if (!$queue) {
$result = $defaultValue;
} else {
$meta = $queue->getMeta();
$result = (!empty($meta['order_amount'])) ? $this->helper->wcPrice($meta['order_amount']) : $defaultValue;
}
}
$this->loggerFactory->getLogger(self::SLUG)->info(
'handleOrderTotalShortcode called',
[
'newsletter_id' => ($newsletter instanceof NewsletterEntity) ? $newsletter->getId() : null,
'subscriber_id' => ($subscriber instanceof SubscriberEntity) ? $subscriber->getId() : null,
'task_id' => ($queue instanceof SendingQueueEntity) ? (($task = $queue->getTask()) ? $task->getId() : null) : null,
'shortcode' => $shortcode,
'result' => $result,
]
);
return $result;
}
public function scheduleEmailWhenOrderIsPlaced($orderId) {
$orderDetails = $this->helper->wcGetOrder($orderId);
if (!$orderDetails || !$orderDetails->get_billing_email()) {
$this->loggerFactory->getLogger(self::SLUG)->info(
'Email not scheduled because the order customer was not found',
['order_id' => $orderId]
);
return;
}
$customerEmail = $orderDetails->get_billing_email();
$customerOrderCount = $this->getCustomerOrderCount($customerEmail);
if ($customerOrderCount > 1) {
$this->loggerFactory->getLogger(self::SLUG)->info(
'Email not scheduled because this is not the first order of the customer',
[
'order_id' => $orderId,
'customer_email' => $customerEmail,
'order_count' => $customerOrderCount,
]
);
return;
}
$meta = [
'order_amount' => $orderDetails->get_total(),
'order_date' => $orderDetails->get_date_created()->getTimestamp(),
'order_id' => $orderDetails->get_id(),
];
$subscriber = $this->subscribersRepository->getWooCommerceSegmentSubscriber($customerEmail);
if (!$subscriber instanceof SubscriberEntity) {
$this->loggerFactory->getLogger(self::SLUG)->info(
'Email not scheduled because the customer was not found as WooCommerce list subscriber',
['order_id' => $orderId, 'customer_email' => $customerEmail]
);
return;
}
$checkEmailWasNotScheduled = function (NewsletterEntity $newsletter) use ($subscriber) {
return !$this->automaticEmailsRepository->wasScheduledForSubscriber((int)$newsletter->getId(), (int)$subscriber->getId());
};
$this->loggerFactory->getLogger(self::SLUG)->info(
'Email scheduled',
[
'order_id' => $orderId,
'customer_email' => $customerEmail,
'subscriber_id' => $subscriber->getId(),
]
);
$this->scheduler->scheduleAutomaticEmail(WooCommerce::SLUG, self::SLUG, $checkEmailWasNotScheduled, $subscriber, $meta);
}
public function getCustomerOrderCount($customerEmail) {
// registered user
$user = WPFunctions::get()->getUserBy('email', $customerEmail);
if ($user) {
return $this->helper->wcGetCustomerOrderCount($user->ID);
}
// guest user
return $this->getGuestCustomerOrderCountByEmail($customerEmail);
}
private function getGuestCustomerOrderCountByEmail(string $customerEmail): int {
$ordersCount = $this->helper->wcGetOrders(
[
'status' => 'all',
'type' => 'shop_order',
'billing_email' => $customerEmail,
'limit' => 1,
'return' => 'ids',
'paginate' => true,
]
)->total;
return intval($ordersCount);
}
}
@@ -0,0 +1,217 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\AutomaticEmails\WooCommerce\Events;
if (!defined('ABSPATH')) exit;
use MailPoet\AutomaticEmails\WooCommerce\WooCommerce;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Newsletter\AutomaticEmailsRepository;
use MailPoet\Newsletter\Scheduler\AutomaticEmailScheduler;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\Helpers;
use MailPoet\WooCommerce\Helper as WCHelper;
use MailPoet\WP\Functions as WPFunctions;
class PurchasedInCategory {
const SLUG = 'woocommerce_product_purchased_in_category';
/** @var WCHelper */
private $woocommerceHelper;
/** @var AutomaticEmailScheduler */
private $scheduler;
/** @var LoggerFactory */
private $loggerFactory;
/** @var AutomaticEmailsRepository */
private $repository;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
WCHelper $woocommerceHelper = null
) {
if ($woocommerceHelper === null) {
$woocommerceHelper = ContainerWrapper::getInstance()->get(WCHelper::class);
}
$this->woocommerceHelper = $woocommerceHelper;
$this->scheduler = ContainerWrapper::getInstance()->get(AutomaticEmailScheduler::class);
$this->loggerFactory = LoggerFactory::getInstance();
$this->repository = ContainerWrapper::getInstance()->get(AutomaticEmailsRepository::class);
$this->subscribersRepository = ContainerWrapper::getInstance()->get(SubscribersRepository::class);
}
public function getEventDetails() {
return [
'slug' => self::SLUG,
'title' => _x('Purchased In This Category', 'This is the name of a type for automatic email for ecommerce. Those emails are sent automatically every time a customer buys for the first time a product in a given category', 'mailpoet'),
'description' => __('Let MailPoet send an email to customers who purchase a product for the first time in a specific category.', 'mailpoet'),
// translators: %s is the name of the category.
'listingScheduleDisplayText' => __('Email sent when a customer buys a product in category: %s', 'mailpoet'),
// translators: %s is the name of the category.
'listingScheduleDisplayTextPlural' => __('Email sent when a customer buys a product in categories: %s', 'mailpoet'),
'afterDelayText' => __('after a purchase', 'mailpoet'),
'timeDelayValues' => [
'immediate' => [
'text' => __('immediately', 'mailpoet'),
'displayAfterTimeNumberField' => false,
],
'minutes' => [
'text' => __('minute(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'hours' => [
'text' => __('hour(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'days' => [
'text' => __('day(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'weeks' => [
'text' => __('week(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
],
'options' => [
'multiple' => true,
'endpoint' => 'product_categories',
'placeholder' => _x('Search category', 'Search input for product category (ecommerce)', 'mailpoet'),
],
];
}
public function init() {
WPFunctions::get()->removeAllFilters('woocommerce_product_purchased_get_categories');
WPFunctions::get()->addFilter(
'woocommerce_product_purchased_get_categories',
[$this, 'getCategories']
);
$acceptedOrderStates = WPFunctions::get()->applyFilters('mailpoet_first_purchase_order_states', ['completed', 'processing']);
foreach ($acceptedOrderStates as $state) {
WPFunctions::get()->addAction(
'woocommerce_order_status_' . $state,
[$this, 'scheduleEmail'],
10,
1
);
}
}
public function getCategories($searchQuery) {
$args = [
'taxonomy' => 'product_cat',
'search' => $searchQuery,
'orderby' => 'name',
'hierarchical' => 0,
'hide_empty' => 1,
'order' => 'ASC',
];
$allCategories = get_categories($args);
return array_map(function($category) {
return [
'id' => $category->term_id, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
'name' => $category->name,
];
}, $allCategories);
}
public function scheduleEmail($orderId) {
$orderDetails = $this->woocommerceHelper->wcGetOrder($orderId);
if (!$orderDetails || !$orderDetails->get_billing_email()) {
$this->loggerFactory->getLogger(self::SLUG)->info(
'Email not scheduled because the order customer was not found',
['order_id' => $orderId]
);
return;
}
$customerEmail = $orderDetails->get_billing_email();
$subscriber = $this->subscribersRepository->getWooCommerceSegmentSubscriber($customerEmail);
if (!$subscriber instanceof SubscriberEntity) {
$this->loggerFactory->getLogger(self::SLUG)->info(
'Email not scheduled because the customer was not found as WooCommerce list subscriber',
['order_id' => $orderId, 'customer_email' => $customerEmail]
);
return;
}
$orderedProductCategories = [];
foreach ($orderDetails->get_items() as $orderItemProduct) {
$product = $orderItemProduct->get_product();
if (!$product instanceof \WC_Product) {
continue;
}
if ($product->get_type() === 'variation') {
// WooCommerce returns a empty list when get_category_ids() is called for a product variation,
// so we need to get the parent product
$product = $this->woocommerceHelper->wcGetProduct($product->get_parent_id());
}
$orderedProductCategories = array_merge($orderedProductCategories, $product->get_category_ids());
}
$schedulingCondition = function(NewsletterEntity $automaticEmail) use ($orderedProductCategories, $subscriber) {
$matchedCategories = $this->getProductCategoryIdsMatchingNewsletterTrigger($automaticEmail, $orderedProductCategories);
if (empty($matchedCategories)) {
return false;
}
if ($this->repository->wasScheduledForSubscriber((int)$automaticEmail->getId(), (int)$subscriber->getId())) {
$sentAllProducts = $this->repository->alreadySentAllProducts((int)$automaticEmail->getId(), (int)$subscriber->getId(), 'orderedProductCategories', $matchedCategories);
if ($sentAllProducts) return false;
}
return true;
};
$this->loggerFactory->getLogger(self::SLUG)->info(
'Email scheduled',
[
'order_id' => $orderId,
'customer_email' => $customerEmail,
'subscriber_id' => $subscriber->getId(),
]
);
$this->scheduler->scheduleAutomaticEmail(
WooCommerce::SLUG,
self::SLUG,
$schedulingCondition,
$subscriber,
['orderedProductCategories' => $orderedProductCategories],
[$this, 'metaModifier']
);
}
public function metaModifier(NewsletterEntity $automaticEmail, array $meta): array {
$orderedProductCategoryIds = $meta['orderedProductCategories'] ?? null;
if (empty($orderedProductCategoryIds)) {
return $meta;
}
$meta['orderedProductCategories'] = $this->getProductCategoryIdsMatchingNewsletterTrigger($automaticEmail, $orderedProductCategoryIds);
return $meta;
}
private function getProductCategoryIdsMatchingNewsletterTrigger(NewsletterEntity $automaticEmail, array $orderedCategoryIds): array {
$automaticEmailMetaValue = $automaticEmail->getOptionValue(NewsletterOptionFieldEntity::NAME_META);
$optionValue = Helpers::isJson($automaticEmailMetaValue) ? json_decode($automaticEmailMetaValue, true) : $automaticEmailMetaValue;
if (!is_array($optionValue) || empty($optionValue['option'])) {
return [];
}
$emailTriggeringCategoryIds = array_column($optionValue['option'], 'id');
return array_intersect($emailTriggeringCategoryIds, $orderedCategoryIds);
}
}
@@ -0,0 +1,220 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\AutomaticEmails\WooCommerce\Events;
if (!defined('ABSPATH')) exit;
use MailPoet\AutomaticEmails\WooCommerce\WooCommerce;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Newsletter\AutomaticEmailsRepository;
use MailPoet\Newsletter\Scheduler\AutomaticEmailScheduler;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\Helpers;
use MailPoet\WooCommerce\Helper as WCHelper;
use MailPoet\WP\Functions as WPFunctions;
class PurchasedProduct {
const SLUG = 'woocommerce_product_purchased';
/**
* @var \MailPoet\WooCommerce\Helper
*/
private $helper;
/** @var AutomaticEmailScheduler */
private $scheduler;
/** @var LoggerFactory */
private $loggerFactory;
/** @var AutomaticEmailsRepository */
private $repository;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
WCHelper $helper = null
) {
if ($helper === null) {
$helper = ContainerWrapper::getInstance()->get(WCHelper::class);
}
$this->helper = $helper;
$this->scheduler = ContainerWrapper::getInstance()->get(AutomaticEmailScheduler::class);
$this->loggerFactory = LoggerFactory::getInstance();
$this->repository = ContainerWrapper::getInstance()->get(AutomaticEmailsRepository::class);
$this->subscribersRepository = ContainerWrapper::getInstance()->get(SubscribersRepository::class);
}
public function init() {
WPFunctions::get()->removeAllFilters('woocommerce_product_purchased_get_products');
WPFunctions::get()->addFilter(
'woocommerce_product_purchased_get_products',
[
$this,
'getProducts',
]
);
$acceptedOrderStates = WPFunctions::get()->applyFilters('mailpoet_first_purchase_order_states', ['completed', 'processing']);
foreach ($acceptedOrderStates as $state) {
WPFunctions::get()->addAction('woocommerce_order_status_' . $state, [
$this,
'scheduleEmailWhenProductIsPurchased',
], 10, 1);
}
}
public function getEventDetails() {
return [
'slug' => self::SLUG,
'title' => __('Purchased This Product', 'mailpoet'),
'description' => __('Let MailPoet send an email to customers who purchase a specific product for the first time.', 'mailpoet'),
// translators: %s is the name of the product.
'listingScheduleDisplayText' => __('Email sent when a customer buys product: %s', 'mailpoet'),
// translators: %s is the name of the products.
'listingScheduleDisplayTextPlural' => __('Email sent when a customer buys products: %s', 'mailpoet'),
'afterDelayText' => __('after a purchase', 'mailpoet'),
'timeDelayValues' => [
'immediate' => [
'text' => __('immediately', 'mailpoet'),
'displayAfterTimeNumberField' => false,
],
'minutes' => [
'text' => __('minute(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'hours' => [
'text' => __('hour(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'days' => [
'text' => __('day(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
'weeks' => [
'text' => __('week(s)', 'mailpoet'),
'displayAfterTimeNumberField' => true,
],
],
'options' => [
'multiple' => true,
'endpoint' => 'products',
'placeholder' => __('Search products', 'mailpoet'),
],
];
}
public function getProducts($productSearchQuery) {
$args = [
'post_type' => 'product',
'post_status' => 'publish',
's' => $productSearchQuery,
'orderby' => 'title',
'order' => 'ASC',
];
$woocommerceProducts = new \WP_Query($args);
$woocommerceProducts = $woocommerceProducts->get_posts();
/** @var \WP_Post[] $woocommerceProducts */
if (empty($woocommerceProducts)) {
$this->loggerFactory->getLogger(self::SLUG)->info(
'no products found',
['search_query' => $productSearchQuery]
);
return;
}
$woocommerceProducts = array_map(function($product) {
return [
'id' => $product->ID,
'name' => $product->post_title, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
];
}, $woocommerceProducts);
return $woocommerceProducts;
}
public function scheduleEmailWhenProductIsPurchased($orderId) {
$orderDetails = $this->helper->wcGetOrder($orderId);
if (!$orderDetails || !$orderDetails->get_billing_email()) {
$this->loggerFactory->getLogger(self::SLUG)->info(
'Email not scheduled because the order customer was not found',
['order_id' => $orderId]
);
return;
}
$customerEmail = $orderDetails->get_billing_email();
$subscriber = $this->subscribersRepository->getWooCommerceSegmentSubscriber($customerEmail);
if (!$subscriber instanceof SubscriberEntity) {
$this->loggerFactory->getLogger(self::SLUG)->info(
'Email not scheduled because the customer was not found as WooCommerce list subscriber',
['order_id' => $orderId, 'customer_email' => $customerEmail]
);
return;
}
$orderedProducts = array_map(function($product) {
return ($product instanceof \WC_Order_Item_Product) ? $product->get_product_id() : null;
}, $orderDetails->get_items());
$orderedProducts = array_values(array_filter($orderedProducts));
$schedulingCondition = function(NewsletterEntity $automaticEmail) use ($orderedProducts, $subscriber) {
$matchedProducts = $this->getProductIdsMatchingNewsletterTrigger($automaticEmail, $orderedProducts);
if (empty($matchedProducts)) {
return false;
}
if ($this->repository->wasScheduledForSubscriber((int)$automaticEmail->getId(), (int)$subscriber->getId())) {
$sentAllProducts = $this->repository->alreadySentAllProducts((int)$automaticEmail->getId(), (int)$subscriber->getId(), 'orderedProducts', $matchedProducts);
if ($sentAllProducts) return false;
}
return true;
};
$this->loggerFactory->getLogger(self::SLUG)->info(
'Email scheduled',
[
'order_id' => $orderId,
'customer_email' => $customerEmail,
'subscriber_id' => $subscriber->getId(),
]
);
return $this->scheduler->scheduleAutomaticEmail(
WooCommerce::SLUG,
self::SLUG,
$schedulingCondition,
$subscriber,
['orderedProducts' => $orderedProducts],
[$this, 'metaModifier']
);
}
public function metaModifier(NewsletterEntity $newsletter, array $meta): array {
$orderedProductIds = $meta['orderedProducts'] ?? null;
if (empty($orderedProductIds)) {
return $meta;
}
$meta['orderedProducts'] = $this->getProductIdsMatchingNewsletterTrigger($newsletter, $orderedProductIds);
return $meta;
}
private function getProductIdsMatchingNewsletterTrigger(NewsletterEntity $automaticEmail, array $orderedProductIds): array {
$automaticEmailMetaValue = $automaticEmail->getOptionValue(NewsletterOptionFieldEntity::NAME_META);
$optionValue = Helpers::isJson($automaticEmailMetaValue) ? json_decode($automaticEmailMetaValue, true) : $automaticEmailMetaValue;
if (!is_array($optionValue) || empty($optionValue['option'])) {
return [];
}
$emailTriggeringProductIds = array_column($optionValue['option'], 'id');
return array_intersect($emailTriggeringProductIds, $orderedProductIds);
}
}
@@ -0,0 +1,122 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\AutomaticEmails\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\AutomaticEmails\AutomaticEmails;
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class WooCommerce {
const SLUG = 'woocommerce';
const EVENTS_FILTER = 'mailpoet_woocommerce_events';
/** @var WooCommerceHelper */
private $woocommerceHelper;
/** @var string[] */
public $availableEvents = [
'AbandonedCart',
'FirstPurchase',
'PurchasedInCategory',
'PurchasedProduct',
];
/** @var bool */
private $woocommerceEnabled;
/** @var WPFunctions */
private $wp;
/** @var WooCommerceEventFactory */
private $eventFactory;
public function __construct(
WPFunctions $wp,
WooCommerceHelper $woocommerceHelper,
WooCommerceEventFactory $eventFactory
) {
$this->wp = $wp;
$this->woocommerceHelper = $woocommerceHelper;
$this->woocommerceEnabled = $this->isWoocommerceEnabled();
$this->eventFactory = $eventFactory;
}
public function init() {
$this->wp->addFilter(
AutomaticEmails::FILTER_PREFIX . self::SLUG,
[
$this,
'setupGroup',
]
);
$this->wp->addFilter(
self::EVENTS_FILTER,
[
$this,
'setupEvents',
]
);
}
public function setupGroup() {
return [
'slug' => self::SLUG,
'title' => __('WooCommerce', 'mailpoet'),
'description' => __('Automatically send an email based on your customers purchase behavior. Enhance your customer service and start increasing sales with WooCommerce follow up emails.', 'mailpoet'),
'events' => $this->wp->applyFilters(self::EVENTS_FILTER, []),
];
}
public function setupEvents($events) {
$customEventDetails = (!$this->woocommerceEnabled) ? [
'actionButtonTitle' => __('WooCommerce is required', 'mailpoet'),
'actionButtonLink' => 'https://wordpress.org/plugins/woocommerce/',
] : [];
foreach ($this->availableEvents as $event) {
$eventInstance = in_array($event, $this->availableEvents, true)
? $this->eventFactory->createEvent($event)
: null;
if (!$eventInstance) {
$this->displayEventWarning($event);
continue;
}
if (method_exists($eventInstance, 'init')) {
$eventInstance->init();
} else {
$this->displayEventWarning($event);
continue;
}
if (method_exists($eventInstance, 'getEventDetails')) {
$eventDetails = array_merge($eventInstance->getEventDetails(), $customEventDetails);
} else {
$this->displayEventWarning($event);
continue;
}
$events[] = $eventDetails;
}
return $events;
}
public function isWoocommerceEnabled() {
return $this->woocommerceHelper->isWooCommerceActive();
}
private function displayEventWarning($event) {
$notice = sprintf(
'%s %s',
// translators: %s is the name of the event.
sprintf(__('WooCommerce %s event is misconfigured.', 'mailpoet'), $event),
__('Please contact our technical support for assistance.', 'mailpoet')
);
Notice::displayWarning($notice);
}
}
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);
namespace MailPoet\AutomaticEmails\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\AutomaticEmails\WooCommerce\Events\AbandonedCart;
use MailPoet\AutomaticEmails\WooCommerce\Events\FirstPurchase;
use MailPoet\AutomaticEmails\WooCommerce\Events\PurchasedInCategory;
use MailPoet\AutomaticEmails\WooCommerce\Events\PurchasedProduct;
use MailPoet\DI\ContainerWrapper;
use MailPoetVendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
class WooCommerceEventFactory {
public const EVENTS_MAP = [
'AbandonedCart' => AbandonedCart::class,
'FirstPurchase' => FirstPurchase::class,
'PurchasedInCategory' => PurchasedInCategory::class,
'PurchasedProduct' => PurchasedProduct::class,
];
/** @var ContainerWrapper */
private $container;
public function __construct(
ContainerWrapper $container
) {
$this->container = $container;
}
/** @return object|null */
public function createEvent(string $eventName) {
$eventClass = self::EVENTS_MAP[$eventName] ?? null;
try {
return $eventClass ? $this->container->get($eventClass) : null;
} catch (ServiceNotFoundException $e) {
return null;
}
}
}
@@ -0,0 +1 @@
<?php