AutomaticEmailScheduler.php
2 years ago
AutomationEmailScheduler.php
1 year ago
LatestNewsletterScheduler.php
2 months ago
PostNotificationScheduler.php
2 months ago
ReEngagementScheduler.php
5 days ago
Scheduler.php
1 year ago
WelcomeScheduler.php
2 years ago
index.php
3 years ago
ReEngagementScheduler.php
182 lines
| 1 | <?php declare(strict_types = 1); |
| 2 | |
| 3 | namespace MailPoet\Newsletter\Scheduler; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use MailPoet\Cron\Workers\SendingQueue\SendingQueue; |
| 9 | use MailPoet\Entities\NewsletterEntity; |
| 10 | use MailPoet\Entities\NewsletterOptionFieldEntity; |
| 11 | use MailPoet\Entities\ScheduledTaskEntity; |
| 12 | use MailPoet\Entities\ScheduledTaskSubscriberEntity; |
| 13 | use MailPoet\Entities\SendingQueueEntity; |
| 14 | use MailPoet\Entities\StatisticsNewsletterEntity; |
| 15 | use MailPoet\Entities\SubscriberEntity; |
| 16 | use MailPoet\Entities\SubscriberSegmentEntity; |
| 17 | use MailPoet\Newsletter\NewslettersRepository; |
| 18 | use MailPoet\Newsletter\Sending\ScheduledTasksRepository; |
| 19 | use MailPoet\Subscribers\TrackingConsentController; |
| 20 | use MailPoetVendor\Carbon\Carbon; |
| 21 | use MailPoetVendor\Doctrine\DBAL\ParameterType; |
| 22 | use MailPoetVendor\Doctrine\ORM\EntityManager; |
| 23 | |
| 24 | class ReEngagementScheduler { |
| 25 | |
| 26 | /** @var NewslettersRepository */ |
| 27 | private $newslettersRepository; |
| 28 | |
| 29 | /** @var ScheduledTasksRepository */ |
| 30 | private $scheduledTasksRepository; |
| 31 | |
| 32 | /** @var EntityManager */ |
| 33 | private $entityManager; |
| 34 | |
| 35 | /** @var TrackingConsentController */ |
| 36 | private $trackingConsentController; |
| 37 | |
| 38 | public function __construct( |
| 39 | NewslettersRepository $newslettersRepository, |
| 40 | ScheduledTasksRepository $scheduledTasksRepository, |
| 41 | EntityManager $entityManager, |
| 42 | TrackingConsentController $trackingConsentController |
| 43 | ) { |
| 44 | $this->newslettersRepository = $newslettersRepository; |
| 45 | $this->scheduledTasksRepository = $scheduledTasksRepository; |
| 46 | $this->entityManager = $entityManager; |
| 47 | $this->trackingConsentController = $trackingConsentController; |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * Schedules sending tasks for re-engagement emails |
| 52 | * @return ScheduledTaskEntity[] |
| 53 | */ |
| 54 | public function scheduleAll(): array { |
| 55 | $scheduled = []; |
| 56 | $emails = $this->newslettersRepository->findActiveByTypes([NewsletterEntity::TYPE_RE_ENGAGEMENT]); |
| 57 | if (!$emails) { |
| 58 | return $scheduled; |
| 59 | } |
| 60 | foreach ($emails as $email) { |
| 61 | $scheduled[] = $this->scheduleForEmail($email); |
| 62 | } |
| 63 | return array_filter($scheduled); |
| 64 | } |
| 65 | |
| 66 | private function scheduleForEmail(NewsletterEntity $newsletter): ?ScheduledTaskEntity { |
| 67 | $scheduledOrRunning = $this->scheduledTasksRepository->findByScheduledAndRunningForNewsletter($newsletter); |
| 68 | if ($scheduledOrRunning) { |
| 69 | return null; |
| 70 | } |
| 71 | $intervalUnit = $newsletter->getOptionValue(NewsletterOptionFieldEntity::NAME_AFTER_TIME_TYPE); |
| 72 | $intervalValue = (int)$newsletter->getOptionValue(NewsletterOptionFieldEntity::NAME_AFTER_TIME_NUMBER); |
| 73 | if (!$intervalValue || !in_array($intervalUnit, ['weeks', 'months'], true)) { |
| 74 | return null; |
| 75 | } |
| 76 | if (!$newsletter->getNewsletterSegments()->count()) { |
| 77 | return null; |
| 78 | } |
| 79 | |
| 80 | $scheduledTask = $this->scheduleTask(); |
| 81 | $enqueuedCount = 0; |
| 82 | foreach ($newsletter->getSegmentIds() as $segmentId) { |
| 83 | $enqueuedCount += $this->enqueueSubscribersForSegment((int)$newsletter->getId(), $segmentId, $scheduledTask, $intervalUnit, $intervalValue); |
| 84 | } |
| 85 | |
| 86 | if ($enqueuedCount) { |
| 87 | $this->createSendingQueue($newsletter, $scheduledTask, $enqueuedCount); |
| 88 | return $scheduledTask; |
| 89 | } else { |
| 90 | // Nothing to send |
| 91 | $this->scheduledTasksRepository->remove($scheduledTask); |
| 92 | $this->scheduledTasksRepository->flush(); |
| 93 | return null; |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | private function scheduleTask(): ScheduledTaskEntity { |
| 98 | // Scheduled task |
| 99 | $scheduledTask = new ScheduledTaskEntity(); |
| 100 | $scheduledTask->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED); |
| 101 | $scheduledTask->setScheduledAt(Carbon::now()->millisecond(0)); |
| 102 | $scheduledTask->setType(SendingQueue::TASK_TYPE); |
| 103 | $scheduledTask->setPriority(SendingQueueEntity::PRIORITY_MEDIUM); |
| 104 | $this->scheduledTasksRepository->persist($scheduledTask); |
| 105 | $this->scheduledTasksRepository->flush(); |
| 106 | return $scheduledTask; |
| 107 | } |
| 108 | |
| 109 | private function createSendingQueue(NewsletterEntity $newsletter, ScheduledTaskEntity $scheduledTask, int $countToProcess): SendingQueueEntity { |
| 110 | // Sending queue |
| 111 | $sendingQueue = new SendingQueueEntity(); |
| 112 | $sendingQueue->setTask($scheduledTask); |
| 113 | $sendingQueue->setNewsletter($newsletter); |
| 114 | $sendingQueue->setCountToProcess($countToProcess); |
| 115 | $sendingQueue->setCountTotal($countToProcess); |
| 116 | $this->entityManager->persist($sendingQueue); |
| 117 | $this->entityManager->flush(); |
| 118 | return $sendingQueue; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Finds subscribers that should receive re-engagement email and saves scheduled tasks subscribers |
| 123 | * @return int Count of enqueued subscribers |
| 124 | */ |
| 125 | private function enqueueSubscribersForSegment(int $newsletterId, int $segmentId, ScheduledTaskEntity $scheduledTask, string $intervalUnit, int $intervalValue): int { |
| 126 | // Parameters for scheduled task subscribers query |
| 127 | $thresholdDate = Carbon::now()->millisecond(0); |
| 128 | if ($intervalUnit === 'months') { |
| 129 | $thresholdDate->subMonths($intervalValue); |
| 130 | } else { |
| 131 | $thresholdDate->subWeeks($intervalValue); |
| 132 | } |
| 133 | $thresholdDateSql = $thresholdDate->toDateTimeString(); |
| 134 | // When checking engagement, we ignore emails that subscribers received in the last 24 hours so that we leave them some time to engage. |
| 135 | // This is prevention for sending re-engagement emails to subscribers who have received a single email very recently. |
| 136 | $upperThresholdDate = Carbon::now()->millisecond(0); |
| 137 | $upperThresholdDate->subDay(); |
| 138 | $upperThresholdDate = $upperThresholdDate->toDateTimeString(); |
| 139 | $taskId = $scheduledTask->getId(); |
| 140 | $subscribedStatus = SubscriberEntity::STATUS_SUBSCRIBED; |
| 141 | $newsletterStatsTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName(); |
| 142 | $scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName(); |
| 143 | $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName(); |
| 144 | $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); |
| 145 | $nowSql = Carbon::now()->millisecond(0)->toDateTimeString(); |
| 146 | |
| 147 | $query = "INSERT IGNORE INTO $scheduledTaskSubscribersTable |
| 148 | (subscriber_id, task_id, processed, created_at) |
| 149 | SELECT DISTINCT ns.subscriber_id as subscriber_id, :taskId as task_id, 0 as processed, :now as created_at |
| 150 | FROM $newsletterStatsTable as ns |
| 151 | JOIN $subscribersTable s ON |
| 152 | ns.subscriber_id = s.id |
| 153 | AND s.deleted_at is NULL |
| 154 | AND s.status = :subscribed |
| 155 | AND s.tracking_consent != 'denied' |
| 156 | AND (:trackUnknown = 1 OR s.tracking_consent != 'unknown') |
| 157 | AND GREATEST(COALESCE(s.created_at, '0'), COALESCE(s.last_subscribed_at, '0'), COALESCE(s.last_engagement_at, '0')) < :thresholdDate |
| 158 | JOIN $subscriberSegmentTable as ss ON ns.subscriber_id = ss.subscriber_id |
| 159 | AND ss.segment_id = :segmentId |
| 160 | AND ss.status = :subscribed |
| 161 | WHERE ns.sent_at > :thresholdDate |
| 162 | AND ns.sent_at < :upperThresholdDate |
| 163 | AND ns.subscriber_id NOT IN ( |
| 164 | SELECT DISTINCT subscriber_id as id FROM $newsletterStatsTable WHERE newsletter_id = :newsletterId AND sent_at > :thresholdDate |
| 165 | ); |
| 166 | "; |
| 167 | |
| 168 | $statement = $this->entityManager->getConnection()->prepare($query); |
| 169 | $statement->bindValue('now', $nowSql, ParameterType::STRING); |
| 170 | $statement->bindValue('taskId', $taskId, ParameterType::INTEGER); |
| 171 | $statement->bindValue('subscribed', $subscribedStatus, ParameterType::STRING); |
| 172 | $statement->bindValue('thresholdDate', $thresholdDateSql, ParameterType::STRING); |
| 173 | $statement->bindValue('upperThresholdDate', $upperThresholdDate, ParameterType::STRING); |
| 174 | $statement->bindValue('newsletterId', $newsletterId, ParameterType::INTEGER); |
| 175 | $statement->bindValue('segmentId', $segmentId, ParameterType::INTEGER); |
| 176 | $statement->bindValue('trackUnknown', $this->trackingConsentController->shouldTrackUnknownConsent() ? 1 : 0, ParameterType::INTEGER); |
| 177 | |
| 178 | $result = $statement->executeQuery(); |
| 179 | return $result->rowCount(); |
| 180 | } |
| 181 | } |
| 182 |