This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,515 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Actions;
if (!defined('ABSPATH')) exit;
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\Step;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Engine\Data\StepValidationArgs;
use MailPoet\Automation\Engine\Exceptions\NotFoundException;
use MailPoet\Automation\Engine\Integration\Action;
use MailPoet\Automation\Engine\Integration\ValidationException;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SegmentPayload;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SubscriberPayload;
use MailPoet\Automation\Integrations\WooCommerce\Payloads\AbandonedCartPayload;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterOptionEntity;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\InvalidStateException;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Options\NewsletterOptionFieldsRepository;
use MailPoet\Newsletter\Options\NewsletterOptionsRepository;
use MailPoet\Newsletter\Scheduler\AutomationEmailScheduler;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\SubscriberSegmentRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema\ObjectSchema;
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
];
// Retry intervals for sending. These are used when the email address
// is not confirmed, and we need send non-transactional emails.
private const OPTIN_RETRY_INTERVALS = [
1 * MINUTE_IN_SECONDS, // ~1 minute
5 * MINUTE_IN_SECONDS, // ~5 minutes
20 * MINUTE_IN_SECONDS, // ~20 minutes
1 * HOUR_IN_SECONDS, // ~1 hour
12 * HOUR_IN_SECONDS, // ~12 hours
1 * DAY_IN_SECONDS, // ~1 day
];
private const WAIT_OPTIN = 'wait_optin';
private const OPTIN_RETRIES = 'optin_retries';
private const TRANSACTIONAL_TRIGGERS = [
'woocommerce:order-status-changed',
'woocommerce:order-created',
'woocommerce:order-completed',
'woocommerce:order-cancelled',
'woocommerce:abandoned-cart',
'woocommerce-subscriptions:subscription-created',
'woocommerce-subscriptions:subscription-expired',
'woocommerce-subscriptions:subscription-payment-failed',
'woocommerce-subscriptions:subscription-renewed',
'woocommerce-subscriptions:subscription-status-changed',
'woocommerce-subscriptions:trial-ended',
'woocommerce-subscriptions:trial-started',
'woocommerce:buys-from-a-tag',
'woocommerce:buys-from-a-category',
'woocommerce:buys-a-product',
];
private AutomationController $automationController;
private SettingsController $settings;
private NewslettersRepository $newslettersRepository;
private SubscriberSegmentRepository $subscriberSegmentRepository;
private SubscribersRepository $subscribersRepository;
private SegmentsRepository $segmentsRepository;
private AutomationEmailScheduler $automationEmailScheduler;
private NewsletterOptionsRepository $newsletterOptionsRepository;
private NewsletterOptionFieldsRepository $newsletterOptionFieldsRepository;
public function __construct(
AutomationController $automationController,
SettingsController $settings,
NewslettersRepository $newslettersRepository,
SubscriberSegmentRepository $subscriberSegmentRepository,
SubscribersRepository $subscribersRepository,
SegmentsRepository $segmentsRepository,
AutomationEmailScheduler $automationEmailScheduler,
NewsletterOptionsRepository $newsletterOptionsRepository,
NewsletterOptionFieldsRepository $newsletterOptionFieldsRepository
) {
$this->automationController = $automationController;
$this->settings = $settings;
$this->newslettersRepository = $newslettersRepository;
$this->subscriberSegmentRepository = $subscriberSegmentRepository;
$this->subscribersRepository = $subscribersRepository;
$this->segmentsRepository = $segmentsRepository;
$this->automationEmailScheduler = $automationEmailScheduler;
$this->newsletterOptionsRepository = $newsletterOptionsRepository;
$this->newsletterOptionFieldsRepository = $newsletterOptionFieldsRepository;
}
public function getKey(): string {
return self::KEY;
}
public function getName(): string {
// translators: automation action title
return __('Send email', 'mailpoet');
}
public function getArgsSchema(): ObjectSchema {
$nameDefault = $this->settings->get('sender.name');
$addressDefault = $this->settings->get('sender.address');
$replyToNameDefault = $this->settings->get('reply_to.name');
$replyToAddressDefault = $this->settings->get('reply_to.address');
$nonEmptyString = Builder::string()->required()->minLength(1);
return Builder::object([
// required fields
'email_id' => Builder::integer()->required(),
'name' => $nonEmptyString->default(__('Send email', 'mailpoet')),
'subject' => $nonEmptyString->default(__('Subject', 'mailpoet')),
'preheader' => Builder::string()->required()->default(''),
'sender_name' => $nonEmptyString->default($nameDefault),
'sender_address' => $nonEmptyString->formatEmail()->default($addressDefault),
// optional fields
'reply_to_name' => ($replyToNameDefault && $replyToNameDefault !== $nameDefault)
? Builder::string()->minLength(1)->default($replyToNameDefault)
: Builder::string()->minLength(1),
'reply_to_address' => ($replyToAddressDefault && $replyToAddressDefault !== $addressDefault)
? Builder::string()->formatEmail()->default($replyToAddressDefault)
: Builder::string()->formatEmail(),
'ga_campaign' => Builder::string()->minLength(1),
]);
}
public function getSubjectKeys(): array {
return [
'mailpoet:subscriber',
];
}
public function validate(StepValidationArgs $args): void {
try {
$this->getEmailForStep($args->getStep());
} catch (InvalidStateException $exception) {
$exception = ValidationException::create()
->withMessage(__('Cannot send the email because it was not found. Please, go to the automation editor and update the email contents.', 'mailpoet'));
$emailId = $args->getStep()->getArgs()['email_id'] ?? '';
if (empty($emailId)) {
$exception->withError('email_id', __("Automation email not found.", 'mailpoet'));
} else {
$exception->withError(
'email_id',
// translators: %s is the ID of email.
sprintf(__("Automation email with ID '%s' not found.", 'mailpoet'), $emailId)
);
}
throw $exception;
}
}
public function run(StepRunArgs $args, StepRunController $controller): void {
$newsletter = $this->getEmailForStep($args->getStep());
$subscriber = $this->getSubscriber($args);
$state = null;
if ($args->isFirstRun()) {
$subscriberStatus = $subscriber->getStatus();
if ($subscriberStatus === SubscriberEntity::STATUS_BOUNCED) {
// translators: %s is the subscriber's status.
throw InvalidStateException::create()->withMessage(sprintf(__("Cannot send the email because the subscriber's status is '%s'.", 'mailpoet'), $subscriberStatus));
}
if ($this->isOptInRequired($newsletter, $subscriber)) {
$controller->getRunLog()->saveLogData([self::WAIT_OPTIN => 1]);
$this->rerunLater($args->getRunNumber(), $controller, $newsletter, $subscriber);
return;
}
$this->scheduleEmail($args, $newsletter, $subscriber);
} else {
// Re-running for opt-in?
$state = $this->getRunLogData($controller);
if (array_key_exists(self::WAIT_OPTIN, $state) && $state[self::WAIT_OPTIN] === 1) {
if ($this->isOptInRequired($newsletter, $subscriber)) {
$this->rerunLater($args->getRunNumber(), $controller, $newsletter, $subscriber);
return;
}
// Subscriber is now confirmed, so we can schedule an email.
$controller->getRunLog()->saveLogData([
self::WAIT_OPTIN => 0,
self::OPTIN_RETRIES => $args->getRunNumber(),
]);
$this->scheduleEmail($args, $newsletter, $subscriber);
}
// Check/sync sending status with the automation step
$success = $this->checkSendingStatus($args, $newsletter, $subscriber);
if ($success) {
return;
}
}
// At this point, we're re-running to check sending status. We need
// to offset opt-in reruns count from sending reruns.
$runNumber = $args->getRunNumber();
$state = $state ?? $this->getRunLogData($controller);
$optinRetryCount = $state[self::OPTIN_RETRIES] ?? 0;
$runNumber -= $optinRetryCount;
$this->rerunLater($runNumber, $controller, $newsletter, $subscriber);
}
private function scheduleEmail(StepRunArgs $args, NewsletterEntity $newsletter, SubscriberEntity $subscriber): void {
$meta = $this->getNewsletterMeta($args);
try {
$this->automationEmailScheduler->createSendingTask($newsletter, $subscriber, $meta);
} catch (Throwable $e) {
throw InvalidStateException::create()->withMessage(__('Could not create sending task.', 'mailpoet'));
}
}
private function getRunLogData(StepRunController $controller): array {
$runLog = $controller->getRunLog()->getLog();
return $runLog->getData();
}
/**
* 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),
* or if we need to wait for subscriber opt-in.
*/
private function rerunLater(int $runNumber, StepRunController $controller, NewsletterEntity $newsletter, SubscriberEntity $subscriber): void {
$nextInterval = self::POLL_INTERVALS[$runNumber - 1] ?? 0;
// Use different intervals when retrying for opt-in.
if ($this->isOptInRequired($newsletter, $subscriber)) {
if ($runNumber > count(self::OPTIN_RETRY_INTERVALS)) {
$subscriberStatus = $subscriber->getStatus();
// translators: %s is the subscriber's status.
throw InvalidStateException::create()->withMessage(sprintf(__("Cannot send the email because the subscriber's status is '%s'.", 'mailpoet'), $subscriberStatus));
}
$nextInterval = self::OPTIN_RETRY_INTERVALS[$runNumber - 1];
}
$controller->scheduleProgress(time() + $nextInterval);
}
private function isOptInRequired(NewsletterEntity $newsletter, SubscriberEntity $subscriber): bool {
$subscriberStatus = $subscriber->getStatus();
if ($newsletter->getType() === NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL) return false;
return $subscriberStatus !== SubscriberEntity::STATUS_SUBSCRIBED;
}
/** @param mixed $data */
public function handleEmailSent($data): void {
if (!is_array($data)) {
throw InvalidStateException::create()->withMessage(
// translators: %s is the type of $data.
sprintf(__('Invalid automation step data. Array expected, got: %s', 'mailpoet'), gettype($data))
);
}
$runId = $data['run_id'] ?? null;
if (!is_int($runId)) {
throw InvalidStateException::create()->withMessage(
// translators: %s is the type of $runId.
sprintf(__("Invalid automation step data. Expected 'run_id' to be an integer, got: %s", 'mailpoet'), gettype($runId))
);
}
$stepId = $data['step_id'] ?? null;
if (!is_string($stepId)) {
throw InvalidStateException::create()->withMessage(
// translators: %s is the type of $runId.
sprintf(__("Invalid automation step data. Expected 'step_id' to be a string, got: %s", 'mailpoet'), gettype($runId))
);
}
$this->automationController->enqueueProgress($runId, $stepId);
}
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.', 'mailpoet'));
}
// email sending failed
if ($scheduledTaskSubscriber->getFailed() === ScheduledTaskSubscriberEntity::FAIL_STATUS_FAILED) {
throw InvalidStateException::create()->withMessage(
// translators: %s is the error message.
sprintf(__('Email failed to send. Error: %s', 'mailpoet'), $scheduledTaskSubscriber->getError() ?: 'Unknown error')
);
}
$wasSent = $scheduledTaskSubscriber->getProcessed() === ScheduledTaskSubscriberEntity::STATUS_PROCESSED;
$isLastRun = $args->getRunNumber() >= 1 + count(self::POLL_INTERVALS);
// email was never sent
if (!$wasSent && $isLastRun) {
$error = __('Email sending process timed out.', 'mailpoet');
$this->automationEmailScheduler->saveError($scheduledTaskSubscriber, $error);
throw InvalidStateException::create()->withMessage($error);
}
return $wasSent;
}
private function getNewsletterMeta(StepRunArgs $args): array {
$meta = [
'automation' => [
'id' => $args->getAutomation()->getId(),
'run_id' => $args->getAutomationRun()->getId(),
'step_id' => $args->getStep()->getId(),
'run_number' => $args->getRunNumber(),
],
];
if ($this->automationHasAbandonedCartTrigger($args->getAutomation())) {
$payload = $args->getSinglePayloadByClass(AbandonedCartPayload::class);
$meta[AbandonedCart::TASK_META_NAME] = $payload->getProductIds();
}
return $meta;
}
private function getSubscriber(StepRunArgs $args): SubscriberEntity {
$subscriberId = $args->getSinglePayloadByClass(SubscriberPayload::class)->getId();
try {
$segmentId = $args->getSinglePayloadByClass(SegmentPayload::class)->getId();
} catch (NotFoundException $e) {
$segmentId = null;
}
// Without segment, fetch subscriber by ID (needed e.g. for "mailpoet:custom-trigger").
// Transactional emails don't need to be checked against segment, no matter if it's set.
if (!$segmentId || $this->isTransactional($args->getStep(), $args->getAutomation())) {
$subscriber = $this->subscribersRepository->findOneById($subscriberId);
if (!$subscriber) {
throw InvalidStateException::create();
}
return $subscriber;
}
// With segment, fetch subscriber segment and check if they are subscribed.
$subscriberSegment = $this->subscriberSegmentRepository->findOneBy([
'subscriber' => $subscriberId,
'segment' => $segmentId,
'status' => SubscriberEntity::STATUS_SUBSCRIBED,
]);
if (!$subscriberSegment) {
$segment = $this->segmentsRepository->findOneById($segmentId);
if (!$segment) { // This state should not happen because it is checked in the validation.
throw InvalidStateException::create()->withMessage(__('Cannot send the email because the list was not found.', 'mailpoet'));
}
// translators: %s is the name of the list.
throw InvalidStateException::create()->withMessage(sprintf(__("Cannot send the email because the subscriber is not subscribed to the '%s' list.", 'mailpoet'), $segment->getName()));
}
$subscriber = $subscriberSegment->getSubscriber();
if (!$subscriber) {
throw InvalidStateException::create();
}
return $subscriber;
}
public function saveEmailSettings(Step $step, Automation $automation): void {
$args = $step->getArgs();
if (!isset($args['email_id']) || !$args['email_id']) {
return;
}
$email = $this->getEmailForStep($step);
$email->setType($this->isTransactional($step, $automation) ? NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL : NewsletterEntity::TYPE_AUTOMATION);
$email->setStatus(NewsletterEntity::STATUS_ACTIVE);
$email->setSubject($args['subject'] ?? '');
$email->setPreheader($args['preheader'] ?? '');
$email->setSenderName($args['sender_name'] ?? '');
$email->setSenderAddress($args['sender_address'] ?? '');
$email->setReplyToName($args['reply_to_name'] ?? '');
$email->setReplyToAddress($args['reply_to_address'] ?? '');
$email->setGaCampaign($args['ga_campaign'] ?? '');
$this->storeNewsletterOption(
$email,
NewsletterOptionFieldEntity::NAME_GROUP,
$this->automationHasWooCommerceTrigger($automation) ? 'woocommerce' : null
);
$this->storeNewsletterOption(
$email,
NewsletterOptionFieldEntity::NAME_EVENT,
$this->automationHasAbandonedCartTrigger($automation) ? 'woocommerce_abandoned_shopping_cart' : null
);
$this->newslettersRepository->persist($email);
$this->newslettersRepository->flush();
}
private function storeNewsletterOption(NewsletterEntity $newsletter, string $optionName, string $optionValue = null): void {
$options = $newsletter->getOptions()->toArray();
foreach ($options as $key => $option) {
if ($option->getName() === $optionName) {
if ($optionValue) {
$option->setValue($optionValue);
return;
}
$newsletter->getOptions()->remove($key);
$this->newsletterOptionsRepository->remove($option);
return;
}
}
if (!$optionValue) {
return;
}
$field = $this->newsletterOptionFieldsRepository->findOneBy([
'name' => $optionName,
'newsletterType' => $newsletter->getType(),
]);
if (!$field) {
return;
}
$option = new NewsletterOptionEntity($newsletter, $field);
$option->setValue($optionValue);
$this->newsletterOptionsRepository->persist($option);
$newsletter->getOptions()->add($option);
}
private function isTransactional(Step $step, Automation $automation): bool {
$triggers = $automation->getTriggers();
$transactionalTriggers = array_filter(
$triggers,
function(Step $step): bool {
return in_array($step->getKey(), self::TRANSACTIONAL_TRIGGERS, true);
}
);
if (!$triggers || count($transactionalTriggers) !== count($triggers)) {
return false;
}
foreach ($transactionalTriggers as $trigger) {
if (!in_array($step->getId(), $trigger->getNextStepIds(), true)) {
return false;
}
}
return true;
}
private function automationHasWooCommerceTrigger(Automation $automation): bool {
return (bool)array_filter(
$automation->getTriggers(),
function(Step $step): bool {
return strpos($step->getKey(), 'woocommerce:') === 0;
}
);
}
private function automationHasAbandonedCartTrigger(Automation $automation): bool {
return (bool)array_filter(
$automation->getTriggers(),
function(Step $step): bool {
return in_array($step->getKey(), ['woocommerce:abandoned-cart'], true);
}
);
}
private function getEmailForStep(Step $step): NewsletterEntity {
$emailId = $step->getArgs()['email_id'] ?? null;
if (!$emailId) {
throw InvalidStateException::create();
}
$email = $this->newslettersRepository->findOneBy([
'id' => $emailId,
]);
if (!$email || !in_array($email->getType(), [NewsletterEntity::TYPE_AUTOMATION, NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL], true)) {
throw InvalidStateException::create()->withMessage(
// translators: %s is the ID of email.
sprintf(__("Automation email with ID '%s' not found.", 'mailpoet'), $emailId)
);
}
return $email;
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics;
if (!defined('ABSPATH')) exit;
use MailPoet\API\REST\API;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\WordPress;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Endpoints\AutomationFlowEndpoint;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Endpoints\OverviewEndpoint;
class Analytics {
/** @var WordPress */
private $wordPress;
public function __construct(
WordPress $wordPress
) {
$this->wordPress = $wordPress;
}
public function register(): void {
$this->wordPress->addAction(Hooks::API_INITIALIZE, function (API $api) {
$api->registerGetRoute('automation/analytics/automation_flow', AutomationFlowEndpoint::class);
$api->registerGetRoute('automation/analytics/overview', OverviewEndpoint::class);
});
}
}
@@ -0,0 +1,101 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics\Controller;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Integrations\MailPoet\Actions\SendEmailAction;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\NewslettersRepository;
class AutomationTimeSpanController {
/** @var AutomationStorage */
private $automationStorage;
/** @var NewslettersRepository */
private $newslettersRepository;
public function __construct(
AutomationStorage $automationStorage,
NewslettersRepository $newslettersRepository
) {
$this->automationStorage = $automationStorage;
$this->newslettersRepository = $newslettersRepository;
}
public function getAutomationsInTimespan(Automation $automation, \DateTimeImmutable $after, \DateTimeImmutable $before): array {
$automationVersions = $this->automationStorage->getAutomationVersionDates($automation->getId());
usort(
$automationVersions,
function (array $a, array $b) {
return $a['created_at'] <=> $b['created_at'];
}
);
// Find all versions, which could have been active in the given time span
$versionIds = [];
foreach ($automationVersions as $automationVersion) {
if ($automationVersion['created_at'] > $before) {
// We are past the time span
break;
}
if (!$versionIds || $automationVersion['created_at'] <= $after) {
// This is the first version in the time span
$versionIds = [(int)$automationVersion['id']];
continue;
}
$versionIds[] = (int)$automationVersion['id'];
}
return count($versionIds) > 0 ? $this->automationStorage->getAutomationWithDifferentVersions($versionIds) : [];
}
/**
* @param Automation $automation
* @param \DateTimeImmutable $after
* @param \DateTimeImmutable $before
* @return NewsletterEntity[]
*/
public function getAutomationEmailsInTimeSpan(Automation $automation, \DateTimeImmutable $after, \DateTimeImmutable $before): array {
$automations = $this->getAutomationsInTimespan($automation, $after, $before);
return count($automations) > 0 ? $this->getEmailsFromAutomations($automations) : [];
}
/**
* @param Automation[] $automations
* @return NewsletterEntity[]
*/
public function getEmailsFromAutomations(array $automations): array {
$emailSteps = [];
foreach ($automations as $automation) {
$emailSteps = array_merge(
$emailSteps,
array_values(
array_filter(
$automation->getSteps(),
function($step) {
return $step->getKey() === SendEmailAction::KEY;
}
)
)
);
}
$emailIds = array_unique(
array_filter(
array_map(
function($step) {
$args = $step->getArgs();
return isset($args['email_id']) ? absint($args['email_id']) : null;
},
$emailSteps
)
)
);
return $this->newslettersRepository->findBy(['id' => $emailIds]);
}
}
@@ -0,0 +1,117 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics\Controller;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Entities\QueryWithCompare;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Entities\StatisticsOpenEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository;
use MailPoet\Newsletter\Statistics\WooCommerceRevenue;
use MailPoet\Newsletter\Url as NewsletterUrl;
class OverviewStatisticsController {
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var NewsletterStatisticsRepository */
private $newsletterStatisticsRepository;
/** @var NewsletterUrl */
private $newsletterUrl;
/** @var AutomationTimeSpanController */
private $automationTimeSpanController;
public function __construct(
NewslettersRepository $newslettersRepository,
NewsletterStatisticsRepository $newsletterStatisticsRepository,
NewsletterUrl $newsletterUrl,
AutomationTimeSpanController $automationTimeSpanController
) {
$this->newslettersRepository = $newslettersRepository;
$this->newsletterStatisticsRepository = $newsletterStatisticsRepository;
$this->newsletterUrl = $newsletterUrl;
$this->automationTimeSpanController = $automationTimeSpanController;
}
public function getStatisticsForAutomation(Automation $automation, QueryWithCompare $query): array {
$currentEmails = $this->automationTimeSpanController->getAutomationEmailsInTimeSpan($automation, $query->getAfter(), $query->getBefore());
$previousEmails = $this->automationTimeSpanController->getAutomationEmailsInTimeSpan($automation, $query->getCompareWithAfter(), $query->getCompareWithBefore());
$data = [
'sent' => ['current' => 0, 'previous' => 0],
'opened' => ['current' => 0, 'previous' => 0],
'clicked' => ['current' => 0, 'previous' => 0],
'orders' => ['current' => 0, 'previous' => 0],
'unsubscribed' => ['current' => 0, 'previous' => 0],
'revenue' => ['current' => 0, 'previous' => 0],
'emails' => [],
];
if (!$currentEmails) {
return $data;
}
$requiredData = [
'totals',
StatisticsClickEntity::class,
StatisticsOpenEntity::class,
WooCommerceRevenue::class,
];
$currentStatistics = $this->newsletterStatisticsRepository->getBatchStatistics(
$currentEmails,
$query->getAfter(),
$query->getBefore(),
$requiredData
);
foreach ($currentStatistics as $newsletterId => $statistic) {
$data['sent']['current'] += $statistic->getTotalSentCount();
$data['opened']['current'] += $statistic->getOpenCount();
$data['clicked']['current'] += $statistic->getClickCount();
$data['unsubscribed']['current'] += $statistic->getUnsubscribeCount();
$data['orders']['current'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getOrdersCount() : 0;
$data['revenue']['current'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getValue() : 0;
$newsletter = $this->newslettersRepository->findOneById($newsletterId);
$data['emails'][$newsletterId]['id'] = $newsletterId;
$data['emails'][$newsletterId]['name'] = $newsletter ? $newsletter->getSubject() : '';
$data['emails'][$newsletterId]['sent']['current'] = $statistic->getTotalSentCount();
$data['emails'][$newsletterId]['sent']['previous'] = 0;
$data['emails'][$newsletterId]['opened'] = $statistic->getOpenCount();
$data['emails'][$newsletterId]['clicked'] = $statistic->getClickCount();
$data['emails'][$newsletterId]['unsubscribed'] = $statistic->getUnsubscribeCount();
$data['emails'][$newsletterId]['orders'] = $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getOrdersCount() : 0;
$data['emails'][$newsletterId]['revenue'] = $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getValue() : 0;
$data['emails'][$newsletterId]['previewUrl'] = $newsletter ? $this->newsletterUrl->getViewInBrowserUrl($newsletter) : '';
$data['emails'][$newsletterId]['order'] = count($data['emails']);
}
$previousStatistics = $this->newsletterStatisticsRepository->getBatchStatistics(
$previousEmails,
$query->getCompareWithAfter(),
$query->getCompareWithBefore(),
$requiredData
);
foreach ($previousStatistics as $newsletterId => $statistic) {
$data['sent']['previous'] += $statistic->getTotalSentCount();
$data['opened']['previous'] += $statistic->getOpenCount();
$data['clicked']['previous'] += $statistic->getClickCount();
$data['unsubscribed']['previous'] += $statistic->getUnsubscribeCount();
$data['orders']['previous'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getOrdersCount() : 0;
$data['revenue']['previous'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getValue() : 0;
if (isset($data['emails'][$newsletterId])) {
$data['emails'][$newsletterId]['sent']['previous'] = $statistic->getTotalSentCount();
}
}
usort($data['emails'], function ($a, $b) {
return $a['order'] <=> $b['order'];
});
return $data;
}
}
@@ -0,0 +1,94 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics\Controller;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\AutomationRun;
use MailPoet\Automation\Engine\Data\AutomationRunLog;
use MailPoet\Automation\Engine\Data\Step;
use MailPoet\Automation\Engine\Storage\AutomationRunLogStorage;
use MailPoet\Automation\Engine\Storage\AutomationRunStorage;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Entities\Query;
class StepStatisticController {
/** @var AutomationRunStorage */
private $automationRunStorage;
/** @var AutomationRunLogStorage */
private $automationRunLogStorage;
public function __construct(
AutomationRunStorage $automationRunStorage,
AutomationRunLogStorage $automationRunLogStorage
) {
$this->automationRunStorage = $automationRunStorage;
$this->automationRunLogStorage = $automationRunLogStorage;
}
public function getWaitingStatistics(Automation $automation, Query $query): array {
$rawData = $this->automationRunStorage->getAutomationStepStatisticForTimeFrame(
$automation->getId(),
AutomationRun::STATUS_RUNNING,
$query->getAfter(),
$query->getBefore()
);
$data = [];
foreach ($automation->getSteps() as $step) {
foreach ($rawData as $rawDatum) {
if ($rawDatum['next_step_id'] === $step->getId()) {
$data[$step->getId()] = (int)$rawDatum['count'];
}
}
}
return $data;
}
public function getFailedStatistics(Automation $automation, Query $query): array {
$rawData = $this->automationRunStorage->getAutomationStepStatisticForTimeFrame(
$automation->getId(),
AutomationRun::STATUS_FAILED,
$query->getAfter(),
$query->getBefore()
);
$data = [];
foreach ($automation->getSteps() as $step) {
foreach ($rawData as $rawDatum) {
if ($rawDatum['next_step_id'] === $step->getId()) {
$data[$step->getId()] = (int)$rawDatum['count'];
}
}
}
return $data;
}
public function getCompletedStatistics(Automation $automation, Query $query): array {
$statistics = $this->automationRunLogStorage->getAutomationRunStatisticsForAutomationInTimeFrame(
$automation->getId(),
AutomationRunLog::STATUS_COMPLETE,
$query->getAfter(),
$query->getBefore()
);
$data = [];
foreach ($automation->getSteps() as $step) {
if ($step->getType() === Step::TYPE_ROOT) {
continue;
}
$data[$step->getId()] = 0;
foreach ($statistics as $stat) {
if ($stat['step_id'] === $step->getId()) {
$data[$step->getId()] = (int)$stat['count'];
}
}
}
return $data;
}
}
@@ -0,0 +1,124 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics\Endpoints;
if (!defined('ABSPATH')) exit;
use MailPoet\API\REST\Request;
use MailPoet\API\REST\Response;
use MailPoet\Automation\Engine\API\Endpoint;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Mappers\AutomationMapper;
use MailPoet\Automation\Engine\Storage\AutomationStatisticsStorage;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Controller\AutomationTimeSpanController;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Controller\StepStatisticController;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Entities\Query;
use MailPoet\Validator\Builder;
class AutomationFlowEndpoint extends Endpoint {
/** @var AutomationStorage */
private $automationStorage;
/** @var AutomationStatisticsStorage */
private $automationStatisticsStorage;
/** @var AutomationMapper */
private $automationMapper;
/** @var AutomationTimeSpanController */
private $automationTimeSpanController;
/** @var StepStatisticController */
private $stepStatisticController;
public function __construct(
AutomationStorage $automationStorage,
AutomationStatisticsStorage $automationStatisticsStorage,
AutomationMapper $automationMapper,
AutomationTimeSpanController $automationTimeSpanController,
StepStatisticController $stepStatisticController
) {
$this->automationStorage = $automationStorage;
$this->automationStatisticsStorage = $automationStatisticsStorage;
$this->automationMapper = $automationMapper;
$this->automationTimeSpanController = $automationTimeSpanController;
$this->stepStatisticController = $stepStatisticController;
}
public function handle(Request $request): Response {
$id = absint(is_numeric($request->getParam('id')) ? $request->getParam('id') : 0);
$automation = $this->automationStorage->getAutomation($id);
if (!$automation) {
throw Exceptions::automationNotFound($id);
}
$query = Query::fromRequest($request);
$automations = $this->automationTimeSpanController->getAutomationsInTimespan($automation, $query->getAfter(), $query->getBefore());
if (!count($automations)) {
throw Exceptions::automationNotFoundInTimeSpan($id);
}
$automation = current($automations);
$shortStatistics = $this->automationStatisticsStorage->getAutomationStats(
$automation->getId(),
null,
$query->getAfter(),
$query->getBefore()
);
$waitingData = $this->stepStatisticController->getWaitingStatistics($automation, $query);
$failedData = $this->stepStatisticController->getFailedStatistics($automation, $query);
try {
$completedData = $this->stepStatisticController->getCompletedStatistics($automation, $query);
} catch (\Throwable $e) {
return new Response([$e->getMessage()], 500);
}
$stepData = [
'total' => $shortStatistics->getEntered(),
];
if ($waitingData) {
$stepData['waiting'] = $waitingData;
}
if ($failedData) {
$stepData['failed'] = $failedData;
}
if ($completedData) {
$stepData['completed'] = $completedData;
}
$data = [
'automation' => $this->automationMapper->buildAutomation($automation, $shortStatistics),
'step_data' => $stepData,
'tree_is_inconsistent' => !$this->isTreeConsistent(...$automations),
];
return new Response($data);
}
private function isTreeConsistent(Automation ...$automations): bool {
if (count($automations) === 1) {
return true;
}
$stepIds = array_map(function (Automation $automation) {
return array_keys($automation->getSteps());
}, $automations);
$compareTo = array_shift($stepIds);
if (!$compareTo) {
return true;
}
foreach ($stepIds as $stepId) {
if (count(array_diff($stepId, $compareTo)) !== 0) {
return false;
}
}
return true;
}
public static function getRequestSchema(): array {
return [
'id' => Builder::integer()->required(),
'query' => Query::getRequestSchema(),
];
}
}
@@ -0,0 +1,51 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics\Endpoints;
if (!defined('ABSPATH')) exit;
use MailPoet\API\REST\Request;
use MailPoet\API\REST\Response;
use MailPoet\Automation\Engine\API\Endpoint;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Controller\OverviewStatisticsController;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Entities\QueryWithCompare;
use MailPoet\Validator\Builder;
class OverviewEndpoint extends Endpoint {
/** @var AutomationStorage */
private $automationStorage;
/** @var OverviewStatisticsController */
private $overviewStatisticsController;
public function __construct(
AutomationStorage $automationStorage,
OverviewStatisticsController $overviewStatisticsController
) {
$this->automationStorage = $automationStorage;
$this->overviewStatisticsController = $overviewStatisticsController;
}
public function handle(Request $request): Response {
$id = absint(is_numeric($request->getParam('id')) ? $request->getParam('id') : 0);
$automation = $this->automationStorage->getAutomation($id);
if (!$automation) {
throw Exceptions::automationNotFound($id);
}
$query = QueryWithCompare::fromRequest($request);
$result = $this->overviewStatisticsController->getStatisticsForAutomation($automation, $query);
return new Response($result);
}
public static function getRequestSchema(): array {
return [
'id' => Builder::integer()->required(),
'query' => QueryWithCompare::getRequestSchema(),
];
}
}
@@ -0,0 +1,151 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics\Entities;
if (!defined('ABSPATH')) exit;
use MailPoet\API\REST\Request;
use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema;
class Query {
/** @var \DateTimeImmutable */
private $primaryAfter;
/** @var \DateTimeImmutable */
private $primaryBefore;
/** @var int */
private $limit;
/** @var string */
private $orderBy;
/** @var string */
private $orderDirection;
/** @var int */
private $page;
/** @var array */
private $filter;
/** @var string | null */
private $search;
public function __construct(
\DateTimeImmutable $primaryAfter,
\DateTimeImmutable $primaryBefore,
int $limit = 25,
string $orderBy = '',
string $orderDirection = 'asc',
int $page = 1,
array $filter = [],
string $search = null
) {
$this->primaryAfter = $primaryAfter;
$this->primaryBefore = $primaryBefore;
$this->limit = $limit;
$this->orderBy = $orderBy;
$this->orderDirection = $orderDirection;
$this->page = $page;
$this->filter = $filter;
$this->search = $search;
}
public function getAfter(): \DateTimeImmutable {
return $this->primaryAfter;
}
public function getBefore(): \DateTimeImmutable {
return $this->primaryBefore;
}
public function getLimit(): int {
return $this->limit;
}
public function getOrderBy(): string {
return $this->orderBy;
}
public function getOrderDirection(): string {
return $this->orderDirection;
}
public function getPage(): int {
return $this->page;
}
public function getFilter(): array {
return $this->filter;
}
public function getSearch(): ?string {
return $this->search;
}
/**
* @param Request $request
* @return Query
* @throws UnexpectedValueException
*/
public static function fromRequest(Request $request) {
$query = $request->getParam('query');
if (!is_array($query)) {
throw new UnexpectedValueException('Invalid query parameters');
}
$primary = $query['primary'] ?? null;
if (!is_array($primary)) {
throw new UnexpectedValueException('Invalid query parameters');
}
$primaryAfter = $primary['after'] ?? null;
$primaryBefore = $primary['before'] ?? null;
if (
!is_string($primaryAfter) ||
!is_string($primaryBefore)
) {
throw new UnexpectedValueException('Invalid query parameters');
}
$limit = $query['limit'] ?? 25;
$orderBy = $query['order_by'] ?? '';
$orderDirection = isset($query['order']) && strtolower($query['order']) === 'asc' ? 'asc' : 'desc';
$page = $query['page'] ?? 1;
$filter = $query['filter'] ?? [];
$search = $query['search'] ?? null;
return new self(
new \DateTimeImmutable($primaryAfter),
new \DateTimeImmutable($primaryBefore),
$limit,
$orderBy,
$orderDirection,
$page,
$filter,
$search
);
}
public static function getRequestSchema(): Schema {
return Builder::object(
[
'primary' => Builder::object(
[
'after' => Builder::string()->formatDateTime()->required(),
'before' => Builder::string()->formatDateTime()->required(),
]
),
'limit' => Builder::integer()->minimum(1)->maximum(100),
'order_by' => Builder::string(),
'order' => Builder::string(),
'page' => Builder::integer()->minimum(1),
'filter' => Builder::object([]),
'search' => Builder::string()->nullable(),
]
);
}
}
@@ -0,0 +1,116 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics\Entities;
if (!defined('ABSPATH')) exit;
use MailPoet\API\REST\Request;
use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema;
class QueryWithCompare extends Query {
/** @var \DateTimeImmutable */
private $secondaryAfter;
/** @var \DateTimeImmutable */
private $secondaryBefore;
public function __construct(
\DateTimeImmutable $primaryAfter,
\DateTimeImmutable $primaryBefore,
\DateTimeImmutable $secondaryAfter,
\DateTimeImmutable $secondaryBefore,
int $limit = 25,
string $orderBy = '',
string $orderDirection = 'asc',
int $page = 0,
array $filter = [],
string $search = null
) {
parent::__construct($primaryAfter, $primaryBefore, $limit, $orderBy, $orderDirection, $page, $filter, $search);
$this->secondaryAfter = $secondaryAfter;
$this->secondaryBefore = $secondaryBefore;
}
public function getCompareWithAfter(): \DateTimeImmutable {
return $this->secondaryAfter;
}
public function getCompareWithBefore(): \DateTimeImmutable {
return $this->secondaryBefore;
}
/**
* @param Request $request
* @return QueryWithCompare
* @throws UnexpectedValueException
*/
public static function fromRequest(Request $request) {
$query = $request->getParam('query');
if (!is_array($query)) {
throw new UnexpectedValueException('Invalid query parameters');
}
$primary = $query['primary'] ?? null;
$secondary = $query['secondary'] ?? null;
if (!is_array($primary) || !is_array($secondary)) {
throw new UnexpectedValueException('Invalid query parameters');
}
$primaryAfter = $primary['after'] ?? null;
$primaryBefore = $primary['before'] ?? null;
$secondaryAfter = $secondary['after'] ?? null;
$secondaryBefore = $secondary['before'] ?? null;
if (
!is_string($primaryAfter) ||
!is_string($primaryBefore) ||
!is_string($secondaryAfter) ||
!is_string($secondaryBefore)
) {
throw new UnexpectedValueException('Invalid query parameters');
}
$limit = $query['limit'] ?? 25;
$orderBy = $query['orderBy'] ?? '';
$orderDirection = $query['orderDirection'] ?? 'asc';
$page = $query['page'] ?? 0;
return new self(
new \DateTimeImmutable($primaryAfter),
new \DateTimeImmutable($primaryBefore),
new \DateTimeImmutable($secondaryAfter),
new \DateTimeImmutable($secondaryBefore),
$limit,
$orderBy,
$orderDirection,
$page
);
}
public static function getRequestSchema(): Schema {
return Builder::object(
[
'primary' => Builder::object(
[
'after' => Builder::string()->formatDateTime()->required(),
'before' => Builder::string()->formatDateTime()->required(),
]
),
'secondary' => Builder::object(
[
'after' => Builder::string()->formatDateTime()->required(),
'before' => Builder::string()->formatDateTime()->required(),
]
),
'limit' => Builder::integer()->minimum(1)->maximum(100),
'orderBy' => Builder::string(),
'orderDirection' => Builder::string(),
'page' => Builder::integer()->minimum(1),
'filter' => Builder::object(),
'search' => Builder::string()->nullable(),
]
);
}
}
@@ -0,0 +1,91 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\ServicesChecker;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Services\AuthorizedSenderDomainController;
use MailPoet\Services\Bridge;
class ContextFactory {
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var Bridge */
private $bridge;
/** @var ServicesChecker */
private $servicesChecker;
/** @var AuthorizedSenderDomainController */
private $authorizedSenderDomainController;
/** @var AuthorizedEmailsController */
private $authorizedEmailsController;
public function __construct(
SegmentsRepository $segmentsRepository,
Bridge $bridge,
ServicesChecker $servicesChecker,
AuthorizedSenderDomainController $authorizedSenderDomainController,
AuthorizedEmailsController $authorizedEmailsController
) {
$this->segmentsRepository = $segmentsRepository;
$this->servicesChecker = $servicesChecker;
$this->bridge = $bridge;
$this->authorizedSenderDomainController = $authorizedSenderDomainController;
$this->authorizedEmailsController = $authorizedEmailsController;
}
/** @return mixed[] */
public function getContextData(): array {
$data = [
'segments' => $this->getSegments(),
'userRoles' => $this->getUserRoles(),
];
if ($this->isMSSEnabled()) {
$data['senderDomainsConfig'] = $this->getSenderDomainsConfig();
}
return $data;
}
private function getSenderDomainsConfig(): array {
$senderDomainsConfig = $this->authorizedSenderDomainController->getContextDataForAutomations();
$senderDomainsConfig['authorizedEmails'] = $this->authorizedEmailsController->getAuthorizedEmailAddresses();
return $senderDomainsConfig;
}
private function isMSSEnabled(): bool {
$mpApiKeyValid = $this->servicesChecker->isMailPoetAPIKeyValid(false, true);
return $mpApiKeyValid && $this->bridge->isMailpoetSendingServiceEnabled();
}
private function getSegments(): array {
$segments = [];
foreach ($this->segmentsRepository->findAll() as $segment) {
$segments[] = [
'id' => $segment->getId(),
'name' => $segment->getName(),
'type' => $segment->getType(),
];
}
return $segments;
}
private function getUserRoles(): array {
$userRoles = [];
foreach (wp_roles()->roles as $role => $details) {
$userRoles[] = [
'id' => $role,
'name' => $details['name'],
];
}
return $userRoles;
}
}
@@ -0,0 +1,40 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Fields;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Field;
use MailPoet\Automation\Integrations\MailPoet\Payloads\NewsletterLinkPayload;
class NewsletterLinkFieldsFactory {
public function getFields(): array {
return [
new Field(
'mailpoet:email-link:url',
Field::TYPE_STRING,
__('Link URL', 'mailpoet'),
function(NewsletterLinkPayload $payload) {
return $payload->getLink()->getUrl();
}
),
new Field(
'mailpoet:email-link:created',
Field::TYPE_DATETIME,
__('Created', 'mailpoet'),
function(NewsletterLinkPayload $payload) {
return $payload->getLink()->getCreatedAt();
}
),
new Field(
'mailpoet:email-link:id',
Field::TYPE_INTEGER,
__('Link ID', 'mailpoet'),
function(NewsletterLinkPayload $payload) {
return $payload->getLink()->getId();
}
),
];
}
}
@@ -0,0 +1,77 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Fields;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\AutomationRun;
use MailPoet\Automation\Engine\Data\Field;
use MailPoet\Automation\Engine\Data\Subject;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SubscriberPayload;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
class SubscriberAutomationFieldsFactory {
/** @var AutomationStorage */
private $automationStorage;
public function __construct(
AutomationStorage $automationStorage
) {
$this->automationStorage = $automationStorage;
}
/** @return Field[] */
public function getFields(): array {
$automations = $this->automationStorage->getAutomations(
array_diff(Automation::STATUS_ALL, [Automation::STATUS_TRASH])
);
$args = [
'options' => array_map(function (Automation $automation) {
return [
'id' => $automation->getId(),
'name' => $automation->getName() . " (#{$automation->getId()})",
];
}, $automations),
'params' => ['in_the_last'],
];
return [
new Field(
'mailpoet:subscriber:automations-entered',
Field::TYPE_ENUM_ARRAY,
__('Automations — entered', 'mailpoet'),
function (SubscriberPayload $payload, array $params = []) {
return $this->getAutomationIds($payload, null, $params);
},
$args
),
new Field(
'mailpoet:subscriber:automations-processing',
Field::TYPE_ENUM_ARRAY,
__('Automations — processing', 'mailpoet'),
function (SubscriberPayload $payload, array $params = []) {
return $this->getAutomationIds($payload, [AutomationRun::STATUS_RUNNING], $params);
},
$args
),
new Field(
'mailpoet:subscriber:automations-exited',
Field::TYPE_ENUM_ARRAY,
__('Automations — exited', 'mailpoet'),
function (SubscriberPayload $payload, array $params = []) {
return $this->getAutomationIds($payload, [AutomationRun::STATUS_COMPLETE], $params);
},
$args
),
];
}
private function getAutomationIds(SubscriberPayload $payload, array $status = null, array $params = []): array {
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
$subject = new Subject(SubscriberSubject::KEY, ['subscriber_id' => $payload->getId()]);
return $this->automationStorage->getAutomationIdsBySubject($subject, $status, $inTheLastSeconds);
}
}
@@ -0,0 +1,172 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Fields;
if (!defined('ABSPATH')) exit;
use DateTimeImmutable;
use MailPoet\Automation\Engine\Data\Field;
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
use MailPoet\Automation\Engine\WordPress;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SubscriberPayload;
use MailPoet\CustomFields\CustomFieldsRepository;
use MailPoet\Entities\CustomFieldEntity;
use MailPoet\Entities\SubscriberCustomFieldEntity;
use MailPoet\Util\DateConverter;
class SubscriberCustomFieldsFactory {
/** @var CustomFieldsRepository */
private $customFieldsRepository;
/** @var WordPress */
private $wordPress;
public function __construct(
CustomFieldsRepository $customFieldsRepository,
WordPress $wordPress
) {
$this->customFieldsRepository = $customFieldsRepository;
$this->wordPress = $wordPress;
}
/** @return Field[] */
public function getFields(): array {
return array_map(function (CustomFieldEntity $customField) {
return $this->getField($customField);
}, $this->customFieldsRepository->findAll());
}
private function getField(CustomFieldEntity $customField): Field {
switch ($customField->getType()) {
case CustomFieldEntity::TYPE_TEXT:
case CustomFieldEntity::TYPE_TEXTAREA:
$validate = $customField->getParams()['validate'] ?? null;
return $validate === 'number'
? $this->createNumberField($customField)
: $this->createStringField($customField);
case CustomFieldEntity::TYPE_CHECKBOX:
return $this->createBooleanField($customField);
case CustomFieldEntity::TYPE_RADIO:
case CustomFieldEntity::TYPE_SELECT:
return $this->createEnumField($customField);
case CustomFieldEntity::TYPE_DATE:
$type = $customField->getParams()['date_type'] ?? null;
if ($type === 'year_month_day' || $type === 'year_month') {
return $this->createDateTimeField($customField);
} elseif ($type === 'year') {
return $this->createYearField($customField);
} elseif ($type === 'month') {
return $this->createMonthField($customField);
} elseif ($type === 'day') {
return $this->createDayField($customField);
} else {
throw new InvalidStateException(sprintf('Unknown date type "%s"', $type));
}
default:
throw new InvalidStateException(sprintf('Unknown custom field type "%s"', $customField->getType()));
}
}
private function createStringField(CustomFieldEntity $customField): Field {
$factory = function (SubscriberPayload $payload) use ($customField) {
return $this->getCustomFieldValue($payload, $customField);
};
return $this->createField($customField, Field::TYPE_STRING, $factory);
}
private function createNumberField(CustomFieldEntity $customField): Field {
$factory = function (SubscriberPayload $payload) use ($customField) {
$value = $this->getCustomFieldValue($payload, $customField);
return is_numeric($value) ? (float)$value : null;
};
return $this->createField($customField, Field::TYPE_NUMBER, $factory);
}
private function createBooleanField(CustomFieldEntity $customField): Field {
$factory = function (SubscriberPayload $payload) use ($customField) {
$value = $this->getCustomFieldValue($payload, $customField);
return $value === null ? null : (bool)$value;
};
return $this->createField($customField, Field::TYPE_BOOLEAN, $factory);
}
private function createEnumField(CustomFieldEntity $customField): Field {
$factory = function (SubscriberPayload $payload) use ($customField) {
$value = $this->getCustomFieldValue($payload, $customField);
return $value === null ? null : $value;
};
return $this->createField($customField, Field::TYPE_ENUM, $factory, [
'options' => array_map(function (array $value) {
return ['id' => $value['value'], 'name' => $value['value']];
}, $customField->getParams()['values'] ?? []),
]);
}
private function createDateTimeField(CustomFieldEntity $customField): Field {
$factory = function (SubscriberPayload $payload) use ($customField) {
return $this->getDateTimeValue($customField, $this->getCustomFieldValue($payload, $customField) ?? '');
};
return $this->createField($customField, Field::TYPE_DATETIME, $factory);
}
private function createYearField(CustomFieldEntity $customField): Field {
$factory = function (SubscriberPayload $payload) use ($customField) {
$value = $this->getDateTimeValue($customField, $this->getCustomFieldValue($payload, $customField) ?? '');
return $value ? (int)$value->format('Y') : null;
};
return $this->createField($customField, Field::TYPE_INTEGER, $factory);
}
private function createMonthField(CustomFieldEntity $customField): Field {
$factory = function (SubscriberPayload $payload) use ($customField) {
$value = $this->getDateTimeValue($customField, $this->getCustomFieldValue($payload, $customField) ?? '');
return $value ? (int)$value->format('n') : null;
};
return $this->createField($customField, Field::TYPE_ENUM, $factory, [
'options' => array_map(function (int $value) {
return ['id' => $value, 'name' => $this->wordPress->getWpLocale()->get_month($value)];
}, range(1, 12)),
]);
}
private function createDayField(CustomFieldEntity $customField): Field {
$factory = function (SubscriberPayload $payload) use ($customField) {
$value = $this->getDateTimeValue($customField, $this->getCustomFieldValue($payload, $customField) ?? '');
return $value ? (int)$value->format('j') : null;
};
return $this->createField($customField, Field::TYPE_ENUM, $factory, [
'options' => array_map(function (int $value) {
return ['id' => $value, 'name' => "$value"];
}, range(1, 31)),
]);
}
private function getCustomFieldValue(SubscriberPayload $payload, CustomFieldEntity $customField): ?string {
$subscriberCustomField = $payload->getSubscriber()->getSubscriberCustomFields()->filter(
function (SubscriberCustomFieldEntity $subscriberCustomField = null) use ($customField) {
return $subscriberCustomField && $subscriberCustomField->getCustomField() === $customField;
}
)->first() ?: null;
return $subscriberCustomField ? $subscriberCustomField->getValue() : null;
}
private function createField(CustomFieldEntity $customField, string $type, callable $factory, array $args = []): Field {
$key = 'mailpoet:subscriber:custom-field:' . $customField->getName();
$name = sprintf(
// translators: %s is the name of the custom field
__('Custom field: %s', 'mailpoet'),
$customField->getParams()['label'] ?? $customField->getName()
);
return new Field($key, $type, $name, $factory, $args);
}
private function getDateTimeValue(CustomFieldEntity $customField, ?string $value): ?DateTimeImmutable {
$dateFormat = $customField->getParams()['date_format'] ?? null;
if (!$dateFormat || !$value) {
return null;
}
$dateString = (new DateConverter())->convertDateToDatetime($value, $dateFormat) ?: null;
return $dateString ? new DateTimeImmutable($dateString, $this->wordPress->wpTimezone()) : null;
}
}
@@ -0,0 +1,259 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Fields;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Field;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SubscriberPayload;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Segments\SegmentsFinder;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Tags\TagRepository;
class SubscriberFieldsFactory {
/** @var SegmentsFinder */
private $segmentsFinder;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var SubscriberAutomationFieldsFactory */
private $automationFieldsFactory;
/** @var SubscriberCustomFieldsFactory */
private $customFieldsFactory;
/** @var TagRepository */
private $tagRepository;
/** @var SubscriberStatisticFieldsFactory */
private $statisticFieldsFactory;
public function __construct(
SegmentsFinder $segmentsFinder,
SegmentsRepository $segmentsRepository,
SubscriberAutomationFieldsFactory $automationFieldsFactory,
SubscriberCustomFieldsFactory $customFieldsFactory,
SubscriberStatisticFieldsFactory $statisticFieldsFactory,
TagRepository $tagRepository
) {
$this->segmentsFinder = $segmentsFinder;
$this->segmentsRepository = $segmentsRepository;
$this->automationFieldsFactory = $automationFieldsFactory;
$this->customFieldsFactory = $customFieldsFactory;
$this->statisticFieldsFactory = $statisticFieldsFactory;
$this->tagRepository = $tagRepository;
}
/** @return Field[] */
public function getFields(): array {
return array_merge(
$this->customFieldsFactory->getFields(),
[
new Field(
'mailpoet:subscriber:email',
Field::TYPE_STRING,
__('Email address', 'mailpoet'),
function (SubscriberPayload $payload) {
return $payload->getEmail();
}
),
new Field(
'mailpoet:subscriber:engagement-score',
Field::TYPE_NUMBER,
__('Engagement score', 'mailpoet'),
function (SubscriberPayload $payload) {
return $payload->getSubscriber()->getEngagementScore();
}
),
new Field(
'mailpoet:subscriber:first-name',
Field::TYPE_STRING,
__('First name', 'mailpoet'),
function (SubscriberPayload $payload) {
return $payload->getSubscriber()->getFirstName();
}
),
new Field(
'mailpoet:subscriber:last-name',
Field::TYPE_STRING,
__('Last name', 'mailpoet'),
function (SubscriberPayload $payload) {
return $payload->getSubscriber()->getLastName();
}
),
new Field(
'mailpoet:subscriber:is-globally-subscribed',
Field::TYPE_BOOLEAN,
__('Is globally subscribed', 'mailpoet'),
function (SubscriberPayload $payload) {
return $payload->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED;
}
),
new Field(
'mailpoet:subscriber:last-engagement-at',
Field::TYPE_DATETIME,
__('Last engaged', 'mailpoet'),
function (SubscriberPayload $payload) {
return $payload->getSubscriber()->getLastEngagementAt();
}
),
new Field(
'mailpoet:subscriber:status',
Field::TYPE_ENUM,
__('Status', 'mailpoet'),
function (SubscriberPayload $payload) {
return $payload->getStatus();
},
[
'options' => [
[
'id' => SubscriberEntity::STATUS_SUBSCRIBED,
'name' => __('Subscribed', 'mailpoet'),
],
[
'id' => SubscriberEntity::STATUS_UNCONFIRMED,
'name' => __('Unconfirmed', 'mailpoet'),
],
[
'id' => SubscriberEntity::STATUS_UNSUBSCRIBED,
'name' => __('Unsubscribed', 'mailpoet'),
],
[
'id' => SubscriberEntity::STATUS_INACTIVE,
'name' => __('Inactive', 'mailpoet'),
],
[
'id' => SubscriberEntity::STATUS_BOUNCED,
'name' => __('Bounced', 'mailpoet'),
],
],
]
),
new Field(
'mailpoet:subscriber:subscription-source',
Field::TYPE_ENUM,
__('Subscription source', 'mailpoet'),
function (SubscriberPayload $payload) {
return $payload->getSubscriber()->getSource();
},
[
'options' => [
[
'id' => 'api',
'name' => __('API', 'mailpoet'),
],
[
'id' => 'form',
'name' => __('Form', 'mailpoet'),
],
[
'id' => 'unknown',
'name' => __('Unknown', 'mailpoet'),
],
[
'id' => 'imported',
'name' => __('Imported', 'mailpoet'),
],
[
'id' => 'administrator',
'name' => __('Administrator', 'mailpoet'),
],
[
'id' => 'wordpress_user',
'name' => __('WordPress user', 'mailpoet'),
],
[
'id' => 'woocommerce_user',
'name' => __('WooCommerce user', 'mailpoet'),
],
[
'id' => 'woocommerce_checkout',
'name' => __('WooCommerce checkout', 'mailpoet'),
],
],
]
),
new Field(
'mailpoet:subscriber:last-subscribed-at',
Field::TYPE_DATETIME,
__('Subscribed date', 'mailpoet'),
function (SubscriberPayload $payload) {
return $payload->getSubscriber()->getLastSubscribedAt();
}
),
new Field(
'mailpoet:subscriber:lists',
Field::TYPE_ENUM_ARRAY,
__('Subscribed lists', 'mailpoet'),
function (SubscriberPayload $payload) {
$value = [];
foreach ($payload->getSubscriber()->getSegments() as $list) {
if ($list->getType() !== SegmentEntity::TYPE_DYNAMIC) {
$value[] = $list->getId();
}
}
return $value;
},
[
'options' => array_map(function ($segment) {
return [
'id' => $segment->getId(),
'name' => $segment->getName(),
];
}, $this->segmentsRepository->findByTypeNotIn([SegmentEntity::TYPE_DYNAMIC])),
]
),
new Field(
'mailpoet:subscriber:tags',
Field::TYPE_ENUM_ARRAY,
__('Tags', 'mailpoet'),
function (SubscriberPayload $payload) {
$value = [];
foreach ($payload->getSubscriber()->getSubscriberTags() as $subscriberTag) {
$tag = $subscriberTag->getTag();
if ($tag) {
$value[] = $tag->getId();
}
}
return $value;
},
[
'options' => array_map(function ($tag) {
return [
'id' => $tag->getId(),
'name' => $tag->getName(),
];
}, $this->tagRepository->findAll()),
]
),
new Field(
'mailpoet:subscriber:segments',
Field::TYPE_ENUM_ARRAY,
__('Segments', 'mailpoet'),
function (SubscriberPayload $payload) {
$segments = $this->segmentsFinder->findDynamicSegments($payload->getSubscriber());
$value = [];
foreach ($segments as $segment) {
$value[] = $segment->getId();
}
return $value;
},
[
'options' => array_map(function ($segment) {
return [
'id' => $segment->getId(),
'name' => $segment->getName(),
];
}, $this->segmentsRepository->findBy(['type' => SegmentEntity::TYPE_DYNAMIC])),
]
),
],
$this->statisticFieldsFactory->getFields(),
$this->automationFieldsFactory->getFields()
);
}
}
@@ -0,0 +1,81 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Fields;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Field;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SubscriberPayload;
use MailPoet\Subscribers\Statistics\SubscriberStatisticsRepository;
use MailPoetVendor\Carbon\Carbon;
class SubscriberStatisticFieldsFactory {
/** @var SubscriberStatisticsRepository */
private $subscriberStatisticsRepository;
public function __construct(
SubscriberStatisticsRepository $subscriberStatisticsRepository
) {
$this->subscriberStatisticsRepository = $subscriberStatisticsRepository;
}
/** @return Field[] */
public function getFields(): array {
return [
new Field(
'mailpoet:subscriber:email-sent-count',
Field::TYPE_INTEGER,
__('Email — sent count', 'mailpoet'),
function (SubscriberPayload $payload, array $params = []) {
$startTime = $this->getStartTime($params);
return $this->subscriberStatisticsRepository->getTotalSentCount($payload->getSubscriber(), $startTime);
},
[
'params' => ['in_the_last'],
]
),
new Field(
'mailpoet:subscriber:email-opened-count',
Field::TYPE_INTEGER,
__('Email — opened count', 'mailpoet'),
function (SubscriberPayload $payload, array $params = []) {
$startTime = $this->getStartTime($params);
return $this->subscriberStatisticsRepository->getStatisticsOpenCount($payload->getSubscriber(), $startTime);
},
[
'params' => ['in_the_last'],
]
),
new Field(
'mailpoet:subscriber:email-machine-opened-count',
Field::TYPE_INTEGER,
__('Email — machine opened count', 'mailpoet'),
function (SubscriberPayload $payload, array $params = []) {
$startTime = $this->getStartTime($params);
return $this->subscriberStatisticsRepository->getStatisticsMachineOpenCount($payload->getSubscriber(), $startTime);
},
[
'params' => ['in_the_last'],
]
),
new Field(
'mailpoet:subscriber:email-clicked-count',
Field::TYPE_INTEGER,
__('Email — clicked count', 'mailpoet'),
function (SubscriberPayload $payload, array $params = []) {
$startTime = $this->getStartTime($params);
return $this->subscriberStatisticsRepository->getStatisticsClickCount($payload->getSubscriber(), $startTime);
},
[
'params' => ['in_the_last'],
]
),
];
}
private function getStartTime(array $params): ?Carbon {
$inTheLastSeconds = $params['in_the_last'] ?? null;
return $inTheLastSeconds ? Carbon::now()->subSeconds((int)$inTheLastSeconds) : null;
}
}
@@ -0,0 +1,94 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Hooks;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\Step;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Engine\WordPress;
use MailPoet\Newsletter\NewsletterDeleteController;
use MailPoet\Newsletter\NewslettersRepository;
class AutomationEditorLoadingHooks {
/** @var WordPress */
private $wp;
/** @var AutomationStorage */
private $automationStorage;
/** @var NewslettersRepository */
private $newslettersRepository;
private NewsletterDeleteController $newsletterDeleteController;
public function __construct(
WordPress $wp,
AutomationStorage $automationStorage,
NewslettersRepository $newslettersRepository,
NewsletterDeleteController $newsletterDeleteController
) {
$this->wp = $wp;
$this->automationStorage = $automationStorage;
$this->newslettersRepository = $newslettersRepository;
$this->newsletterDeleteController = $newsletterDeleteController;
}
public function init(): void {
$this->wp->addAction(Hooks::EDITOR_BEFORE_LOAD, [$this, 'beforeEditorLoad']);
}
public function beforeEditorLoad(int $automationId): void {
$automation = $this->automationStorage->getAutomation($automationId);
if (!$automation) {
return;
}
$this->disconnectEmptyEmailsFromSendEmailStep($automation);
}
private function disconnectEmptyEmailsFromSendEmailStep(Automation $automation): void {
$sendEmailSteps = array_filter(
$automation->getSteps(),
function(Step $step): bool {
return $step->getKey() === 'mailpoet:send-email';
}
);
foreach ($sendEmailSteps as $step) {
$emailId = $step->getArgs()['email_id'] ?? 0;
if (!$emailId) {
continue;
}
$newsletterEntity = $this->newslettersRepository->findOneById($emailId);
if ($newsletterEntity && $newsletterEntity->getBody() !== null) {
continue;
}
$this->newsletterDeleteController->bulkDelete([$emailId]);
$args = $step->getArgs();
unset($args['email_id']);
$updatedStep = new Step(
$step->getId(),
$step->getType(),
$step->getKey(),
$args,
$step->getNextSteps()
);
$steps = array_merge(
$automation->getSteps(),
[$updatedStep->getId() => $updatedStep]
);
$automation->setSteps($steps);
//To be valid, an email would need to be associated to an active automation.
if ($automation->getStatus() === Automation::STATUS_ACTIVE) {
$automation->setStatus(Automation::STATUS_DRAFT);
}
$this->automationStorage->updateAutomation($automation);
}
}
}
@@ -0,0 +1,67 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Hooks;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Storage\AutomationRunStorage;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
use MailPoet\Util\Security;
use MailPoet\WP\Functions as WPFunctions;
class CreateAutomationRunHook {
private AutomationRunStorage $automationRunStorage;
private WPFunctions $wp;
public function __construct(
AutomationRunStorage $automationRunStorage,
WPFunctions $wp
) {
$this->automationRunStorage = $automationRunStorage;
$this->wp = $wp;
}
public function init(): void {
$this->wp->addAction(Hooks::AUTOMATION_RUN_CREATE, [$this, 'createAutomationRun'], 5, 2);
}
public function createAutomationRun(bool $result, StepRunArgs $args): bool {
if (!$result) {
return $result;
}
$automation = $args->getAutomation();
$runOnlyOnce = $automation->getMeta('mailpoet:run-once-per-subscriber');
if (!$runOnlyOnce) {
return true;
}
$subscriberSubject = array_values($args->getAutomationRun()->getSubjects(SubscriberSubject::KEY))[0] ?? null;
if (!$subscriberSubject) {
return true;
}
// Use locking mechanism to minimize the risk of race conditions.
// WP transients don't provide atomic operations, so we can't guarantee
// race-condition safety with a 100% certainty, but we can significantly
// minimize the risk by generating and re-checking a unique lock value.
$key = sprintf('mailpoet:run-once-per-subscriber:[%s][%s]', $automation->getId(), $subscriberSubject->getHash());
// 1. If lock already exists, do not create automation run.
$value = $this->wp->getTransient($key);
if ($value) {
return false;
}
// 2. If lock does not exist, create it with a unique value.
$value = Security::generateRandomString(16);
$this->wp->setTransient($key, $value, MINUTE_IN_SECONDS);
// 3. If no automation run exist, ensure that the lock wasn't updated by another process.
$count = $this->automationRunStorage->getCountByAutomationAndSubject($automation, $subscriberSubject);
return $count === 0 && $this->wp->getTransient($key) === $value;
}
}
@@ -0,0 +1,145 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Integration;
use MailPoet\Automation\Engine\Registry;
use MailPoet\Automation\Engine\WordPress;
use MailPoet\Automation\Integrations\MailPoet\Actions\SendEmailAction;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Analytics;
use MailPoet\Automation\Integrations\MailPoet\Hooks\AutomationEditorLoadingHooks;
use MailPoet\Automation\Integrations\MailPoet\Hooks\CreateAutomationRunHook;
use MailPoet\Automation\Integrations\MailPoet\Subjects\NewsletterLinkSubject;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SegmentSubject;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
use MailPoet\Automation\Integrations\MailPoet\SubjectTransformers\CommentSubjectToSubscriberSubjectTransformer;
use MailPoet\Automation\Integrations\MailPoet\SubjectTransformers\OrderSubjectToSegmentSubjectTransformer;
use MailPoet\Automation\Integrations\MailPoet\SubjectTransformers\OrderSubjectToSubscriberSubjectTransformer;
use MailPoet\Automation\Integrations\MailPoet\SubjectTransformers\SubscriberSubjectToWordPressUserSubjectTransformer;
use MailPoet\Automation\Integrations\MailPoet\Templates\TemplatesFactory;
use MailPoet\Automation\Integrations\MailPoet\Triggers\SomeoneSubscribesTrigger;
use MailPoet\Automation\Integrations\MailPoet\Triggers\UserRegistrationTrigger;
class MailPoetIntegration implements Integration {
/** @var ContextFactory */
private $contextFactory;
/** @var SegmentSubject */
private $segmentSubject;
/** @var SubscriberSubject */
private $subscriberSubject;
/** @var NewsletterLinkSubject */
private $emailLinkSubject;
/** @var SomeoneSubscribesTrigger */
private $someoneSubscribesTrigger;
/** @var UserRegistrationTrigger */
private $userRegistrationTrigger;
/** @var SendEmailAction */
private $sendEmailAction;
/** @var AutomationEditorLoadingHooks */
private $automationEditorLoadingHooks;
/** @var CreateAutomationRunHook */
private $createAutomationRunHook;
/** @var OrderSubjectToSubscriberSubjectTransformer */
private $orderToSubscriberTransformer;
/** @var OrderSubjectToSegmentSubjectTransformer */
private $orderToSegmentTransformer;
/** @var SubscriberSubjectToWordPressUserSubjectTransformer */
private $subscriberToWordPressUserTransformer;
/** @var CommentSubjectToSubscriberSubjectTransformer */
private $commentToSubscriberTransformer;
/** @var TemplatesFactory */
private $templatesFactory;
/** @var Analytics */
private $registerAnalytics;
/** @var WordPress */
private $wordPress;
public function __construct(
ContextFactory $contextFactory,
SegmentSubject $segmentSubject,
SubscriberSubject $subscriberSubject,
NewsletterLinkSubject $emailLinkSubject,
OrderSubjectToSubscriberSubjectTransformer $orderToSubscriberTransformer,
OrderSubjectToSegmentSubjectTransformer $orderToSegmentTransformer,
SubscriberSubjectToWordPressUserSubjectTransformer $subscriberToWordPressUserTransformer,
CommentSubjectToSubscriberSubjectTransformer $commentToSubscriberTransformer,
SomeoneSubscribesTrigger $someoneSubscribesTrigger,
UserRegistrationTrigger $userRegistrationTrigger,
SendEmailAction $sendEmailAction,
AutomationEditorLoadingHooks $automationEditorLoadingHooks,
CreateAutomationRunHook $createAutomationRunHook,
TemplatesFactory $templatesFactory,
Analytics $registerAnalytics,
WordPress $wordPress
) {
$this->contextFactory = $contextFactory;
$this->segmentSubject = $segmentSubject;
$this->subscriberSubject = $subscriberSubject;
$this->emailLinkSubject = $emailLinkSubject;
$this->orderToSubscriberTransformer = $orderToSubscriberTransformer;
$this->orderToSegmentTransformer = $orderToSegmentTransformer;
$this->subscriberToWordPressUserTransformer = $subscriberToWordPressUserTransformer;
$this->commentToSubscriberTransformer = $commentToSubscriberTransformer;
$this->someoneSubscribesTrigger = $someoneSubscribesTrigger;
$this->userRegistrationTrigger = $userRegistrationTrigger;
$this->sendEmailAction = $sendEmailAction;
$this->automationEditorLoadingHooks = $automationEditorLoadingHooks;
$this->createAutomationRunHook = $createAutomationRunHook;
$this->templatesFactory = $templatesFactory;
$this->registerAnalytics = $registerAnalytics;
$this->wordPress = $wordPress;
}
public function register(Registry $registry): void {
$registry->addContextFactory('mailpoet', function () {
return $this->contextFactory->getContextData();
});
$registry->addSubject($this->segmentSubject);
$registry->addSubject($this->subscriberSubject);
$registry->addSubject($this->emailLinkSubject);
$registry->addTrigger($this->someoneSubscribesTrigger);
$registry->addTrigger($this->userRegistrationTrigger);
$registry->addAction($this->sendEmailAction);
$registry->addSubjectTransformer($this->orderToSubscriberTransformer);
$registry->addSubjectTransformer($this->orderToSegmentTransformer);
$registry->addSubjectTransformer($this->subscriberToWordPressUserTransformer);
$registry->addSubjectTransformer($this->commentToSubscriberTransformer);
foreach ($this->templatesFactory->createTemplates() as $template) {
$registry->addTemplate($template);
}
// sync step args (subject, preheader, etc.) to email settings
$registry->onBeforeAutomationStepSave(
[$this->sendEmailAction, 'saveEmailSettings'],
$this->sendEmailAction->getKey()
);
// execute send email step progress when email is sent
$this->wordPress->addAction('mailpoet_automation_email_sent', [$this->sendEmailAction, 'handleEmailSent']);
$this->automationEditorLoadingHooks->init();
$this->createAutomationRunHook->init();
$this->registerAnalytics->register();
}
}
@@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Payloads;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Integration\Payload;
use MailPoet\Entities\NewsletterLinkEntity;
class NewsletterLinkPayload implements Payload {
/** @var NewsletterLinkEntity */
private $linkEntity;
public function __construct(
NewsletterLinkEntity $linkEntity
) {
$this->linkEntity = $linkEntity;
}
public function getId(): ?int {
return $this->linkEntity->getId();
}
public function getLink(): NewsletterLinkEntity {
return $this->linkEntity;
}
}
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Payloads;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Integration\Payload;
use MailPoet\Entities\SegmentEntity;
use MailPoet\InvalidStateException;
class SegmentPayload implements Payload {
/** @var SegmentEntity */
private $segment;
public function __construct(
SegmentEntity $segment
) {
$this->segment = $segment;
}
public function getId(): int {
$id = $this->segment->getId();
if (!$id) {
throw new InvalidStateException();
}
return $id;
}
public function getName(): string {
return $this->segment->getName();
}
public function getType(): string {
return $this->segment->getType();
}
}
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Payloads;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Integration\Payload;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\InvalidStateException;
class SubscriberPayload implements Payload {
/** @var SubscriberEntity */
private $subscriber;
public function __construct(
SubscriberEntity $subscriber
) {
$this->subscriber = $subscriber;
}
public function getId(): int {
$id = $this->subscriber->getId();
if (!$id) {
throw new InvalidStateException();
}
return $id;
}
public function getEmail(): string {
return $this->subscriber->getEmail();
}
public function getStatus(): string {
return $this->subscriber->getStatus();
}
public function isWpUser(): bool {
return $this->subscriber->isWPUser();
}
public function getWpUserId(): ?int {
return $this->subscriber->getWpUserId();
}
public function getSubscriber(): SubscriberEntity {
return $this->subscriber;
}
}
@@ -0,0 +1,67 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\SubjectTransformers;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Subject;
use MailPoet\Automation\Engine\Integration\SubjectTransformer;
use MailPoet\Automation\Engine\WordPress;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
use MailPoet\Automation\Integrations\WordPress\Subjects\CommentSubject;
use MailPoet\Subscribers\SubscribersRepository;
class CommentSubjectToSubscriberSubjectTransformer implements SubjectTransformer {
/** @var WordPress */
private $wp;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
WordPress $wp,
SubscribersRepository $subscribersRepository
) {
$this->wp = $wp;
$this->subscribersRepository = $subscribersRepository;
}
public function transform(Subject $data): ?Subject {
if ($this->accepts() !== $data->getKey()) {
throw new \InvalidArgumentException('Invalid subject type');
}
$commentId = (int)$data->getArgs()['comment_id'];
$comment = $this->wp->getComment($commentId);
if (!$comment instanceof \WP_Comment) {
return null;
}
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$email = $comment->comment_author_email;
if (!$this->wp->isEmail($email)) {
return null;
}
$subscriber = $this->subscribersRepository->findOneBy(['email' => $email]);
if (!$subscriber) {
return null;
}
return new Subject(
SubscriberSubject::KEY,
[
'subscriber_id' => $subscriber->getId(),
]
);
}
public function returns(): string {
return SubscriberSubject::KEY;
}
public function accepts(): string {
return CommentSubject::KEY;
}
}
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\SubjectTransformers;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Subject;
use MailPoet\Automation\Engine\Integration\SubjectTransformer;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SegmentSubject;
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject;
use MailPoet\Segments\SegmentsRepository;
class OrderSubjectToSegmentSubjectTransformer implements SubjectTransformer {
/** @var SegmentsRepository */
private $segmentRepository;
public function __construct(
SegmentsRepository $segmentRepository
) {
$this->segmentRepository = $segmentRepository;
}
public function accepts(): string {
return OrderSubject::KEY;
}
public function returns(): string {
return SegmentSubject::KEY;
}
public function transform(Subject $data): Subject {
if ($this->accepts() !== $data->getKey()) {
throw new \InvalidArgumentException('Invalid subject type');
}
$wooCommerceSegment = $this->segmentRepository->getWooCommerceSegment();
return new Subject(SegmentSubject::KEY, ['segment_id' => $wooCommerceSegment->getId()]);
}
}
@@ -0,0 +1,88 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\SubjectTransformers;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Subject;
use MailPoet\Automation\Engine\Integration\SubjectTransformer;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject;
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Segments;
use MailPoet\Subscribers\SubscribersRepository;
class OrderSubjectToSubscriberSubjectTransformer implements SubjectTransformer {
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var Segments\WooCommerce */
private $woocommerce;
/** @var WooCommerce */
private $woocommerceHelper;
public function __construct(
SubscribersRepository $subscribersRepository,
Segments\WooCommerce $woocommerce,
WooCommerce $woocommerceHelper
) {
$this->subscribersRepository = $subscribersRepository;
$this->woocommerce = $woocommerce;
$this->woocommerceHelper = $woocommerceHelper;
}
public function transform(Subject $data): Subject {
if ($this->accepts() !== $data->getKey()) {
throw new \InvalidArgumentException('Invalid subject type');
}
$subscriber = $this->findOrCreateSubscriber($data);
if (!$subscriber instanceof SubscriberEntity) {
throw new \InvalidArgumentException('Subscriber not found');
}
return new Subject(SubscriberSubject::KEY, ['subscriber_id' => $subscriber->getId()]);
}
public function accepts(): string {
return OrderSubject::KEY;
}
public function returns(): string {
return SubscriberSubject::KEY;
}
private function findOrCreateSubscriber(Subject $order): ?SubscriberEntity {
$subscriber = $this->findSubscriber($order);
if ($subscriber) {
return $subscriber;
}
$orderId = $order->getArgs()['order_id'] ?? null;
if (!$orderId) {
return null;
}
$this->woocommerce->synchronizeGuestCustomer($orderId);
return $this->findSubscriber($order);
}
private function findSubscriber(Subject $order): ?SubscriberEntity {
$orderId = $order->getArgs()['order_id'] ?? null;
if (!$orderId) {
return null;
}
$wcOrder = $this->woocommerceHelper->wcGetOrder($orderId);
if (!$wcOrder instanceof \WC_Order) {
return null;
}
$billingEmail = $wcOrder->get_billing_email();
return $billingEmail ?
$this->subscribersRepository->findOneBy(['email' => $billingEmail]) :
$this->subscribersRepository->findOneBy(['wpUserId' => $wcOrder->get_user_id()]);
}
}
@@ -0,0 +1,43 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\SubjectTransformers;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Subject;
use MailPoet\Automation\Engine\Integration\SubjectTransformer;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
use MailPoet\Automation\Integrations\WordPress\Subjects\UserSubject;
use MailPoet\Subscribers\SubscribersRepository;
class SubscriberSubjectToWordPressUserSubjectTransformer implements SubjectTransformer {
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
SubscribersRepository $subscribersRepository
) {
$this->subscribersRepository = $subscribersRepository;
}
public function accepts(): string {
return SubscriberSubject::KEY;
}
public function returns(): string {
return UserSubject::KEY;
}
public function transform(Subject $data): Subject {
if ($this->accepts() !== $data->getKey()) {
throw new \InvalidArgumentException('Invalid subject type');
}
$subscriber = $this->subscribersRepository->findOneById((int)$data->getArgs()['subscriber_id']);
if (!$subscriber) {
throw new \InvalidArgumentException('Subscriber not found');
}
return new Subject(UserSubject::KEY, ['user_id' => $subscriber->getWpUserId()]);
}
}
@@ -0,0 +1,68 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Subjects;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
use MailPoet\Automation\Engine\Integration\Payload;
use MailPoet\Automation\Engine\Integration\Subject;
use MailPoet\Automation\Integrations\MailPoet\Fields\NewsletterLinkFieldsFactory;
use MailPoet\Automation\Integrations\MailPoet\Payloads\NewsletterLinkPayload;
use MailPoet\Cron\Workers\StatsNotifications\NewsletterLinkRepository;
use MailPoet\NotFoundException;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema\ObjectSchema;
/**
* @implements Subject<NewsletterLinkPayload>
*/
class NewsletterLinkSubject implements Subject {
const KEY = 'mailpoet:email-link';
/** @var NewsletterLinkFieldsFactory */
private $emailLinkFieldsFactory;
/** @var NewsletterLinkRepository */
private $newsletterLinkRepository;
public function __construct(
NewsletterLinkFieldsFactory $emailLinkFieldsFactory,
NewsletterLinkRepository $newsletterLinkRepository
) {
$this->emailLinkFieldsFactory = $emailLinkFieldsFactory;
$this->newsletterLinkRepository = $newsletterLinkRepository;
}
public function getKey(): string {
return self::KEY;
}
public function getName(): string {
// translators: automation subject (entity entering automation) title
return __('Email link', 'mailpoet');
}
public function getArgsSchema(): ObjectSchema {
return Builder::object([
'link_id' => Builder::integer()->minimum(1)->required(),
]);
}
public function getFields(): array {
return $this->emailLinkFieldsFactory->getFields();
}
public function getPayload(SubjectData $subjectData): Payload {
$linkId = $subjectData->getArgs()['link_id'];
$linkEntity = $this->newsletterLinkRepository->findOneById($linkId);
if (!$linkEntity) {
throw NotFoundException::create()->withMessage(sprintf("Email link with ID '%d' not found", $linkId));
}
return new NewsletterLinkPayload($linkEntity);
}
}
@@ -0,0 +1,82 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Subjects;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Field;
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
use MailPoet\Automation\Engine\Integration\Payload;
use MailPoet\Automation\Engine\Integration\Subject;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SegmentPayload;
use MailPoet\NotFoundException;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema\ObjectSchema;
/**
* @implements Subject<SegmentPayload>
*/
class SegmentSubject implements Subject {
const KEY = 'mailpoet:segment';
/** @var SegmentsRepository */
private $segmentsRepository;
public function __construct(
SegmentsRepository $segmentsRepository
) {
$this->segmentsRepository = $segmentsRepository;
}
public function getKey(): string {
return self::KEY;
}
public function getName(): string {
// translators: automation subject (entity entering automation) title
return __('MailPoet segment', 'mailpoet');
}
public function getArgsSchema(): ObjectSchema {
return Builder::object([
'segment_id' => Builder::integer()->required(),
]);
}
public function getPayload(SubjectData $subjectData): Payload {
$id = $subjectData->getArgs()['segment_id'];
$segment = $this->segmentsRepository->findOneById($id);
if (!$segment) {
// translators: %d is the ID.
throw NotFoundException::create()->withMessage(sprintf(__("Segment with ID '%d' not found.", 'mailpoet'), $id));
}
return new SegmentPayload($segment);
}
/** @return Field[] */
public function getFields(): array {
return [
// phpcs:disable Squiz.PHP.CommentedOutCode.Found -- temporarily hide those fields
/*
new Field(
'mailpoet:segment:id',
Field::TYPE_INTEGER,
__('Segment ID', 'mailpoet'),
function (SegmentPayload $payload) {
return $payload->getId();
}
),
new Field(
'mailpoet:segment:name',
Field::TYPE_STRING,
__('Segment name', 'mailpoet'),
function (SegmentPayload $payload) {
return $payload->getName();
}
),
*/
];
}
}
@@ -0,0 +1,68 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Subjects;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Field;
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
use MailPoet\Automation\Engine\Integration\Payload;
use MailPoet\Automation\Engine\Integration\Subject;
use MailPoet\Automation\Integrations\MailPoet\Fields\SubscriberFieldsFactory;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SubscriberPayload;
use MailPoet\NotFoundException;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema\ObjectSchema;
/**
* @implements Subject<SubscriberPayload>
*/
class SubscriberSubject implements Subject {
const KEY = 'mailpoet:subscriber';
/** @var SubscriberFieldsFactory */
private $subscriberFieldsFactory;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
SubscriberFieldsFactory $subscriberFieldsFactory,
SubscribersRepository $subscribersRepository
) {
$this->subscriberFieldsFactory = $subscriberFieldsFactory;
$this->subscribersRepository = $subscribersRepository;
}
public function getKey(): string {
return self::KEY;
}
public function getName(): string {
// translators: automation subject (entity entering automation) title
return __('MailPoet subscriber', 'mailpoet');
}
public function getArgsSchema(): ObjectSchema {
return Builder::object([
'subscriber_id' => Builder::integer()->required(),
]);
}
public function getPayload(SubjectData $subjectData): Payload {
$id = $subjectData->getArgs()['subscriber_id'];
$subscriber = $this->subscribersRepository->findOneById($id);
if (!$subscriber) {
// translators: %d is the ID.
throw NotFoundException::create()->withMessage(sprintf(__("Subscriber with ID '%d' not found.", 'mailpoet'), $id));
}
return new SubscriberPayload($subscriber);
}
/** @return Field[] */
public function getFields(): array {
return $this->subscriberFieldsFactory->getFields();
}
}
@@ -0,0 +1,394 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Templates;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\AutomationTemplate;
use MailPoet\Automation\Engine\Templates\AutomationBuilder;
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
class TemplatesFactory {
/** @var AutomationBuilder */
private $builder;
/** @var WooCommerce */
private $woocommerce;
public function __construct(
AutomationBuilder $builder,
WooCommerce $woocommerce
) {
$this->builder = $builder;
$this->woocommerce = $woocommerce;
}
public function createTemplates(): array {
$templates = [
$this->createSubscriberWelcomeEmailTemplate(),
$this->createUserWelcomeEmailTemplate(),
$this->createSubscriberWelcomeSeriesTemplate(),
$this->createUserWelcomeSeriesTemplate(),
];
if ($this->woocommerce->isWooCommerceActive()) {
$templates[] = $this->createFirstPurchaseTemplate();
$templates[] = $this->createThankLoyalCustomersTemplate();
$templates[] = $this->createWinBackCustomersTemplate();
$templates[] = $this->createAbandonedCartTemplate();
$templates[] = $this->createAbandonedCartCampaignTemplate();
$templates[] = $this->createPurchasedProductTemplate();
$templates[] = $this->createPurchasedProductWithTagTemplate();
$templates[] = $this->createPurchasedInCategoryTemplate();
}
return $templates;
}
private function createSubscriberWelcomeEmailTemplate(): AutomationTemplate {
return new AutomationTemplate(
'subscriber-welcome-email',
'welcome',
__('Welcome new subscribers', 'mailpoet'),
__(
'Send a welcome email when someone subscribes to your list. Optionally, you can choose to send this email after a specified period.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Welcome new subscribers', 'mailpoet'),
[
['key' => 'mailpoet:someone-subscribes'],
['key' => 'core:delay', 'args' => ['delay' => 1, 'delay_type' => 'MINUTES']],
['key' => 'mailpoet:send-email'],
],
[
'mailpoet:run-once-per-subscriber' => true,
]
);
},
[
'automationSteps' => 1,
],
AutomationTemplate::TYPE_DEFAULT
);
}
private function createUserWelcomeEmailTemplate(): AutomationTemplate {
return new AutomationTemplate(
'user-welcome-email',
'welcome',
__('Welcome new WordPress users', 'mailpoet'),
__(
'Send a welcome email when a new WordPress user registers to your website. Optionally, you can choose to send this email after a specified period.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Welcome new WordPress users', 'mailpoet'),
[
['key' => 'mailpoet:wp-user-registered'],
['key' => 'core:delay', 'args' => ['delay' => 1, 'delay_type' => 'MINUTES']],
['key' => 'mailpoet:send-email'],
],
[
'mailpoet:run-once-per-subscriber' => true,
]
);
},
[
'automationSteps' => 1,
],
AutomationTemplate::TYPE_DEFAULT
);
}
private function createSubscriberWelcomeSeriesTemplate(): AutomationTemplate {
return new AutomationTemplate(
'subscriber-welcome-series',
'welcome',
__('Welcome series for new subscribers', 'mailpoet'),
__(
'Welcome new subscribers and start building a relationship with them. Send an email immediately after someone subscribes to your list to introduce your brand and a follow-up two days later to keep the conversation going.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Welcome series for new subscribers', 'mailpoet'),
[]
);
},
[
'automationSteps' => 2,
],
AutomationTemplate::TYPE_PREMIUM
);
}
private function createUserWelcomeSeriesTemplate(): AutomationTemplate {
return new AutomationTemplate(
'user-welcome-series',
'welcome',
__('Welcome series for new WordPress users', 'mailpoet'),
__(
'Welcome new WordPress users to your site. Send an email immediately after a WordPress user registers. Send a follow-up email two days later with more in-depth information.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Welcome series for new WordPress users', 'mailpoet'),
[]
);
},
[
'automationSteps' => 2,
],
AutomationTemplate::TYPE_PREMIUM
);
}
private function createFirstPurchaseTemplate(): AutomationTemplate {
return new AutomationTemplate(
'first-purchase',
'woocommerce',
__('Celebrate first-time buyers', 'mailpoet'),
__(
'Welcome your first-time customers by sending an email with a special offer for their next purchase. Make them feel appreciated within your brand.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Celebrate first-time buyers', 'mailpoet'),
[
[
'key' => 'woocommerce:order-completed',
'filters' => [
'operator' => 'and',
'groups' => [
[
'operator' => 'and',
'filters' => [
['field' => 'woocommerce:order:is-first-order', 'condition' => 'is', 'value' => true],
],
],
],
],
],
[
'key' => 'mailpoet:send-email',
'args' => [
'name' => __('Thank you', 'mailpoet'),
'subject' => __('Thank You for Choosing Us!', 'mailpoet'),
],
],
],
[
'mailpoet:run-once-per-subscriber' => true,
]
);
},
[
'automationSteps' => 1,
],
AutomationTemplate::TYPE_DEFAULT
);
}
private function createThankLoyalCustomersTemplate(): AutomationTemplate {
return new AutomationTemplate(
'thank-loyal-customers',
'woocommerce',
__('Thank loyal customers', 'mailpoet'),
__(
'These are your most important customers. Make them feel special by sending a thank you note for supporting your brand.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Thank loyal customers', 'mailpoet'),
[]
);
},
[
'automationSteps' => 1,
],
AutomationTemplate::TYPE_PREMIUM
);
}
private function createWinBackCustomersTemplate(): AutomationTemplate {
return new AutomationTemplate(
'win-back-customers',
'woocommerce',
__('Win-back customers', 'mailpoet'),
__(
'Rekindle the relationship with past customers by reminding them of their favorite products and showcasing whats new, encouraging a return to your brand.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Win-back customers', 'mailpoet'),
[]
);
},
[
'automationSteps' => 4,
],
AutomationTemplate::TYPE_PREMIUM
);
}
private function createAbandonedCartTemplate(): AutomationTemplate {
return new AutomationTemplate(
'abandoned-cart',
'abandoned-cart',
__('Abandoned cart reminder', 'mailpoet'),
__(
'Nudge your shoppers to complete the purchase after they have added a product to the cart but havent completed the order.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Abandoned cart reminder', 'mailpoet'),
[
['key' => 'woocommerce:abandoned-cart'],
[
'key' => 'mailpoet:send-email',
'args' => [
'name' => __('Abandoned cart', 'mailpoet'),
'subject' => __('Looks like you forgot something', 'mailpoet'),
],
],
]
);
},
[
'automationSteps' => 1,
],
AutomationTemplate::TYPE_DEFAULT
);
}
private function createAbandonedCartCampaignTemplate(): AutomationTemplate {
return new AutomationTemplate(
'abandoned-cart-campaign',
'abandoned-cart',
__('Abandoned cart campaign', 'mailpoet'),
__(
'Encourage your potential customers to finalize their purchase when they have added items to their cart but havent finished the order yet. Offer a coupon code as a last resort to convert them to customers.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Abandoned cart campaign', 'mailpoet'),
[]
);
},
[
'automationSteps' => 5,
],
AutomationTemplate::TYPE_PREMIUM
);
}
private function createPurchasedProductTemplate(): AutomationTemplate {
return new AutomationTemplate(
'purchased-product',
'woocommerce',
__('Purchased a product', 'mailpoet'),
__(
'Share care instructions or simply thank the customer for making an order.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Purchased a product', 'mailpoet'),
[
[
'key' => 'woocommerce:buys-a-product',
],
[
'key' => 'mailpoet:send-email',
'args' => [
'name' => __('Important information about your order', 'mailpoet'),
'subject' => __('Important information about your order', 'mailpoet'),
],
],
]
);
},
[
'automationSteps' => 1,
],
AutomationTemplate::TYPE_DEFAULT
);
}
private function createPurchasedProductWithTagTemplate(): AutomationTemplate {
return new AutomationTemplate(
'purchased-product-with-tag',
'woocommerce',
__('Purchased a product with a tag', 'mailpoet'),
__(
'Share care instructions or simply thank the customer for making an order.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Purchased a product with a tag', 'mailpoet'),
[
[
'key' => 'woocommerce:buys-from-a-tag',
],
[
'key' => 'mailpoet:send-email',
'args' => [
'name' => __('Important information about your order', 'mailpoet'),
'subject' => __('Important information about your order', 'mailpoet'),
],
],
]
);
},
[
'automationSteps' => 1,
],
AutomationTemplate::TYPE_DEFAULT
);
}
private function createPurchasedInCategoryTemplate(): AutomationTemplate {
return new AutomationTemplate(
'purchased-in-category',
'woocommerce',
__('Purchased in a category', 'mailpoet'),
__(
'Share care instructions or simply thank the customer for making an order.',
'mailpoet'
),
function (): Automation {
return $this->builder->createFromSequence(
__('Purchased in a category', 'mailpoet'),
[
[
'key' => 'woocommerce:buys-from-a-category',
],
[
'key' => 'mailpoet:send-email',
'args' => [
'name' => __('Important information about your order', 'mailpoet'),
'subject' => __('Important information about your order', 'mailpoet'),
],
],
]
);
},
[
'automationSteps' => 1,
],
AutomationTemplate::TYPE_DEFAULT
);
}
}
@@ -0,0 +1,96 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Triggers;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Engine\Data\StepValidationArgs;
use MailPoet\Automation\Engine\Data\Subject;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Integration\Trigger;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SegmentPayload;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SegmentSubject;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\InvalidStateException;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema\ObjectSchema;
use MailPoet\WP\Functions as WPFunctions;
class SomeoneSubscribesTrigger implements Trigger {
const KEY = 'mailpoet:someone-subscribes';
/** @var WPFunctions */
private $wp;
/** @var SegmentsRepository */
private $segmentsRepository;
public function __construct(
WPFunctions $wp,
SegmentsRepository $segmentsRepository
) {
$this->wp = $wp;
$this->segmentsRepository = $segmentsRepository;
}
public function getKey(): string {
return 'mailpoet:someone-subscribes';
}
public function getName(): string {
// translators: automation trigger title
return __('Someone subscribes', 'mailpoet');
}
public function getArgsSchema(): ObjectSchema {
return Builder::object([
'segment_ids' => Builder::array(Builder::number()),
]);
}
public function getSubjectKeys(): array {
return [
SubscriberSubject::KEY,
SegmentSubject::KEY,
];
}
public function validate(StepValidationArgs $args): void {
}
public function registerHooks(): void {
$this->wp->addAction('mailpoet_segment_subscribed', [$this, 'handleSubscription'], 10, 2);
}
public function handleSubscription(SubscriberSegmentEntity $subscriberSegment): void {
$segment = $subscriberSegment->getSegment();
$subscriber = $subscriberSegment->getSubscriber();
if (!$segment || !$subscriber) {
throw new InvalidStateException();
}
$this->wp->doAction(Hooks::TRIGGER, $this, [
new Subject(SegmentSubject::KEY, ['segment_id' => $segment->getId()]),
new Subject(SubscriberSubject::KEY, ['subscriber_id' => $subscriber->getId()]),
]);
}
public function isTriggeredBy(StepRunArgs $args): bool {
$segmentId = $args->getSinglePayloadByClass(SegmentPayload::class)->getId();
$segment = $this->segmentsRepository->findOneById($segmentId);
if (!$segment || $segment->getType() !== SegmentEntity::TYPE_DEFAULT) {
return false;
}
// Triggers when no segment IDs defined (= any segment) or the current segment paylo.
$triggerArgs = $args->getStep()->getArgs();
$segmentIds = $triggerArgs['segment_ids'] ?? [];
return !is_array($segmentIds) || !$segmentIds || in_array($segmentId, $segmentIds, true);
}
}
@@ -0,0 +1,105 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Triggers;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Engine\Data\StepValidationArgs;
use MailPoet\Automation\Engine\Data\Subject;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Integration\Trigger;
use MailPoet\Automation\Engine\WordPress;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SegmentPayload;
use MailPoet\Automation\Integrations\MailPoet\Payloads\SubscriberPayload;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SegmentSubject;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\InvalidStateException;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema\ObjectSchema;
class UserRegistrationTrigger implements Trigger {
const KEY = 'mailpoet:wp-user-registered';
/** @var WordPress */
private $wp;
private $subscribersRepository;
public function __construct(
WordPress $wp,
SubscribersRepository $subscribersRepository
) {
$this->wp = $wp;
$this->subscribersRepository = $subscribersRepository;
}
public function getKey(): string {
return self::KEY;
}
public function getName(): string {
// translators: automation trigger title
return __('WordPress user registers', 'mailpoet');
}
public function getArgsSchema(): ObjectSchema {
return Builder::object([
'roles' => Builder::array(Builder::string()),
]);
}
public function getSubjectKeys(): array {
return [
SegmentSubject::KEY,
SubscriberSubject::KEY,
];
}
public function validate(StepValidationArgs $args): void {
}
public function registerHooks(): void {
$this->wp->addAction('mailpoet_segment_subscribed', [$this, 'handleSubscription']);
}
public function handleSubscription(SubscriberSegmentEntity $subscriberSegment): void {
$segment = $subscriberSegment->getSegment();
$subscriber = $subscriberSegment->getSubscriber();
if (!$segment || !$subscriber) {
throw new InvalidStateException();
}
$this->wp->doAction(Hooks::TRIGGER, $this, [
new Subject(SegmentSubject::KEY, ['segment_id' => $segment->getId()]),
new Subject(SubscriberSubject::KEY, ['subscriber_id' => $subscriber->getId()]),
]);
}
public function isTriggeredBy(StepRunArgs $args): bool {
$segmentPayload = $args->getSinglePayloadByClass(SegmentPayload::class);
if ($segmentPayload->getType() !== SegmentEntity::TYPE_WP_USERS) {
return false;
}
$subscriberPayload = $args->getSinglePayloadByClass(SubscriberPayload::class);
$this->subscribersRepository->refresh($subscriberPayload->getSubscriber());
if (!$subscriberPayload->isWPUser()) {
return false;
}
$user = $this->wp->getUserBy('id', (int)$subscriberPayload->getWpUserId());
if (!$user) {
return false;
}
$triggerArgs = $args->getStep()->getArgs();
$roles = $triggerArgs['roles'] ?? [];
return !is_array($roles) || !$roles || count(array_intersect($user->roles, $roles)) > 0;
}
}