diff --git a/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/email.tsx b/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/email.tsx index 18c1e82420..fb0b3e6482 100644 --- a/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/email.tsx +++ b/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/email.tsx @@ -43,6 +43,7 @@ const componentsMap = { EmailOpensAbsoluteCountFields, [EmailActionTypes.CLICKED]: EmailClickStatisticsFields, [EmailActionTypes.OPENED]: EmailOpenStatisticsFields, + [EmailActionTypes.WAS_SENT]: EmailOpenStatisticsFields, [EmailActionTypes.MACHINE_OPENED]: EmailOpenStatisticsFields, [EmailActionTypes.CLICKED_ANY]: null, }; diff --git a/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/email_options.ts b/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/email_options.ts index 172eda429c..c471783f82 100644 --- a/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/email_options.ts +++ b/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/email_options.ts @@ -12,6 +12,11 @@ export const EmailSegmentOptions = [ label: MailPoet.I18n.t('emailActionMachineOpensAbsoluteCount'), group: SegmentTypes.Email, }, + { + value: EmailActionTypes.WAS_SENT, + label: MailPoet.I18n.t('emailActionWasSent'), + group: SegmentTypes.Email, + }, { value: EmailActionTypes.OPENED, label: MailPoet.I18n.t('emailActionOpened'), diff --git a/mailpoet/assets/js/src/segments/dynamic/types.ts b/mailpoet/assets/js/src/segments/dynamic/types.ts index 3127c5e8ef..b3ce51b1a7 100644 --- a/mailpoet/assets/js/src/segments/dynamic/types.ts +++ b/mailpoet/assets/js/src/segments/dynamic/types.ts @@ -12,6 +12,7 @@ export enum EmailActionTypes { MACHINE_OPENS_ABSOLUTE_COUNT = 'machineOpensAbsoluteCount', OPENED = 'opened', MACHINE_OPENED = 'machineOpened', + WAS_SENT = 'wasSent', CLICKED = 'clicked', CLICKED_ANY = 'clickedAny', } diff --git a/mailpoet/lib/Segments/DynamicSegments/Filters/EmailAction.php b/mailpoet/lib/Segments/DynamicSegments/Filters/EmailAction.php index f7225c0ddd..c70ed04d75 100644 --- a/mailpoet/lib/Segments/DynamicSegments/Filters/EmailAction.php +++ b/mailpoet/lib/Segments/DynamicSegments/Filters/EmailAction.php @@ -18,16 +18,18 @@ use MailPoetVendor\Doctrine\ORM\EntityManager; class EmailAction implements Filter { const ACTION_OPENED = 'opened'; const ACTION_MACHINE_OPENED = 'machineOpened'; - /** @deprecated */ + /** @deprecated */ const ACTION_NOT_OPENED = 'notOpened'; const ACTION_CLICKED = 'clicked'; - /** @deprecated */ + const ACTION_WAS_SENT = 'wasSent'; + /** @deprecated */ const ACTION_NOT_CLICKED = 'notClicked'; const ALLOWED_ACTIONS = [ self::ACTION_OPENED, self::ACTION_MACHINE_OPENED, self::ACTION_CLICKED, + self::ACTION_WAS_SENT, EmailActionClickAny::TYPE, EmailOpensAbsoluteCountAction::TYPE, EmailOpensAbsoluteCountAction::MACHINE_TYPE, @@ -35,11 +37,15 @@ class EmailAction implements Filter { /** @var EntityManager */ private $entityManager; + /** @var FilterHelper */ + private $filterHelper; public function __construct( - EntityManager $entityManager + EntityManager $entityManager, + FilterHelper $filterHelper ) { $this->entityManager = $entityManager; + $this->filterHelper = $filterHelper; } public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder { @@ -49,6 +55,8 @@ class EmailAction implements Filter { if ($action === self::ACTION_CLICKED) { return $this->applyForClickedActions($queryBuilder, $filterData, $parameterSuffix); + } elseif ($action === self::ACTION_WAS_SENT) { + return $this->applyForWasSentAction($queryBuilder, $filterData, $parameterSuffix); } else { return $this->applyForOpenedActions($queryBuilder, $filterData, $parameterSuffix); } @@ -175,4 +183,36 @@ class EmailAction implements Filter { } return $clause; } + + private function applyForWasSentAction(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData, string $parameterSuffix): QueryBuilder { + $newsletters = (array)$filterData->getParam('newsletters'); + $operator = $filterData->getParam('operator') ?? DynamicSegmentFilterData::OPERATOR_ANY; + $subscribersTable = $this->filterHelper->getSubscribersTable(); + $statisticsNewslettersTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName(); + + if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) { + $queryBuilder->leftJoin( + $this->filterHelper->getSubscribersTable(), + $statisticsNewslettersTable, + 'statisticsNewsletter', + "$subscribersTable.id = statisticsNewsletter.subscriber_id AND statisticsNewsletter.newsletter_id IN (:newsletters" . $parameterSuffix . ')' + ) + ->setParameter('newsletters' . $parameterSuffix, $newsletters, Connection::PARAM_INT_ARRAY) + ->andWhere('statisticsNewsletter.subscriber_id IS NULL'); + } else { + $queryBuilder->innerJoin( + $subscribersTable, + $statisticsNewslettersTable, + 'statisticsNewsletter', + "statisticsNewsletter.subscriber_id = $subscribersTable.id AND statisticsNewsletter.newsletter_id IN (:newsletters" . $parameterSuffix . ')' + )->setParameter('newsletters' . $parameterSuffix, $newsletters, Connection::PARAM_INT_ARRAY); + + if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) { + $queryBuilder->groupBy('subscriber_id'); + $queryBuilder->having('COUNT(1) = ' . count($newsletters)); + } + } + + return $queryBuilder; + } } diff --git a/mailpoet/tests/integration/Segments/DynamicSegments/Filters/EmailActionTest.php b/mailpoet/tests/integration/Segments/DynamicSegments/Filters/EmailActionTest.php index cd023336ce..3e09c55082 100644 --- a/mailpoet/tests/integration/Segments/DynamicSegments/Filters/EmailActionTest.php +++ b/mailpoet/tests/integration/Segments/DynamicSegments/Filters/EmailActionTest.php @@ -292,6 +292,71 @@ class EmailActionTest extends \MailPoetTest { $this->assertEqualsCanonicalizing(['opened_machine@example.com'], $emails); } + public function testSentEmailAny(): void { + $subscriber1 = $this->createSubscriber('1@example.com'); + $subscriber2 = $this->createSubscriber('2@example.com'); + $this->createSubscriber('3@example.com'); + + + $this->createStatsNewsletter($subscriber1, $this->newsletter); + $this->createStatsNewsletter($subscriber2, $this->newsletter2); + + $segmentFilterData = $this->getSegmentFilterData(EmailAction::ACTION_WAS_SENT, [ + 'newsletters' => [$this->newsletter->getId(), $this->newsletter2->getId()], + 'operator' => DynamicSegmentFilterData::OPERATOR_ANY, + ]); + + $emails = $this->tester->getSubscriberEmailsMatchingDynamicFilter($segmentFilterData, $this->emailAction); + + expect($emails)->contains('1@example.com'); + expect($emails)->contains('2@example.com'); + expect($emails)->notContains('3@example.com'); + } + + public function testSentEmailAll(): void { + $subscriber1 = $this->createSubscriber('1@example.com'); + $subscriber2 = $this->createSubscriber('2@example.com'); + $subscriber3 = $this->createSubscriber('3@example.com'); + + $this->createStatsNewsletter($subscriber1, $this->newsletter); + $this->createStatsNewsletter($subscriber1, $this->newsletter2); + $this->createStatsNewsletter($subscriber2, $this->newsletter2); + $this->createStatsNewsletter($subscriber3, $this->newsletter2); + + $segmentFilterData = $this->getSegmentFilterData(EmailAction::ACTION_WAS_SENT, [ + 'newsletters' => [$this->newsletter->getId(), $this->newsletter2->getId()], + 'operator' => DynamicSegmentFilterData::OPERATOR_ALL, + ]); + + $emails = $this->tester->getSubscriberEmailsMatchingDynamicFilter($segmentFilterData, $this->emailAction); + + expect($emails)->contains('1@example.com'); + expect($emails)->notContains('2@example.com'); + expect($emails)->notContains('3@example.com'); + } + + public function testSentEmailNone(): void { + $subscriber1 = $this->createSubscriber('1@example.com'); + $subscriber2 = $this->createSubscriber('2@example.com'); + $subscriber3 = $this->createSubscriber('3@example.com'); + + $this->createStatsNewsletter($subscriber1, $this->newsletter); + $this->createStatsNewsletter($subscriber1, $this->newsletter2); + $this->createStatsNewsletter($subscriber2, $this->newsletter2); + $this->createStatsNewsletter($subscriber3, $this->newsletter3); + + $segmentFilterData = $this->getSegmentFilterData(EmailAction::ACTION_WAS_SENT, [ + 'newsletters' => [$this->newsletter->getId(), $this->newsletter2->getId()], + 'operator' => DynamicSegmentFilterData::OPERATOR_NONE, + ]); + + $emails = $this->tester->getSubscriberEmailsMatchingDynamicFilter($segmentFilterData, $this->emailAction); + + expect($emails)->notContains('1@example.com'); + expect($emails)->notContains('2@example.com'); + expect($emails)->contains('3@example.com'); + } + private function getSegmentFilterData(string $action, array $data): DynamicSegmentFilterData { return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $action, $data); } diff --git a/mailpoet/views/segments.html b/mailpoet/views/segments.html index f1bf35533a..01dca3f7e9 100644 --- a/mailpoet/views/segments.html +++ b/mailpoet/views/segments.html @@ -137,6 +137,7 @@ 'selectUserRolePlaceholder': __('Search user roles'), 'selectCustomFieldPlaceholder': __('Select custom field'), 'emailActionOpened': _x('opened', 'Dynamic segment creation: when newsletter was opened'), + 'emailActionWasSent': _x('was sent', 'Dynamic segment creation: when newsletter was sent'), 'emailActionMachineOpened': _x('machine-opened', 'Dynamic segment creation: list of all subscribers that opened the newsletter automatically in the background'), 'emailActionOpensAbsoluteCount': __('# of opens'), 'emailActionMachineOpensAbsoluteCount': __('# of machine-opens'),