Increase number of checks if the email was sent in automation

[MAILPOET-6175]
This commit is contained in:
Jan Jakes
2024-08-05 16:53:52 +02:00
committed by Aschepikov
parent 8868c98c25
commit 172ffb7b61
2 changed files with 72 additions and 30 deletions

View File

@ -6,7 +6,6 @@ use MailPoet\AutomaticEmails\WooCommerce\Events\AbandonedCart;
use MailPoet\Automation\Engine\Control\AutomationController;
use MailPoet\Automation\Engine\Control\StepRunController;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\AutomationRun;
use MailPoet\Automation\Engine\Data\Step;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Engine\Data\StepValidationArgs;
@ -36,6 +35,18 @@ use Throwable;
class SendEmailAction implements Action {
const KEY = 'mailpoet:send-email';
// Intervals to poll for email status after sending. These are only
// used when immediate status sync fails or the email is never sent.
private const POLL_INTERVALS = [
5 * MINUTE_IN_SECONDS, // ~5 minutes
10 * MINUTE_IN_SECONDS, // ~15 minutes
45 * MINUTE_IN_SECONDS, // ~1 hour
4 * HOUR_IN_SECONDS, // ~5 hours ...from email scheduling
19 * HOUR_IN_SECONDS, // ~1 day
4 * DAY_IN_SECONDS, // ~5 days
25 * DAY_IN_SECONDS, // ~1 month
];
private const TRANSACTIONAL_TRIGGERS = [
'woocommerce:order-status-changed',
'woocommerce:order-created',
@ -158,33 +169,38 @@ class SendEmailAction implements Action {
public function run(StepRunArgs $args, StepRunController $controller): void {
$newsletter = $this->getEmailForStep($args->getStep());
$subscriber = $this->getSubscriber($args);
$run = $args->getAutomationRun();
// sync sending status with the automation step
if (!$args->isFirstRun()) {
$this->checkSendingStatus($newsletter, $subscriber, $run);
return;
if ($args->isFirstRun()) {
// run #1: schedule email sending
$subscriberStatus = $subscriber->getStatus();
if ($newsletter->getType() !== NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL && $subscriberStatus !== SubscriberEntity::STATUS_SUBSCRIBED) {
throw InvalidStateException::create()->withMessage(sprintf("Cannot schedule a newsletter for subscriber ID '%s' because their status is '%s'.", $subscriber->getId(), $subscriberStatus));
}
if ($subscriberStatus === SubscriberEntity::STATUS_BOUNCED) {
throw InvalidStateException::create()->withMessage(sprintf("Cannot schedule an email for subscriber ID '%s' because their status is '%s'.", $subscriber->getId(), $subscriberStatus));
}
$meta = $this->getNewsletterMeta($args);
try {
$this->automationEmailScheduler->createSendingTask($newsletter, $subscriber, $meta);
} catch (Throwable $e) {
throw InvalidStateException::create()->withMessage('Could not create sending task.');
}
} else {
// run #N: check/sync sending status with the automation step
$success = $this->checkSendingStatus($args, $newsletter, $subscriber);
if ($success) {
return;
}
}
$subscriberStatus = $subscriber->getStatus();
if ($newsletter->getType() !== NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL && $subscriberStatus !== SubscriberEntity::STATUS_SUBSCRIBED) {
throw InvalidStateException::create()->withMessage(sprintf("Cannot schedule a newsletter for subscriber ID '%s' because their status is '%s'.", $subscriber->getId(), $subscriberStatus));
}
if ($subscriberStatus === SubscriberEntity::STATUS_BOUNCED) {
throw InvalidStateException::create()->withMessage(sprintf("Cannot schedule an email for subscriber ID '%s' because their status is '%s'.", $subscriber->getId(), $subscriberStatus));
}
$meta = $this->getNewsletterMeta($args);
try {
$this->automationEmailScheduler->createSendingTask($newsletter, $subscriber, $meta);
} catch (Throwable $e) {
throw InvalidStateException::create()->withMessage('Could not create sending task.');
}
// schedule a progress run to sync email sending status to the automation step
// (1 month is a timout, the progress will normally be executed after sending)
$controller->scheduleProgress(time() + MONTH_IN_SECONDS);
// Schedule a progress run to sync the email sending status to the automation step.
// Normally, a progress run is executed immediately after sending; we're scheduling
// these runs to poll for the status if sync fails or email never sends (timeout).
$nextInterval = self::POLL_INTERVALS[$args->getRunNumber() - 1] ?? 0;
$controller->scheduleProgress(time() + $nextInterval);
}
/** @param mixed $data */
@ -212,8 +228,8 @@ class SendEmailAction implements Action {
$this->automationController->enqueueProgress($runId, $stepId);
}
private function checkSendingStatus(NewsletterEntity $newsletter, SubscriberEntity $subscriber, AutomationRun $run): void {
$scheduledTaskSubscriber = $this->automationEmailScheduler->getScheduledTaskSubscriber($newsletter, $subscriber, $run);
private function checkSendingStatus(StepRunArgs $args, NewsletterEntity $newsletter, SubscriberEntity $subscriber): bool {
$scheduledTaskSubscriber = $this->automationEmailScheduler->getScheduledTaskSubscriber($newsletter, $subscriber, $args->getAutomationRun());
if (!$scheduledTaskSubscriber) {
throw InvalidStateException::create()->withMessage('Email failed to schedule.');
}
@ -225,14 +241,17 @@ class SendEmailAction implements Action {
);
}
$wasSent = $scheduledTaskSubscriber->getProcessed() === ScheduledTaskSubscriberEntity::STATUS_PROCESSED;
$isLastRun = $args->getRunNumber() >= count(self::POLL_INTERVALS);
// email was never sent
if ($scheduledTaskSubscriber->getProcessed() !== ScheduledTaskSubscriberEntity::STATUS_PROCESSED) {
if (!$wasSent && $isLastRun) {
$error = 'Email sending process timed out.';
$this->automationEmailScheduler->saveError($scheduledTaskSubscriber, $error);
throw InvalidStateException::create()->withMessage($error);
}
// email was sent, complete the run
return $wasSent;
}
private function getNewsletterMeta(StepRunArgs $args): array {

View File

@ -4,6 +4,7 @@ namespace MailPoet\Test\Automation\Integrations\MailPoet\Actions;
use Codeception\Stub\Expected;
use MailPoet\Automation\Engine\Builder\UpdateAutomationController;
use MailPoet\Automation\Engine\Control\ActionScheduler;
use MailPoet\Automation\Engine\Control\AutomationController;
use MailPoet\Automation\Engine\Control\StepRunControllerFactory;
use MailPoet\Automation\Engine\Data\Automation;
@ -63,6 +64,7 @@ class SendEmailActionTest extends \MailPoetTest {
public function _before() {
parent::_before();
$this->cleanup();
$this->scheduledTasksRepository = $this->diContainer->get(ScheduledTasksRepository::class);
$this->segmentsRepository = $this->diContainer->get(SegmentsRepository::class);
@ -74,6 +76,11 @@ class SendEmailActionTest extends \MailPoetTest {
$this->automation = new Automation('test-automation', [], new \WP_User());
}
public function _after() {
parent::_after();
$this->cleanup();
}
public function testItReturnsRequiredSubjects() {
$this->assertSame(['mailpoet:subscriber'], $this->action->getSubjectKeys());
}
@ -191,7 +198,17 @@ class SendEmailActionTest extends \MailPoetTest {
$scheduledTaskSubscriber->setFailed(ScheduledTaskSubscriberEntity::FAIL_STATUS_OK);
$scheduledTaskSubscriber->setError(null);
// email was never sent
// email was not sent yet, scheduling next progress (no exception)
$actionScheduler = $this->diContainer->get(ActionScheduler::class);
$this->assertCount(0, $actionScheduler->getScheduledActions());
$this->action->run($args, $controller);
$actions = array_values($actionScheduler->getScheduledActions());
$this->assertCount(1, $actions);
$this->assertSame('mailpoet/automation/step', $actions[0]->get_hook());
$this->assertSame([['automation_run_id' => $run->getId(), 'step_id' => 'step-id', 'run_number' => 3]], $actions[0]->get_args());
// email was never sent (7th run is the last check after ~1 month)
$args = new StepRunArgs($automation, $run, $step, $this->getSubjectEntries($subjects), 7);
$this->assertThrowsExceptionWithMessage(
'Email sending process timed out.',
function() use ($args, $controller) {
@ -513,4 +530,10 @@ class SendEmailActionTest extends \MailPoetTest {
}
$this->assertSame($expectedMessage, $error);
}
private function cleanup(): void {
global $wpdb;
$wpdb->query('TRUNCATE ' . $wpdb->prefix . 'actionscheduler_actions');
$wpdb->query('TRUNCATE ' . $wpdb->prefix . 'actionscheduler_claims');
}
}