diff --git a/lib/Config/Database.php b/lib/Config/Database.php index 152885ee91..d5c70d1d34 100644 --- a/lib/Config/Database.php +++ b/lib/Config/Database.php @@ -86,6 +86,7 @@ class Database { $statistics_forms = Env::$db_prefix . 'statistics_forms'; $mapping_to_external_entities = Env::$db_prefix . 'mapping_to_external_entities'; $log = Env::$db_prefix . 'log'; + $stats_notifications = Env::$db_prefix . 'stats_notifications'; define('MP_SETTINGS_TABLE', $settings); define('MP_SEGMENTS_TABLE', $segments); @@ -112,6 +113,7 @@ class Database { define('MP_STATISTICS_FORMS_TABLE', $statistics_forms); define('MP_MAPPING_TO_EXTERNAL_ENTITIES_TABLE', $mapping_to_external_entities); define('MP_LOG_TABLE', $log); + define('MP_STATS_NOTIFICATIONS_TABLE', $stats_notifications); } } } diff --git a/lib/Config/Migrator.php b/lib/Config/Migrator.php index fd02fcbbd8..574f8b34ee 100644 --- a/lib/Config/Migrator.php +++ b/lib/Config/Migrator.php @@ -23,6 +23,7 @@ class Migrator { 'settings', 'custom_fields', 'scheduled_tasks', + 'stats_notifications', 'scheduled_task_subscribers', 'sending_queues', 'subscribers', @@ -131,12 +132,26 @@ class Migrator { return $this->sqlify(__FUNCTION__, $attributes); } - function scheduledTaskSubscribers() { + function statsNotifications() { $attributes = array( + 'id int(11) unsigned NOT NULL AUTO_INCREMENT,', + 'newsletter_id int(11) unsigned NOT NULL,', + 'task_id int(11) unsigned NOT NULL,', + 'created_at TIMESTAMP NULL,', + 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', + 'PRIMARY KEY (id),', + 'UNIQUE KEY newsletter_id_task_id (newsletter_id, task_id),', + 'KEY task_id (task_id)', + ); + return $this->sqlify(__FUNCTION__, $attributes); + } + + function scheduledTaskSubscribers() { + $attributes = array ( 'task_id int(11) unsigned NOT NULL,', 'subscriber_id int(11) unsigned NOT NULL,', 'processed int(1) NOT NULL,', - 'failed int(1) NOT NULL DEFAULT 0,', + 'failed SMALLINT(1) NOT NULL DEFAULT 0,', 'error text NULL,', 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,', 'PRIMARY KEY (task_id, subscriber_id),', diff --git a/lib/Config/Populator.php b/lib/Config/Populator.php index 9150ad639e..43db70790a 100644 --- a/lib/Config/Populator.php +++ b/lib/Config/Populator.php @@ -194,6 +194,15 @@ class Populator { ]); } + $stats_notifications = Setting::getValue('stats_notifications'); + if(empty($stats_notifications)) { + $sender = Setting::getValue('sender', []); + Setting::setValue('stats_notifications', [ + 'enabled' => true, + 'address' => isset($sender['address'])? $sender['address'] : null, + ]); + } + // reset mailer log MailerLog::resetMailerLog(); } diff --git a/lib/Cron/Daemon.php b/lib/Cron/Daemon.php index fc0c86daa4..a2ee283efe 100644 --- a/lib/Cron/Daemon.php +++ b/lib/Cron/Daemon.php @@ -1,20 +1,19 @@ timer = microtime(true); + $this->workers_factory = $workers_factory; } function run($settings_daemon_data) { @@ -22,6 +21,7 @@ class Daemon { CronHelper::saveDaemon($settings_daemon_data); try { $this->executeMigrationWorker(); + $this->executeStatsNotificationsWorker(); $this->executeScheduleWorker(); $this->executeQueueWorker(); $this->executeSendingServiceKeyCheckWorker(); @@ -35,32 +35,37 @@ class Daemon { } function executeScheduleWorker() { - $scheduler = new SchedulerWorker($this->timer); + $scheduler = $this->workers_factory->createScheduleWorker($this->timer); return $scheduler->process(); } function executeQueueWorker() { - $queue = new SendingQueueWorker(new SendingErrorHandler(), $this->timer); + $queue = $this->workers_factory->createQueueWorker($this->timer); return $queue->process(); } + function executeStatsNotificationsWorker() { + $worker = $this->workers_factory->createStatsNotificationsWorker($this->timer); + return $worker->process(); + } + function executeSendingServiceKeyCheckWorker() { - $worker = new SendingServiceKeyCheckWorker($this->timer); + $worker = $this->workers_factory->createSendingServiceKeyCheckWorker($this->timer); return $worker->process(); } function executePremiumKeyCheckWorker() { - $worker = new PremiumKeyCheckWorker($this->timer); + $worker = $this->workers_factory->createPremiumKeyCheckWorker($this->timer); return $worker->process(); } function executeBounceWorker() { - $bounce = new BounceWorker($this->timer); + $bounce = $this->workers_factory->createBounceWorker($this->timer); return $bounce->process(); } function executeMigrationWorker() { - $migration = new MigrationWorker($this->timer); + $migration = $this->workers_factory->createMigrationWorker($this->timer); return $migration->process(); } diff --git a/lib/Cron/Triggers/WordPress.php b/lib/Cron/Triggers/WordPress.php index 9bfc417db8..5f97591df2 100644 --- a/lib/Cron/Triggers/WordPress.php +++ b/lib/Cron/Triggers/WordPress.php @@ -8,6 +8,7 @@ use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker; use MailPoet\Cron\Workers\Bounce as BounceWorker; use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck as PremiumKeyCheckWorker; use MailPoet\Cron\Workers\KeyCheck\SendingServiceKeyCheck as SendingServiceKeyCheckWorker; +use MailPoet\Cron\Workers\StatsNotifications\Worker; use MailPoet\Mailer\MailerLog; use MailPoet\Models\Setting; use MailPoet\Services\Bridge; @@ -44,6 +45,8 @@ class WordPress { $premium_key_specified = Bridge::isPremiumKeySpecified(); $premium_keycheck_due_tasks = PremiumKeyCheckWorker::getDueTasks(); $premium_keycheck_future_tasks = PremiumKeyCheckWorker::getFutureTasks(); + // stats notifications + $stats_notifications_tasks = (bool)Worker::getDueTasks(); // check requirements for each worker $sending_queue_active = (($scheduled_queues || $running_queues) && !$sending_limit_reached && !$sending_is_paused); $bounce_sync_active = ($mp_sending_enabled && ($bounce_due_tasks || !$bounce_future_tasks)); @@ -57,6 +60,7 @@ class WordPress { || $bounce_sync_active || $sending_service_key_check_active || $premium_key_check_active + || $stats_notifications_tasks ); } diff --git a/lib/Cron/Workers/SendingQueue/SendingQueue.php b/lib/Cron/Workers/SendingQueue/SendingQueue.php index dc49b1db0a..970c5b654b 100644 --- a/lib/Cron/Workers/SendingQueue/SendingQueue.php +++ b/lib/Cron/Workers/SendingQueue/SendingQueue.php @@ -5,6 +5,7 @@ use MailPoet\Cron\CronHelper; use MailPoet\Cron\Workers\SendingQueue\Tasks\Links; use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask; use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask; +use MailPoet\Cron\Workers\StatsNotifications\Scheduler as StatsNotificationsScheduler; use MailPoet\Logging\Logger; use MailPoet\Mailer\MailerError; use MailPoet\Mailer\MailerLog; @@ -26,11 +27,15 @@ class SendingQueue { const BATCH_SIZE = 20; const TASK_BATCH_SIZE = 5; + /** @var StatsNotificationsScheduler */ + public $stats_notifications_scheduler; + /** @var SendingErrorHandler */ private $error_handler; - function __construct(SendingErrorHandler $error_handler, $timer = false, $mailer_task = false, $newsletter_task = false) { + function __construct(SendingErrorHandler $error_handler, StatsNotificationsScheduler $stats_notifications_scheduler, $timer = false, $mailer_task = false, $newsletter_task = false) { $this->error_handler = $error_handler; + $this->stats_notifications_scheduler = $stats_notifications_scheduler; $this->mailer_task = ($mailer_task) ? $mailer_task : new MailerTask(); $this->newsletter_task = ($newsletter_task) ? $newsletter_task : new NewsletterTask(); $this->timer = ($timer) ? $timer : microtime(true); @@ -99,6 +104,7 @@ class SendingQueue { ); if($queue->status === ScheduledTaskModel::STATUS_COMPLETED) { $this->newsletter_task->markNewsletterAsSent($newsletter, $queue); + $this->stats_notifications_scheduler->schedule($newsletter); } $this->enforceSendingAndExecutionLimits(); } diff --git a/lib/Cron/Workers/StatsNotifications/Scheduler.php b/lib/Cron/Workers/StatsNotifications/Scheduler.php new file mode 100644 index 0000000000..ba094f17f4 --- /dev/null +++ b/lib/Cron/Workers/StatsNotifications/Scheduler.php @@ -0,0 +1,84 @@ +shouldSchedule($newsletter)) { + return false; + } + + $task = ScheduledTask::create(); + $task->type = Worker::TASK_TYPE; + $task->status = ScheduledTask::STATUS_SCHEDULED; + $task->scheduled_at = $this->getNextRunDate(); + $task->save(); + + $stats_notifications = StatsNotification::create(); + $stats_notifications->newsletter_id = $newsletter->id; + $stats_notifications->task_id = $task->id; + $stats_notifications->save(); + } + + private function shouldSchedule(Newsletter $newsletter) { + if($this->isDisabled()) { + return false; + } + if($this->isTaskScheduled($newsletter->id)) { + return false; + } + if(($newsletter->type !== Newsletter::TYPE_NOTIFICATION) && ($newsletter->type !== Newsletter::TYPE_STANDARD)) { + return false; + } + return true; + } + + private function isDisabled() { + $settings = Setting::getValue(Worker::SETTINGS_KEY); + if(!is_array($settings)) { + return true; + } + if(!isset($settings['enabled'])) { + return true; + } + if(!isset($settings['address'])) { + return true; + } + if(empty(trim($settings['address']))) { + return true; + } + if(!(bool)Setting::getValue('tracking.enabled')) { + return true; + } + return !(bool)$settings['enabled']; + } + + private function isTaskScheduled($newsletter_id) { + $existing = ScheduledTask::table_alias('tasks') + ->join(StatsNotification::$_table, 'tasks.id = notification.task_id', 'notification') + ->where('tasks.type', Worker::TASK_TYPE) + ->where('notification.newsletter_id', $newsletter_id) + ->findMany(); + return (bool)$existing; + } + + private function getNextRunDate() { + $date = new Carbon(); + $date->addHours(self::HOURS_TO_SEND_AFTER_NEWSLETTER); + return $date; + } + +} diff --git a/lib/Cron/Workers/StatsNotifications/Worker.php b/lib/Cron/Workers/StatsNotifications/Worker.php new file mode 100644 index 0000000000..3a216dfff7 --- /dev/null +++ b/lib/Cron/Workers/StatsNotifications/Worker.php @@ -0,0 +1,138 @@ +timer = $timer ?: microtime(true); + $this->renderer = $renderer; + $this->mailer = $mailer; + } + + /** @throws \Exception */ + function process() { + $settings = Setting::getValue(self::SETTINGS_KEY); + $this->mailer->sender = $this->mailer->getSenderNameAndAddress($this->constructSenderEmail()); + foreach(self::getDueTasks() as $task) { + try { + $this->mailer->send($this->constructNewsletter($task), $settings['address']); + } catch(\Exception $e) { + if(WP_DEBUG) { + throw $e; + } + } finally { + $this->markTaskAsFinished($task); + } + CronHelper::enforceExecutionLimit($this->timer); + } + } + + private function constructSenderEmail() { + $url_parts = parse_url(home_url()); + $site_name = strtolower($url_parts['host']); + if(strpos($site_name, 'www.') === 0) { + $site_name = substr($site_name, 4); + } + return [ + 'address' => self::SENDER_EMAIL_PREFIX . $site_name, + 'name' => self::SENDER_EMAIL_PREFIX . $site_name, + ]; + } + + public static function getDueTasks() { + $date = new Carbon(); + return ScheduledTask::orderByAsc('priority') + ->orderByAsc('updated_at') + ->whereNull('deleted_at') + ->where('status', ScheduledTask::STATUS_SCHEDULED) + ->whereLte('scheduled_at', $date) + ->where('type', self::TASK_TYPE) + ->limit(Sending::RESULT_BATCH_SIZE) + ->findMany(); + } + + private function constructNewsletter(ScheduledTask $task) { + $newsletter = $this->getNewsletter($task); + $link = NewsletterLink::findTopLinkForNewsletter($newsletter); + $context = $this->prepareContext($newsletter, $link); + return [ + 'subject' => sprintf(_x('Stats for email %s', 'title of an automatic email containing statistics (newsletter open rate, click rate, etc)', 'mailpoet'), $newsletter->subject), + 'body' => [ + 'html' => $this->renderer->render('emails/statsNotification.html', $context), + 'text' => $this->renderer->render('emails/statsNotification.txt', $context), + ], + ]; + } + + private function getNewsletter(ScheduledTask $task) { + $statsNotificationModel = $task->statsNotification()->findOne(); + $newsletter = $statsNotificationModel->newsletter()->findOne(); + if(!$newsletter) { + throw new \Exception('Newsletter not found'); + } + return $newsletter + ->withSendingQueue() + ->withTotalSent() + ->withStatistics(); + } + + private function prepareContext(Newsletter $newsletter, NewsletterLink $link = null) { + $clicked = ($newsletter->statistics['clicked'] * 100) / $newsletter->total_sent; + $opened = ($newsletter->statistics['opened'] * 100) / $newsletter->total_sent; + $unsubscribed = ($newsletter->statistics['unsubscribed'] * 100) / $newsletter->total_sent; + $context = [ + 'subject' => $newsletter->subject, + 'preheader' => sprintf(_x( + '%1$s%% opens, %2$s%% clicks, %3$s%% unsubscribes in a nutshell.', 'newsletter open rate, click rate and unsubscribe rate', 'mailpoet'), + number_format($opened, 2), + number_format($clicked, 2), + number_format($unsubscribed, 2) + ), + 'topLinkClicks' => 0, + 'linkSettings' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-settings#basics'), + 'linkStats' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-newsletters#/stats/' . $newsletter->id()), + 'premiumPage' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-premium'), + 'premiumPluginActive' => is_plugin_active('mailpoet-premium/mailpoet-premium.php'), + 'clicked' => $clicked, + 'opened' => $opened, + ]; + if($link) { + $context['topLinkClicks'] = (int)$link->clicksCount; + $context['topLink'] = $link->url; + } + return $context; + } + + private function markTaskAsFinished(ScheduledTask $task) { + $task->status = ScheduledTask::STATUS_COMPLETED; + $task->processed_at = new Carbon; + $task->scheduled_at = null; + $task->save(); + } + +} diff --git a/lib/Cron/Workers/WorkersFactory.php b/lib/Cron/Workers/WorkersFactory.php new file mode 100644 index 0000000000..88107246a7 --- /dev/null +++ b/lib/Cron/Workers/WorkersFactory.php @@ -0,0 +1,74 @@ +sending_error_handler = $sending_error_handler; + $this->scheduler = $scheduler; + $this->mailer = $mailer; + $this->renderer = $renderer; + } + + /** @return SchedulerWorker */ + function createScheduleWorker($timer) { + return new SchedulerWorker($timer); + } + + /** @return SendingQueueWorker */ + function createQueueWorker($timer) { + return new SendingQueueWorker($this->sending_error_handler, $this->scheduler, $timer); + } + + function createStatsNotificationsWorker($timer) { + return new StatsNotificationsWorker($this->mailer, $this->renderer, $timer); + } + + /** @return SendingServiceKeyCheckWorker */ + function createSendingServiceKeyCheckWorker($timer) { + return new SendingServiceKeyCheckWorker($timer); + } + + /** @return PremiumKeyCheckWorker */ + function createPremiumKeyCheckWorker($timer) { + return new PremiumKeyCheckWorker($timer); + } + + /** @return BounceWorker */ + function createBounceWorker($timer) { + return new BounceWorker($timer); + } + + /** @return MigrationWorker */ + function createMigrationWorker($timer) { + return new MigrationWorker($timer); + } + +} diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index f865a2a4c8..681afb4b66 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -51,9 +51,13 @@ class ContainerConfigurator implements IContainerConfigurator { // Config $container->autowire(\MailPoet\Config\AccessControl::class)->setPublic(true); $container->autowire(\MailPoet\Config\Hooks::class)->setPublic(true); + $container->register(\MailPoet\Config\Renderer::class)->setFactory([__CLASS__, 'createRenderer']); // Cron $container->autowire(\MailPoet\Cron\Daemon::class)->setPublic(true); $container->autowire(\MailPoet\Cron\DaemonHttpRunner::class)->setPublic(true); + $container->autowire(\MailPoet\Cron\Workers\WorkersFactory::class)->setPublic(true); + $container->autowire(\MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler::class)->setPublic(true); + $container->autowire(\MailPoet\Cron\Workers\StatsNotifications\Scheduler::class); // Listing $container->autowire(\MailPoet\Listing\BulkActionController::class)->setPublic(true); $container->autowire(\MailPoet\Listing\Handler::class)->setPublic(true); @@ -62,6 +66,8 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Router\Endpoints\Subscription::class)->setPublic(true); $container->autowire(\MailPoet\Router\Endpoints\Track::class)->setPublic(true); $container->autowire(\MailPoet\Router\Endpoints\ViewInBrowser::class)->setPublic(true); + // Mailer + $container->autowire(\MailPoet\Mailer\Mailer::class); // Subscribers $container->autowire(\MailPoet\Subscribers\NewSubscriberNotificationMailer::class)->setPublic(true); $container->autowire(\MailPoet\Subscribers\ConfirmationEmailMailer::class)->setPublic(true); @@ -94,4 +100,10 @@ class ContainerConfigurator implements IContainerConfigurator { } return $container->get(IContainerConfigurator::PREMIUM_CONTAINER_SERVICE_SLUG)->get($id); } + + static function createRenderer() { + $caching = !WP_DEBUG; + $debugging = WP_DEBUG; + return new \MailPoet\Config\Renderer($caching, $debugging); + } } diff --git a/lib/Models/NewsletterLink.php b/lib/Models/NewsletterLink.php index 661858455a..b29f2f2586 100644 --- a/lib/Models/NewsletterLink.php +++ b/lib/Models/NewsletterLink.php @@ -5,4 +5,23 @@ if(!defined('ABSPATH')) exit; class NewsletterLink extends Model { public static $_table = MP_NEWSLETTER_LINKS_TABLE; + + static function findTopLinkForNewsletter(Newsletter $newsletter) { + $link = self::selectExpr('links.*') + ->selectExpr('count(*)', 'clicksCount') + ->tableAlias('links') + ->innerJoin(StatisticsClicks::$_table, + array('clicks.link_id', '=', 'links.id'), + 'clicks') + ->where('newsletter_id', $newsletter->id()) + ->groupBy('links.id') + ->orderByDesc('clicksCount') + ->limit(1) + ->findOne(); + if(!$link) { + return null; + } + return $link; + } + } diff --git a/lib/Models/ScheduledTask.php b/lib/Models/ScheduledTask.php index 9cdb71e0c6..a37c8c5440 100644 --- a/lib/Models/ScheduledTask.php +++ b/lib/Models/ScheduledTask.php @@ -37,6 +37,15 @@ class ScheduledTask extends Model { ); } + /** @return StatsNotification */ + function statsNotification() { + return $this->hasOne( + StatsNotification::class, + 'task_id', + 'id' + ); + } + function pause() { $this->set('status', self::STATUS_PAUSED); $this->save(); diff --git a/lib/Models/StatsNotification.php b/lib/Models/StatsNotification.php new file mode 100644 index 0000000000..b02cb2720a --- /dev/null +++ b/lib/Models/StatsNotification.php @@ -0,0 +1,42 @@ +hasOne( + Newsletter::class, + 'id', + 'newsletter_id' + ); + } + + /** @return StatsNotification */ + static function createOrUpdate($data = array()) { + $model = null; + + if(isset($data['id']) && (int)$data['id'] > 0) { + $model = static::findOne((int)$data['id']); + } + + if(!$model && isset($data['task_id']) && $data['newsletter_id']) { + $model = self::where('newsletter_id', $data['newsletter_id']) + ->where('task_id', $data['task_id']) + ->findOne(); + } + + if(!$model) { + $model = static::create(); + $model->hydrate($data); + } else { + unset($data['id']); + $model->set($data); + } + + return $model->save(); + } + +} diff --git a/lib/Subscribers/NewSubscriberNotificationMailer.php b/lib/Subscribers/NewSubscriberNotificationMailer.php index 71034eb9d0..f96870c5da 100644 --- a/lib/Subscribers/NewSubscriberNotificationMailer.php +++ b/lib/Subscribers/NewSubscriberNotificationMailer.php @@ -6,6 +6,7 @@ use MailPoet\Config\Renderer; use MailPoet\Models\Segment; use MailPoet\Models\Setting; use MailPoet\Models\Subscriber; +use MailPoet\WP\Functions; class NewSubscriberNotificationMailer { @@ -18,11 +19,15 @@ class NewSubscriberNotificationMailer { /** @var \MailPoet\Mailer\Mailer */ private $mailer; + /** @var Functions */ + private $wordpress_functions; + /** * @param \MailPoet\Mailer\Mailer|null $mailer * @param Renderer|null $renderer + * @param Functions|null $wordpress_functions */ - function __construct($mailer = null, $renderer = null) { + function __construct($mailer = null, $renderer = null, $wordpress_functions = null) { if($renderer) { $this->renderer = $renderer; } else { @@ -30,6 +35,11 @@ class NewSubscriberNotificationMailer { $debugging = WP_DEBUG; $this->renderer = new Renderer($caching, $debugging); } + if($wordpress_functions) { + $this->wordpress_functions = $wordpress_functions; + } else { + $this->wordpress_functions = new Functions(); + } if($mailer) { $this->mailer = $mailer; } else { @@ -75,7 +85,7 @@ class NewSubscriberNotificationMailer { } private function constructSenderEmail() { - $url_parts = parse_url(home_url()); + $url_parts = parse_url($this->wordpress_functions->homeUrl()); $site_name = strtolower($url_parts['host']); if(substr($site_name, 0, 4) === 'www.') { $site_name = substr($site_name, 4); diff --git a/lib/WP/Functions.php b/lib/WP/Functions.php index a98c359f44..9a39ff27e1 100644 --- a/lib/WP/Functions.php +++ b/lib/WP/Functions.php @@ -28,6 +28,10 @@ class Functions { return call_user_func_array('current_time', func_get_args()); } + function homeUrl() { + return call_user_func_array('home_url', func_get_args()); + } + function getImageInfo($id) { /* * In some cases wp_get_attachment_image_src ignore the second parameter diff --git a/tests/integration/Cron/CronHelperTest.php b/tests/integration/Cron/CronHelperTest.php index e3283b16ea..5b834b68de 100644 --- a/tests/integration/Cron/CronHelperTest.php +++ b/tests/integration/Cron/CronHelperTest.php @@ -17,6 +17,10 @@ class CronHelperTest extends \MailPoetTest { Setting::setValue('cron_trigger', array( 'method' => 'none' )); + Setting::setValue('sender', array( + 'name' => 'John Doe', + 'address' => 'john.doe@example.com' + )); } function testItDefinesConstants() { diff --git a/tests/integration/Cron/DaemonHttpRunnerTest.php b/tests/integration/Cron/DaemonHttpRunnerTest.php index 3e37415e02..5a80a7546c 100644 --- a/tests/integration/Cron/DaemonHttpRunnerTest.php +++ b/tests/integration/Cron/DaemonHttpRunnerTest.php @@ -6,6 +6,8 @@ use Codeception\Stub\Expected; use MailPoet\Cron\CronHelper; use MailPoet\Cron\Daemon; use MailPoet\Cron\DaemonHttpRunner; +use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler; +use MailPoet\Cron\Workers\WorkersFactory; use MailPoet\Models\Setting; class DaemonHttpRunnerTest extends \MailPoetTest { @@ -20,27 +22,25 @@ class DaemonHttpRunnerTest extends \MailPoetTest { } function testItDoesNotRunWithoutRequestData() { - $daemon = Stub::construct( - new DaemonHttpRunner(new Daemon()), - array(), - array( + $daemon = Stub::make( + DaemonHttpRunner::class, + [ 'abortWithError' => function($message) { return $message; } - ) + ] ); expect($daemon->run(false))->equals('Invalid or missing request data.'); } function testItDoesNotRunWhenThereIsInvalidOrMissingToken() { - $daemon = Stub::construct( - new DaemonHttpRunner(new Daemon()), - array(), - array( + $daemon = Stub::make( + DaemonHttpRunner::class, + [ 'abortWithError' => function($message) { return $message; } - ) + ] ); $daemon->settings_daemon_data = array( 'token' => 123 @@ -52,14 +52,19 @@ class DaemonHttpRunnerTest extends \MailPoetTest { $data = array( 'token' => 123 ); - $daemon = Stub::make(new Daemon(), array( - 'executeScheduleWorker' => function() { - throw new \Exception('Message'); - }, - 'executeQueueWorker' => function() { - throw new \Exception(); - }, - ), $this); + $daemon = Stub::make( + Daemon::class, + [ + 'executeScheduleWorker' => function() { + throw new \Exception('Message'); + }, + 'executeQueueWorker' => function() { + throw new \Exception(); + }, + 'executeMigrationWorker' => null, + 'executeStatsNotificationsWorker' => null, + ] + ); $daemon_http_runner = Stub::make(new DaemonHttpRunner($daemon), array( 'pauseExecution' => null, 'callSelf' => null @@ -72,16 +77,14 @@ class DaemonHttpRunnerTest extends \MailPoetTest { } function testItCanPauseExecution() { - $daemon = Stub::make(new Daemon(), array( - 'executeScheduleWorker' => null, - 'executeQueueWorker' => null, - ), $this); - $daemon_http_runner = Stub::make(new DaemonHttpRunner($daemon), array( + $daemon = Stub::makeEmpty(Daemon::class); + $daemon_http_runner = Stub::make(DaemonHttpRunner::class, array( 'pauseExecution' => Expected::exactly(1, function($pause_delay) { expect($pause_delay)->lessThan(CronHelper::DAEMON_EXECUTION_LIMIT); expect($pause_delay)->greaterThan(CronHelper::DAEMON_EXECUTION_LIMIT - 1); }), - 'callSelf' => null + 'callSelf' => null, + 'terminateRequest' => null, ), $this); $data = array( 'token' => 123 @@ -93,7 +96,7 @@ class DaemonHttpRunnerTest extends \MailPoetTest { function testItTerminatesExecutionWhenDaemonIsDeleted() { - $daemon = Stub::make(new DaemonHttpRunner(new Daemon()), array( + $daemon = Stub::make(DaemonHttpRunner::class, array( 'executeScheduleWorker' => function() { Setting::deleteValue(CronHelper::DAEMON_SETTING); }, @@ -105,12 +108,12 @@ class DaemonHttpRunnerTest extends \MailPoetTest { 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct(new Daemon()); + $daemon->__construct(Stub::makeEmpty(Daemon::class)); $daemon->run($data); } function testItTerminatesExecutionWhenDaemonTokenChangesAndKeepsChangedToken() { - $daemon = Stub::make(new DaemonHttpRunner(new Daemon()), array( + $daemon = Stub::make(DaemonHttpRunner::class, array( 'executeScheduleWorker' => function() { Setting::setValue( CronHelper::DAEMON_SETTING, @@ -125,14 +128,14 @@ class DaemonHttpRunnerTest extends \MailPoetTest { 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct(new Daemon()); + $daemon->__construct(Stub::makeEmpty(Daemon::class)); $daemon->run($data); $data_after_run = Setting::getValue(CronHelper::DAEMON_SETTING); expect($data_after_run['token'], 567); } function testItTerminatesExecutionWhenDaemonIsDeactivated() { - $daemon = Stub::make(new DaemonHttpRunner(new Daemon()), [ + $daemon = Stub::make(DaemonHttpRunner::class, [ 'executeScheduleWorker' => null, 'executeQueueWorker' => null, 'pauseExecution' => null, @@ -143,34 +146,40 @@ class DaemonHttpRunnerTest extends \MailPoetTest { 'status' => CronHelper::DAEMON_STATUS_INACTIVE, ]; Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct(new Daemon()); + $daemon->__construct(Stub::makeEmpty(Daemon::class)); $daemon->run($data); } function testItUpdatesDaemonTokenDuringExecution() { - $daemon_http_runner = Stub::make(new DaemonHttpRunner(new Daemon()), array( + $daemon_http_runner = Stub::make(DaemonHttpRunner::class, array( 'executeScheduleWorker' => null, 'executeQueueWorker' => null, 'pauseExecution' => null, - 'callSelf' => null + 'callSelf' => null, + 'terminateRequest' => null, ), $this); $data = array( 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon_http_runner->__construct(new Daemon()); + $daemon_http_runner->__construct(Stub::makeEmptyExcept(Daemon::class, 'run')); $daemon_http_runner->run($data); $updated_daemon = Setting::getValue(CronHelper::DAEMON_SETTING); expect($updated_daemon['token'])->equals($daemon_http_runner->token); } function testItUpdatesTimestampsDuringExecution() { - $daemon = Stub::make(new Daemon(), array( - 'executeScheduleWorker' => function() { - sleep(2); - }, - 'executeQueueWorker' => null, - ), $this); + $daemon = Stub::make(Daemon::class, [ + 'executeScheduleWorker' => function() { + sleep(2); + }, + 'executeQueueWorker' => function() { + throw new \Exception(); + }, + 'executeMigrationWorker' => null, + 'executeStatsNotificationsWorker' => null, + ] + ); $daemon_http_runner = Stub::make(new DaemonHttpRunner($daemon), array( 'pauseExecution' => null, 'callSelf' => null @@ -192,25 +201,26 @@ class DaemonHttpRunnerTest extends \MailPoetTest { function testItCanRun() { ignore_user_abort(0); expect(ignore_user_abort())->equals(0); - $daemon = Stub::make(new DaemonHttpRunner(new Daemon()), array( + $daemon = Stub::make(DaemonHttpRunner::class, array( 'pauseExecution' => null, // workers should be executed 'executeScheduleWorker' => Expected::exactly(1), 'executeQueueWorker' => Expected::exactly(1), // daemon should call itself 'callSelf' => Expected::exactly(1), + 'terminateRequest' => null, ), $this); $data = array( 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct(new Daemon()); + $daemon->__construct(Stub::makeEmptyExcept(Daemon::class, 'run')); $daemon->run($data); expect(ignore_user_abort())->equals(1); } function testItRespondsToPingRequest() { - $daemon = Stub::make(new DaemonHttpRunner(new Daemon()), array( + $daemon = Stub::make(DaemonHttpRunner::class, array( 'terminateRequest' => Expected::exactly(1, function($message) { expect($message)->equals('pong'); }) diff --git a/tests/integration/Cron/DaemonTest.php b/tests/integration/Cron/DaemonTest.php index 1053478f03..0e0cee380c 100644 --- a/tests/integration/Cron/DaemonTest.php +++ b/tests/integration/Cron/DaemonTest.php @@ -6,39 +6,44 @@ use Codeception\Stub\Expected; use MailPoet\Cron\CronHelper; use MailPoet\Cron\DaemonHttpRunner; use MailPoet\Cron\Daemon; +use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler; +use MailPoet\Cron\Workers\WorkersFactory; use MailPoet\Models\Setting; class DaemonTest extends \MailPoetTest { function testItCanExecuteWorkers() { - $daemon = Stub::make(new Daemon(), array( + $daemon = Stub::make(Daemon::class, array( 'executeScheduleWorker' => Expected::exactly(1), 'executeQueueWorker' => Expected::exactly(1), - 'pauseExecution' => null, - 'callSelf' => null + 'executeMigrationWorker' => null, + 'executeStatsNotificationsWorker' => null, + 'executeSendingServiceKeyCheckWorker' => null, + 'executePremiumKeyCheckWorker' => null, + 'executeBounceWorker' => null, ), $this); $data = array( 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct($data); $daemon->run([]); } function testItCanRun() { - $daemon = Stub::make(new Daemon(), array( - 'pauseExecution' => null, + $daemon = Stub::make(Daemon::class, array( // workers should be executed 'executeScheduleWorker' => Expected::exactly(1), 'executeQueueWorker' => Expected::exactly(1), - // daemon should call itself - 'callSelf' => Expected::exactly(1), + 'executeMigrationWorker' => Expected::exactly(1), + 'executeStatsNotificationsWorker' => Expected::exactly(1), + 'executeSendingServiceKeyCheckWorker' => Expected::exactly(1), + 'executePremiumKeyCheckWorker' => Expected::exactly(1), + 'executeBounceWorker' => Expected::exactly(1) ), $this); $data = array( 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct(); $daemon->run($data); } diff --git a/tests/integration/Cron/Workers/SendingQueue/SendingQueueTest.php b/tests/integration/Cron/Workers/SendingQueue/SendingQueueTest.php index 80cc27d515..3756782b91 100644 --- a/tests/integration/Cron/Workers/SendingQueue/SendingQueueTest.php +++ b/tests/integration/Cron/Workers/SendingQueue/SendingQueueTest.php @@ -12,6 +12,7 @@ use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler; use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker; use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask; use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask; +use MailPoet\Cron\Workers\StatsNotifications\Scheduler as StatsNotificationsScheduler; use MailPoet\Mailer\MailerLog; use MailPoet\Models\Newsletter; use MailPoet\Models\NewsletterLink; @@ -36,6 +37,9 @@ class SendingQueueTest extends \MailPoetTest { /** @var SendingErrorHandler */ private $sending_error_handler; + /** @var Scheduler */ + private $stats_notifications_worker; + function _before() { $wp_users = get_users(); wp_set_current_user($wp_users[0]->ID); @@ -76,7 +80,8 @@ class SendingQueueTest extends \MailPoetTest { $this->newsletter_link->hash = 'abcde'; $this->newsletter_link->save(); $this->sending_error_handler = new SendingErrorHandler(); - $this->sending_queue_worker = new SendingQueueWorker($this->sending_error_handler); + $this->stats_notifications_worker = new StatsNotificationsScheduler(); + $this->sending_queue_worker = new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker); } private function getDirectUnsubscribeURL() { @@ -106,20 +111,20 @@ class SendingQueueTest extends \MailPoetTest { // constructor accepts timer argument $timer = microtime(true) - 5; - $sending_queue_worker = new SendingQueueWorker($this->sending_error_handler, $timer); + $sending_queue_worker = new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker, $timer); expect($sending_queue_worker->timer)->equals($timer); } function testItEnforcesExecutionLimitsBeforeQueueProcessing() { $sending_queue_worker = Stub::make( - new SendingQueueWorker($this->sending_error_handler), + new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker), array( 'processQueue' => Expected::never(), 'enforceSendingAndExecutionLimits' => Expected::exactly(1, function() { throw new \Exception(); }) ), $this); - $sending_queue_worker->__construct($this->sending_error_handler); + $sending_queue_worker->__construct($this->sending_error_handler, $this->stats_notifications_worker); try { $sending_queue_worker->process(); self::fail('Execution limits function was not called.'); @@ -130,12 +135,13 @@ class SendingQueueTest extends \MailPoetTest { function testItEnforcesExecutionLimitsAfterSendingWhenQueueStatusIsNotSetToComplete() { $sending_queue_worker = Stub::make( - new SendingQueueWorker($this->sending_error_handler), + new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker), array( 'enforceSendingAndExecutionLimits' => Expected::exactly(1) ), $this); $sending_queue_worker->__construct( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -164,12 +170,13 @@ class SendingQueueTest extends \MailPoetTest { $queue = $this->queue; $queue->status = SendingQueue::STATUS_COMPLETED; $sending_queue_worker = Stub::make( - new SendingQueueWorker($this->sending_error_handler), + new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker), array( 'enforceSendingAndExecutionLimits' => Expected::never() ), $this); $sending_queue_worker->__construct( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -193,7 +200,7 @@ class SendingQueueTest extends \MailPoetTest { function testItEnforcesExecutionLimitsAfterQueueProcessing() { $sending_queue_worker = Stub::make( - new SendingQueueWorker($this->sending_error_handler), + new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker), array( 'processQueue' => function() { // this function returns a queue object @@ -201,7 +208,7 @@ class SendingQueueTest extends \MailPoetTest { }, 'enforceSendingAndExecutionLimits' => Expected::exactly(2) ), $this); - $sending_queue_worker->__construct($this->sending_error_handler); + $sending_queue_worker->__construct($this->sending_error_handler, $this->stats_notifications_worker); $sending_queue_worker->process(); } @@ -225,6 +232,7 @@ class SendingQueueTest extends \MailPoetTest { $directUnsubscribeURL = $this->getDirectUnsubscribeURL(); $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -246,6 +254,7 @@ class SendingQueueTest extends \MailPoetTest { $trackedUnsubscribeURL = $this->getTrackedUnsubscribeURL(); $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -265,6 +274,7 @@ class SendingQueueTest extends \MailPoetTest { function testItCanProcessSubscribersOneByOne() { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -309,6 +319,7 @@ class SendingQueueTest extends \MailPoetTest { function testItCanProcessSubscribersInBulk() { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -356,6 +367,7 @@ class SendingQueueTest extends \MailPoetTest { function testItProcessesStandardNewsletters() { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -410,6 +422,7 @@ class SendingQueueTest extends \MailPoetTest { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::makeEmpty(new MailerTask(), array(), $this) ); @@ -425,6 +438,7 @@ class SendingQueueTest extends \MailPoetTest { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -591,9 +605,10 @@ class SendingQueueTest extends \MailPoetTest { 'updateProcessedSubscribers' => false )); $sending_task->id = 100; - $sending_queue_worker = Stub::make(new SendingQueueWorker($this->sending_error_handler)); + $sending_queue_worker = Stub::make(new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker)); $sending_queue_worker->__construct( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -627,6 +642,7 @@ class SendingQueueTest extends \MailPoetTest { function testItDoesNotUpdateNewsletterHashDuringSending() { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -650,7 +666,7 @@ class SendingQueueTest extends \MailPoetTest { return $custom_batch_size_value; }; Hooks::addFilter('mailpoet_cron_worker_sending_queue_batch_size', $filter); - $sending_queue_worker = new SendingQueueWorker($this->sending_error_handler); + $sending_queue_worker = new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker); expect($sending_queue_worker->batch_size)->equals($custom_batch_size_value); Hooks::removeFilter('mailpoet_cron_worker_sending_queue_batch_size', $filter); } diff --git a/tests/integration/Cron/Workers/StatsNotifications/SchedulerTest.php b/tests/integration/Cron/Workers/StatsNotifications/SchedulerTest.php new file mode 100644 index 0000000000..a4fc1af259 --- /dev/null +++ b/tests/integration/Cron/Workers/StatsNotifications/SchedulerTest.php @@ -0,0 +1,116 @@ +stats_notifications = new Scheduler(); + Setting::setValue(Worker::SETTINGS_KEY, [ + 'enabled' => true, + 'address' => 'email@example.com' + ]); + Setting::setValue('tracking.enabled', true); + } + + function testShouldSchedule() { + $newsletter_id = 5; + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isInstanceOf(StatsNotification::class); + $task = ScheduledTask::where('id', $notification->task_id)->findOne(); + expect($task)->isInstanceOf(ScheduledTask::class); + } + + function testShouldNotScheduleIfTrackingIsDisabled() { + Setting::setValue('tracking.enabled', false); + $newsletter_id = 13; + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + + function testShouldNotScheduleIfDisabled() { + $newsletter_id = 6; + Setting::setValue(Worker::SETTINGS_KEY, [ + 'enabled' => false, + 'address' => 'email@example.com' + ]); + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + + function testShouldNotScheduleIfSettingsMissing() { + $newsletter_id = 7; + Setting::setValue(Worker::SETTINGS_KEY, []); + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + + function testShouldNotScheduleIfEmailIsMissing() { + $newsletter_id = 8; + Setting::setValue(Worker::SETTINGS_KEY, [ + 'enabled' => true, + ]); + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + + function testShouldNotScheduleIfEmailIsEmpty() { + $newsletter_id = 9; + Setting::setValue(Worker::SETTINGS_KEY, [ + 'enabled' => true, + 'address' => ' ' + ]); + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + + function testShouldNotScheduleIfAlreadyScheduled() { + $newsletter_id = 10; + $existing_task = ScheduledTask::createOrUpdate([ + 'type' => Worker::TASK_TYPE, + 'status' => ScheduledTask::STATUS_SCHEDULED, + 'scheduled_at' => '2017-01-02 12:13:14', + ]); + $existing_notification = StatsNotification::createOrUpdate([ + 'newsletter_id' => $newsletter_id, + 'task_id' => $existing_task->id, + ]); + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notifications = StatsNotification::where('newsletter_id', $newsletter_id)->findMany(); + expect($notifications)->count(1); + $tasks = ScheduledTask::where('id', $notifications[0]->task_id)->findMany(); + expect($tasks)->count(1); + expect($existing_notification->id)->equals($notifications[0]->id); + expect($existing_task->id)->equals($tasks[0]->id); + } + + function testShouldNotScheduleIfInvalidType() { + $newsletter_id = 11; + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, Newsletter::TYPE_WELCOME]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + +} diff --git a/tests/integration/Cron/Workers/StatsNotifications/WorkerTest.php b/tests/integration/Cron/Workers/StatsNotifications/WorkerTest.php new file mode 100644 index 0000000000..7adfb0f3ff --- /dev/null +++ b/tests/integration/Cron/Workers/StatsNotifications/WorkerTest.php @@ -0,0 +1,224 @@ +mailer = $this->createMock(Mailer::class); + $this->renderer = $this->createMock(Renderer::class); + $this->stats_notifications = new Worker($this->mailer, $this->renderer); + Setting::setValue(Worker::SETTINGS_KEY, [ + 'enabled' => true, + 'address' => 'email@example.com' + ]); + $newsletter = Newsletter::createOrUpdate([ + 'subject' => 'Email Subject1', + 'type' => Newsletter::TYPE_STANDARD, + ]); + $sending_task = ScheduledTask::createOrUpdate([ + 'type' => 'sending', + 'status' => ScheduledTask::STATUS_COMPLETED, + ]); + $stats_notifications_task = ScheduledTask::createOrUpdate([ + 'type' => Worker::TASK_TYPE, + 'status' => ScheduledTask::STATUS_SCHEDULED, + 'scheduled_at' => '2017-01-02 12:13:14', + 'processed_at' => null, + ]); + StatsNotification::createOrUpdate([ + 'newsletter_id' => $newsletter->id(), + 'task_id' => $stats_notifications_task->id(), + ]); + $queue = SendingQueue::createOrUpdate([ + 'newsletter_rendered_subject' => 'Email Subject', + 'task_id' => $sending_task->id(), + 'newsletter_id' => $newsletter->id(), + 'count_processed' => 5, + ]); + $link = NewsletterLink::createOrUpdate([ + 'url' => 'Link url', + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'hash' => 'xyz', + ]); + StatisticsClicks::createOrUpdate([ + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'subscriber_id' => '5', + 'link_id' => $link->id(), + 'count' => 5, + 'created_at' => '2018-01-02 15:16:17', + ]); + $link2 = NewsletterLink::createOrUpdate([ + 'url' => 'Link url2', + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'hash' => 'xyzd', + ]); + StatisticsClicks::createOrUpdate([ + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'subscriber_id' => '6', + 'link_id' => $link2->id(), + 'count' => 5, + 'created_at' => '2018-01-02 15:16:17', + ]); + StatisticsClicks::createOrUpdate([ + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'subscriber_id' => '7', + 'link_id' => $link2->id(), + 'count' => 5, + 'created_at' => '2018-01-02 15:16:17', + ]); + StatisticsOpens::createOrUpdate([ + 'subscriber_id' => '10', + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'created_at' => '2017-01-02 12:23:45', + ]); + StatisticsOpens::createOrUpdate([ + 'subscriber_id' => '11', + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'created_at' => '2017-01-02 21:23:45', + ]); + StatisticsUnsubscribes::createOrUpdate([ + 'subscriber_id' => '12', + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'created_at' => '2017-01-02 21:23:45', + ]); + } + + function testRendersTemplate() { + $this->renderer->expects($this->exactly(2)) + ->method('render'); + $this->renderer->expects($this->at(0)) + ->method('render') + ->with($this->equalTo('emails/statsNotification.html')); + + $this->renderer->expects($this->at(1)) + ->method('render') + ->with($this->equalTo('emails/statsNotification.txt')); + + $this->stats_notifications->process(); + } + + function testAddsSubjectToContext() { + $this->renderer->expects($this->exactly(2)) // html + text template + ->method('render') + ->with( + $this->anything(), + $this->callback(function($context){ + return $context['subject'] === 'Email Subject1'; + })); + + $this->stats_notifications->process(); + } + + function testAddsPreHeaderToContext() { + $this->renderer->expects($this->exactly(2)) // html + text template + ->method('render') + ->with( + $this->anything(), + $this->callback(function($context){ + return $context['preheader'] === '40.00% opens, 60.00% clicks, 20.00% unsubscribes in a nutshell.'; + })); + + $this->stats_notifications->process(); + } + + function testAddsWPUrlsToContext() { + $this->renderer->expects($this->exactly(2)) // html + text template + ->method('render') + ->with( + $this->anything(), + $this->callback(function($context){ + return strpos($context['linkSettings'], 'mailpoet-settings') + && strpos($context['linkStats'], 'mailpoet-newsletters#/stats'); + })); + + $this->stats_notifications->process(); + } + + function testAddsLinksToContext() { + $this->renderer->expects($this->exactly(2)) // html + text template + ->method('render') + ->with( + $this->anything(), + $this->callback(function($context){ + return ($context['topLink'] === 'Link url2') + && ($context['topLinkClicks'] === 2); + })); + + $this->stats_notifications->process(); + } + + function testSends() { + $this->mailer->expects($this->once()) + ->method('send'); + + $this->stats_notifications->process(); + } + + function testItWorksForNewsletterWithNoStats() { + $newsletter = Newsletter::createOrUpdate([ + 'subject' => 'Email Subject2', + 'type' => Newsletter::TYPE_STANDARD, + ]); + $sending_task = ScheduledTask::createOrUpdate([ + 'type' => 'sending', + 'status' => ScheduledTask::STATUS_COMPLETED, + ]); + $stats_notifications_task = ScheduledTask::createOrUpdate([ + 'type' => Worker::TASK_TYPE, + 'status' => ScheduledTask::STATUS_SCHEDULED, + 'scheduled_at' => '2016-01-02 12:13:14', + 'processed_at' => null, + ]); + StatsNotification::createOrUpdate([ + 'newsletter_id' => $newsletter->id(), + 'task_id' => $stats_notifications_task->id(), + ]); + SendingQueue::createOrUpdate([ + 'newsletter_rendered_subject' => 'Email Subject2', + 'task_id' => $sending_task->id(), + 'newsletter_id' => $newsletter->id(), + 'count_processed' => 15, + ]); + + $this->mailer->expects($this->exactly(2)) + ->method('send'); + + $this->stats_notifications->process(); + } + +} diff --git a/tests/integration/Subscribers/NewSubscriberNotificationMailerTest.php b/tests/integration/Subscribers/NewSubscriberNotificationMailerTest.php index d3da5fa3b3..9d2cb369b7 100644 --- a/tests/integration/Subscribers/NewSubscriberNotificationMailerTest.php +++ b/tests/integration/Subscribers/NewSubscriberNotificationMailerTest.php @@ -8,6 +8,7 @@ use MailPoet\Mailer\Mailer; use MailPoet\Models\Segment; use MailPoet\Models\Setting; use MailPoet\Models\Subscriber; +use MailPoet\WP\Functions; class NewSubscriberNotificationMailerTest extends \MailPoetTest { @@ -85,7 +86,6 @@ class NewSubscriberNotificationMailerTest extends \MailPoetTest { function testItRemovesWwwFromSenderAddress() { Setting::setValue(NewSubscriberNotificationMailer::SETTINGS_KEY, ['enabled' => true,'address' => 'a@b.c']); - update_option( 'home', 'http://www.example.com/xyz' ); $mailer = Stub::makeEmpty(Mailer::class, [ 'getSenderNameAndAddress' => @@ -96,7 +96,14 @@ class NewSubscriberNotificationMailerTest extends \MailPoetTest { }), ], $this); - $service = new NewSubscriberNotificationMailer($mailer); + $functions = Stub::makeEmpty(Functions::class, [ + 'homeUrl' => + Expected::once(function() { + return 'http://www.example.com/xyz'; + }), + ], $this); + + $service = new NewSubscriberNotificationMailer($mailer, null, $functions); $service->send($this->subscriber, $this->segments); } } diff --git a/views/emails/statsNotification.html b/views/emails/statsNotification.html new file mode 100644 index 0000000000..a39438c01f --- /dev/null +++ b/views/emails/statsNotification.html @@ -0,0 +1,456 @@ + + + + + + + <%= subject %> + + + +<% if opened > 30 %> + <% set openedColor = '2993ab' %> +<% elseif opened > 10 %> + <% set openedColor = 'f0b849' %> +<% else %> + <% set openedColor = 'd54e21' %> +<% endif %> +<% if clicked > 3 %> + <% set clickedColor = '2993ab' %> +<% elseif clicked > 1 %> + <% set clickedColor = 'f0b849' %> +<% else %> + <% set clickedColor = 'd54e21' %> +<% endif %> + + + + + + + + + + +
+ + + + + + + + + <% if topLinkClicks > 0 %> + + + + + + + <% endif %> + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ new_logo_orange +
+

+ <%= __('Your stats are in!') %> +

+
+

+ <%= subject %> +

+
+
+
+ + + + + + +
+ +
+ + + + + + + + + + + + +
+ +
+

+ + <%= number_format_i18n(opened) %>% + +

+
+ + + +
+ + <%= __('open rate') %> + +
+
+
+ +
+ + + + + + + + + + + + +
+ +
+

+ + <%= number_format_i18n(clicked) %>% + +

+
+ + + +
+ + <%= __('click rate') %> + +
+
+
+
+ + + + + + +
+ + + + + + +
+ + + + +
+
+
+
+
+ + + + + + +
+ + + + + + + + + +
+ + + +
+ <% if topLink starts with 'http' %> + + <%= topLink %> + + <% else %> + <%= topLink %> + <% endif %> +
+ + + +
+ + <%= __('%s unique clicks')|replace({'%s': topLinkClicks}) %> + +
+
+
+
+ + + + + + +
+ + + + + + + + + <% if premiumPluginActive %> + + + + <% else %> + + + + + + + <% endif %> + + + + +
+ + + + +
+
+
+ +
+ + + +
+ <%= __('See more stats in the Premium version, like all the links that were clicked or which subscribers opened your emails. You can also create segments of subscribers by clicks and opens.') %> +
+
+ +
+
+
+ + + + + + +
+ + + + + + + + + +
+ new_logo_white +
+
+
+ + + diff --git a/views/emails/statsNotification.txt b/views/emails/statsNotification.txt new file mode 100644 index 0000000000..5a595a7596 --- /dev/null +++ b/views/emails/statsNotification.txt @@ -0,0 +1,40 @@ +<%= __('Your stats are in!') %> + +<%= subject %> + +<%= __('open rate') %>: <%= number_format_i18n(opened) %>% +<% if opened > 30 %> + <%= __('EXCELLENT') %> +<% elseif opened > 10 %> + <%= __('GOOD') %> +<% else %> + <%= __('BAD') %> +<% endif %> + +<%= __('click rate') %>: <%= number_format_i18n(clicked) %>% +<% if clicked > 3 %> + <%= __('EXCELLENT') %> +<% elseif clicked > 1 %> + <%= __('GOOD') %> +<% else %> + <%= __('BAD') %> +<% endif %> + +<% if topLinkClicks > 0 %> +<%= __('Most clicked link') %> + <%= topLink %> + + <%= __('%s unique clicks')|replace({'%s': topLinkClicks}) %> +<% endif %> + +<% if premiumPluginActive %> +<%= __('View all stats') %> + <%= linkStats %> +<% else %> +<%= __('See Premium features') %> + <%= premiumPage %> +<% endif %> + +<%= __('How to improve my open rate?') %> https://mailpoet.com/how-to-improve-open-rates +<%= __('And my click rate?') %> https://mailpoet.com/how-to-improve-click-rates +<%= __('Disable these emails') %> <%= linkSettings %> diff --git a/views/settings.html b/views/settings.html index af307c6f3c..ff8221c744 100644 --- a/views/settings.html +++ b/views/settings.html @@ -98,6 +98,15 @@ } else { $('#settings_subscriber_email_notification_error').hide(); } + var stats_notifications_enabled = $('input[name="stats_notifications[enabled]"]:checked').val(), + stats_notifications_address = $('input[name="stats_notifications[address]"]').val().trim(); + if (stats_notifications_enabled && stats_notifications_address == '') { + $('#settings_stats_notifications_error').show(); + window.location.href = '#basics'; + errorFound = true; + } else { + $('#settings_stats_notifications_error').hide(); + } // stop processing if an error was found if (errorFound) { return false; @@ -182,6 +191,7 @@ $('#settings_re_captcha_tokens_error').hide(); $('#settings_subscriber_email_notification_error').hide(); + $('#settings_stats_notifications_error').hide(); function toggleLinuxCronSettings() { if ($('input[name="cron_trigger[method]"]:checked').val() === '<%= cron_trigger.linux_cron %>') { diff --git a/views/settings/basics.html b/views/settings/basics.html index 6442df58d9..626374f283 100644 --- a/views/settings/basics.html +++ b/views/settings/basics.html @@ -281,6 +281,48 @@ + + + + +

+ <%= __('Enter the email address that should receive your newsletter’s stats 24 hours after it has been sent.') %> + +

+ +   + +
+ +
+

+ <%= __('Please fill the email address.') %> +
+ @@ -296,9 +338,9 @@ type="radio" name="subscriber_email_notification[enabled]" value="1" - <% if(settings.subscriber_email_notification.enabled) %> - checked - <% endif %> + <% if(settings.subscriber_email_notification.enabled) %> + checked + <% endif %> /><%= __('Yes') %>   @@ -307,21 +349,22 @@ type="radio" name="subscriber_email_notification[enabled]" value="" - <% if not(settings.subscriber_email_notification.enabled) %> - checked - <% endif %> + <% if not(settings.subscriber_email_notification.enabled) %> + checked + <% endif %> /><%= __('No') %>
+ id="subscriber_email_notification[address]" + name="subscriber_email_notification[address]" + value="<%= settings.subscriber_email_notification.address %>" + placeholder="me@mydomain.com" />
-
- <%= __('Please fill the email address.') %> -
+
+ <%= __('Please fill the email address.') %> +
+