diff --git a/lib/Analytics/Reporter.php b/lib/Analytics/Reporter.php index a34c407773..cb8da3425e 100644 --- a/lib/Analytics/Reporter.php +++ b/lib/Analytics/Reporter.php @@ -8,6 +8,7 @@ use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Newsletter\NewslettersRepository; use MailPoet\Segments\DynamicSegments\DynamicSegmentFilterRepository; use MailPoet\Segments\DynamicSegments\Filters\EmailAction; +use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny; use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction; use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields; use MailPoet\Segments\DynamicSegments\Filters\SubscriberSubscribedDate; @@ -151,7 +152,7 @@ class Reporter { 'Segment > # of opens' => $this->isFilterTypeActive(DynamicSegmentFilterData::TYPE_EMAIL, EmailOpensAbsoluteCountAction::TYPE), 'Segment > # of orders' => $this->isFilterTypeActive(DynamicSegmentFilterData::TYPE_WOOCOMMERCE, WooCommerceNumberOfOrders::ACTION_NUMBER_OF_ORDERS), 'Segment > clicked' => $this->isFilterTypeActive(DynamicSegmentFilterData::TYPE_EMAIL, EmailAction::ACTION_CLICKED), - 'Segment > clicked any email' => $this->isFilterTypeActive(DynamicSegmentFilterData::TYPE_EMAIL, EmailAction::ACTION_CLICKED_ANY), + 'Segment > clicked any email' => $this->isFilterTypeActive(DynamicSegmentFilterData::TYPE_EMAIL, EmailActionClickAny::TYPE), 'Segment > not clicked' => $this->isFilterTypeActive(DynamicSegmentFilterData::TYPE_EMAIL, EmailAction::ACTION_NOT_CLICKED), 'Segment > not opened' => $this->isFilterTypeActive(DynamicSegmentFilterData::TYPE_EMAIL, EmailAction::ACTION_NOT_OPENED), 'Segment > opened' => $this->isFilterTypeActive(DynamicSegmentFilterData::TYPE_EMAIL, EmailAction::ACTION_OPENED), diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index 5529767664..5ba6438dff 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -289,6 +289,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Segments\DynamicSegments\FilterHandler::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\DynamicSegmentFilterRepository::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\EmailAction::class)->setPublic(true); + $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\SubscriberScore::class)->setPublic(true); diff --git a/lib/Segments/DynamicSegments/FilterDataMapper.php b/lib/Segments/DynamicSegments/FilterDataMapper.php index 1a2bcca802..71741cac87 100644 --- a/lib/Segments/DynamicSegments/FilterDataMapper.php +++ b/lib/Segments/DynamicSegments/FilterDataMapper.php @@ -5,6 +5,7 @@ namespace MailPoet\Segments\DynamicSegments; use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException; use MailPoet\Segments\DynamicSegments\Filters\EmailAction; +use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny; use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction; use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields; use MailPoet\Segments\DynamicSegments\Filters\SubscriberScore; @@ -123,7 +124,7 @@ class FilterDataMapper { ) { return $this->createEmailOpensAbsoluteCount($data); } - if ($data['action'] === EmailAction::ACTION_CLICKED_ANY) { + if ($data['action'] === EmailActionClickAny::TYPE) { return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $data['action'], [ 'connect' => $data['connect'], ]); diff --git a/lib/Segments/DynamicSegments/FilterFactory.php b/lib/Segments/DynamicSegments/FilterFactory.php index d67837de7c..49dc187ed2 100644 --- a/lib/Segments/DynamicSegments/FilterFactory.php +++ b/lib/Segments/DynamicSegments/FilterFactory.php @@ -6,6 +6,7 @@ use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\DynamicSegmentFilterEntity; use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException; use MailPoet\Segments\DynamicSegments\Filters\EmailAction; +use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny; use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction; use MailPoet\Segments\DynamicSegments\Filters\Filter; use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields; @@ -57,13 +58,15 @@ class FilterFactory { /** @var MailPoetCustomFields */ private $mailPoetCustomFields; - /** - * @var SubscriberSegment - */ + /** @var SubscriberSegment */ private $subscriberSegment; + /** @var EmailActionClickAny */ + private $emailActionClickAny; + public function __construct( EmailAction $emailAction, + EmailActionClickAny $emailActionClickAny, UserRole $userRole, MailPoetCustomFields $mailPoetCustomFields, WooCommerceProduct $wooCommerceProduct, @@ -90,6 +93,7 @@ class FilterFactory { $this->subscriberScore = $subscriberScore; $this->mailPoetCustomFields = $mailPoetCustomFields; $this->subscriberSegment = $subscriberSegment; + $this->emailActionClickAny = $emailActionClickAny; } public function getFilterForFilterEntity(DynamicSegmentFilterEntity $filter): Filter { @@ -127,6 +131,8 @@ class FilterFactory { $countActions = [EmailOpensAbsoluteCountAction::TYPE, EmailOpensAbsoluteCountAction::MACHINE_TYPE]; if (in_array($action, $countActions)) { return $this->emailOpensAbsoluteCount; + } elseif ($action === EmailActionClickAny::TYPE) { + return $this->emailActionClickAny; } return $this->emailAction; } diff --git a/lib/Segments/DynamicSegments/Filters/EmailAction.php b/lib/Segments/DynamicSegments/Filters/EmailAction.php index 5f88f8ed72..2b493bc257 100644 --- a/lib/Segments/DynamicSegments/Filters/EmailAction.php +++ b/lib/Segments/DynamicSegments/Filters/EmailAction.php @@ -3,7 +3,6 @@ namespace MailPoet\Segments\DynamicSegments\Filters; use MailPoet\Entities\DynamicSegmentFilterEntity; -use MailPoet\Entities\NewsletterLinkEntity; use MailPoet\Entities\StatisticsClickEntity; use MailPoet\Entities\StatisticsNewsletterEntity; use MailPoet\Entities\StatisticsOpenEntity; @@ -18,7 +17,6 @@ class EmailAction implements Filter { const ACTION_MACHINE_OPENED = 'machineOpened'; const ACTION_NOT_OPENED = 'notOpened'; const ACTION_CLICKED = 'clicked'; - const ACTION_CLICKED_ANY = 'clickedAny'; const ACTION_NOT_CLICKED = 'notClicked'; const ALLOWED_ACTIONS = [ @@ -27,7 +25,7 @@ class EmailAction implements Filter { self::ACTION_NOT_OPENED, self::ACTION_CLICKED, self::ACTION_NOT_CLICKED, - self::ACTION_CLICKED_ANY, + EmailActionClickAny::TYPE, EmailOpensAbsoluteCountAction::TYPE, EmailOpensAbsoluteCountAction::MACHINE_TYPE, ]; @@ -35,7 +33,6 @@ class EmailAction implements Filter { const CLICK_ACTIONS = [ self::ACTION_CLICKED, self::ACTION_NOT_CLICKED, - self::ACTION_CLICKED_ANY, ]; /** @var EntityManager */ @@ -56,7 +53,6 @@ class EmailAction implements Filter { $statsSentTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName(); $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); - $newsletterLinksTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName(); if (in_array($action, self::CLICK_ACTIONS, true)) { $statsTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName(); } else { @@ -78,24 +74,6 @@ class EmailAction implements Filter { $this->createNotStatsJoinCondition($filter, $action, $linkId, $parameterSuffix) )->setParameter('newsletter' . $parameterSuffix, $newsletterId); $where .= ' AND stats.id IS NULL'; - } else if ($action === self::ACTION_CLICKED_ANY) { - $excludedLinks = [ - '[link:subscription_unsubscribe_url]', - '[link:subscription_instant_unsubscribe_url]', - '[link:newsletter_view_in_browser_url]', - '[link:subscription_manage_url]', - ]; - $queryBuilder = $queryBuilder->innerJoin( - $subscribersTable, - $statsTable, - 'stats', - "stats.subscriber_id = $subscribersTable.id" - )->innerJoin( - 'stats', - $newsletterLinksTable, - 'newsletterLinks', - "stats.link_id = newsletterLinks.id AND newsletterLinks.URL NOT IN ('" . join("', '", $excludedLinks) . "')" - ); } else { $queryBuilder = $queryBuilder->innerJoin( $subscribersTable, diff --git a/lib/Segments/DynamicSegments/Filters/EmailActionClickAny.php b/lib/Segments/DynamicSegments/Filters/EmailActionClickAny.php new file mode 100644 index 0000000000..555c56f23a --- /dev/null +++ b/lib/Segments/DynamicSegments/Filters/EmailActionClickAny.php @@ -0,0 +1,50 @@ +entityManager = $entityManager; + } + + public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder { + $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + $newsletterLinksTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName(); + + $statsTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName(); + + $excludedLinks = [ + '[link:subscription_unsubscribe_url]', + '[link:subscription_instant_unsubscribe_url]', + '[link:newsletter_view_in_browser_url]', + '[link:subscription_manage_url]', + ]; + $queryBuilder = $queryBuilder->innerJoin( + $subscribersTable, + $statsTable, + 'stats', + "stats.subscriber_id = $subscribersTable.id" + )->innerJoin( + 'stats', + $newsletterLinksTable, + 'newsletterLinks', + "stats.link_id = newsletterLinks.id AND newsletterLinks.URL NOT IN ('" . join("', '", $excludedLinks) . "')" + ); + + return $queryBuilder; + } +} diff --git a/tests/integration/Segments/DynamicSegments/Filters/EmailActionClickAnyTest.php b/tests/integration/Segments/DynamicSegments/Filters/EmailActionClickAnyTest.php new file mode 100644 index 0000000000..17786464b5 --- /dev/null +++ b/tests/integration/Segments/DynamicSegments/Filters/EmailActionClickAnyTest.php @@ -0,0 +1,162 @@ +cleanData(); + $this->emailAction = $this->diContainer->get(EmailActionClickAny::class); + $this->newsletter = new NewsletterEntity(); + $task = new ScheduledTaskEntity(); + $this->entityManager->persist($task); + $queue = new SendingQueueEntity(); + $queue->setNewsletter($this->newsletter); + $queue->setTask($task); + $this->entityManager->persist($queue); + $this->newsletter->getQueues()->add($queue); + $this->newsletter->setSubject('newsletter 1'); + $this->newsletter->setStatus('sent'); + $this->newsletter->setType(NewsletterEntity::TYPE_STANDARD); + $this->entityManager->persist($this->newsletter); + $this->entityManager->flush(); + + $this->subscriberOpenedClicked = $this->createSubscriber('opened_clicked@example.com'); + $this->subscriberOpenedNotClicked = $this->createSubscriber('opened_not_clicked@example.com'); + $this->subscriberNotOpened = $this->createSubscriber('not_opened@example.com'); + $this->subscriberNotSent = $this->createSubscriber('not_sent@example.com'); + + $this->createStatsNewsletter($this->subscriberOpenedClicked); + $this->createStatsNewsletter($this->subscriberOpenedNotClicked); + $this->createStatsNewsletter($this->subscriberNotOpened); + + $this->createStatisticsOpens($this->subscriberOpenedClicked); + $this->createStatisticsOpens($this->subscriberOpenedNotClicked); + + $this->addClickedToLink('http://example.com', $this->newsletter, $this->subscriberOpenedClicked); + } + + public function testGetClickedAnyLink() { + $subscriberClickedExcludedLinks = $this->createSubscriber('opened_clicked_excluded@example.com'); + $this->createStatsNewsletter($subscriberClickedExcludedLinks); + $this->createStatisticsOpens($subscriberClickedExcludedLinks); + $this->addClickedToLink('[link:subscription_unsubscribe_url]', $this->newsletter, $subscriberClickedExcludedLinks); + $this->addClickedToLink('[link:subscription_instant_unsubscribe_url]', $this->newsletter, $subscriberClickedExcludedLinks); + $this->addClickedToLink('[link:newsletter_view_in_browser_url]', $this->newsletter, $subscriberClickedExcludedLinks); + $this->addClickedToLink('[link:subscription_manage_url]', $this->newsletter, $subscriberClickedExcludedLinks); + + $segmentFilter = $this->getSegmentFilter(EmailActionClickAny::TYPE); + $statement = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute(); + $this->assertInstanceOf(Statement::class, $statement); + $result = $statement->fetchAll(); + expect(count($result))->equals(1); + $subscriber1 = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']); + $this->assertInstanceOf(SubscriberEntity::class, $subscriber1); + expect($subscriber1->getEmail())->equals('opened_clicked@example.com'); + } + + private function getQueryBuilder() { + $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + return $this->entityManager + ->getConnection() + ->createQueryBuilder() + ->select("$subscribersTable.id") + ->from($subscribersTable); + } + + private function getSegmentFilter(string $action, int $newsletterId = null, int $linkId = null): DynamicSegmentFilterEntity { + $segmentFilterData = new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $action, [ + 'newsletter_id' => $newsletterId, + 'link_id' => $linkId, + ]); + $segment = new SegmentEntity('Dynamic Segment', SegmentEntity::TYPE_DYNAMIC, 'description'); + $this->entityManager->persist($segment); + $dynamicSegmentFilter = new DynamicSegmentFilterEntity($segment, $segmentFilterData); + $this->entityManager->persist($dynamicSegmentFilter); + $segment->addDynamicFilter($dynamicSegmentFilter); + return $dynamicSegmentFilter; + } + + private function createSubscriber(string $email) { + $subscriber = new SubscriberEntity(); + $subscriber->setEmail($email); + $subscriber->setLastName('Last'); + $subscriber->setFirstName('First'); + $subscriber->setStatus(SubscriberEntity::STATUS_SUBSCRIBED); + $this->entityManager->persist($subscriber); + $this->entityManager->flush(); + return $subscriber; + } + + private function createStatsNewsletter(SubscriberEntity $subscriber) { + $queue = $this->newsletter->getLatestQueue(); + assert($queue instanceof SendingQueueEntity); + $stats = new StatisticsNewsletterEntity($this->newsletter, $queue, $subscriber); + $this->entityManager->persist($stats); + $this->entityManager->flush(); + return $stats; + } + + private function createStatisticsOpens(SubscriberEntity $subscriber) { + $queue = $this->newsletter->getLatestQueue(); + assert($queue instanceof SendingQueueEntity); + $open = new StatisticsOpenEntity($this->newsletter, $queue, $subscriber); + $this->entityManager->persist($open); + $this->entityManager->flush(); + return $open; + } + + private function addClickedToLink(string $link, NewsletterEntity $newsletter, SubscriberEntity $subscriber) { + $queue = $newsletter->getLatestQueue(); + $this->assertInstanceOf(SendingQueueEntity::class, $queue); + $link = new NewsletterLinkEntity($this->newsletter, $queue, $link, uniqid()); + $this->entityManager->persist($link); + $this->entityManager->flush(); + $click = new StatisticsClickEntity( + $newsletter, + $queue, + $subscriber, + $link, + 1 + ); + $this->entityManager->persist($click); + $this->entityManager->flush(); + } + + public function _after() { + $this->cleanData(); + } + + private function cleanData() { + $this->truncateEntity(NewsletterEntity::class); + $this->truncateEntity(SubscriberEntity::class); + $this->truncateEntity(StatisticsOpenEntity::class); + $this->truncateEntity(StatisticsClickEntity::class); + $this->truncateEntity(StatisticsNewsletterEntity::class); + $this->truncateEntity(NewsletterLinkEntity::class); + $this->truncateEntity(UserAgentEntity::class); + } +} diff --git a/tests/integration/Segments/DynamicSegments/Filters/EmailActionTest.php b/tests/integration/Segments/DynamicSegments/Filters/EmailActionTest.php index a5b55b7723..3890362781 100644 --- a/tests/integration/Segments/DynamicSegments/Filters/EmailActionTest.php +++ b/tests/integration/Segments/DynamicSegments/Filters/EmailActionTest.php @@ -128,25 +128,6 @@ class EmailActionTest extends \MailPoetTest { expect($subscriber2->getEmail())->equals('not_opened@example.com'); } - public function testGetClickedAnyLink() { - $subscriberClickedExcludedLinks = $this->createSubscriber('opened_clicked_excluded@example.com'); - $this->createStatsNewsletter($subscriberClickedExcludedLinks); - $this->createStatisticsOpens($subscriberClickedExcludedLinks); - $this->addClickedToLink('[link:subscription_unsubscribe_url]', $this->newsletter, $subscriberClickedExcludedLinks); - $this->addClickedToLink('[link:subscription_instant_unsubscribe_url]', $this->newsletter, $subscriberClickedExcludedLinks); - $this->addClickedToLink('[link:newsletter_view_in_browser_url]', $this->newsletter, $subscriberClickedExcludedLinks); - $this->addClickedToLink('[link:subscription_manage_url]', $this->newsletter, $subscriberClickedExcludedLinks); - - $segmentFilter = $this->getSegmentFilter(EmailAction::ACTION_CLICKED_ANY); - $statement = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute(); - $this->assertInstanceOf(Statement::class, $statement); - $result = $statement->fetchAll(); - expect(count($result))->equals(1); - $subscriber1 = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']); - $this->assertInstanceOf(SubscriberEntity::class, $subscriber1); - expect($subscriber1->getEmail())->equals('opened_clicked@example.com'); - } - public function testGetNotClickedWithWrongLink() { $segmentFilter = $this->getSegmentFilter(EmailAction::ACTION_NOT_CLICKED, (int)$this->newsletter->getId(), 2); $statement = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute();