diff --git a/lib/Cron/Daemon.php b/lib/Cron/Daemon.php
index 2c87b00169..a2ee283efe 100644
--- a/lib/Cron/Daemon.php
+++ b/lib/Cron/Daemon.php
@@ -21,9 +21,9 @@ class Daemon {
CronHelper::saveDaemon($settings_daemon_data);
try {
$this->executeMigrationWorker();
+ $this->executeStatsNotificationsWorker();
$this->executeScheduleWorker();
$this->executeQueueWorker();
- $this->executeStatsNotificationsWorker();
$this->executeSendingServiceKeyCheckWorker();
$this->executePremiumKeyCheckWorker();
$this->executeBounceWorker();
diff --git a/lib/Cron/Workers/StatsNotifications/Worker.php b/lib/Cron/Workers/StatsNotifications/Worker.php
index 44a9152feb..a779721a42 100644
--- a/lib/Cron/Workers/StatsNotifications/Worker.php
+++ b/lib/Cron/Workers/StatsNotifications/Worker.php
@@ -2,17 +2,16 @@
namespace MailPoet\Cron\Workers\StatsNotifications;
+use Carbon\Carbon;
use MailPoet\Config\Renderer;
use MailPoet\Cron\CronHelper;
use MailPoet\Mailer\Mailer;
+use MailPoet\Models\Newsletter;
+use MailPoet\Models\NewsletterLink;
+use MailPoet\Models\ScheduledTask;
use MailPoet\Models\Setting;
+use MailPoet\Tasks\Sending;
-/**
- * TODO:
- * - add processing of this task to Daemon
- * - check JIRA what to do next and how to send the newsletter
- * - see \MailPoet\Subscribers\NewSubscriberNotificationMailer how to send an email, now with DI everything should be easy
- */
class Worker {
const TASK_TYPE = 'stats_notification';
@@ -38,16 +37,19 @@ class Worker {
/** @throws \Exception */
function process() {
$settings = Setting::getValue(self::SETTINGS_KEY);
- try {
- $this->mailer->getSenderNameAndAddress($this->constructSenderEmail());
- $this->mailer->send($this->constructNewsletter(), $settings['address']);
- } catch(\Exception $e) {
- if(WP_DEBUG) {
- throw $e;
+ $this->mailer->sender = $this->mailer->getSenderNameAndAddress($this->constructSenderEmail());
+ foreach($this->getTasks() 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);
}
-
- CronHelper::enforceExecutionLimit($this->timer);
}
private function constructSenderEmail() {
@@ -62,18 +64,62 @@ class Worker {
];
}
- private function constructNewsletter() {
- $context = [
- 'link_settings' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-settings'),
- 'link_premium' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-premium'),
- ];
+ private function getTasks() {
+ $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(__('New subscriber to ', 'mailpoet')),
+ '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/newSubscriberNotification.html', $context),
- 'text' => $this->renderer->render('emails/newSubscriberNotification.txt', $context),
+ 'html' => $this->renderer->render('emails/statsNotification.html', $context),
],
];
}
+ private function getNewsletter(ScheduledTask $task) {
+ $statsNotificationModel = $task->statsNotification()->findOne();
+ return $statsNotificationModel
+ ->newsletter()
+ ->findOne()
+ ->withSendingQueue()
+ ->withTotalSent()
+ ->withStatistics();
+ }
+
+ private function prepareContext(Newsletter $newsletter, NewsletterLink $link) {
+ return [
+ '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(($newsletter->statistics['clicked'] * 100) / $newsletter->total_sent, 2),
+ number_format(($newsletter->statistics['opened'] * 100) / $newsletter->total_sent,2),
+ number_format(($newsletter->statistics['unsubscribed'] * 100) / $newsletter->total_sent,2)
+ ),
+ 'topLinkClicks' => $link->clicksCount,
+ 'topLink' => $link->url,
+ '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()),
+ 'premiumPluginActive' => is_plugin_active('mailpoet-premium/mailpoet-premium.php'),
+ ];
+ }
+
+ 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/Models/NewsletterLink.php b/lib/Models/NewsletterLink.php
index 661858455a..a9b92ea365 100644
--- a/lib/Models/NewsletterLink.php
+++ b/lib/Models/NewsletterLink.php
@@ -5,4 +5,19 @@ if(!defined('ABSPATH')) exit;
class NewsletterLink extends Model {
public static $_table = MP_NEWSLETTER_LINKS_TABLE;
+
+ static function findTopLinkForNewsletter(Newsletter $newsletter) {
+ return 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();
+ }
+
}
diff --git a/lib/Models/ScheduledTask.php b/lib/Models/ScheduledTask.php
index 5b6cffcea9..650180f190 100644
--- a/lib/Models/ScheduledTask.php
+++ b/lib/Models/ScheduledTask.php
@@ -32,6 +32,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
index 8a162bf85f..b02cb2720a 100644
--- a/lib/Models/StatsNotification.php
+++ b/lib/Models/StatsNotification.php
@@ -5,6 +5,15 @@ namespace MailPoet\Models;
class StatsNotification extends Model {
public static $_table = MP_STATS_NOTIFICATIONS_TABLE;
+ /** @return Newsletter */
+ public function newsletter() {
+ return $this->hasOne(
+ Newsletter::class,
+ 'id',
+ 'newsletter_id'
+ );
+ }
+
/** @return StatsNotification */
static function createOrUpdate($data = array()) {
$model = null;
diff --git a/tests/integration/Cron/Workers/StatsNotifications/WorkerTest.php b/tests/integration/Cron/Workers/StatsNotifications/WorkerTest.php
new file mode 100644
index 0000000000..0121ac0b20
--- /dev/null
+++ b/tests/integration/Cron/Workers/StatsNotifications/WorkerTest.php
@@ -0,0 +1,186 @@
+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->once())
+ ->method('render')
+ ->with(
+ $this->stringContains('statsNotification.html'),
+ $this->callback(function($context){
+ return is_array($context);
+ }));
+
+ $this->stats_notifications->process();
+ }
+
+ function testAddsSubjectToContext() {
+ $this->renderer->expects($this->once())
+ ->method('render')
+ ->with(
+ $this->stringContains('statsNotification.html'),
+ $this->callback(function($context){
+ return $context['subject'] === 'Email Subject1';
+ }));
+
+ $this->stats_notifications->process();
+ }
+
+ function testAddsPreHeaderToContext() {
+ $this->renderer->expects($this->once())
+ ->method('render')
+ ->with(
+ $this->stringContains('statsNotification.html'),
+ $this->callback(function($context){
+ return $context['preheader'] === '60.00% opens, 40.00% clicks, 20.00% unsubscribes in a nutshell.';
+ }));
+
+ $this->stats_notifications->process();
+ }
+
+ function testAddsWPUrlsToContext() {
+ $this->renderer->expects($this->once())
+ ->method('render')
+ ->with(
+ $this->stringContains('statsNotification.html'),
+ $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->once())
+ ->method('render')
+ ->with(
+ $this->stringContains('statsNotification.html'),
+ $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();
+ }
+
+}
diff --git a/views/emails/statsNotification.html b/views/emails/statsNotification.html
new file mode 100644
index 0000000000..a0f47b4f18
--- /dev/null
+++ b/views/emails/statsNotification.html
@@ -0,0 +1,421 @@
+
+
+
+
+
+
+ <%= subject %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+ <%= __('Your stats are in!') %>
+
+ |
+
+
+
+
+ <%= subject %>
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ 29.8%
+
+
+ |
+
+
+
+
+
+
+
+ <%= __('open rate') %>
+
+ |
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ 3.1%
+
+
+ |
+
+
+
+
+
+
+
+ <%= __('click rate') %>
+
+ |
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= __('%s unique clicks')|replace({'%s': topLinkClicks}) %>
+
+ |
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ <%= __('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.') %>
+ |
+
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+