diff --git a/lib/AutomaticEmails/WooCommerce/Events/PurchasedInCategory.php b/lib/AutomaticEmails/WooCommerce/Events/PurchasedInCategory.php index 691e4bcb41..26cf8b1ef2 100644 --- a/lib/AutomaticEmails/WooCommerce/Events/PurchasedInCategory.php +++ b/lib/AutomaticEmails/WooCommerce/Events/PurchasedInCategory.php @@ -118,7 +118,10 @@ class PurchasedInCategory { $meta = $automaticEmail->getMeta(); if (empty($meta['option'])) return false; - if ($automaticEmail->wasScheduledForSubscriber($subscriber->id)) return false; + if ($automaticEmail->wasScheduledForSubscriber($subscriber->id)) { + $sentAllProducts = $automaticEmail->alreadySentAllProducts($subscriber->id, 'orderedProductCategories', $orderedProductCategories); + if ($sentAllProducts) return false; + } $metaCategories = array_column($meta['option'], 'id'); $matchedCategories = array_intersect($metaCategories, $orderedProductCategories); @@ -138,7 +141,7 @@ class PurchasedInCategory { self::SLUG, $schedulingCondition, $subscriber->id, - ['orderedProducts' => $orderedProductCategories] + ['orderedProductCategories' => $orderedProductCategories] ); } } diff --git a/lib/AutomaticEmails/WooCommerce/Events/PurchasedProduct.php b/lib/AutomaticEmails/WooCommerce/Events/PurchasedProduct.php index 5aa4062ec4..c5532badc0 100644 --- a/lib/AutomaticEmails/WooCommerce/Events/PurchasedProduct.php +++ b/lib/AutomaticEmails/WooCommerce/Events/PurchasedProduct.php @@ -4,6 +4,7 @@ namespace MailPoet\AutomaticEmails\WooCommerce\Events; use MailPoet\AutomaticEmails\WooCommerce\WooCommerce; use MailPoet\Logging\LoggerFactory; +use MailPoet\Models\Newsletter; use MailPoet\Models\Subscriber; use MailPoet\Newsletter\Scheduler\AutomaticEmailScheduler; use MailPoet\WooCommerce\Helper as WCHelper; @@ -120,11 +121,14 @@ class PurchasedProduct { }, $orderDetails->get_items()); $orderedProducts = array_values(array_filter($orderedProducts)); - $schedulingCondition = function($automaticEmail) use ($orderedProducts, $subscriber) { + $schedulingCondition = function(Newsletter $automaticEmail) use ($orderedProducts, $subscriber) { $meta = $automaticEmail->getMeta(); if (empty($meta['option'])) return false; - if ($automaticEmail->wasScheduledForSubscriber($subscriber->id)) return false; + if ($automaticEmail->wasScheduledForSubscriber($subscriber->id)) { + $sentAllProducts = $automaticEmail->alreadySentAllProducts($subscriber->id, 'orderedProducts', $orderedProducts); + if ($sentAllProducts) return false; + } $metaProducts = array_column($meta['option'], 'id'); $matchedProducts = array_intersect($metaProducts, $orderedProducts); diff --git a/lib/Models/Newsletter.php b/lib/Models/Newsletter.php index 3f42d9b26f..e965319886 100644 --- a/lib/Models/Newsletter.php +++ b/lib/Models/Newsletter.php @@ -369,19 +369,41 @@ class Newsletter extends Model { } public function wasScheduledForSubscriber($subscriberId) { + $query = SendingQueue::selectExpr('COUNT(*)', 'count'); + $query = $this->getAllQueuesForSubscscriberQuery($query, $subscriberId); /** @var \stdClass */ - $queue = SendingQueue::rawQuery( - "SELECT COUNT(*) as count - FROM `" . SendingQueue::$_table . "` - JOIN `" . ScheduledTask::$_table . "` ON " . SendingQueue::$_table . ".task_id = " . ScheduledTask::$_table . ".id - JOIN `" . ScheduledTaskSubscriber::$_table . "` ON " . ScheduledTask::$_table . ".id = " . ScheduledTaskSubscriber::$_table . ".task_id - WHERE " . ScheduledTaskSubscriber::$_table . ".subscriber_id = " . $subscriberId . " - AND " . SendingQueue::$_table . ".newsletter_id = " . $this->id - )->findOne(); + $queue = $query->findOne(); return ((int)$queue->count) > 0; } + private function getAllQueuesForSubscscriberQuery($orm, $subscriberId) { + return $orm->tableAlias('queue') + ->join(ScheduledTask::$_table, ['queue.task_id', '=', 'task.id'], 'task') + ->join(ScheduledTaskSubscriber::$_table, ['subscriber.task_id', '=', 'task.id'], 'subscriber') + ->where('queue.newsletter_id', $this->id) + ->where('subscriber.subscriber_id', $subscriberId); + } + + /** + * Check for automatic emails. + * Search products/categories in meta if all of the ordered products have already been sent to the subscriber. + */ + public function alreadySentAllProducts(int $subscriberId, string $orderedKey, array $ordered): bool { + $query = SendingQueue::select('queue.*'); + $queues = $this->getAllQueuesForSubscscriberQuery($query, $subscriberId)->findMany(); + $sent = []; + foreach ($queues as $queue) { + $meta = $queue->getMeta(); + if (isset($meta[$orderedKey])) { + $sent = array_merge($sent, $meta[$orderedKey]); + } + } + $notSentProducts = array_diff($ordered, $sent); + + return empty($notSentProducts); + } + public static function filterWithOptions($orm, $type) { $orm = $orm->select(MP_NEWSLETTERS_TABLE . '.*'); $optionFields = NewsletterOptionField::findArray(); diff --git a/tasks/phpstan/phpstan-baseline-integration-tests.neon b/tasks/phpstan/phpstan-baseline-integration-tests.neon index ab60278f0c..1392280591 100644 --- a/tasks/phpstan/phpstan-baseline-integration-tests.neon +++ b/tasks/phpstan/phpstan-baseline-integration-tests.neon @@ -145,11 +145,6 @@ parameters: count: 2 path: ../../tests/integration/AutomaticEmails/WooCommerce/Events/AbandonedCartTest.php - - - message: "#^Property MailPoet\\\\AutomaticEmails\\\\WooCommerce\\\\Events\\\\PurchasedInCategoryTest\\:\\:\\$woocommerceHelper \\(PHPUnit\\\\Framework\\\\MockObject\\\\MockObject\\) does not accept MailPoet\\\\WooCommerce\\\\Helper\\.$#" - count: 1 - path: ../../tests/integration/AutomaticEmails/WooCommerce/Events/PurchasedInCategoryTest.php - - message: "#^Cannot call method has_cap\\(\\) on WP_Role\\|null\\.$#" count: 6 diff --git a/tests/integration/AutomaticEmails/WooCommerce/Events/PurchasedInCategoryTest.php b/tests/integration/AutomaticEmails/WooCommerce/Events/PurchasedInCategoryTest.php index d0d003f6e2..d808ea8dce 100644 --- a/tests/integration/AutomaticEmails/WooCommerce/Events/PurchasedInCategoryTest.php +++ b/tests/integration/AutomaticEmails/WooCommerce/Events/PurchasedInCategoryTest.php @@ -21,7 +21,7 @@ use PHPUnit\Framework\MockObject\MockObject; class PurchasedInCategoryTest extends \MailPoetTest { - /** @var MockObject */ + /** @var MockObject&WCHelper */ private $woocommerceHelper; /** @var PurchasedInCategory */ @@ -37,7 +37,7 @@ class PurchasedInCategoryTest extends \MailPoetTest { ORM::raw_execute('TRUNCATE ' . ScheduledTaskSubscriber::$_table); WPFunctions::set(new WPFunctions); WPFunctions::get()->removeAllFilters('woocommerce_payment_complete'); - $this->woocommerceHelper = $this->makeEmpty(WCHelper::class, []); + $this->woocommerceHelper = $this->createMock(WCHelper::class); $this->event = new PurchasedInCategory($this->woocommerceHelper); } @@ -69,29 +69,7 @@ class PurchasedInCategoryTest extends \MailPoetTest { } public function testItSchedules() { - $newsletter = Newsletter::createOrUpdate( - [ - 'subject' => 'WooCommerce', - 'preheader' => 'preheader', - 'type' => Newsletter::TYPE_AUTOMATIC, - 'status' => Newsletter::STATUS_ACTIVE, - ] - ); - $this->_createNewsletterOption( - [ - 'group' => WooCommerce::SLUG, - 'event' => PurchasedInCategory::SLUG, - 'afterTimeType' => 'days', - 'afterTimeNumber' => 1, - 'meta' => json_encode( - [ - 'option' => [ - ['id' => '15'], - ], - ]), - ], - $newsletter->id - ); + $newsletter = $this->_createNewsletter(); $customerEmail = 'email@example.com'; $order = $this->getOrderMock(['15', '16']); @@ -104,27 +82,53 @@ class PurchasedInCategoryTest extends \MailPoetTest { ->method('get_billing_email') ->will($this->returnValue($customerEmail)); - $subscriber = Subscriber::createOrUpdate(Fixtures::get('subscriber_template')); - $subscriber->email = $customerEmail; - $subscriber->isWoocommerceUser = 1; - $subscriber->status = Subscriber::STATUS_SUBSCRIBED; - $subscriber->save(); - - $subscriberSegment = SubscriberSegment::create(); - $subscriberSegment->hydrate([ - 'subscriber_id' => $subscriber->id, - 'segment_id' => Segment::getWooCommerceSegment()->id, - 'status' => Subscriber::STATUS_SUBSCRIBED, - ]); - $subscriberSegment->save(); + $this->_createSubscriber($customerEmail); $this->event->scheduleEmail(3); $scheduledTask = Sending::getByNewsletterId($newsletter->id); $queue = $scheduledTask->queue(); - expect($queue->getMeta())->equals(['orderedProducts' => ['15', '16']]); + expect($queue->getMeta())->equals(['orderedProductCategories' => ['15', '16']]); expect($scheduledTask)->notEmpty(); } + public function testItSchedulesOnlyOnce() { + $newsletter = $this->_createNewsletter(); + + $customerEmail = 'email@example.com'; + $order = $this->getOrderMock(['15', '16']); + $this->woocommerceHelper = $this->createMock(WCHelper::class); + $this->woocommerceHelper + ->expects($this->any()) + ->method('wcGetOrder') + ->will($this->returnValue($order)); + $order + ->expects($this->any()) + ->method('get_billing_email') + ->will($this->returnValue($customerEmail)); + + $this->_createSubscriber($customerEmail); + + $this->event = new PurchasedInCategory($this->woocommerceHelper); + $this->event->scheduleEmail(3); + $queue1 = SendingQueue::where('newsletter_id', $newsletter->id)->findMany(); + expect($queue1)->notEmpty(); + + $order = $this->getOrderMock(['15']); + $this->woocommerceHelper = $this->createMock(WCHelper::class); + $this->woocommerceHelper + ->expects($this->any()) + ->method('wcGetOrder') + ->will($this->returnValue($order)); + $order + ->expects($this->any()) + ->method('get_billing_email') + ->will($this->returnValue($customerEmail)); + $this->event = new PurchasedInCategory($this->woocommerceHelper); + $this->event->scheduleEmail(4); + $queue2 = SendingQueue::where('newsletter_id', $newsletter->id)->findMany(); + expect($queue1)->count(count($queue2)); + } + private function getOrderMock($categories = ['123']) { $productMock = $this->getMockBuilder(\WC_Product::class) ->disableOriginalConstructor() @@ -156,6 +160,51 @@ class PurchasedInCategoryTest extends \MailPoetTest { return $orderMock; } + private function _createNewsletter() { + $newsletter = Newsletter::createOrUpdate( + [ + 'subject' => 'WooCommerce', + 'preheader' => 'preheader', + 'type' => Newsletter::TYPE_AUTOMATIC, + 'status' => Newsletter::STATUS_ACTIVE, + ] + ); + $this->_createNewsletterOption( + [ + 'sendTo' => 'user', + 'group' => WooCommerce::SLUG, + 'event' => PurchasedInCategory::SLUG, + 'afterTimeType' => 'days', + 'afterTimeNumber' => 1, + 'meta' => json_encode( + [ + 'option' => [ + ['id' => '15'], + ], + ]), + ], + $newsletter->id + ); + return $newsletter; + } + + private function _createSubscriber($customerEmail) { + $subscriber = Subscriber::createOrUpdate(Fixtures::get('subscriber_template')); + $subscriber->email = $customerEmail; + $subscriber->isWoocommerceUser = 1; + $subscriber->status = Subscriber::STATUS_SUBSCRIBED; + $subscriber->save(); + + $subscriberSegment = SubscriberSegment::create(); + $subscriberSegment->hydrate([ + 'subscriber_id' => $subscriber->id, + 'segment_id' => Segment::getWooCommerceSegment()->id, + 'status' => Subscriber::STATUS_SUBSCRIBED, + ]); + $subscriberSegment->save(); + return $subscriber; + } + public function _createNewsletterOption(array $options, $newsletterId) { foreach ($options as $option => $value) { $newsletterOptionField = NewsletterOptionField::where('name', $option) diff --git a/tests/integration/AutomaticEmails/WooCommerce/Events/PurchasedProductTest.php b/tests/integration/AutomaticEmails/WooCommerce/Events/PurchasedProductTest.php index bd62d001f4..fac2f533b0 100644 --- a/tests/integration/AutomaticEmails/WooCommerce/Events/PurchasedProductTest.php +++ b/tests/integration/AutomaticEmails/WooCommerce/Events/PurchasedProductTest.php @@ -90,6 +90,83 @@ class PurchasedProductTest extends \MailPoetTest { expect($result)->isEmpty(); } + public function testItDoesNotScheduleEmailWhenAlreadySent() { + $newsletter = Newsletter::createOrUpdate( + [ + 'subject' => 'WooCommerce', + 'preheader' => 'preheader', + 'type' => Newsletter::TYPE_AUTOMATIC, + 'status' => Newsletter::STATUS_ACTIVE, + ] + ); + $productId = 1000; + $this->_createNewsletterOption( + [ + 'sendTo' => 'user', + 'group' => WooCommerce::SLUG, + 'event' => PurchasedProduct::SLUG, + 'afterTimeType' => 'days', + 'afterTimeNumber' => 1, + 'meta' => json_encode( + [ + 'option' => [ + ['id' => $productId], + ], + ]), + ], + $newsletter->id + ); + $customerEmail = 'test@example.com'; + $subscriber = Subscriber::createOrUpdate(Fixtures::get('subscriber_template')); + $subscriber->email = $customerEmail; + $subscriber->isWoocommerceUser = 1; + $subscriber->status = Subscriber::STATUS_SUBSCRIBED; + $subscriber->save(); + + $subscriberSegment = SubscriberSegment::create(); + $subscriberSegment->hydrate([ + 'subscriber_id' => $subscriber->id, + 'segment_id' => Segment::getWooCommerceSegment()->id, + 'status' => Subscriber::STATUS_SUBSCRIBED, + ]); + $subscriberSegment->save(); + + $orderDetails = Stub::make( + new OrderDetails(), + [ + 'get_billing_email' => 'test@example.com', + 'get_items' => function() use ($productId) { + return [ + Stub::make( + \WC_Order_Item_Product::class, + [ + 'get_product_id' => $productId, + ] + ), + ]; + }, + ] + ); + $orderDetails->total = 'order_total'; + $orderId = 12; + $helper = Stub::make(WCHelper::class, [ + 'wcGetOrder' => $orderDetails, + ]); + + $event = new PurchasedProduct($helper); + + // ensure there are no existing scheduled tasks + $scheduledTask = Sending::getByNewsletterId($newsletter->id); + expect($scheduledTask)->false(); + + $event->scheduleEmailWhenProductIsPurchased($orderId); + $queue1 = SendingQueue::where('newsletter_id', $newsletter->id)->findMany(); + + $event->scheduleEmailWhenProductIsPurchased($orderId); + $queue2 = SendingQueue::where('newsletter_id', $newsletter->id)->findMany(); + expect($queue1)->count(count($queue2)); + } + public function testItDoesNotScheduleEmailWhenPurchasedProductDoesNotMatchConfiguredProductIds() { WPFunctions::get()->removeAllFilters('woocommerce_order_status_completed'); $newsletter = Newsletter::createOrUpdate( @@ -107,6 +184,7 @@ class PurchasedProductTest extends \MailPoetTest { ]; $this->_createNewsletterOption( [ + 'sendTo' => 'user', 'group' => WooCommerce::SLUG, 'event' => PurchasedProduct::SLUG, 'afterTimeType' => 'days',