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
PurchasedProduct.php
227 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 PurchasedProduct { |
| 22 | const SLUG = 'woocommerce_product_purchased'; |
| 23 | /** |
| 24 | * @var \MailPoet\WooCommerce\Helper |
| 25 | */ |
| 26 | private $helper; |
| 27 | |
| 28 | /** @var AutomaticEmailScheduler */ |
| 29 | private $scheduler; |
| 30 | |
| 31 | /** @var LoggerFactory */ |
| 32 | private $loggerFactory; |
| 33 | |
| 34 | /** @var AutomaticEmailsRepository */ |
| 35 | private $repository; |
| 36 | |
| 37 | /** @var SubscribersRepository */ |
| 38 | private $subscribersRepository; |
| 39 | |
| 40 | public function __construct( |
| 41 | ?WCHelper $helper = null |
| 42 | ) { |
| 43 | if ($helper === null) { |
| 44 | $helper = ContainerWrapper::getInstance()->get(WCHelper::class); |
| 45 | } |
| 46 | $this->helper = $helper; |
| 47 | $this->scheduler = ContainerWrapper::getInstance()->get(AutomaticEmailScheduler::class); |
| 48 | $this->loggerFactory = LoggerFactory::getInstance(); |
| 49 | $this->repository = ContainerWrapper::getInstance()->get(AutomaticEmailsRepository::class); |
| 50 | $this->subscribersRepository = ContainerWrapper::getInstance()->get(SubscribersRepository::class); |
| 51 | } |
| 52 | |
| 53 | public function init() { |
| 54 | WPFunctions::get()->removeAllFilters('woocommerce_product_purchased_get_products'); |
| 55 | WPFunctions::get()->addFilter( |
| 56 | 'woocommerce_product_purchased_get_products', |
| 57 | [ |
| 58 | $this, |
| 59 | 'getProducts', |
| 60 | ] |
| 61 | ); |
| 62 | |
| 63 | |
| 64 | $acceptedOrderStates = WPFunctions::get()->applyFilters('mailpoet_first_purchase_order_states', ['completed', 'processing']); |
| 65 | if (!is_array($acceptedOrderStates)) { |
| 66 | $acceptedOrderStates = ['completed', 'processing']; |
| 67 | } |
| 68 | foreach ($acceptedOrderStates as $state) { |
| 69 | if (!is_string($state)) { |
| 70 | continue; |
| 71 | } |
| 72 | WPFunctions::get()->addAction('woocommerce_order_status_' . $state, [ |
| 73 | $this, |
| 74 | 'scheduleEmailWhenProductIsPurchased', |
| 75 | ], 10, 1); |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | public function getEventDetails() { |
| 80 | return [ |
| 81 | 'slug' => self::SLUG, |
| 82 | 'title' => __('Purchased This Product', 'mailpoet'), |
| 83 | 'description' => __('Let MailPoet send an email to customers who purchase a specific product for the first time.', 'mailpoet'), |
| 84 | // translators: %s is the name of the product. |
| 85 | 'listingScheduleDisplayText' => __('Email sent when a customer buys product: %s', 'mailpoet'), |
| 86 | // translators: %s is the name of the products. |
| 87 | 'listingScheduleDisplayTextPlural' => __('Email sent when a customer buys products: %s', 'mailpoet'), |
| 88 | 'afterDelayText' => __('after a purchase', 'mailpoet'), |
| 89 | 'timeDelayValues' => [ |
| 90 | 'immediate' => [ |
| 91 | 'text' => __('immediately', 'mailpoet'), |
| 92 | 'displayAfterTimeNumberField' => false, |
| 93 | ], |
| 94 | 'minutes' => [ |
| 95 | 'text' => __('minute(s)', 'mailpoet'), |
| 96 | 'displayAfterTimeNumberField' => true, |
| 97 | ], |
| 98 | 'hours' => [ |
| 99 | 'text' => __('hour(s)', 'mailpoet'), |
| 100 | 'displayAfterTimeNumberField' => true, |
| 101 | ], |
| 102 | 'days' => [ |
| 103 | 'text' => __('day(s)', 'mailpoet'), |
| 104 | 'displayAfterTimeNumberField' => true, |
| 105 | ], |
| 106 | 'weeks' => [ |
| 107 | 'text' => __('week(s)', 'mailpoet'), |
| 108 | 'displayAfterTimeNumberField' => true, |
| 109 | ], |
| 110 | ], |
| 111 | 'options' => [ |
| 112 | 'multiple' => true, |
| 113 | 'endpoint' => 'products', |
| 114 | 'placeholder' => __('Search products', 'mailpoet'), |
| 115 | ], |
| 116 | ]; |
| 117 | } |
| 118 | |
| 119 | public function getProducts($productSearchQuery) { |
| 120 | $args = [ |
| 121 | 'post_type' => 'product', |
| 122 | 'post_status' => 'publish', |
| 123 | 's' => $productSearchQuery, |
| 124 | 'orderby' => 'title', |
| 125 | 'order' => 'ASC', |
| 126 | ]; |
| 127 | $woocommerceProducts = new \WP_Query($args); |
| 128 | $woocommerceProducts = $woocommerceProducts->get_posts(); |
| 129 | /** @var \WP_Post[] $woocommerceProducts */ |
| 130 | if (empty($woocommerceProducts)) { |
| 131 | $this->loggerFactory->getLogger(self::SLUG)->info( |
| 132 | 'no products found', |
| 133 | ['search_query' => $productSearchQuery] |
| 134 | ); |
| 135 | return; |
| 136 | } |
| 137 | |
| 138 | $woocommerceProducts = array_map(function($product) { |
| 139 | return [ |
| 140 | 'id' => $product->ID, |
| 141 | 'name' => $product->post_title, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps |
| 142 | ]; |
| 143 | }, $woocommerceProducts); |
| 144 | return $woocommerceProducts; |
| 145 | } |
| 146 | |
| 147 | public function scheduleEmailWhenProductIsPurchased($orderId) { |
| 148 | $orderDetails = $this->helper->wcGetOrder($orderId); |
| 149 | if (!$orderDetails || !$orderDetails->get_billing_email()) { |
| 150 | $this->loggerFactory->getLogger(self::SLUG)->info( |
| 151 | 'Email not scheduled because the order customer was not found', |
| 152 | ['order_id' => $orderId] |
| 153 | ); |
| 154 | return; |
| 155 | } |
| 156 | $customerEmail = $orderDetails->get_billing_email(); |
| 157 | |
| 158 | $subscriber = $this->subscribersRepository->getWooCommerceSegmentSubscriber($customerEmail); |
| 159 | |
| 160 | if (!$subscriber instanceof SubscriberEntity) { |
| 161 | $this->loggerFactory->getLogger(self::SLUG)->info( |
| 162 | 'Email not scheduled because the customer was not found as WooCommerce list subscriber', |
| 163 | ['order_id' => $orderId, 'customer_email' => $customerEmail] |
| 164 | ); |
| 165 | return; |
| 166 | } |
| 167 | |
| 168 | $orderedProducts = array_map(function($product) { |
| 169 | return ($product instanceof \WC_Order_Item_Product) ? $product->get_product_id() : null; |
| 170 | }, $orderDetails->get_items()); |
| 171 | $orderedProducts = array_values(array_filter($orderedProducts)); |
| 172 | |
| 173 | $schedulingCondition = function(NewsletterEntity $automaticEmail) use ($orderedProducts, $subscriber) { |
| 174 | $matchedProducts = $this->getProductIdsMatchingNewsletterTrigger($automaticEmail, $orderedProducts); |
| 175 | if (empty($matchedProducts)) { |
| 176 | return false; |
| 177 | } |
| 178 | |
| 179 | if ($this->repository->wasScheduledForSubscriber((int)$automaticEmail->getId(), (int)$subscriber->getId())) { |
| 180 | $sentAllProducts = $this->repository->alreadySentAllProducts((int)$automaticEmail->getId(), (int)$subscriber->getId(), 'orderedProducts', $matchedProducts); |
| 181 | if ($sentAllProducts) return false; |
| 182 | } |
| 183 | |
| 184 | return true; |
| 185 | }; |
| 186 | |
| 187 | $this->loggerFactory->getLogger(self::SLUG)->info( |
| 188 | 'Email scheduled', |
| 189 | [ |
| 190 | 'order_id' => $orderId, |
| 191 | 'customer_email' => $customerEmail, |
| 192 | 'subscriber_id' => $subscriber->getId(), |
| 193 | ] |
| 194 | ); |
| 195 | return $this->scheduler->scheduleAutomaticEmail( |
| 196 | WooCommerce::SLUG, |
| 197 | self::SLUG, |
| 198 | $schedulingCondition, |
| 199 | $subscriber, |
| 200 | ['orderedProducts' => $orderedProducts], |
| 201 | [$this, 'metaModifier'] |
| 202 | ); |
| 203 | } |
| 204 | |
| 205 | public function metaModifier(NewsletterEntity $newsletter, array $meta): array { |
| 206 | $orderedProductIds = $meta['orderedProducts'] ?? null; |
| 207 | if (empty($orderedProductIds)) { |
| 208 | return $meta; |
| 209 | } |
| 210 | $meta['orderedProducts'] = $this->getProductIdsMatchingNewsletterTrigger($newsletter, $orderedProductIds); |
| 211 | |
| 212 | return $meta; |
| 213 | } |
| 214 | |
| 215 | private function getProductIdsMatchingNewsletterTrigger(NewsletterEntity $automaticEmail, array $orderedProductIds): array { |
| 216 | $automaticEmailMetaValue = $automaticEmail->getOptionValue(NewsletterOptionFieldEntity::NAME_META); |
| 217 | $optionValue = Helpers::isJson($automaticEmailMetaValue) ? json_decode($automaticEmailMetaValue, true) : $automaticEmailMetaValue; |
| 218 | |
| 219 | if (!is_array($optionValue) || empty($optionValue['option']) || !is_array($optionValue['option'])) { |
| 220 | return []; |
| 221 | } |
| 222 | $emailTriggeringProductIds = array_column($optionValue['option'], 'id'); |
| 223 | |
| 224 | return array_intersect($emailTriggeringProductIds, $orderedProductIds); |
| 225 | } |
| 226 | } |
| 227 |