AbandonedCart.php
1 year ago
FirstPurchase.php
3 months ago
PurchasedInCategory.php
3 months ago
PurchasedProduct.php
3 months ago
index.php
3 years ago
PurchasedInCategory.php
224 lines
| 1 | <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing |
| 2 | |
| 3 | namespace MailPoet\AutomaticEmails\WooCommerce\Events; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use MailPoet\AutomaticEmails\WooCommerce\WooCommerce; |
| 9 | use MailPoet\DI\ContainerWrapper; |
| 10 | use MailPoet\Entities\NewsletterEntity; |
| 11 | use MailPoet\Entities\NewsletterOptionFieldEntity; |
| 12 | use MailPoet\Entities\SubscriberEntity; |
| 13 | use MailPoet\Logging\LoggerFactory; |
| 14 | use MailPoet\Newsletter\AutomaticEmailsRepository; |
| 15 | use MailPoet\Newsletter\Scheduler\AutomaticEmailScheduler; |
| 16 | use MailPoet\Subscribers\SubscribersRepository; |
| 17 | use MailPoet\Util\Helpers; |
| 18 | use MailPoet\WooCommerce\Helper as WCHelper; |
| 19 | use MailPoet\WP\Functions as WPFunctions; |
| 20 | |
| 21 | class PurchasedInCategory { |
| 22 | const SLUG = 'woocommerce_product_purchased_in_category'; |
| 23 | |
| 24 | /** @var WCHelper */ |
| 25 | private $woocommerceHelper; |
| 26 | |
| 27 | /** @var AutomaticEmailScheduler */ |
| 28 | private $scheduler; |
| 29 | |
| 30 | /** @var LoggerFactory */ |
| 31 | private $loggerFactory; |
| 32 | |
| 33 | /** @var AutomaticEmailsRepository */ |
| 34 | private $repository; |
| 35 | |
| 36 | /** @var SubscribersRepository */ |
| 37 | private $subscribersRepository; |
| 38 | |
| 39 | public function __construct( |
| 40 | ?WCHelper $woocommerceHelper = null |
| 41 | ) { |
| 42 | if ($woocommerceHelper === null) { |
| 43 | $woocommerceHelper = ContainerWrapper::getInstance()->get(WCHelper::class); |
| 44 | } |
| 45 | $this->woocommerceHelper = $woocommerceHelper; |
| 46 | $this->scheduler = ContainerWrapper::getInstance()->get(AutomaticEmailScheduler::class); |
| 47 | $this->loggerFactory = LoggerFactory::getInstance(); |
| 48 | $this->repository = ContainerWrapper::getInstance()->get(AutomaticEmailsRepository::class); |
| 49 | $this->subscribersRepository = ContainerWrapper::getInstance()->get(SubscribersRepository::class); |
| 50 | } |
| 51 | |
| 52 | public function getEventDetails() { |
| 53 | return [ |
| 54 | 'slug' => self::SLUG, |
| 55 | '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'), |
| 56 | 'description' => __('Let MailPoet send an email to customers who purchase a product for the first time in a specific category.', 'mailpoet'), |
| 57 | // translators: %s is the name of the category. |
| 58 | 'listingScheduleDisplayText' => __('Email sent when a customer buys a product in category: %s', 'mailpoet'), |
| 59 | // translators: %s is the name of the category. |
| 60 | 'listingScheduleDisplayTextPlural' => __('Email sent when a customer buys a product in categories: %s', 'mailpoet'), |
| 61 | 'afterDelayText' => __('after a purchase', 'mailpoet'), |
| 62 | 'timeDelayValues' => [ |
| 63 | 'immediate' => [ |
| 64 | 'text' => __('immediately', 'mailpoet'), |
| 65 | 'displayAfterTimeNumberField' => false, |
| 66 | ], |
| 67 | 'minutes' => [ |
| 68 | 'text' => __('minute(s)', 'mailpoet'), |
| 69 | 'displayAfterTimeNumberField' => true, |
| 70 | ], |
| 71 | 'hours' => [ |
| 72 | 'text' => __('hour(s)', 'mailpoet'), |
| 73 | 'displayAfterTimeNumberField' => true, |
| 74 | ], |
| 75 | 'days' => [ |
| 76 | 'text' => __('day(s)', 'mailpoet'), |
| 77 | 'displayAfterTimeNumberField' => true, |
| 78 | ], |
| 79 | 'weeks' => [ |
| 80 | 'text' => __('week(s)', 'mailpoet'), |
| 81 | 'displayAfterTimeNumberField' => true, |
| 82 | ], |
| 83 | ], |
| 84 | 'options' => [ |
| 85 | 'multiple' => true, |
| 86 | 'endpoint' => 'product_categories', |
| 87 | 'placeholder' => _x('Search category', 'Search input for product category (ecommerce)', 'mailpoet'), |
| 88 | ], |
| 89 | ]; |
| 90 | } |
| 91 | |
| 92 | public function init() { |
| 93 | WPFunctions::get()->removeAllFilters('woocommerce_product_purchased_get_categories'); |
| 94 | WPFunctions::get()->addFilter( |
| 95 | 'woocommerce_product_purchased_get_categories', |
| 96 | [$this, 'getCategories'] |
| 97 | ); |
| 98 | |
| 99 | $acceptedOrderStates = WPFunctions::get()->applyFilters('mailpoet_first_purchase_order_states', ['completed', 'processing']); |
| 100 | if (!is_array($acceptedOrderStates)) { |
| 101 | $acceptedOrderStates = ['completed', 'processing']; |
| 102 | } |
| 103 | foreach ($acceptedOrderStates as $state) { |
| 104 | if (!is_string($state)) { |
| 105 | continue; |
| 106 | } |
| 107 | WPFunctions::get()->addAction( |
| 108 | 'woocommerce_order_status_' . $state, |
| 109 | [$this, 'scheduleEmail'], |
| 110 | 10, |
| 111 | 1 |
| 112 | ); |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | public function getCategories($searchQuery) { |
| 117 | $args = [ |
| 118 | 'taxonomy' => 'product_cat', |
| 119 | 'search' => $searchQuery, |
| 120 | 'orderby' => 'name', |
| 121 | 'hierarchical' => 0, |
| 122 | 'hide_empty' => 1, |
| 123 | 'order' => 'ASC', |
| 124 | ]; |
| 125 | $allCategories = get_categories($args); |
| 126 | |
| 127 | return array_map(function($category) { |
| 128 | return [ |
| 129 | 'id' => $category->term_id, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps |
| 130 | 'name' => $category->name, |
| 131 | ]; |
| 132 | }, $allCategories); |
| 133 | } |
| 134 | |
| 135 | public function scheduleEmail($orderId) { |
| 136 | $orderDetails = $this->woocommerceHelper->wcGetOrder($orderId); |
| 137 | if (!$orderDetails || !$orderDetails->get_billing_email()) { |
| 138 | $this->loggerFactory->getLogger(self::SLUG)->info( |
| 139 | 'Email not scheduled because the order customer was not found', |
| 140 | ['order_id' => $orderId] |
| 141 | ); |
| 142 | return; |
| 143 | } |
| 144 | $customerEmail = $orderDetails->get_billing_email(); |
| 145 | |
| 146 | $subscriber = $this->subscribersRepository->getWooCommerceSegmentSubscriber($customerEmail); |
| 147 | |
| 148 | if (!$subscriber instanceof SubscriberEntity) { |
| 149 | $this->loggerFactory->getLogger(self::SLUG)->info( |
| 150 | 'Email not scheduled because the customer was not found as WooCommerce list subscriber', |
| 151 | ['order_id' => $orderId, 'customer_email' => $customerEmail] |
| 152 | ); |
| 153 | return; |
| 154 | } |
| 155 | |
| 156 | $orderedProductCategories = []; |
| 157 | foreach ($orderDetails->get_items() as $orderItemProduct) { |
| 158 | $product = $orderItemProduct->get_product(); |
| 159 | if (!$product instanceof \WC_Product) { |
| 160 | continue; |
| 161 | } |
| 162 | if ($product->get_type() === 'variation') { |
| 163 | // WooCommerce returns a empty list when get_category_ids() is called for a product variation, |
| 164 | // so we need to get the parent product |
| 165 | $product = $this->woocommerceHelper->wcGetProduct($product->get_parent_id()); |
| 166 | } |
| 167 | $orderedProductCategories = array_merge($orderedProductCategories, $product->get_category_ids()); |
| 168 | } |
| 169 | |
| 170 | $schedulingCondition = function(NewsletterEntity $automaticEmail) use ($orderedProductCategories, $subscriber) { |
| 171 | $matchedCategories = $this->getProductCategoryIdsMatchingNewsletterTrigger($automaticEmail, $orderedProductCategories); |
| 172 | if (empty($matchedCategories)) { |
| 173 | return false; |
| 174 | } |
| 175 | |
| 176 | if ($this->repository->wasScheduledForSubscriber((int)$automaticEmail->getId(), (int)$subscriber->getId())) { |
| 177 | $sentAllProducts = $this->repository->alreadySentAllProducts((int)$automaticEmail->getId(), (int)$subscriber->getId(), 'orderedProductCategories', $matchedCategories); |
| 178 | if ($sentAllProducts) return false; |
| 179 | } |
| 180 | |
| 181 | return true; |
| 182 | }; |
| 183 | |
| 184 | $this->loggerFactory->getLogger(self::SLUG)->info( |
| 185 | 'Email scheduled', |
| 186 | [ |
| 187 | 'order_id' => $orderId, |
| 188 | 'customer_email' => $customerEmail, |
| 189 | 'subscriber_id' => $subscriber->getId(), |
| 190 | ] |
| 191 | ); |
| 192 | $this->scheduler->scheduleAutomaticEmail( |
| 193 | WooCommerce::SLUG, |
| 194 | self::SLUG, |
| 195 | $schedulingCondition, |
| 196 | $subscriber, |
| 197 | ['orderedProductCategories' => $orderedProductCategories], |
| 198 | [$this, 'metaModifier'] |
| 199 | ); |
| 200 | } |
| 201 | |
| 202 | public function metaModifier(NewsletterEntity $automaticEmail, array $meta): array { |
| 203 | $orderedProductCategoryIds = $meta['orderedProductCategories'] ?? null; |
| 204 | if (empty($orderedProductCategoryIds)) { |
| 205 | return $meta; |
| 206 | } |
| 207 | $meta['orderedProductCategories'] = $this->getProductCategoryIdsMatchingNewsletterTrigger($automaticEmail, $orderedProductCategoryIds); |
| 208 | |
| 209 | return $meta; |
| 210 | } |
| 211 | |
| 212 | private function getProductCategoryIdsMatchingNewsletterTrigger(NewsletterEntity $automaticEmail, array $orderedCategoryIds): array { |
| 213 | $automaticEmailMetaValue = $automaticEmail->getOptionValue(NewsletterOptionFieldEntity::NAME_META); |
| 214 | $optionValue = Helpers::isJson($automaticEmailMetaValue) ? json_decode($automaticEmailMetaValue, true) : $automaticEmailMetaValue; |
| 215 | |
| 216 | if (!is_array($optionValue) || empty($optionValue['option']) || !is_array($optionValue['option'])) { |
| 217 | return []; |
| 218 | } |
| 219 | $emailTriggeringCategoryIds = array_column($optionValue['option'], 'id'); |
| 220 | |
| 221 | return array_intersect($emailTriggeringCategoryIds, $orderedCategoryIds); |
| 222 | } |
| 223 | } |
| 224 |