init
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\Core\Actions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Control\StepRunController;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Engine\Data\StepValidationArgs;
|
||||
use MailPoet\Automation\Engine\Integration\Action;
|
||||
use MailPoet\Automation\Engine\Integration\ValidationException;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class DelayAction implements Action {
|
||||
public const KEY = 'core:delay';
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation action title
|
||||
return _x('Delay', 'noun', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'delay' => Builder::integer()->required()->minimum(1),
|
||||
'delay_type' => Builder::string()->required()->pattern('^(MINUTES|DAYS|HOURS|WEEKS)$')->default('HOURS'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSubjectKeys(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function validate(StepValidationArgs $args): void {
|
||||
$seconds = $this->calculateSeconds($args->getStep());
|
||||
if ($seconds <= 0) {
|
||||
throw ValidationException::create()
|
||||
->withError('delay', __('A delay must have a positive value', 'mailpoet'));
|
||||
}
|
||||
if ($seconds > 2 * YEAR_IN_SECONDS) {
|
||||
throw ValidationException::create()
|
||||
->withError('delay', __("A delay can't be longer than two years", 'mailpoet'));
|
||||
}
|
||||
}
|
||||
|
||||
public function run(StepRunArgs $args, StepRunController $controller): void {
|
||||
if ($args->isFirstRun()) {
|
||||
$controller->scheduleProgress(time() + self::calculateSeconds($args->getStep()));
|
||||
}
|
||||
}
|
||||
|
||||
public static function calculateSeconds(Step $step): int {
|
||||
$delay = (int)($step->getArgs()['delay'] ?? null);
|
||||
switch ($step->getArgs()['delay_type']) {
|
||||
case "MINUTES":
|
||||
return $delay * MINUTE_IN_SECONDS;
|
||||
case "HOURS":
|
||||
return $delay * HOUR_IN_SECONDS;
|
||||
case "DAYS":
|
||||
return $delay * DAY_IN_SECONDS;
|
||||
case "WEEKS":
|
||||
return $delay * WEEK_IN_SECONDS;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\Core\Actions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Control\FilterHandler;
|
||||
use MailPoet\Automation\Engine\Control\StepRunController;
|
||||
use MailPoet\Automation\Engine\Data\FilterGroup;
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Engine\Data\StepValidationArgs;
|
||||
use MailPoet\Automation\Engine\Integration\Action;
|
||||
use MailPoet\Automation\Engine\Integration\ValidationException;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class IfElseAction implements Action {
|
||||
public const KEY = 'core:if-else';
|
||||
|
||||
/** @var FilterHandler */
|
||||
private $filterHandler;
|
||||
|
||||
public function __construct(
|
||||
FilterHandler $filterHandler
|
||||
) {
|
||||
$this->filterHandler = $filterHandler;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation action title
|
||||
return __('If/Else', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object();
|
||||
}
|
||||
|
||||
public function getSubjectKeys(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function validate(StepValidationArgs $args): void {
|
||||
$step = $args->getStep();
|
||||
|
||||
// validate next steps
|
||||
$nextSteps = $step->getNextSteps();
|
||||
if (count($nextSteps) !== 2) {
|
||||
throw ValidationException::create()->withError(
|
||||
'if_else_next_steps_count',
|
||||
__('If/Else action must have exactly two next steps.', 'mailpoet')
|
||||
);
|
||||
}
|
||||
|
||||
// validate conditions
|
||||
$groups = $step->getFilters() ? $step->getFilters()->getGroups() : [];
|
||||
$conditions = array_map(function (FilterGroup $group) {
|
||||
return $group->getFilters();
|
||||
}, $groups);
|
||||
|
||||
if (count($conditions) === 0) {
|
||||
throw ValidationException::create()->withError(
|
||||
'if_else_conditions_count',
|
||||
__('If/Else action must have at least one condition set.', 'mailpoet')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function run(StepRunArgs $args, StepRunController $controller): void {
|
||||
$matches = $this->filterHandler->matchesFilters($args);
|
||||
$controller->scheduleNextStepByIndex($matches ? 0 : 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\Core;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\Core\Actions\DelayAction;
|
||||
use MailPoet\Automation\Integrations\Core\Actions\IfElseAction;
|
||||
|
||||
class CoreIntegration implements Integration {
|
||||
/** @var DelayAction */
|
||||
private $delayAction;
|
||||
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
/** @var IfElseAction */
|
||||
private $ifElseAction;
|
||||
|
||||
public function __construct(
|
||||
DelayAction $delayAction,
|
||||
IfElseAction $ifElseAction,
|
||||
WordPress $wordPress
|
||||
) {
|
||||
$this->delayAction = $delayAction;
|
||||
$this->ifElseAction = $ifElseAction;
|
||||
$this->wordPress = $wordPress;
|
||||
}
|
||||
|
||||
public function register(Registry $registry): void {
|
||||
$registry->addAction($this->delayAction);
|
||||
$registry->addAction($this->ifElseAction);
|
||||
|
||||
$registry->addFilter(new Filters\BooleanFilter());
|
||||
$registry->addFilter(new Filters\NumberFilter());
|
||||
$registry->addFilter(new Filters\IntegerFilter());
|
||||
$registry->addFilter(new Filters\StringFilter());
|
||||
$registry->addFilter(new Filters\DateTimeFilter($this->wordPress->wpTimezone()));
|
||||
$registry->addFilter(new Filters\EnumFilter());
|
||||
$registry->addFilter(new Filters\EnumArrayFilter());
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\Core\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter as FilterData;
|
||||
use MailPoet\Automation\Engine\Integration\Filter;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class BooleanFilter implements Filter {
|
||||
public const CONDITION_IS = 'is';
|
||||
|
||||
public function getFieldType(): string {
|
||||
return Field::TYPE_BOOLEAN;
|
||||
}
|
||||
|
||||
public function getConditions(): array {
|
||||
return [
|
||||
self::CONDITION_IS => __('is', 'mailpoet'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getArgsSchema(string $condition): ObjectSchema {
|
||||
return Builder::object([
|
||||
'value' => Builder::boolean()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFieldParams(FilterData $data): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function matches(FilterData $data, $value): bool {
|
||||
$filterValue = $data->getArgs()['value'] ?? null;
|
||||
if (!is_bool($value) || !is_bool($filterValue)) {
|
||||
return false;
|
||||
}
|
||||
return $data->getCondition() === self::CONDITION_IS && $value === $filterValue;
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\Core\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter as FilterData;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Integration\Filter;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class DateTimeFilter implements Filter {
|
||||
public const CONDITION_BEFORE = 'before';
|
||||
public const CONDITION_AFTER = 'after';
|
||||
public const CONDITION_ON = 'on';
|
||||
public const CONDITION_NOT_ON = 'not-on';
|
||||
public const CONDITION_IN_THE_LAST = 'in-the-last';
|
||||
public const CONDITION_NOT_IN_THE_LAST = 'not-in-the-last';
|
||||
public const CONDITION_IS_SET = 'is-set';
|
||||
public const CONDITION_IS_NOT_SET = 'is-not-set';
|
||||
public const CONDITION_ON_THE_DAYS_OF_THE_WEEK = 'on-the-days-of-the-week';
|
||||
|
||||
public const FORMAT_DATETIME = 'Y-m-d\TH:i:s';
|
||||
public const FORMAT_DATE = 'Y-m-d';
|
||||
|
||||
public const REGEX_DATETIME = '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$';
|
||||
public const REGEX_DATE = '^\d{4}-\d{2}-\d{2}$';
|
||||
|
||||
/** @var DateTimeZone */
|
||||
private $localTimezone;
|
||||
|
||||
public function __construct(
|
||||
DateTimeZone $localTimezone
|
||||
) {
|
||||
$this->localTimezone = $localTimezone;
|
||||
}
|
||||
|
||||
public function getFieldType(): string {
|
||||
return Field::TYPE_DATETIME;
|
||||
}
|
||||
|
||||
public function getConditions(): array {
|
||||
return [
|
||||
self::CONDITION_BEFORE => __('before', 'mailpoet'),
|
||||
self::CONDITION_AFTER => __('after', 'mailpoet'),
|
||||
self::CONDITION_ON => __('on', 'mailpoet'),
|
||||
self::CONDITION_NOT_ON => __('not on', 'mailpoet'),
|
||||
self::CONDITION_IN_THE_LAST => __('in the last', 'mailpoet'),
|
||||
self::CONDITION_NOT_IN_THE_LAST => __('not in the last', 'mailpoet'),
|
||||
self::CONDITION_IS_SET => __('is set', 'mailpoet'),
|
||||
self::CONDITION_IS_NOT_SET => __('is not set', 'mailpoet'),
|
||||
self::CONDITION_ON_THE_DAYS_OF_THE_WEEK => __('on the day(s) of the week', 'mailpoet'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getArgsSchema(string $condition): ObjectSchema {
|
||||
switch ($condition) {
|
||||
case self::CONDITION_BEFORE:
|
||||
case self::CONDITION_AFTER:
|
||||
return Builder::object([
|
||||
'value' => Builder::string()->pattern(self::REGEX_DATETIME)->required(),
|
||||
]);
|
||||
case self::CONDITION_ON:
|
||||
case self::CONDITION_NOT_ON:
|
||||
return Builder::object([
|
||||
'value' => Builder::string()->pattern(self::REGEX_DATE)->required(),
|
||||
]);
|
||||
case self::CONDITION_IN_THE_LAST:
|
||||
case self::CONDITION_NOT_IN_THE_LAST:
|
||||
return Builder::object([
|
||||
'value' => Builder::object([
|
||||
'number' => Builder::integer()->minimum(1)->required(),
|
||||
'unit' => Builder::string()->pattern('^days|weeks|months$')->required(),
|
||||
])->required(),
|
||||
]);
|
||||
case self::CONDITION_IS_SET:
|
||||
case self::CONDITION_IS_NOT_SET:
|
||||
return Builder::object([]);
|
||||
case self::CONDITION_ON_THE_DAYS_OF_THE_WEEK:
|
||||
return Builder::object([
|
||||
'value' => Builder::array(Builder::integer()->minimum(0)->maximum(6))->minItems(1)->required(),
|
||||
]);
|
||||
default:
|
||||
throw new InvalidStateException();
|
||||
}
|
||||
}
|
||||
|
||||
public function getFieldParams(FilterData $data): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function matches(FilterData $data, $value): bool {
|
||||
$filterValue = $data->getArgs()['value'] ?? null;
|
||||
$condition = $data->getCondition();
|
||||
|
||||
// is set/is not set
|
||||
if (in_array($condition, [self::CONDITION_IS_SET, self::CONDITION_IS_NOT_SET], true)) {
|
||||
return $this->matchesSet($condition, $value);
|
||||
}
|
||||
|
||||
// in the last/not in the last
|
||||
if (in_array($condition, [self::CONDITION_IN_THE_LAST, self::CONDITION_NOT_IN_THE_LAST], true)) {
|
||||
return $this->matchesInTheLast($condition, $filterValue, $value);
|
||||
}
|
||||
|
||||
// on the day(s) of the week
|
||||
if ($condition === self::CONDITION_ON_THE_DAYS_OF_THE_WEEK) {
|
||||
return $this->matchesOnTheDaysOfTheWeek($filterValue, $value);
|
||||
}
|
||||
|
||||
// other conditions
|
||||
if (!is_string($filterValue) || !$value instanceof DateTimeInterface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$datetime = $this->convertToLocalTimezone($value);
|
||||
switch ($condition) {
|
||||
case 'before':
|
||||
$ref = DateTimeImmutable::createFromFormat(self::FORMAT_DATETIME, $filterValue, $this->localTimezone);
|
||||
return $ref && $datetime < $ref;
|
||||
case 'after':
|
||||
$ref = DateTimeImmutable::createFromFormat(self::FORMAT_DATETIME, $filterValue, $this->localTimezone);
|
||||
return $ref && $datetime > $ref;
|
||||
case 'on':
|
||||
return $datetime->format(self::FORMAT_DATE) === $filterValue;
|
||||
case 'not-on':
|
||||
return $datetime->format(self::FORMAT_DATE) !== $filterValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed $value */
|
||||
private function matchesSet(string $condition, $value): bool {
|
||||
switch ($condition) {
|
||||
case self::CONDITION_IS_SET:
|
||||
return $value !== null;
|
||||
case self::CONDITION_IS_NOT_SET:
|
||||
return $value === null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $filterValue
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function matchesInTheLast(string $condition, $filterValue, $value): bool {
|
||||
if (!is_array($filterValue) || !isset($filterValue['number']) || !isset($filterValue['unit']) || !$value instanceof DateTimeInterface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$number = $filterValue['number'];
|
||||
$unit = $filterValue['unit'];
|
||||
if (!is_integer($number) || !in_array($unit, ['days', 'weeks', 'months'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$now = new DateTimeImmutable('now', $this->localTimezone);
|
||||
$ref = $now->modify("-$number $unit");
|
||||
$matches = $ref <= $value && $value <= $now;
|
||||
return $condition === self::CONDITION_IN_THE_LAST ? $matches : !$matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $filterValue
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function matchesOnTheDaysOfTheWeek($filterValue, $value): bool {
|
||||
if (!is_array($filterValue) || !$value instanceof DateTimeInterface) {
|
||||
return false;
|
||||
}
|
||||
foreach ($filterValue as $day) {
|
||||
if (!is_integer($day) || $day < 0 || $day > 6) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$date = $this->convertToLocalTimezone($value);
|
||||
$day = (int)$date->format('w');
|
||||
return in_array($day, $filterValue, true);
|
||||
}
|
||||
|
||||
private function convertToLocalTimezone(DateTimeInterface $datetime): DateTimeImmutable {
|
||||
$value = DateTimeImmutable::createFromFormat('U', (string)$datetime->getTimestamp(), $this->localTimezone);
|
||||
if (!$value) {
|
||||
throw new InvalidStateException('Failed to convert datetime to WP timezone');
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\Core\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter as FilterData;
|
||||
use MailPoet\Automation\Engine\Integration\Filter;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class EnumArrayFilter implements Filter {
|
||||
public const CONDITION_MATCHES_ANY_OF = 'matches-any-of';
|
||||
public const CONDITION_MATCHES_ALL_OF = 'matches-all-of';
|
||||
public const CONDITION_MATCHES_NONE_OF = 'matches-none-of';
|
||||
|
||||
public function getFieldType(): string {
|
||||
return Field::TYPE_ENUM_ARRAY;
|
||||
}
|
||||
|
||||
public function getConditions(): array {
|
||||
return [
|
||||
self::CONDITION_MATCHES_ANY_OF => __('matches any of', 'mailpoet'),
|
||||
self::CONDITION_MATCHES_ALL_OF => __('matches all of', 'mailpoet'),
|
||||
self::CONDITION_MATCHES_NONE_OF => __('matches none of', 'mailpoet'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getArgsSchema(string $condition): ObjectSchema {
|
||||
$paramsSchema = Builder::object([
|
||||
'in_the_last' => Builder::object([
|
||||
'number' => Builder::integer()->required()->minimum(1),
|
||||
'unit' => Builder::string()->required()->pattern('^(days)$')->default('days'),
|
||||
]),
|
||||
]);
|
||||
|
||||
return Builder::object([
|
||||
'value' => Builder::oneOf([
|
||||
Builder::array(Builder::string())->minItems(1),
|
||||
Builder::array(Builder::integer())->minItems(1),
|
||||
])->required(),
|
||||
'params' => $paramsSchema,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFieldParams(FilterData $data): array {
|
||||
$paramData = $data->getArgs()['params'] ?? [];
|
||||
$params = [];
|
||||
|
||||
$inTheLastUnit = $paramData['in_the_last']['unit'] ?? null;
|
||||
$inTheLastNumber = $paramData['in_the_last']['number'] ?? null;
|
||||
if ($inTheLastUnit === 'days' && $inTheLastNumber !== null) {
|
||||
$params['in_the_last'] = $inTheLastNumber * DAY_IN_SECONDS;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function matches(FilterData $data, $value): bool {
|
||||
$filterValue = $data->getArgs()['value'] ?? null;
|
||||
if (!is_array($value) || !is_array($filterValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filterValue = array_unique($filterValue, SORT_REGULAR);
|
||||
$value = array_unique($value, SORT_REGULAR);
|
||||
|
||||
$filterCount = count($filterValue);
|
||||
$matchedCount = count(array_intersect($value, $filterValue));
|
||||
switch ($data->getCondition()) {
|
||||
case self::CONDITION_MATCHES_ANY_OF:
|
||||
return $filterCount > 0 && $matchedCount > 0;
|
||||
case self::CONDITION_MATCHES_ALL_OF:
|
||||
return $filterCount > 0 && $matchedCount === count($filterValue);
|
||||
case self::CONDITION_MATCHES_NONE_OF:
|
||||
return $matchedCount === 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\Core\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter as FilterData;
|
||||
use MailPoet\Automation\Engine\Integration\Filter;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class EnumFilter implements Filter {
|
||||
public const IS_ANY_OF = 'is-any-of';
|
||||
public const IS_NONE_OF = 'is-none-of';
|
||||
|
||||
public function getFieldType(): string {
|
||||
return Field::TYPE_ENUM;
|
||||
}
|
||||
|
||||
public function getConditions(): array {
|
||||
return [
|
||||
self::IS_ANY_OF => __('is any of', 'mailpoet'),
|
||||
self::IS_NONE_OF => __('is none of', 'mailpoet'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getArgsSchema(string $condition): ObjectSchema {
|
||||
$paramsSchema = Builder::object([
|
||||
'in_the_last' => Builder::object([
|
||||
'number' => Builder::integer()->required()->minimum(1),
|
||||
'unit' => Builder::string()->required()->pattern('^(days)$')->default('days'),
|
||||
]),
|
||||
]);
|
||||
|
||||
return Builder::object([
|
||||
'value' => Builder::oneOf([
|
||||
Builder::array(Builder::string())->minItems(1),
|
||||
Builder::array(Builder::integer())->minItems(1),
|
||||
])->required(),
|
||||
'params' => $paramsSchema,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFieldParams(FilterData $data): array {
|
||||
$paramData = $data->getArgs()['params'] ?? [];
|
||||
$params = [];
|
||||
|
||||
$inTheLastUnit = $paramData['in_the_last']['unit'] ?? null;
|
||||
$inTheLastNumber = $paramData['in_the_last']['number'] ?? null;
|
||||
if ($inTheLastUnit === 'days' && $inTheLastNumber !== null) {
|
||||
$params['in_the_last'] = $inTheLastNumber * DAY_IN_SECONDS;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function matches(FilterData $data, $value): bool {
|
||||
$filterValue = $data->getArgs()['value'] ?? null;
|
||||
if (!is_scalar($value) || !is_array($filterValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filterValue = array_unique($filterValue, SORT_REGULAR);
|
||||
switch ($data->getCondition()) {
|
||||
case self::IS_ANY_OF:
|
||||
return in_array($value, $filterValue, true);
|
||||
case self::IS_NONE_OF:
|
||||
return !in_array($value, $filterValue, true);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\Core\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter as FilterData;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class IntegerFilter extends NumberFilter {
|
||||
public function getFieldType(): string {
|
||||
return Field::TYPE_INTEGER;
|
||||
}
|
||||
|
||||
public function getArgsSchema(string $condition): ObjectSchema {
|
||||
$paramsSchema = Builder::object([
|
||||
'in_the_last' => Builder::object([
|
||||
'number' => Builder::integer()->required()->minimum(1),
|
||||
'unit' => Builder::string()->required()->pattern('^(days)$')->default('days'),
|
||||
]),
|
||||
]);
|
||||
|
||||
switch ($condition) {
|
||||
case self::CONDITION_BETWEEN:
|
||||
case self::CONDITION_NOT_BETWEEN:
|
||||
return Builder::object([
|
||||
'value' => Builder::array(Builder::integer())->minItems(2)->maxItems(2)->required(),
|
||||
'params' => $paramsSchema,
|
||||
]);
|
||||
case self::CONDITION_IS_SET:
|
||||
case self::CONDITION_IS_NOT_SET:
|
||||
return Builder::object([
|
||||
'params' => $paramsSchema,
|
||||
]);
|
||||
default:
|
||||
return Builder::object([
|
||||
'value' => Builder::integer()->required(),
|
||||
'params' => $paramsSchema,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getFieldParams(FilterData $data): array {
|
||||
$paramData = $data->getArgs()['params'] ?? [];
|
||||
$params = [];
|
||||
|
||||
$inTheLastUnit = $paramData['in_the_last']['unit'] ?? null;
|
||||
$inTheLastNumber = $paramData['in_the_last']['number'] ?? null;
|
||||
if ($inTheLastUnit === 'days' && $inTheLastNumber !== null) {
|
||||
$params['in_the_last'] = $inTheLastNumber * DAY_IN_SECONDS;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function matches(FilterData $data, $value): bool {
|
||||
$matches = parent::matches($data, $value);
|
||||
if (!$matches) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($value) && !$this->isWholeNumber($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filterValue = $data->getArgs()['value'] ?? null;
|
||||
if (is_array($filterValue)) {
|
||||
foreach ($filterValue as $filterValueItem) {
|
||||
if (!$this->isWholeNumber($filterValueItem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isset($filterValue) && !$this->isWholeNumber($filterValue)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @param mixed $value */
|
||||
private function isWholeNumber($value): bool {
|
||||
return is_int($value) || (is_float($value) && $value === floor($value));
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\Core\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter as FilterData;
|
||||
use MailPoet\Automation\Engine\Integration\Filter;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class NumberFilter implements Filter {
|
||||
public const CONDITION_EQUALS = 'equals';
|
||||
public const CONDITION_NOT_EQUAL = 'not-equal';
|
||||
public const CONDITION_GREATER_THAN = 'greater-than';
|
||||
public const CONDITION_LESS_THAN = 'less-than';
|
||||
public const CONDITION_BETWEEN = 'between';
|
||||
public const CONDITION_NOT_BETWEEN = 'not-between';
|
||||
public const CONDITION_IS_MULTIPLE_OF = 'is-multiple-of';
|
||||
public const CONDITION_IS_NOT_MULTIPLE_OF = 'is-not-multiple-of';
|
||||
public const CONDITION_IS_SET = 'is-set';
|
||||
public const CONDITION_IS_NOT_SET = 'is-not-set';
|
||||
|
||||
public function getFieldType(): string {
|
||||
return Field::TYPE_NUMBER;
|
||||
}
|
||||
|
||||
public function getConditions(): array {
|
||||
return [
|
||||
self::CONDITION_EQUALS => __('equals', 'mailpoet'),
|
||||
self::CONDITION_NOT_EQUAL => __('not equal', 'mailpoet'),
|
||||
self::CONDITION_GREATER_THAN => __('greater than', 'mailpoet'),
|
||||
self::CONDITION_LESS_THAN => __('less than', 'mailpoet'),
|
||||
self::CONDITION_BETWEEN => __('between', 'mailpoet'),
|
||||
self::CONDITION_NOT_BETWEEN => __('not between', 'mailpoet'),
|
||||
self::CONDITION_IS_MULTIPLE_OF => __('is multiple of', 'mailpoet'),
|
||||
self::CONDITION_IS_NOT_MULTIPLE_OF => __('is not multiple of', 'mailpoet'),
|
||||
self::CONDITION_IS_SET => __('is set', 'mailpoet'),
|
||||
self::CONDITION_IS_NOT_SET => __('is not set', 'mailpoet'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getArgsSchema(string $condition): ObjectSchema {
|
||||
$paramsSchema = Builder::object([
|
||||
'in_the_last' => Builder::object([
|
||||
'number' => Builder::integer()->required()->minimum(1),
|
||||
'unit' => Builder::string()->required()->pattern('^(days)$')->default('days'),
|
||||
]),
|
||||
]);
|
||||
|
||||
switch ($condition) {
|
||||
case self::CONDITION_BETWEEN:
|
||||
case self::CONDITION_NOT_BETWEEN:
|
||||
return Builder::object([
|
||||
'value' => Builder::array(Builder::number())->minItems(2)->maxItems(2)->required(),
|
||||
'params' => $paramsSchema,
|
||||
]);
|
||||
case self::CONDITION_IS_SET:
|
||||
case self::CONDITION_IS_NOT_SET:
|
||||
return Builder::object([
|
||||
'params' => $paramsSchema,
|
||||
]);
|
||||
default:
|
||||
return Builder::object([
|
||||
'value' => Builder::number()->required(),
|
||||
'params' => $paramsSchema,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getFieldParams(FilterData $data): array {
|
||||
$paramData = $data->getArgs()['params'] ?? [];
|
||||
$params = [];
|
||||
|
||||
$inTheLastUnit = $paramData['in_the_last']['unit'] ?? null;
|
||||
$inTheLastNumber = $paramData['in_the_last']['number'] ?? null;
|
||||
if ($inTheLastUnit === 'days' && $inTheLastNumber !== null) {
|
||||
$params['in_the_last'] = $inTheLastNumber * DAY_IN_SECONDS;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $value
|
||||
*/
|
||||
public function matches(FilterData $data, $value): bool {
|
||||
$filterValue = $data->getArgs()['value'] ?? null;
|
||||
$condition = $data->getCondition();
|
||||
|
||||
// is between/not between
|
||||
if (in_array($condition, [self::CONDITION_BETWEEN, self::CONDITION_NOT_BETWEEN], true)) {
|
||||
return $this->matchesBetween($condition, $value, $filterValue);
|
||||
}
|
||||
|
||||
// is set/is not set
|
||||
if (in_array($condition, [self::CONDITION_IS_SET, self::CONDITION_IS_NOT_SET], true)) {
|
||||
return $this->matchesSet($condition, $value);
|
||||
}
|
||||
|
||||
if (!$this->isNumber($value) || !$this->isNumber($filterValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value = floatval($value);
|
||||
$filterValue = floatval($filterValue);
|
||||
|
||||
switch ($condition) {
|
||||
case self::CONDITION_EQUALS:
|
||||
return $value === $filterValue;
|
||||
case self::CONDITION_NOT_EQUAL:
|
||||
return $value !== $filterValue;
|
||||
case self::CONDITION_GREATER_THAN:
|
||||
return $value > $filterValue;
|
||||
case self::CONDITION_LESS_THAN:
|
||||
return $value < $filterValue;
|
||||
case self::CONDITION_IS_MULTIPLE_OF:
|
||||
return fmod($value, $filterValue) === 0.0;
|
||||
case self::CONDITION_IS_NOT_MULTIPLE_OF:
|
||||
return fmod($value, $filterValue) !== 0.0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float|null $value
|
||||
* @param mixed $filterValue
|
||||
*/
|
||||
private function matchesBetween(string $condition, $value, $filterValue): bool {
|
||||
if (!is_array($filterValue) || count($filterValue) !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isNumber($filterValue[0]) || !$this->isNumber($filterValue[1]) || $filterValue[0] >= $filterValue[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isNumber($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var float $value */
|
||||
$value = floatval($value);
|
||||
$from = floatval($filterValue[0]);
|
||||
$to = floatval($filterValue[1]);
|
||||
|
||||
switch ($condition) {
|
||||
case self::CONDITION_BETWEEN:
|
||||
return $value > $from && $value < $to;
|
||||
case self::CONDITION_NOT_BETWEEN:
|
||||
return $value <= $from || $value >= $to;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed $value */
|
||||
private function matchesSet(string $condition, $value): bool {
|
||||
switch ($condition) {
|
||||
case self::CONDITION_IS_SET:
|
||||
return $value !== null;
|
||||
case self::CONDITION_IS_NOT_SET:
|
||||
return $value === null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed $value */
|
||||
private function isNumber($value): bool {
|
||||
return is_integer($value) || is_float($value);
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\Core\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter as FilterData;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Integration\Filter;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class StringFilter implements Filter {
|
||||
public const CONDITION_CONTAINS = 'contains';
|
||||
public const CONDITION_DOES_NOT_CONTAIN = 'does-not-contain';
|
||||
public const CONDITION_IS = 'is';
|
||||
public const CONDITION_IS_NOT = 'is-not';
|
||||
public const CONDITION_STARTS_WITH = 'starts-with';
|
||||
public const CONDITION_ENDS_WITH = 'ends-with';
|
||||
public const CONDITION_IS_BLANK = 'is-blank';
|
||||
public const CONDITION_IS_NOT_BLANK = 'is-not-blank';
|
||||
public const CONDITION_MATCHES_REGEX = 'matches-regex';
|
||||
|
||||
public function getFieldType(): string {
|
||||
return Field::TYPE_STRING;
|
||||
}
|
||||
|
||||
public function getConditions(): array {
|
||||
return [
|
||||
self::CONDITION_IS => __('is', 'mailpoet'),
|
||||
self::CONDITION_IS_NOT => __('is not', 'mailpoet'),
|
||||
self::CONDITION_CONTAINS => __('contains', 'mailpoet'),
|
||||
self::CONDITION_DOES_NOT_CONTAIN => __('does not contain', 'mailpoet'),
|
||||
self::CONDITION_STARTS_WITH => __('starts with', 'mailpoet'),
|
||||
self::CONDITION_ENDS_WITH => __('ends with', 'mailpoet'),
|
||||
self::CONDITION_IS_BLANK => __('is blank', 'mailpoet'),
|
||||
self::CONDITION_IS_NOT_BLANK => __('is not blank', 'mailpoet'),
|
||||
self::CONDITION_MATCHES_REGEX => __('matches regex', 'mailpoet'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getArgsSchema(string $condition): ObjectSchema {
|
||||
switch ($condition) {
|
||||
case self::CONDITION_IS_BLANK:
|
||||
case self::CONDITION_IS_NOT_BLANK:
|
||||
return Builder::object([]);
|
||||
default:
|
||||
return Builder::object(['value' => Builder::string()->required()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getFieldParams(FilterData $data): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function matches(FilterData $data, $value): bool {
|
||||
$filterValue = $data->getArgs()['value'] ?? null;
|
||||
if (!is_string($value) || !is_string($filterValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// match regex as it is
|
||||
$condition = $data->getCondition();
|
||||
if ($condition === self::CONDITION_MATCHES_REGEX) {
|
||||
return $this->matchesRegex($filterValue, $value);
|
||||
}
|
||||
|
||||
// match all other conditions case insensitively
|
||||
$value = mb_strtolower($value);
|
||||
$filterValue = mb_strtolower($filterValue);
|
||||
|
||||
switch ($data->getCondition()) {
|
||||
case self::CONDITION_IS:
|
||||
return $value === $filterValue;
|
||||
case self::CONDITION_IS_NOT:
|
||||
return $value !== $filterValue;
|
||||
case self::CONDITION_CONTAINS:
|
||||
return str_contains($value, $filterValue);
|
||||
case self::CONDITION_DOES_NOT_CONTAIN:
|
||||
return !str_contains($value, $filterValue);
|
||||
case self::CONDITION_STARTS_WITH:
|
||||
return str_starts_with($value, $filterValue);
|
||||
case self::CONDITION_ENDS_WITH:
|
||||
return str_ends_with($value, $filterValue);
|
||||
case self::CONDITION_IS_BLANK:
|
||||
return strlen($value) === 0;
|
||||
case self::CONDITION_IS_NOT_BLANK:
|
||||
return strlen($value) > 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function matchesRegex(string $regex, string $value): bool {
|
||||
// add '/' delimiters, if missing
|
||||
if (!@preg_match('#^/.*/[a-z]*$#ui', $regex)) {
|
||||
$regex = '/' . str_replace('/', '\\/', $regex) . '/u';
|
||||
}
|
||||
|
||||
// add unicode flag, if not present
|
||||
if (!@preg_match('#/.*u.*$#ui', $regex)) {
|
||||
$regex .= 'u';
|
||||
}
|
||||
|
||||
if (@preg_match($regex, '') === false) {
|
||||
throw new InvalidStateException("Invalid regular expression: '$regex'");
|
||||
}
|
||||
|
||||
return @preg_match($regex, $value) === 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+515
@@ -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 @@
|
||||
<?php
|
||||
+31
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
+101
@@ -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]);
|
||||
}
|
||||
}
|
||||
+117
@@ -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;
|
||||
}
|
||||
}
|
||||
+94
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+124
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+51
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+151
@@ -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(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
+116
@@ -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(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+40
@@ -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();
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
+77
@@ -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);
|
||||
}
|
||||
}
|
||||
+172
@@ -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;
|
||||
}
|
||||
}
|
||||
+259
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
+81
@@ -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 @@
|
||||
<?php
|
||||
+94
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
@@ -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 @@
|
||||
<?php
|
||||
+145
@@ -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();
|
||||
}
|
||||
}
|
||||
+30
@@ -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;
|
||||
}
|
||||
}
|
||||
+37
@@ -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();
|
||||
}
|
||||
}
|
||||
+49
@@ -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 @@
|
||||
<?php
|
||||
+67
@@ -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;
|
||||
}
|
||||
}
|
||||
+42
@@ -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()]);
|
||||
}
|
||||
}
|
||||
+88
@@ -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()]);
|
||||
}
|
||||
}
|
||||
+43
@@ -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()]);
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+68
@@ -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);
|
||||
}
|
||||
}
|
||||
+82
@@ -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();
|
||||
}
|
||||
),
|
||||
*/
|
||||
];
|
||||
}
|
||||
}
|
||||
+68
@@ -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 @@
|
||||
<?php
|
||||
+394
@@ -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 what’s 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 haven’t 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 haven’t 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 @@
|
||||
<?php
|
||||
+96
@@ -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);
|
||||
}
|
||||
}
|
||||
+105
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class ContextFactory {
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $woocommerce;
|
||||
|
||||
public function __construct(
|
||||
WooCommerce $woocommerce
|
||||
) {
|
||||
$this->woocommerce = $woocommerce;
|
||||
}
|
||||
|
||||
/** @return mixed[] */
|
||||
public function getContextData(): array {
|
||||
|
||||
if (!$this->woocommerce->isWooCommerceActive()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$context = [
|
||||
'order_statuses' => $this->woocommerce->wcGetOrderStatuses(),
|
||||
'review_ratings_enabled' => $this->woocommerce->wcReviewRatingsEnabled(),
|
||||
];
|
||||
return $context;
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
||||
|
||||
class CustomerFieldsFactory {
|
||||
/** @var CustomerOrderFieldsFactory */
|
||||
private $customerOrderFieldsFactory;
|
||||
|
||||
/** @var CustomerReviewFieldsFactory */
|
||||
private $customerReviewFieldsFactory;
|
||||
|
||||
public function __construct(
|
||||
CustomerOrderFieldsFactory $customerOrderFieldsFactory,
|
||||
CustomerReviewFieldsFactory $customerReviewFieldsFactory
|
||||
) {
|
||||
$this->customerOrderFieldsFactory = $customerOrderFieldsFactory;
|
||||
$this->customerReviewFieldsFactory = $customerReviewFieldsFactory;
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return array_merge(
|
||||
[
|
||||
new Field(
|
||||
'woocommerce:customer:billing-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing company', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingCompany();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing phone', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingPhone();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing city', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingCity();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing postcode', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingPostcode();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing state/county', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingState();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-country',
|
||||
Field::TYPE_ENUM,
|
||||
__('Billing country', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getBillingCountry();
|
||||
},
|
||||
[
|
||||
'options' => $this->getBillingCountryOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping company', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingCompany();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping phone', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingPhone();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping city', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingCity();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping postcode', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingPostcode();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping state/county', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingState();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-country',
|
||||
Field::TYPE_ENUM,
|
||||
__('Shipping country', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
return $payload->getShippingCountry();
|
||||
},
|
||||
[
|
||||
'options' => $this->getShippingCountryOptions(),
|
||||
]
|
||||
),
|
||||
],
|
||||
$this->customerOrderFieldsFactory->getFields(),
|
||||
$this->customerReviewFieldsFactory->getFields()
|
||||
);
|
||||
}
|
||||
|
||||
private function getBillingCountryOptions(): array {
|
||||
$options = [];
|
||||
foreach (WC()->countries->get_allowed_countries() as $code => $name) {
|
||||
$options[] = ['id' => $code, 'name' => $name];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getShippingCountryOptions(): array {
|
||||
$options = [];
|
||||
foreach (WC()->countries->get_shipping_countries() as $code => $name) {
|
||||
$options[] = ['id' => $code, 'name' => $name];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
||||
use WC_Customer;
|
||||
use WC_Order;
|
||||
use WC_Order_Item_Product;
|
||||
use WC_Product;
|
||||
|
||||
class CustomerOrderFieldsFactory {
|
||||
/** @var WooCommerce */
|
||||
private $wooCommerce;
|
||||
|
||||
/** @var TermOptionsBuilder */
|
||||
private $termOptionsBuilder;
|
||||
|
||||
/** @var TermParentsLoader */
|
||||
private $termParentsLoader;
|
||||
|
||||
public function __construct(
|
||||
WooCommerce $wooCommerce,
|
||||
TermOptionsBuilder $termOptionsBuilder,
|
||||
TermParentsLoader $termParentsLoader
|
||||
) {
|
||||
$this->wooCommerce = $wooCommerce;
|
||||
$this->termOptionsBuilder = $termOptionsBuilder;
|
||||
$this->termParentsLoader = $termParentsLoader;
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return [
|
||||
new Field(
|
||||
'woocommerce:customer:spent-total',
|
||||
Field::TYPE_NUMBER,
|
||||
__('Total spent', 'mailpoet'),
|
||||
function (CustomerPayload $payload, array $params = []) {
|
||||
$customer = $payload->getCustomer();
|
||||
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
||||
if (!$customer) {
|
||||
$order = $payload->getOrder();
|
||||
return $order && $this->isInTheLastSeconds($order, $inTheLastSeconds) ? $payload->getTotalSpent() : 0.0;
|
||||
}
|
||||
return $inTheLastSeconds === null
|
||||
? $payload->getTotalSpent()
|
||||
: $this->getRecentSpentTotal($customer, $inTheLastSeconds);
|
||||
},
|
||||
[
|
||||
'params' => ['in_the_last'],
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:spent-average',
|
||||
Field::TYPE_NUMBER,
|
||||
__('Average spent', 'mailpoet'),
|
||||
function (CustomerPayload $payload, array $params = []) {
|
||||
$customer = $payload->getCustomer();
|
||||
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
||||
|
||||
if (!$customer) {
|
||||
$order = $payload->getOrder();
|
||||
return $order && $this->isInTheLastSeconds($order, $inTheLastSeconds) ? $payload->getAverageSpent() : 0.0;
|
||||
}
|
||||
|
||||
if ($inTheLastSeconds === null) {
|
||||
return $payload->getAverageSpent();
|
||||
} else {
|
||||
$totalSpent = $this->getRecentSpentTotal($customer, $inTheLastSeconds);
|
||||
$orderCount = $this->getRecentOrderCount($customer, $inTheLastSeconds);
|
||||
return $orderCount > 0 ? ($totalSpent / $orderCount) : 0.0;
|
||||
}
|
||||
},
|
||||
[
|
||||
'params' => ['in_the_last'],
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:order-count',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Order count', 'mailpoet'),
|
||||
function (CustomerPayload $payload, array $params = []) {
|
||||
$customer = $payload->getCustomer();
|
||||
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
||||
if (!$customer) {
|
||||
$order = $payload->getOrder();
|
||||
return $order && $this->isInTheLastSeconds($order, $inTheLastSeconds) ? $payload->getOrderCount() : 0;
|
||||
}
|
||||
return $inTheLastSeconds === null
|
||||
? $payload->getOrderCount()
|
||||
: $this->getRecentOrderCount($customer, $inTheLastSeconds);
|
||||
},
|
||||
[
|
||||
'params' => ['in_the_last'],
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:first-paid-order-date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('First paid order date', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
if (!$customer) {
|
||||
$order = $payload->getOrder();
|
||||
return $order && $order->is_paid() ? $order->get_date_created() : null;
|
||||
}
|
||||
return $this->getPaidOrderDate($customer, true);
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:last-paid-order-date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Last paid order date', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
if (!$customer) {
|
||||
$order = $payload->getOrder();
|
||||
return $order && $order->is_paid() ? $order->get_date_created() : null;
|
||||
}
|
||||
return $this->getPaidOrderDate($customer, false);
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:purchased-categories',
|
||||
Field::TYPE_ENUM_ARRAY,
|
||||
__('Purchased categories', 'mailpoet'),
|
||||
function (CustomerPayload $payload, array $params = []) {
|
||||
$customer = $payload->getCustomer();
|
||||
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
||||
if (!$customer) {
|
||||
$order = $payload->getOrder();
|
||||
$items = $order && $order->is_paid() && $this->isInTheLastSeconds($order, $inTheLastSeconds) ? $order->get_items() : [];
|
||||
$ids = [];
|
||||
foreach ($items as $item) {
|
||||
$product = $item instanceof WC_Order_Item_Product ? $item->get_product() : null;
|
||||
$ids = array_merge($ids, $product instanceof WC_Product ? $product->get_category_ids() : []);
|
||||
}
|
||||
$ids = array_unique($ids);
|
||||
} else {
|
||||
$ids = $this->getOrderProductTermIds($customer, 'product_cat', $inTheLastSeconds);
|
||||
}
|
||||
$ids = array_merge($ids, $this->termParentsLoader->getParentIds($ids));
|
||||
sort($ids);
|
||||
return $ids;
|
||||
},
|
||||
[
|
||||
'options' => $this->termOptionsBuilder->getTermOptions('product_cat'),
|
||||
'params' => ['in_the_last'],
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:purchased-tags',
|
||||
Field::TYPE_ENUM_ARRAY,
|
||||
__('Purchased tags', 'mailpoet'),
|
||||
function (CustomerPayload $payload, array $params = []) {
|
||||
$customer = $payload->getCustomer();
|
||||
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
||||
if (!$customer) {
|
||||
$order = $payload->getOrder();
|
||||
$items = $order && $order->is_paid() && $this->isInTheLastSeconds($order, $inTheLastSeconds) ? $order->get_items() : [];
|
||||
$ids = [];
|
||||
foreach ($items as $item) {
|
||||
$product = $item instanceof WC_Order_Item_Product ? $item->get_product() : null;
|
||||
$ids = array_merge($ids, $product instanceof WC_Product ? $product->get_tag_ids() : []);
|
||||
}
|
||||
$ids = array_unique($ids);
|
||||
} else {
|
||||
$ids = $this->getOrderProductTermIds($customer, 'product_tag', $inTheLastSeconds);
|
||||
}
|
||||
sort($ids);
|
||||
return $ids;
|
||||
},
|
||||
[
|
||||
'options' => $this->termOptionsBuilder->getTermOptions('product_tag'),
|
||||
'params' => ['in_the_last'],
|
||||
]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private function getRecentSpentTotal(WC_Customer $customer, int $inTheLastSeconds): float {
|
||||
global $wpdb;
|
||||
$statuses = array_map(function (string $status) {
|
||||
return "wc-$status";
|
||||
}, $this->wooCommerce->wcGetIsPaidStatuses());
|
||||
|
||||
if ($this->wooCommerce->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
return (float)$wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT SUM(o.total_amount)
|
||||
FROM %i o
|
||||
WHERE o.customer_id = %d
|
||||
AND o.status IN (' . implode(',', array_fill(0, count($statuses), '%s')) . ')
|
||||
AND o.date_created_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)
|
||||
',
|
||||
array_merge(
|
||||
[
|
||||
$wpdb->prefix . 'wc_orders',
|
||||
$customer->get_id(),
|
||||
],
|
||||
$statuses,
|
||||
[$inTheLastSeconds]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (float)$wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT SUM(pm_total.meta_value)
|
||||
FROM {$wpdb->posts} p
|
||||
LEFT JOIN {$wpdb->postmeta} pm_user ON p.ID = pm_user.post_id AND pm_user.meta_key = '_customer_user'
|
||||
LEFT JOIN {$wpdb->postmeta} pm_total ON p.ID = pm_total.post_id AND pm_total.meta_key = '_order_total'
|
||||
WHERE p.post_type = 'shop_order'
|
||||
AND p.post_status IN (" . implode(',', array_fill(0, count($statuses), '%s')) . ")
|
||||
AND pm_user.meta_value = %d
|
||||
AND p.post_date_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)
|
||||
",
|
||||
array_merge(
|
||||
$statuses,
|
||||
[$customer->get_id(), $inTheLastSeconds]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function getRecentOrderCount(WC_Customer $customer, int $inTheLastSeconds): int {
|
||||
global $wpdb;
|
||||
$statuses = array_keys($this->wooCommerce->wcGetOrderStatuses());
|
||||
|
||||
if ($this->wooCommerce->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
return (int)$wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT COUNT(o.id)
|
||||
FROM %i o
|
||||
WHERE o.customer_id = %d
|
||||
AND o.status IN (' . implode(',', array_fill(0, count($statuses), '%s')) . ')
|
||||
AND o.date_created_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)
|
||||
',
|
||||
array_merge(
|
||||
[
|
||||
$wpdb->prefix . 'wc_orders',
|
||||
$customer->get_id(),
|
||||
],
|
||||
$statuses,
|
||||
[$inTheLastSeconds]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (int)$wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(p.ID)
|
||||
FROM {$wpdb->posts} p
|
||||
LEFT JOIN {$wpdb->postmeta} pm_user ON p.ID = pm_user.post_id AND pm_user.meta_key = '_customer_user'
|
||||
WHERE p.post_type = 'shop_order'
|
||||
AND p.post_status IN (" . implode(',', array_fill(0, count($statuses), '%s')) . ")
|
||||
AND pm_user.meta_value = %d
|
||||
AND p.post_date_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)
|
||||
",
|
||||
array_merge(
|
||||
$statuses,
|
||||
[
|
||||
$customer->get_id(),
|
||||
$inTheLastSeconds,
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function getPaidOrderDate(WC_Customer $customer, bool $fetchFirst): ?DateTimeImmutable {
|
||||
global $wpdb;
|
||||
$sorting = $fetchFirst ? 'ASC' : 'DESC';
|
||||
$statuses = array_map(function (string $status) {
|
||||
return "wc-$status";
|
||||
}, $this->wooCommerce->wcGetIsPaidStatuses());
|
||||
|
||||
if ($this->wooCommerce->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$date = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
'
|
||||
SELECT o.date_created_gmt
|
||||
FROM %i o
|
||||
WHERE o.customer_id = %d
|
||||
AND o.status IN (' . implode(',', array_fill(0, count($statuses), '%s')) . ')
|
||||
AND o.total_amount > 0
|
||||
ORDER BY o.date_created_gmt ' . $sorting /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The argument is safe. */ . '
|
||||
LIMIT 1
|
||||
',
|
||||
array_merge(
|
||||
[
|
||||
$wpdb->prefix . 'wc_orders',
|
||||
$customer->get_id(),
|
||||
],
|
||||
$statuses
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$date = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT p.post_date_gmt
|
||||
FROM {$wpdb->prefix}posts p
|
||||
LEFT JOIN {$wpdb->prefix}postmeta pm_total ON p.ID = pm_total.post_id AND pm_total.meta_key = '_order_total'
|
||||
LEFT JOIN {$wpdb->prefix}postmeta pm_user ON p.ID = pm_user.post_id AND pm_user.meta_key = '_customer_user'
|
||||
WHERE p.post_type = 'shop_order'
|
||||
AND p.post_status IN (" . implode(',', array_fill(0, count($statuses), '%s')) . ")
|
||||
AND pm_user.meta_value = %d
|
||||
AND pm_total.meta_value > 0
|
||||
ORDER BY p.post_date_gmt " . $sorting /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The argument is safe. */ . "
|
||||
LIMIT 1
|
||||
",
|
||||
array_merge(
|
||||
$statuses,
|
||||
[$customer->get_id()]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $date ? new DateTimeImmutable($date, new DateTimeZone('GMT')) : null;
|
||||
}
|
||||
|
||||
private function getOrderProductTermIds(WC_Customer $customer, string $taxonomy, int $inTheLastSeconds = null): array {
|
||||
global $wpdb;
|
||||
|
||||
$statuses = array_map(function (string $status) {
|
||||
return "wc-$status";
|
||||
}, $this->wooCommerce->wcGetIsPaidStatuses());
|
||||
$statusesPlaceholder = implode(',', array_fill(0, count($statuses), '%s'));
|
||||
|
||||
// get all product categories that the customer has purchased
|
||||
if ($this->wooCommerce->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$inTheLastFilter = isset($inTheLastSeconds) ? 'AND o.date_created_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)' : '';
|
||||
|
||||
$orderIdsSubquery = "
|
||||
SELECT o.id
|
||||
FROM %i o
|
||||
WHERE o.status IN ($statusesPlaceholder)
|
||||
AND o.customer_id = %d
|
||||
$inTheLastFilter
|
||||
";
|
||||
$orderIdsSubqueryArgs = array_merge(
|
||||
[$wpdb->prefix . 'wc_orders'],
|
||||
$statuses,
|
||||
[$customer->get_id()],
|
||||
$inTheLastSeconds ? [$inTheLastSeconds] : []
|
||||
);
|
||||
} else {
|
||||
$inTheLastFilter = isset($inTheLastSeconds) ? 'AND p.post_date_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)' : '';
|
||||
|
||||
$orderIdsSubquery = "
|
||||
SELECT p.ID
|
||||
FROM {$wpdb->posts} p
|
||||
LEFT JOIN {$wpdb->postmeta} pm_user ON p.ID = pm_user.post_id AND pm_user.meta_key = '_customer_user'
|
||||
WHERE p.post_type = 'shop_order'
|
||||
AND p.post_status IN ($statusesPlaceholder)
|
||||
AND pm_user.meta_value = %d
|
||||
$inTheLastFilter
|
||||
";
|
||||
$orderIdsSubqueryArgs = array_merge(
|
||||
$statuses,
|
||||
[$customer->get_id()],
|
||||
$inTheLastSeconds ? [$inTheLastSeconds] : []
|
||||
);
|
||||
}
|
||||
|
||||
$result = $wpdb->get_col(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The number of replacements is dynamic.
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT DISTINCT tt.term_id
|
||||
FROM {$wpdb->term_taxonomy} tt
|
||||
JOIN %i AS oi ON oi.order_id IN (" . $orderIdsSubquery . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The subquery uses placeholders. */ ") AND oi.order_item_type = 'line_item'
|
||||
JOIN %i AS pid ON oi.order_item_id = pid.order_item_id AND pid.meta_key = '_product_id'
|
||||
JOIN {$wpdb->posts} p ON pid.meta_value = p.ID
|
||||
JOIN {$wpdb->term_relationships} tr ON IF(p.post_type = 'product_variation', p.post_parent, p.ID) = tr.object_id AND tr.term_taxonomy_id = tt.term_taxonomy_id
|
||||
WHERE tt.taxonomy = %s
|
||||
ORDER BY tt.term_id ASC
|
||||
",
|
||||
array_merge(
|
||||
[$wpdb->prefix . 'woocommerce_order_items'],
|
||||
$orderIdsSubqueryArgs,
|
||||
[
|
||||
$wpdb->prefix . 'woocommerce_order_itemmeta',
|
||||
(string)($taxonomy),
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
return array_map('intval', $result);
|
||||
}
|
||||
|
||||
private function isInTheLastSeconds(WC_Order $order, ?int $inTheLastSeconds): bool {
|
||||
if ($inTheLastSeconds === null) {
|
||||
return true;
|
||||
}
|
||||
return $order->get_date_created() >= new DateTimeImmutable("-$inTheLastSeconds seconds");
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
||||
use WC_Customer;
|
||||
|
||||
class CustomerReviewFieldsFactory {
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wordPress
|
||||
) {
|
||||
$this->wordPress = $wordPress;
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return [
|
||||
new Field(
|
||||
'woocommerce:customer:review-count',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Review count', 'mailpoet'),
|
||||
function (CustomerPayload $payload, array $params = []) {
|
||||
$customer = $payload->getCustomer();
|
||||
if (!$customer) {
|
||||
return 0;
|
||||
}
|
||||
$inTheLastSeconds = isset($params['in_the_last']) ? (int)$params['in_the_last'] : null;
|
||||
return $this->getUniqueProductReviewCount($customer, $inTheLastSeconds);
|
||||
},
|
||||
[
|
||||
'params' => ['in_the_last'],
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:last-review-date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Last review date', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $this->getLastReviewDate($customer) : null;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the customer's review count excluding multiple reviews on the same product.
|
||||
* Inspired by AutomateWoo implementation.
|
||||
*/
|
||||
private function getUniqueProductReviewCount(WC_Customer $customer, int $inTheLastSeconds = null): int {
|
||||
global $wpdb;
|
||||
|
||||
$inTheLastFilter = isset($inTheLastSeconds) ? 'AND c.comment_date_gmt >= DATE_SUB(current_timestamp, INTERVAL %d SECOND)' : '';
|
||||
|
||||
return (int)$wpdb->get_var(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- The number of replacements is dynamic.
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(DISTINCT c.comment_post_ID) FROM {$wpdb->comments} c
|
||||
JOIN {$wpdb->posts} p ON c.comment_post_ID = p.ID
|
||||
WHERE p.post_type = 'product'
|
||||
AND c.comment_parent = 0
|
||||
AND c.comment_approved = 1
|
||||
AND c.comment_type = 'review'
|
||||
AND (c.user_ID = %d OR c.comment_author_email = %s)
|
||||
" . $inTheLastFilter . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The condition uses placeholders. */ "
|
||||
",
|
||||
array_merge(
|
||||
[
|
||||
$customer->get_id(),
|
||||
$customer->get_email(),
|
||||
],
|
||||
$inTheLastSeconds ? [$inTheLastSeconds] : []
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function getLastReviewDate(WC_Customer $customer): ?DateTimeImmutable {
|
||||
global $wpdb;
|
||||
|
||||
$date = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT c.comment_date
|
||||
FROM {$wpdb->comments} c
|
||||
JOIN {$wpdb->posts} p ON c.comment_post_ID = p.ID
|
||||
WHERE p.post_type = 'product'
|
||||
AND c.comment_parent = 0
|
||||
AND c.comment_approved = 1
|
||||
AND c.comment_type = 'review'
|
||||
AND (c.user_ID = %d OR c.comment_author_email = %s)
|
||||
ORDER BY c.comment_date DESC
|
||||
LIMIT 1
|
||||
",
|
||||
[$customer->get_id(), $customer->get_email()]
|
||||
)
|
||||
);
|
||||
return $date ? new DateTimeImmutable($date, $this->wordPress->wpTimezone()) : null;
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
||||
use WC_Order;
|
||||
use WC_Order_Item_Product;
|
||||
use WC_Payment_Gateway;
|
||||
use WC_Product;
|
||||
use WP_Post;
|
||||
|
||||
class OrderFieldsFactory {
|
||||
/** @var TermOptionsBuilder */
|
||||
private $termOptionsBuilder;
|
||||
|
||||
/** @var TermParentsLoader */
|
||||
private $termParentsLoader;
|
||||
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $wooCommerce;
|
||||
|
||||
public function __construct(
|
||||
TermOptionsBuilder $termOptionsBuilder,
|
||||
TermParentsLoader $termParentsLoader,
|
||||
WordPress $wordPress,
|
||||
WooCommerce $wooCommerce
|
||||
) {
|
||||
$this->termOptionsBuilder = $termOptionsBuilder;
|
||||
$this->termParentsLoader = $termParentsLoader;
|
||||
$this->wordPress = $wordPress;
|
||||
$this->wooCommerce = $wooCommerce;
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return array_merge(
|
||||
[
|
||||
new Field(
|
||||
'woocommerce:order:billing-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing company', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_company();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:billing-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing phone', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_phone();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:billing-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing city', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_city();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:billing-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing postcode', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_postcode();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:billing-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing state/county', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_state();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:billing-country',
|
||||
Field::TYPE_ENUM,
|
||||
__('Billing country', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_billing_country();
|
||||
},
|
||||
[
|
||||
'options' => $this->getBillingCountryOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping company', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_company();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping phone', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_phone();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping city', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_city();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping postcode', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_postcode();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping state/county', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_state();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:shipping-country',
|
||||
Field::TYPE_ENUM,
|
||||
__('Shipping country', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_shipping_country();
|
||||
},
|
||||
[
|
||||
'options' => $this->getShippingCountryOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:created-date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Created date', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_date_created();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:paid-date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Paid date', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_date_paid();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:customer-note',
|
||||
Field::TYPE_STRING,
|
||||
__('Customer provided note', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_customer_note();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:payment-method',
|
||||
Field::TYPE_ENUM,
|
||||
__('Payment method', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_payment_method();
|
||||
},
|
||||
[
|
||||
'options' => $this->getOrderPaymentOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:status',
|
||||
Field::TYPE_ENUM,
|
||||
__('Status', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_status();
|
||||
},
|
||||
[
|
||||
'options' => $this->getOrderStatusOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:total',
|
||||
Field::TYPE_NUMBER,
|
||||
__('Total', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return (float)$payload->getOrder()->get_total();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:coupons',
|
||||
Field::TYPE_ENUM_ARRAY,
|
||||
__('Used coupons', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
return $payload->getOrder()->get_coupon_codes();
|
||||
},
|
||||
[
|
||||
'options' => $this->getCouponOptions(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:is-first-order',
|
||||
Field::TYPE_BOOLEAN,
|
||||
__('Is first order', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
$order = $payload->getOrder();
|
||||
return !$this->previousOrderExists($order);
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:categories',
|
||||
Field::TYPE_ENUM_ARRAY,
|
||||
__('Categories', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
$products = $this->getProducts($payload->getOrder());
|
||||
$categoryIds = [];
|
||||
foreach ($products as $product) {
|
||||
$categoryIds = array_merge($categoryIds, $product->get_category_ids());
|
||||
}
|
||||
$categoryIds = array_merge($categoryIds, $this->termParentsLoader->getParentIds($categoryIds));
|
||||
sort($categoryIds);
|
||||
return array_unique($categoryIds);
|
||||
},
|
||||
[
|
||||
'options' => $this->termOptionsBuilder->getTermOptions('product_cat'),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:tags',
|
||||
Field::TYPE_ENUM_ARRAY,
|
||||
__('Tags', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
$products = $this->getProducts($payload->getOrder());
|
||||
$tagIds = [];
|
||||
foreach ($products as $product) {
|
||||
$tagIds = array_merge($tagIds, $product->get_tag_ids());
|
||||
}
|
||||
sort($tagIds);
|
||||
return array_unique($tagIds);
|
||||
},
|
||||
[
|
||||
'options' => $this->termOptionsBuilder->getTermOptions('product_tag'),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:order:products',
|
||||
Field::TYPE_ENUM_ARRAY,
|
||||
__('Products', 'mailpoet'),
|
||||
function (OrderPayload $payload) {
|
||||
$products = $this->getProducts($payload->getOrder());
|
||||
return array_map(function (WC_Product $product) {
|
||||
return $product->get_id();
|
||||
}, $products);
|
||||
},
|
||||
[
|
||||
'options' => $this->getProductOptions(),
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getBillingCountryOptions(): array {
|
||||
$options = [];
|
||||
foreach (WC()->countries->get_allowed_countries() as $code => $name) {
|
||||
$options[] = ['id' => $code, 'name' => $name];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getShippingCountryOptions(): array {
|
||||
$options = [];
|
||||
foreach (WC()->countries->get_shipping_countries() as $code => $name) {
|
||||
$options[] = ['id' => $code, 'name' => $name];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getOrderPaymentOptions(): array {
|
||||
$gateways = WC()->payment_gateways()->get_available_payment_gateways();
|
||||
$options = [];
|
||||
foreach ($gateways as $gateway) {
|
||||
if ($gateway instanceof WC_Payment_Gateway && $gateway->enabled === 'yes') {
|
||||
$options[] = ['id' => $gateway->id, 'name' => $gateway->title];
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getOrderStatusOptions(): array {
|
||||
$statuses = $this->wooCommerce->wcGetOrderStatuses();
|
||||
$options = [];
|
||||
foreach ($statuses as $id => $name) {
|
||||
$options[] = [
|
||||
// WooCommerce order statuses are internally saved with 'wc-' prefix:
|
||||
// https://github.com/woocommerce/woocommerce/blob/9c58f198/plugins/woocommerce/includes/wc-order-functions.php#L98-L109
|
||||
// However, when getting the status from the order object, it doesn't have the prefix.
|
||||
// To make the status codes consistent, we remove the prefix here and only work with unprefixed statuses.
|
||||
'id' => substr($id, 0, 3) === 'wc-' ? substr($id, 3) : $id,
|
||||
'name' => $name,
|
||||
];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getCouponOptions(): array {
|
||||
$coupons = $this->wordPress->getPosts([
|
||||
'post_type' => 'shop_coupon',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'name',
|
||||
'order' => 'asc',
|
||||
]);
|
||||
|
||||
$options = [];
|
||||
foreach ($coupons as $coupon) {
|
||||
if ($coupon instanceof WP_Post) {
|
||||
// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$options[] = ['id' => $coupon->post_title, 'name' => $coupon->post_title];
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function previousOrderExists(WC_Order $order): bool {
|
||||
$dateCreated = $order->get_date_created() ?? new DateTimeImmutable('now', $this->wordPress->wpTimezone());
|
||||
$query = [
|
||||
'date_created' => '<=' . $dateCreated->getTimestamp(),
|
||||
'limit' => 2,
|
||||
'return' => 'ids',
|
||||
];
|
||||
|
||||
if ($order->get_customer_id() > 0) {
|
||||
$query['customer_id'] = $order->get_customer_id();
|
||||
} else {
|
||||
$query['billing_email'] = $order->get_billing_email();
|
||||
}
|
||||
|
||||
$orderIds = (array)$this->wooCommerce->wcGetOrders($query);
|
||||
return count($orderIds) > 1 && min($orderIds) < $order->get_id();
|
||||
}
|
||||
|
||||
/** @return WC_Product[] */
|
||||
private function getProducts(WC_Order $order): array {
|
||||
$products = [];
|
||||
foreach ($order->get_items() as $item) {
|
||||
if (!$item instanceof WC_Order_Item_Product) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$product = $item->get_product();
|
||||
if (!$product instanceof WC_Product) {
|
||||
continue;
|
||||
}
|
||||
if (!$product->is_type('variation')) {
|
||||
$products[] = $product;
|
||||
continue;
|
||||
}
|
||||
|
||||
$parentProduct = $this->wooCommerce->wcGetProduct($product->get_parent_id());
|
||||
if (!$parentProduct instanceof WC_Product) {
|
||||
continue;
|
||||
}
|
||||
$products[] = $parentProduct;
|
||||
}
|
||||
return array_unique($products);
|
||||
}
|
||||
|
||||
private function getProductOptions(): array {
|
||||
global $wpdb;
|
||||
$products = $wpdb->get_results(
|
||||
"
|
||||
SELECT ID, post_title
|
||||
FROM {$wpdb->posts}
|
||||
WHERE post_type = 'product'
|
||||
AND post_status = 'publish'
|
||||
ORDER BY post_title ASC
|
||||
",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map(function ($product) {
|
||||
/** @var array{ID:int, post_title:string} $product */
|
||||
$id = $product['ID'];
|
||||
$title = $product['post_title'];
|
||||
return ['id' => (int)$id, 'name' => "$title (#$id)"];
|
||||
}, (array)$products);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use WP_Error;
|
||||
use WP_Term;
|
||||
|
||||
class TermOptionsBuilder {
|
||||
/** @var WordPress */
|
||||
private $wordPress;
|
||||
|
||||
/** @var array<string, array<array{id: int, name: string}>> */
|
||||
private $cache = [];
|
||||
|
||||
public function __construct(
|
||||
WordPress $wordPress
|
||||
) {
|
||||
$this->wordPress = $wordPress;
|
||||
}
|
||||
|
||||
/** @return array<array{id: int, name: string}> */
|
||||
public function getTermOptions(string $taxonomy): array {
|
||||
if (!isset($this->cache[$taxonomy])) {
|
||||
$this->cache[$taxonomy] = $this->fetchTermOptions($taxonomy);
|
||||
}
|
||||
return $this->cache[$taxonomy];
|
||||
}
|
||||
|
||||
public function resetCache(): void {
|
||||
$this->cache = [];
|
||||
}
|
||||
|
||||
/** @return array<array{id: int, name: string}> */
|
||||
private function fetchTermOptions(string $taxonomy): array {
|
||||
/** @var WP_Term[]|WP_Error $terms */
|
||||
$terms = $this->wordPress->getTerms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'orderby' => 'name']);
|
||||
if ($terms instanceof WP_Error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$termsMap = [];
|
||||
foreach ($terms as $term) {
|
||||
$termsMap[$term->parent][] = $term;
|
||||
}
|
||||
return $this->buildTermsList($termsMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<WP_Term>> $termsMap
|
||||
* @return array<array{id: int, name: string}>
|
||||
*/
|
||||
private function buildTermsList(array $termsMap, int $parentId = 0): array {
|
||||
$list = [];
|
||||
foreach ($termsMap[$parentId] ?? [] as $term) {
|
||||
$id = $term->term_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$list[] = ['id' => $id, 'name' => $term->name];
|
||||
if (isset($termsMap[$id])) {
|
||||
foreach ($this->buildTermsList($termsMap, $id) as $child) {
|
||||
$list[] = ['id' => $child['id'], 'name' => "$term->name | {$child['name']}"];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class TermParentsLoader {
|
||||
/**
|
||||
* @param int[] $termIds
|
||||
* @return int[]
|
||||
*/
|
||||
public function getParentIds(array $termIds): array {
|
||||
global $wpdb;
|
||||
if (count($termIds) === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT DISTINCT tt.parent
|
||||
FROM {$wpdb->term_taxonomy} AS tt
|
||||
WHERE tt.parent != 0
|
||||
AND tt.term_id IN (" . implode(',', array_fill(0, count($termIds), '%s')) . ")
|
||||
",
|
||||
$termIds
|
||||
)
|
||||
);
|
||||
$parentIds = array_map('intval', $result);
|
||||
if (count($parentIds) === 0) {
|
||||
return [];
|
||||
}
|
||||
return array_values(
|
||||
array_unique(
|
||||
array_merge($parentIds, $this->getParentIds($parentIds))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
|
||||
class AbandonedCartPayload implements Payload {
|
||||
|
||||
/** @var \WC_Customer */
|
||||
private $customer;
|
||||
|
||||
/** @var \DateTimeImmutable */
|
||||
private $lastActivityAt;
|
||||
|
||||
/** @var int[] */
|
||||
private $productIds;
|
||||
|
||||
/**
|
||||
* @param \WC_Customer $customer
|
||||
* @param \DateTimeImmutable $lastActivityAt
|
||||
* @param int[] $productIds
|
||||
*/
|
||||
public function __construct(
|
||||
\WC_Customer $customer,
|
||||
\DateTimeImmutable $lastActivityAt,
|
||||
array $productIds
|
||||
) {
|
||||
|
||||
$this->customer = $customer;
|
||||
$this->lastActivityAt = $lastActivityAt;
|
||||
$this->productIds = $productIds;
|
||||
}
|
||||
|
||||
public function getLastActivityAt(): \DateTimeImmutable {
|
||||
return $this->lastActivityAt;
|
||||
}
|
||||
|
||||
public function getCustomer(): \WC_Customer {
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getProductIds(): array {
|
||||
return $this->productIds;
|
||||
}
|
||||
|
||||
public function getTotal(): float {
|
||||
$total = 0.0;
|
||||
foreach ($this->productIds as $productId) {
|
||||
$product = wc_get_product($productId);
|
||||
if ($product) {
|
||||
$total += (float)$product->get_price();
|
||||
}
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use WC_Customer;
|
||||
use WC_Order;
|
||||
|
||||
class CustomerPayload implements Payload {
|
||||
private ?WC_Customer $customer;
|
||||
private ?WC_Order $order;
|
||||
|
||||
public function __construct(
|
||||
WC_Customer $customer = null,
|
||||
WC_Order $order = null
|
||||
) {
|
||||
$this->customer = $customer;
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
public function getBillingCompany(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_company() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_company();
|
||||
}
|
||||
|
||||
public function getBillingPhone(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_phone() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_phone();
|
||||
}
|
||||
|
||||
public function getBillingCity(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_city() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_city();
|
||||
}
|
||||
|
||||
public function getBillingPostcode(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_postcode() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_postcode();
|
||||
}
|
||||
|
||||
public function getBillingState(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_state() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_state();
|
||||
}
|
||||
|
||||
public function getBillingCountry(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_billing_country() : null;
|
||||
}
|
||||
return (string)$this->customer->get_billing_country();
|
||||
}
|
||||
|
||||
public function getShippingCompany(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_company() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_company();
|
||||
}
|
||||
|
||||
public function getShippingPhone(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_phone() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_phone();
|
||||
}
|
||||
|
||||
public function getShippingCity(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_city() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_city();
|
||||
}
|
||||
|
||||
public function getShippingPostcode(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_postcode() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_postcode();
|
||||
}
|
||||
|
||||
public function getShippingState(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_state() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_state();
|
||||
}
|
||||
|
||||
public function getShippingCountry(): ?string {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? (string)$this->order->get_shipping_country() : null;
|
||||
}
|
||||
return (string)$this->customer->get_shipping_country();
|
||||
}
|
||||
|
||||
public function getTotalSpent(): float {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order && $this->order->is_paid() ? (float)$this->order->get_total() : 0.0;
|
||||
}
|
||||
return (float)$this->customer->get_total_spent();
|
||||
}
|
||||
|
||||
public function getAverageSpent(): float {
|
||||
$totalSpent = $this->getTotalSpent();
|
||||
$orderCount = $this->getOrderCount();
|
||||
return $orderCount > 0 ? ($totalSpent / $orderCount) : 0.0;
|
||||
}
|
||||
|
||||
public function getOrderCount(): int {
|
||||
if ($this->isGuest()) {
|
||||
return $this->order ? 1 : 0;
|
||||
}
|
||||
return (int)$this->customer->get_order_count();
|
||||
}
|
||||
|
||||
public function getCustomer(): ?WC_Customer {
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function getOrder(): ?WC_Order {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->customer ? $this->customer->get_id() : 0;
|
||||
}
|
||||
|
||||
/** @phpstan-assert-if-true null $this->customer */
|
||||
public function isGuest(): bool {
|
||||
return $this->customer === null;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
|
||||
class OrderPayload implements Payload {
|
||||
|
||||
/** @var \WC_Order */
|
||||
private $order;
|
||||
|
||||
public function __construct(
|
||||
\WC_Order $order
|
||||
) {
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
public function getOrder(): \WC_Order {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
public function getEmail(): string {
|
||||
return $this->order->get_billing_email();
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->order->get_id();
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
|
||||
class OrderStatusChangePayload implements Payload {
|
||||
|
||||
/** @var string */
|
||||
private $from;
|
||||
|
||||
/** @var string */
|
||||
private $to;
|
||||
|
||||
public function __construct(
|
||||
string $from,
|
||||
string $to
|
||||
) {
|
||||
$this->from = $from;
|
||||
$this->to = $to;
|
||||
}
|
||||
|
||||
public function getFrom(): string {
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
public function getTo(): string {
|
||||
return $this->to;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\SubjectTransformers;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Subject;
|
||||
use MailPoet\Automation\Engine\Integration\SubjectTransformer;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject;
|
||||
use MailPoet\Automation\Integrations\WordPress\Subjects\UserSubject;
|
||||
|
||||
class WordPressUserSubjectToWooCommerceCustomerSubjectTransformer implements SubjectTransformer {
|
||||
public function accepts(): string {
|
||||
return UserSubject::KEY;
|
||||
}
|
||||
|
||||
public function returns(): string {
|
||||
return CustomerSubject::KEY;
|
||||
}
|
||||
|
||||
public function transform(Subject $data): Subject {
|
||||
if ($this->accepts() !== $data->getKey()) {
|
||||
throw new \InvalidArgumentException('Invalid subject type');
|
||||
}
|
||||
return new Subject(CustomerSubject::KEY, ['customer_id' => $data->getArgs()['user_id']]);
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Subjects;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Subject as SubjectData;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\AbandonedCartPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
/**
|
||||
* @implements Subject<AbandonedCartPayload>
|
||||
*/
|
||||
class AbandonedCartSubject implements Subject {
|
||||
const KEY = 'woocommerce:abandoned_cart';
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $woocommerceHelper;
|
||||
|
||||
public function __construct(
|
||||
WooCommerce $woocommerceHelper
|
||||
) {
|
||||
$this->woocommerceHelper = $woocommerceHelper;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation subject (entity entering automation) title
|
||||
return __('WooCommerce abandoned cart', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'user_id' => Builder::integer()->required(),
|
||||
'last_activity_at' => Builder::string()->required()->default(30),
|
||||
'product_ids' => Builder::array(Builder::integer())->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPayload(SubjectData $subjectData): Payload {
|
||||
if (!$this->woocommerceHelper->isWooCommerceActive()) {
|
||||
throw InvalidStateException::create()->withMessage('WooCommerce is not active');
|
||||
}
|
||||
$lastActivityAt = \DateTimeImmutable::createFromFormat(\DateTime::W3C, $subjectData->getArgs()['last_activity_at']);
|
||||
if (!$lastActivityAt) {
|
||||
throw InvalidStateException::create()->withMessage('Invalid abandoned cart time');
|
||||
}
|
||||
|
||||
$customer = new \WC_Customer($subjectData->getArgs()['user_id']);
|
||||
|
||||
return new AbandonedCartPayload($customer, $lastActivityAt, $subjectData->getArgs()['product_ids']);
|
||||
}
|
||||
|
||||
public function getFields(): array {
|
||||
return [
|
||||
new Field(
|
||||
'woocommerce:cart:cart-total',
|
||||
Field::TYPE_NUMBER,
|
||||
__('Cart total', 'mailpoet'),
|
||||
function (AbandonedCartPayload $payload) {
|
||||
return $payload->getTotal();
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\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\WooCommerce\Fields\CustomerFieldsFactory;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
||||
use MailPoet\NotFoundException;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
use WC_Customer;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* @implements Subject<CustomerPayload>
|
||||
*/
|
||||
class CustomerSubject implements Subject {
|
||||
const KEY = 'woocommerce:customer';
|
||||
|
||||
/** @var CustomerFieldsFactory */
|
||||
private $customerFieldsFactory;
|
||||
|
||||
public function __construct(
|
||||
CustomerFieldsFactory $customerFieldsFactory
|
||||
) {
|
||||
$this->customerFieldsFactory = $customerFieldsFactory;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation subject (entity entering automation) title
|
||||
return __('WooCommerce customer', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'customer_id' => Builder::integer()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPayload(SubjectData $subjectData): Payload {
|
||||
$args = $subjectData->getArgs();
|
||||
$customerId = isset($args['customer_id']) ? (int)$args['customer_id'] : null;
|
||||
$orderId = isset($args['order_id']) ? (int)$args['order_id'] : null;
|
||||
|
||||
$order = $orderId === null ? null : wc_get_order($orderId);
|
||||
$order = $order instanceof WC_Order ? $order : null;
|
||||
|
||||
if (!$customerId) {
|
||||
return new CustomerPayload(null, $order);
|
||||
}
|
||||
|
||||
$customer = new WC_Customer($customerId);
|
||||
if (!$customer->get_id()) {
|
||||
// translators: %d is the ID of the customer.
|
||||
throw NotFoundException::create()->withMessage(sprintf(__("Customer with ID '%d' not found.", 'mailpoet'), $customerId));
|
||||
}
|
||||
|
||||
return new CustomerPayload($customer, $order);
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return $this->customerFieldsFactory->getFields();
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\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\WooCommerce\Payloads\OrderStatusChangePayload;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
/**
|
||||
* @implements Subject<OrderStatusChangePayload>
|
||||
*/
|
||||
class OrderStatusChangeSubject implements Subject {
|
||||
|
||||
const KEY = 'woocommerce:order-status-changed';
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation subject (entity entering automation) title
|
||||
return __('WooCommerce order status change', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'from' => Builder::string()->required(),
|
||||
'to' => Builder::string()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPayload(SubjectData $subjectData): Payload {
|
||||
$from = $subjectData->getArgs()['from'];
|
||||
$to = $subjectData->getArgs()['to'];
|
||||
|
||||
return new OrderStatusChangePayload($from, $to);
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getFields(): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\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\WooCommerce\Fields\OrderFieldsFactory;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
||||
use MailPoet\NotFoundException;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
/**
|
||||
* @implements Subject<OrderPayload>
|
||||
*/
|
||||
class OrderSubject implements Subject {
|
||||
|
||||
const KEY = 'woocommerce:order';
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $woocommerce;
|
||||
|
||||
/** @var OrderFieldsFactory */
|
||||
private $orderFieldsFactory;
|
||||
|
||||
public function __construct(
|
||||
OrderFieldsFactory $orderFieldsFactory,
|
||||
WooCommerce $woocommerce
|
||||
) {
|
||||
$this->woocommerce = $woocommerce;
|
||||
$this->orderFieldsFactory = $orderFieldsFactory;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation subject (entity entering automation) title
|
||||
return __('WooCommerce order', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'order_id' => Builder::integer()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPayload(SubjectData $subjectData): Payload {
|
||||
$id = $subjectData->getArgs()['order_id'];
|
||||
$order = $this->woocommerce->wcGetOrder($id);
|
||||
if (!$order instanceof \WC_Order) {
|
||||
// translators: %d is the order ID.
|
||||
throw NotFoundException::create()->withMessage(sprintf(__("Order with ID '%d' not found.", 'mailpoet'), $id));
|
||||
}
|
||||
return new OrderPayload($order);
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getFields(): array {
|
||||
return $this->orderFieldsFactory->getFields();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\AbandonedCart;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\AutomaticEmails\WooCommerce\Events\AbandonedCart;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Cron\Workers\Automations\AbandonedCartWorker;
|
||||
use MailPoet\Entities\ScheduledTaskEntity;
|
||||
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
|
||||
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
|
||||
class AbandonedCartHandler {
|
||||
|
||||
const TASK_ABANDONED_CART = 'automation_abandoned_cart';
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
/** @var ScheduledTasksRepository */
|
||||
private $tasksRepository;
|
||||
|
||||
/** @var ScheduledTaskSubscribersRepository */
|
||||
private $taskSubscribersRepository;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp,
|
||||
ScheduledTasksRepository $tasksRepository,
|
||||
ScheduledTaskSubscribersRepository $taskSubscribersRepository,
|
||||
AutomationStorage $automationStorage
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->tasksRepository = $tasksRepository;
|
||||
$this->taskSubscribersRepository = $taskSubscribersRepository;
|
||||
$this->automationStorage = $automationStorage;
|
||||
}
|
||||
|
||||
public function registerHooks(): void {
|
||||
$this->wp->addAction(
|
||||
AbandonedCart::HOOK_SCHEDULE,
|
||||
[
|
||||
$this,
|
||||
'schedule',
|
||||
],
|
||||
10,
|
||||
2
|
||||
);
|
||||
$this->wp->addAction(
|
||||
AbandonedCart::HOOK_RE_SCHEDULE,
|
||||
[
|
||||
$this,
|
||||
'reschedule',
|
||||
]
|
||||
);
|
||||
$this->wp->addAction(
|
||||
AbandonedCart::HOOK_CANCEL,
|
||||
[
|
||||
$this,
|
||||
'cancel',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SubscriberEntity $subscriber
|
||||
* @param int[] $productIds
|
||||
* @return void
|
||||
*/
|
||||
public function schedule(SubscriberEntity $subscriber, array $productIds) {
|
||||
|
||||
$abandonedCartAutomations = $this->automationStorage->getActiveAutomationsByTriggerKey(AbandonedCartTrigger::KEY);
|
||||
$this->cancel($subscriber);
|
||||
array_map(
|
||||
function (Automation $automation) use ($subscriber, $productIds) {
|
||||
$this->scheduleForSingleAutomation($subscriber, $productIds, $automation);
|
||||
},
|
||||
$abandonedCartAutomations
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SubscriberEntity $subscriber
|
||||
* @param int[] $productIds
|
||||
* @param Automation $automation
|
||||
* @return void
|
||||
* @throws InvalidStateException
|
||||
*/
|
||||
private function scheduleForSingleAutomation(SubscriberEntity $subscriber, array $productIds, Automation $automation) {
|
||||
$trigger = $automation->getTrigger(AbandonedCartTrigger::KEY);
|
||||
if (!$trigger) {
|
||||
throw new InvalidStateException(sprintf('Abandoned cart trigger is missing from automation %d', $automation->getId()));
|
||||
}
|
||||
|
||||
$wait = $trigger->getArgs()['wait'] * 60;
|
||||
$scheduledAt = Carbon::now()->millisecond(0)->addSeconds($wait);
|
||||
$task = new ScheduledTaskEntity();
|
||||
$task->setType(AbandonedCartWorker::TASK_TYPE);
|
||||
|
||||
$lastActivity = Carbon::now()->millisecond(0);
|
||||
$task->setCreatedAt($lastActivity);
|
||||
$task->setPriority(ScheduledTaskEntity::PRIORITY_MEDIUM);
|
||||
$task->setMeta([
|
||||
'product_ids' => $productIds,
|
||||
'automation_id' => $automation->getId(),
|
||||
'automation_version' => $automation->getVersionId(),
|
||||
]);
|
||||
$task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
|
||||
$task->setScheduledAt($scheduledAt);
|
||||
$this->tasksRepository->persist($task);
|
||||
$this->tasksRepository->flush();
|
||||
|
||||
$taskSubscriber = new ScheduledTaskSubscriberEntity($task, $subscriber);
|
||||
$task->getSubscribers()->add($taskSubscriber);
|
||||
$this->taskSubscribersRepository->persist($taskSubscriber);
|
||||
$this->taskSubscribersRepository->flush();
|
||||
}
|
||||
|
||||
public function reschedule(SubscriberEntity $subscriber): void {
|
||||
$tasks = $this->tasksRepository->findByTypeAndSubscriber(AbandonedCartWorker::TASK_TYPE, $subscriber);
|
||||
if (!$tasks) {
|
||||
return;
|
||||
}
|
||||
$this->cancel($subscriber);
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$meta = $task->getMeta();
|
||||
$automation = isset($meta['automation_id']) ? $this->automationStorage->getAutomation((int)$meta['automation_id']) : null;
|
||||
if (!$automation) {
|
||||
continue;
|
||||
}
|
||||
$this->scheduleForSingleAutomation($subscriber, $meta['product_ids'] ?? [], $automation);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function cancel(SubscriberEntity $subscriber): void {
|
||||
$existingTasks = $this->tasksRepository->findByTypeAndSubscriber(AbandonedCartWorker::TASK_TYPE, $subscriber);
|
||||
if (!$existingTasks) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($existingTasks as $task) {
|
||||
if ($task->getStatus() !== ScheduledTaskEntity::STATUS_SCHEDULED) {
|
||||
continue;
|
||||
}
|
||||
foreach ($task->getSubscribers() as $taskSubscriber) {
|
||||
$this->taskSubscribersRepository->remove($taskSubscriber);
|
||||
}
|
||||
$this->tasksRepository->remove($task);
|
||||
}
|
||||
|
||||
$this->tasksRepository->flush();
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\AbandonedCart;
|
||||
|
||||
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\Storage\AutomationRunStorage;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\MailPoet\Subjects\SegmentSubject;
|
||||
use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\AbandonedCartPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\AbandonedCartSubject;
|
||||
use MailPoet\Cron\Workers\Automations\AbandonedCartWorker;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Segments\SegmentsRepository;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
|
||||
class AbandonedCartTrigger implements Trigger {
|
||||
|
||||
const KEY = 'woocommerce:abandoned-cart';
|
||||
|
||||
/** @var AbandonedCartHandler */
|
||||
private $abandonedCartHandler;
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
/** @var AutomationRunStorage */
|
||||
private $automationRunStorage;
|
||||
|
||||
public function __construct(
|
||||
AbandonedCartHandler $abandonedCartHandler,
|
||||
AutomationRunStorage $automationRunStorage,
|
||||
SegmentsRepository $segmentsRepository,
|
||||
WordPress $wp
|
||||
) {
|
||||
$this->abandonedCartHandler = $abandonedCartHandler;
|
||||
$this->automationRunStorage = $automationRunStorage;
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('User abandons cart', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getSubjectKeys(): array {
|
||||
return [
|
||||
SubscriberSubject::KEY,
|
||||
AbandonedCartSubject::KEY,
|
||||
SegmentSubject::KEY,
|
||||
];
|
||||
}
|
||||
|
||||
public function registerHooks(): void {
|
||||
$this->abandonedCartHandler->registerHooks();
|
||||
$this->wp->addAction(
|
||||
AbandonedCartWorker::ACTION,
|
||||
[
|
||||
$this,
|
||||
'handle',
|
||||
],
|
||||
10,
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SubscriberEntity $subscriber
|
||||
* @param int[] $productIds
|
||||
* @param \DateTime $lastAcivityAt
|
||||
* @return void
|
||||
*/
|
||||
public function handle(
|
||||
SubscriberEntity $subscriber,
|
||||
array $productIds,
|
||||
\DateTime $lastAcivityAt
|
||||
): void {
|
||||
|
||||
if (!$productIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wooSegment = $this->segmentsRepository->getWooCommerceSegment();
|
||||
|
||||
$subjects = [
|
||||
new Subject(AbandonedCartSubject::KEY, ['user_id' => $subscriber->getWpUserId(), 'last_activity_at' => $lastAcivityAt->format(\DateTime::W3C), 'product_ids' => $productIds]),
|
||||
new Subject(SubscriberSubject::KEY, ['subscriber_id' => $subscriber->getId()]),
|
||||
new Subject(SegmentSubject::KEY, ['segment_id' => $wooSegment->getId()]),
|
||||
];
|
||||
$this->wp->doAction(Hooks::TRIGGER, $this, $subjects);
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
$abandonedCartSubject = $args->getSingleSubjectEntryByClass(AbandonedCartSubject::class);
|
||||
$abandonedCartPayload = $args->getSinglePayloadByClass(AbandonedCartPayload::class);
|
||||
$lastActivityAt = $abandonedCartPayload->getLastActivityAt();
|
||||
|
||||
$compareDate = Carbon::now()->millisecond(0)->subMinutes($args->getStep()->getArgs()['wait']);
|
||||
if ($lastActivityAt > $compareDate) {
|
||||
return false;
|
||||
}
|
||||
$automation = $args->getAutomation();
|
||||
$existingRuns = $this->automationRunStorage->getCountByAutomationAndSubject(
|
||||
$automation,
|
||||
$abandonedCartSubject->getSubjectData()
|
||||
);
|
||||
if ($existingRuns) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'wait' => Builder::integer()->required()->minimum(1)->default(30),
|
||||
]);
|
||||
}
|
||||
|
||||
public function validate(StepValidationArgs $args): void {
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Control\FilterHandler;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter;
|
||||
use MailPoet\Automation\Engine\Data\FilterGroup;
|
||||
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\Storage\AutomationRunStorage;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumArrayFilter;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumFilter;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderStatusChangeSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
|
||||
|
||||
class BuysAProductTrigger implements Trigger {
|
||||
public const KEY = 'woocommerce:buys-a-product';
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
/** @var WooCommerceHelper */
|
||||
private $wc;
|
||||
|
||||
/** @var AutomationRunStorage */
|
||||
private $automationRunStorage;
|
||||
|
||||
/** @var FilterHandler */
|
||||
private $filterHandler;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp,
|
||||
WooCommerceHelper $wc,
|
||||
AutomationRunStorage $automationRunStorage,
|
||||
FilterHandler $filterHandler
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->wc = $wc;
|
||||
$this->automationRunStorage = $automationRunStorage;
|
||||
$this->filterHandler = $filterHandler;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Customer buys a product', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'product_ids' => Builder::array(
|
||||
Builder::integer()
|
||||
)->minItems(1)->required(),
|
||||
'to' => Builder::string()->required()->default('wc-completed'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSubjectKeys(): array {
|
||||
return [
|
||||
OrderSubject::KEY,
|
||||
OrderStatusChangeSubject::KEY,
|
||||
CustomerSubject::KEY,
|
||||
];
|
||||
}
|
||||
|
||||
public function validate(StepValidationArgs $args): void {
|
||||
}
|
||||
|
||||
public function registerHooks(): void {
|
||||
$this->wp->addAction(
|
||||
'woocommerce_order_status_changed',
|
||||
[
|
||||
$this,
|
||||
'handle',
|
||||
],
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $orderId
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @return void
|
||||
*/
|
||||
public function handle($orderId, $from, $to): void {
|
||||
$order = $this->wc->wcGetOrder($orderId);
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
$this->wp->doAction(Hooks::TRIGGER, $this, [
|
||||
new Subject(OrderSubject::KEY, ['order_id' => $orderId]),
|
||||
new Subject(CustomerSubject::KEY, ['customer_id' => $order->get_customer_id(), 'order_id' => $orderId]),
|
||||
new Subject(OrderStatusChangeSubject::KEY, ['from' => $from, 'to' => $to]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
|
||||
//Trigger the run only once.
|
||||
$orderSubjectData = $args->getSingleSubjectEntryByClass(OrderSubject::class)->getSubjectData();
|
||||
if ($this->automationRunStorage->getCountByAutomationAndSubject($args->getAutomation(), $orderSubjectData) > 0) {
|
||||
return false;
|
||||
}
|
||||
$group = new FilterGroup(
|
||||
'',
|
||||
FilterGroup::OPERATOR_AND,
|
||||
$this->getFilters($args)
|
||||
);
|
||||
return $this->filterHandler->matchesGroup($group, $args);
|
||||
}
|
||||
|
||||
protected function getFilters(StepRunArgs $args): array {
|
||||
$triggerArgs = $args->getStep()->getArgs();
|
||||
$filters = [
|
||||
Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM_ARRAY,
|
||||
'field_key' => 'woocommerce:order:products',
|
||||
'condition' => EnumArrayFilter::CONDITION_MATCHES_ANY_OF,
|
||||
'args' => [
|
||||
'value' => $triggerArgs['product_ids'] ?? [],
|
||||
],
|
||||
]),
|
||||
];
|
||||
$status = str_replace('wc-', '', $triggerArgs['to'] ?? 'completed');
|
||||
if ($status === 'any') {
|
||||
return $filters;
|
||||
}
|
||||
|
||||
$filters[] = Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM,
|
||||
'field_key' => 'woocommerce:order:status',
|
||||
'condition' => EnumFilter::IS_ANY_OF,
|
||||
'args' => [
|
||||
'value' => [$status],
|
||||
],
|
||||
]);
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter;
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumArrayFilter;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumFilter;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class BuysFromACategoryTrigger extends BuysAProductTrigger {
|
||||
const KEY = 'woocommerce:buys-from-a-category';
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'category_ids' => Builder::array(
|
||||
Builder::integer()
|
||||
)->minItems(1)->required(),
|
||||
'to' => Builder::string()->required()->default('wc-completed'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Customer buys from a category', 'mailpoet');
|
||||
}
|
||||
|
||||
protected function getFilters(StepRunArgs $args): array {
|
||||
$triggerArgs = $args->getStep()->getArgs();
|
||||
$filters = [
|
||||
Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM_ARRAY,
|
||||
'field_key' => 'woocommerce:order:categories',
|
||||
'condition' => EnumArrayFilter::CONDITION_MATCHES_ANY_OF,
|
||||
'args' => [
|
||||
'value' => $triggerArgs['category_ids'] ?? [],
|
||||
],
|
||||
]),
|
||||
];
|
||||
$status = str_replace('wc-', '', $triggerArgs['to'] ?? 'completed');
|
||||
if ($status === 'any') {
|
||||
return $filters;
|
||||
}
|
||||
|
||||
$filters[] = Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM,
|
||||
'field_key' => 'woocommerce:order:status',
|
||||
'condition' => EnumFilter::IS_ANY_OF,
|
||||
'args' => [
|
||||
'value' => [$status],
|
||||
],
|
||||
]);
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\Data\Filter;
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumArrayFilter;
|
||||
use MailPoet\Automation\Integrations\Core\Filters\EnumFilter;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class BuysFromATagTrigger extends BuysAProductTrigger {
|
||||
|
||||
|
||||
const KEY = 'woocommerce:buys-from-a-tag';
|
||||
|
||||
public function getKey(): string {
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Customer buys from a tag', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'tag_ids' => Builder::array(
|
||||
Builder::integer()
|
||||
)->minItems(1)->required(),
|
||||
'to' => Builder::string()->required()->default('wc-completed'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getFilters(StepRunArgs $args): array {
|
||||
$triggerArgs = $args->getStep()->getArgs();
|
||||
$filters = [
|
||||
Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM_ARRAY,
|
||||
'field_key' => 'woocommerce:order:tags',
|
||||
'condition' => EnumArrayFilter::CONDITION_MATCHES_ANY_OF,
|
||||
'args' => [
|
||||
'value' => $triggerArgs['tag_ids'] ?? [],
|
||||
],
|
||||
]),
|
||||
];
|
||||
$status = str_replace('wc-', '', $triggerArgs['to'] ?? 'completed');
|
||||
if ($status === 'any') {
|
||||
return $filters;
|
||||
}
|
||||
|
||||
$filters[] = Filter::fromArray([
|
||||
'id' => '',
|
||||
'field_type' => Field::TYPE_ENUM,
|
||||
'field_key' => 'woocommerce:order:status',
|
||||
'condition' => EnumFilter::IS_ANY_OF,
|
||||
'args' => [
|
||||
'value' => [$status],
|
||||
],
|
||||
]);
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderStatusChangePayload;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class OrderCancelledTrigger extends OrderStatusChangedTrigger {
|
||||
public function getKey(): string {
|
||||
return 'woocommerce:order-cancelled';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Order cancelled', 'mailpoet');
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
/** @var OrderStatusChangePayload $orderPayload */
|
||||
$orderPayload = $args->getSinglePayloadByClass(OrderStatusChangePayload::class);
|
||||
return $orderPayload->getTo() === 'cancelled';
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object();
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\StepRunArgs;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderStatusChangePayload;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class OrderCompletedTrigger extends OrderStatusChangedTrigger {
|
||||
public function getKey(): string {
|
||||
return 'woocommerce:order-completed';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Order completed', 'mailpoet');
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
$orderPayload = $args->getSinglePayloadByClass(OrderStatusChangePayload::class);
|
||||
return $orderPayload->getTo() === 'completed';
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object();
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders;
|
||||
|
||||
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\WooCommerce\Subjects\CustomerSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class OrderCreatedTrigger implements Trigger {
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
/** @var int[] */
|
||||
private $processedOrders = [];
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return 'woocommerce:order-created';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Order created', 'mailpoet');
|
||||
}
|
||||
|
||||
public function registerHooks(): void {
|
||||
$this->wp->addAction(
|
||||
'woocommerce_new_order',
|
||||
[
|
||||
$this,
|
||||
'handleCreate',
|
||||
],
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $orderId
|
||||
* @param \WC_Order $order
|
||||
* @return void
|
||||
*/
|
||||
public function handleCreate($orderId, $order) {
|
||||
|
||||
if (in_array($orderId, $this->processedOrders)) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Creating an order via wc_create_order() does not yet set crucial information like the customer's email address.
|
||||
* It just creates the order object and saves it to the database. We need therefore to wait for the order to have at least the billing address stored.
|
||||
**/
|
||||
if (!$order->get_billing_email()) {
|
||||
add_action(
|
||||
'woocommerce_after_order_object_save',
|
||||
function($order) use ($orderId) {
|
||||
if ((int)$orderId !== (int)$order->get_id()) {
|
||||
return;
|
||||
}
|
||||
$this->handleCreate($order->get_id(), $order);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
$this->processedOrders[] = $orderId;
|
||||
$this->wp->doAction(Hooks::TRIGGER, $this, [
|
||||
new Subject(OrderSubject::KEY, ['order_id' => $order->get_id()]),
|
||||
new Subject(CustomerSubject::KEY, ['customer_id' => $order->get_customer_id(), 'order_id' => $order->get_id()]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
/**
|
||||
* If we come to this point we always want to trigger the automation.
|
||||
* The evaluation whether this is a "new" order is done in the handleCreate() method.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object();
|
||||
}
|
||||
|
||||
public function getSubjectKeys(): array {
|
||||
return [
|
||||
OrderSubject::KEY,
|
||||
CustomerSubject::KEY,
|
||||
];
|
||||
}
|
||||
|
||||
public function validate(StepValidationArgs $args): void {
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders;
|
||||
|
||||
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\WooCommerce\Payloads\OrderStatusChangePayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderStatusChangeSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class OrderStatusChangedTrigger implements Trigger {
|
||||
|
||||
/** @var WordPress */
|
||||
protected $wp;
|
||||
|
||||
/** @var WooCommerce */
|
||||
protected $woocommerce;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp,
|
||||
WooCommerce $woocommerceHelper
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->woocommerce = $woocommerceHelper;
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return 'woocommerce:order-status-changed';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
// translators: automation trigger title
|
||||
return __('Order status changed', 'mailpoet');
|
||||
}
|
||||
|
||||
public function getArgsSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'from' => Builder::string()->required()->default('any'),
|
||||
'to' => Builder::string()->required()->default('wc-completed'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSubjectKeys(): array {
|
||||
return [
|
||||
OrderSubject::KEY,
|
||||
OrderStatusChangeSubject::KEY,
|
||||
CustomerSubject::KEY,
|
||||
];
|
||||
}
|
||||
|
||||
public function validate(StepValidationArgs $args): void {
|
||||
}
|
||||
|
||||
public function registerHooks(): void {
|
||||
$this->wp->addAction(
|
||||
'woocommerce_order_status_changed',
|
||||
[
|
||||
$this,
|
||||
'handle',
|
||||
],
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
public function handle(int $orderId, string $oldStatus, string $newStatus): void {
|
||||
$order = $this->woocommerce->wcGetOrder($orderId);
|
||||
if (!$order instanceof \WC_Order) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->wp->doAction(Hooks::TRIGGER, $this, [
|
||||
new Subject(OrderStatusChangeSubject::KEY, ['from' => $oldStatus, 'to' => $newStatus]),
|
||||
new Subject(OrderSubject::KEY, ['order_id' => $order->get_id()]),
|
||||
new Subject(CustomerSubject::KEY, ['customer_id' => $order->get_customer_id(), 'order_id' => $order->get_id()]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function isTriggeredBy(StepRunArgs $args): bool {
|
||||
/** @var OrderStatusChangePayload $orderPayload */
|
||||
$orderPayload = $args->getSinglePayloadByClass(OrderStatusChangePayload::class);
|
||||
$triggerArgs = $args->getStep()->getArgs();
|
||||
$configuredFrom = $triggerArgs['from'] ? str_replace('wc-', '', $triggerArgs['from']) : null;
|
||||
$configuredTo = $triggerArgs['to'] ? str_replace('wc-', '', $triggerArgs['to']) : null;
|
||||
if ($configuredFrom !== 'any' && $orderPayload->getFrom() !== $configuredFrom) {
|
||||
return false;
|
||||
}
|
||||
if ($configuredTo !== 'any' && $orderPayload->getTo() !== $configuredTo) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
use stdClass;
|
||||
use WC_Order;
|
||||
|
||||
class WooCommerce {
|
||||
public function isWooCommerceActive(): bool {
|
||||
return class_exists('WooCommerce');
|
||||
}
|
||||
|
||||
public function wcGetIsPaidStatuses(): array {
|
||||
return wc_get_is_paid_statuses();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function wcGetOrderStatuses(): array {
|
||||
return wc_get_order_statuses();
|
||||
}
|
||||
|
||||
public function isWooCommerceCustomOrdersTableEnabled(): bool {
|
||||
return $this->isWooCommerceActive()
|
||||
&& method_exists(OrderUtil::class, 'custom_orders_table_usage_is_enabled')
|
||||
&& OrderUtil::custom_orders_table_usage_is_enabled();
|
||||
}
|
||||
|
||||
/** @return WC_Order[]|stdClass */
|
||||
public function wcGetOrders(array $args = []) {
|
||||
return wc_get_orders($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $product
|
||||
* @return \WC_Product|null|false
|
||||
*/
|
||||
public function wcGetProduct($product) {
|
||||
return wc_get_product($product);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|bool $order
|
||||
* @return bool|\WC_Order|\WC_Order_Refund
|
||||
*/
|
||||
public function wcGetOrder($order = false) {
|
||||
return wc_get_order($order);
|
||||
}
|
||||
|
||||
public function wcGetOrderStatusName(string $status): string {
|
||||
return wc_get_order_status_name($status);
|
||||
}
|
||||
|
||||
public function wcReviewRatingsEnabled(): bool {
|
||||
return wc_review_ratings_enabled();
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\AbandonedCartSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderStatusChangeSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\SubjectTransformers\WordPressUserSubjectToWooCommerceCustomerSubjectTransformer;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\AbandonedCart\AbandonedCartTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysAProductTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromACategoryTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromATagTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCancelledTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCompletedTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCreatedTrigger;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderStatusChangedTrigger;
|
||||
|
||||
class WooCommerceIntegration {
|
||||
|
||||
/** @var OrderStatusChangedTrigger */
|
||||
private $orderStatusChangedTrigger;
|
||||
|
||||
/** @var OrderCreatedTrigger */
|
||||
private $orderCreatedTrigger;
|
||||
|
||||
/** @var OrderCompletedTrigger */
|
||||
private $orderCompletedTrigger;
|
||||
|
||||
private $orderCancelledTrigger;
|
||||
|
||||
/** @var AbandonedCartTrigger */
|
||||
private $abandonedCartTrigger;
|
||||
|
||||
/** @var BuysAProductTrigger */
|
||||
private $buysAProductTrigger;
|
||||
|
||||
/** @var BuysFromATagTrigger */
|
||||
private $buysFromATagTrigger;
|
||||
|
||||
/** @var BuysFromACategoryTrigger */
|
||||
private $buysFromACategoryTrigger;
|
||||
|
||||
/** @var AbandonedCartSubject */
|
||||
private $abandonedCartSubject;
|
||||
|
||||
/** @var OrderStatusChangeSubject */
|
||||
private $orderStatusChangeSubject;
|
||||
|
||||
/** @var OrderSubject */
|
||||
private $orderSubject;
|
||||
|
||||
/** @var CustomerSubject */
|
||||
private $customerSubject;
|
||||
|
||||
/** @var ContextFactory */
|
||||
private $contextFactory;
|
||||
|
||||
/** @var WordPressUserSubjectToWooCommerceCustomerSubjectTransformer */
|
||||
private $wordPressUserToWooCommerceCustomerTransformer;
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $wooCommerce;
|
||||
|
||||
public function __construct(
|
||||
OrderStatusChangedTrigger $orderStatusChangedTrigger,
|
||||
OrderCreatedTrigger $orderCreatedTrigger,
|
||||
OrderCompletedTrigger $orderCompletedTrigger,
|
||||
OrderCancelledTrigger $orderCancelledTrigger,
|
||||
AbandonedCartTrigger $abandonedCartTrigger,
|
||||
BuysAProductTrigger $buysAProductTrigger,
|
||||
BuysFromACategoryTrigger $buysFromACategoryTrigger,
|
||||
BuysFromATagTrigger $buysFromATagTrigger,
|
||||
AbandonedCartSubject $abandonedCartSubject,
|
||||
OrderStatusChangeSubject $orderStatusChangeSubject,
|
||||
OrderSubject $orderSubject,
|
||||
CustomerSubject $customerSubject,
|
||||
ContextFactory $contextFactory,
|
||||
WordPressUserSubjectToWooCommerceCustomerSubjectTransformer $wordPressUserToWooCommerceCustomerTransformer,
|
||||
WooCommerce $wooCommerce
|
||||
) {
|
||||
$this->orderStatusChangedTrigger = $orderStatusChangedTrigger;
|
||||
$this->orderCreatedTrigger = $orderCreatedTrigger;
|
||||
$this->orderCompletedTrigger = $orderCompletedTrigger;
|
||||
$this->orderCancelledTrigger = $orderCancelledTrigger;
|
||||
$this->abandonedCartTrigger = $abandonedCartTrigger;
|
||||
$this->buysAProductTrigger = $buysAProductTrigger;
|
||||
$this->buysFromACategoryTrigger = $buysFromACategoryTrigger;
|
||||
$this->buysFromATagTrigger = $buysFromATagTrigger;
|
||||
$this->abandonedCartSubject = $abandonedCartSubject;
|
||||
$this->orderStatusChangeSubject = $orderStatusChangeSubject;
|
||||
$this->orderSubject = $orderSubject;
|
||||
$this->customerSubject = $customerSubject;
|
||||
$this->contextFactory = $contextFactory;
|
||||
$this->wordPressUserToWooCommerceCustomerTransformer = $wordPressUserToWooCommerceCustomerTransformer;
|
||||
$this->wooCommerce = $wooCommerce;
|
||||
}
|
||||
|
||||
public function register(Registry $registry): void {
|
||||
if (!$this->wooCommerce->isWooCommerceActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$registry->addContextFactory('woocommerce', function () {
|
||||
return $this->contextFactory->getContextData();
|
||||
});
|
||||
|
||||
$registry->addSubject($this->abandonedCartSubject);
|
||||
$registry->addSubject($this->orderSubject);
|
||||
$registry->addSubject($this->orderStatusChangeSubject);
|
||||
$registry->addSubject($this->customerSubject);
|
||||
$registry->addTrigger($this->orderStatusChangedTrigger);
|
||||
$registry->addTrigger($this->orderCreatedTrigger);
|
||||
$registry->addTrigger($this->orderCompletedTrigger);
|
||||
$registry->addTrigger($this->orderCancelledTrigger);
|
||||
$registry->addTrigger($this->abandonedCartTrigger);
|
||||
$registry->addTrigger($this->buysAProductTrigger);
|
||||
$registry->addTrigger($this->buysFromACategoryTrigger);
|
||||
$registry->addTrigger($this->buysFromATagTrigger);
|
||||
$registry->addSubjectTransformer($this->wordPressUserToWooCommerceCustomerTransformer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WordPress;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
|
||||
class ContextFactory {
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
/** @return mixed[] */
|
||||
public function getContextData(): array {
|
||||
return [
|
||||
'comment_statuses' => $this->getCommentStatuses(),
|
||||
'post_types' => $this->getPostTypes(),
|
||||
'taxonomies' => $this->getTaxonomies(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
private function getCommentStatuses(): array {
|
||||
$statiMap = $this->wp->getCommentStatuses();
|
||||
$stati = [];
|
||||
foreach ($statiMap as $id => $name) {
|
||||
$stati[] = [
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
];
|
||||
}
|
||||
return $stati;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, array<string, bool>|bool|string>>
|
||||
*/
|
||||
private function getPostTypes(): array {
|
||||
/** @var \WP_Post_Type[] $postTypes */
|
||||
$postTypes = $this->wp->getPostTypes([], 'objects');
|
||||
return array_values(array_map(function(\WP_Post_Type $type): array {
|
||||
|
||||
$supports = ['comments' => false];
|
||||
foreach (array_keys($supports) as $key) {
|
||||
$supports[$key] = $this->wp->postTypeSupports($type->name, $key);
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $type->name,
|
||||
'label' => $type->label,
|
||||
'supports' => $supports,
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'show_in_rest' => $type->show_in_rest,
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'rest_base' => $type->rest_base,
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'rest_namespace' => $type->rest_namespace,
|
||||
'public' => $type->public,
|
||||
];
|
||||
},
|
||||
$postTypes));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, string[]|bool|string>>
|
||||
*/
|
||||
private function getTaxonomies(): array {
|
||||
/** @var \WP_Taxonomy[] $taxonomies */
|
||||
$taxonomies = array_filter(
|
||||
$this->wp->getTaxonomies([], 'objects'),
|
||||
function($object): bool {
|
||||
return $object instanceof \WP_Taxonomy;
|
||||
}
|
||||
);
|
||||
return array_values(array_map(
|
||||
function(\WP_Taxonomy $taxonomy): array {
|
||||
return [
|
||||
'name' => $taxonomy->name,
|
||||
'label' => $taxonomy->label,
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'show_in_rest' => $taxonomy->show_in_rest,
|
||||
'public' => $taxonomy->public,
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'rest_base' => $taxonomy->rest_base,
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'rest_namespace' => $taxonomy->rest_namespace,
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'object_type' => (array)$taxonomy->object_type,
|
||||
];
|
||||
},
|
||||
$taxonomies
|
||||
));
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WordPress\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\WordPress\Payloads\CommentPayload;
|
||||
|
||||
class CommentFieldsFactory {
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Field[]
|
||||
*/
|
||||
public function getFields(): array {
|
||||
return [
|
||||
new Field(
|
||||
'wordpress:comment:id',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Comment ID', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
return $payload->getCommentId();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:author-name',
|
||||
Field::TYPE_STRING,
|
||||
__('Comment author name', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? $comment->comment_author : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:author-email',
|
||||
Field::TYPE_STRING,
|
||||
__('Comment author email', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? $comment->comment_author_email : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:author-url',
|
||||
Field::TYPE_STRING,
|
||||
__('Comment author URL', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? $comment->comment_author_url : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:author-ip',
|
||||
Field::TYPE_STRING,
|
||||
__('Comment author IP', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? $comment->comment_author_IP : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Comment date', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? $comment->comment_date_gmt : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:content',
|
||||
Field::TYPE_STRING,
|
||||
__('Comment content', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? $comment->comment_content : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:karma',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Comment karma', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? (int)$comment->comment_karma : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:status',
|
||||
Field::TYPE_ENUM,
|
||||
__('Comment status', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$status = $this->wp->wpGetCommentStatus($payload->getCommentId());
|
||||
if (!is_string($status)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* wp_get_comment_status returns 'unapproved' and 'approved' where get_comment_statuses returns 'hold' and 'approve'
|
||||
* We need to normalize the status for matches.
|
||||
*/
|
||||
if ($status === 'approved') {
|
||||
$status = 'approve';
|
||||
}
|
||||
if ($status === 'unapproved') {
|
||||
$status = 'hold';
|
||||
}
|
||||
return $status;
|
||||
},
|
||||
[
|
||||
'options' => $this->getCommentStatuses(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:comment-agent',
|
||||
Field::TYPE_STRING,
|
||||
__('Comment user agent', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? $comment->comment_agent : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:comment-type',
|
||||
Field::TYPE_STRING,
|
||||
__('Comment type', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? $comment->comment_type : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:comment-parent',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Comment parent ID', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? (int)$comment->comment_parent : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:comment:has-children',
|
||||
Field::TYPE_BOOLEAN,
|
||||
__('Comment has replies', 'mailpoet'),
|
||||
function (CommentPayload $payload) {
|
||||
$comment = $payload->getComment();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $comment ? count($comment->get_children()) > 0 : false;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private function getCommentStatuses(): array {
|
||||
$statuses = $this->wp->getCommentStatuses();
|
||||
return array_values(array_map(
|
||||
function($name, $id): array {
|
||||
return [
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
];
|
||||
},
|
||||
$statuses,
|
||||
array_keys($statuses)
|
||||
));
|
||||
}
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WordPress\Fields;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
use MailPoet\Automation\Integrations\WordPress\Payloads\PostPayload;
|
||||
|
||||
class PostFieldsFactory {
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
WordPress $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Field[]
|
||||
*/
|
||||
public function getFields(): array {
|
||||
return [
|
||||
new Field(
|
||||
'wordpress:post:id',
|
||||
Field::TYPE_INTEGER,
|
||||
__('ID', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
return $payload->getPostId();
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:type',
|
||||
Field::TYPE_ENUM,
|
||||
__('Post type', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_type : null;
|
||||
},
|
||||
[
|
||||
'options' => $this->getPostTypes(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:status',
|
||||
Field::TYPE_ENUM,
|
||||
__('Post status', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_status : null;
|
||||
},
|
||||
[
|
||||
'options' => $this->getPostStatuses(),
|
||||
]
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:content',
|
||||
Field::TYPE_STRING,
|
||||
__('Post Content', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_content : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:title',
|
||||
Field::TYPE_STRING,
|
||||
__('Post title', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_title : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Post date', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_date_gmt : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:modified',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Post last modified', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_modified_gmt : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:author',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Post author ID', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? (int)$post->post_author : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:excerpt',
|
||||
Field::TYPE_STRING,
|
||||
__('Post excerpt', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_excerpt : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:comment-status',
|
||||
Field::TYPE_BOOLEAN,
|
||||
__('Post open for comments', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->comment_status === 'open' : false;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:ping-status',
|
||||
Field::TYPE_BOOLEAN,
|
||||
__('Post open for pings', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->ping_status === 'open' : false;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:password',
|
||||
Field::TYPE_STRING,
|
||||
__('Post password', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_password : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:slug',
|
||||
Field::TYPE_STRING,
|
||||
__('Post slug', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_name : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:parent',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Post parent ID', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_parent : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:has-parent',
|
||||
Field::TYPE_BOOLEAN,
|
||||
__('Post has parent', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->post_parent > 0 : false;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:guid',
|
||||
Field::TYPE_STRING,
|
||||
__('Post guid', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
return $post ? $post->guid : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:menu-order',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Post menu order', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->menu_order : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'wordpress:post:comment-count',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Number of post comments', 'mailpoet'),
|
||||
function (PostPayload $payload) {
|
||||
$post = $payload->getPost();
|
||||
//phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
return $post ? $post->comment_count : 0;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private function getPostStatuses(): array {
|
||||
$statuses = $this->wp->getPostStatuses();
|
||||
return array_values(array_map(
|
||||
function($status, $index): array {
|
||||
return [
|
||||
'id' => $index,
|
||||
'name' => $status,
|
||||
];
|
||||
},
|
||||
$statuses,
|
||||
array_keys($statuses)
|
||||
));
|
||||
}
|
||||
|
||||
private function getPostTypes(): array {
|
||||
/** @var \WP_Post_Type[] $postTypes */
|
||||
$postTypes = $this->wp->getPostTypes([], 'objects');
|
||||
return array_values(array_map(
|
||||
function(\WP_Post_Type $type): array {
|
||||
return [
|
||||
'id' => $type->name,
|
||||
'name' => $type->label,
|
||||
];
|
||||
},
|
||||
array_filter(
|
||||
$postTypes,
|
||||
function(\WP_Post_Type $type): bool {
|
||||
return (bool)$type->public;
|
||||
}
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WordPress\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
|
||||
class CommentPayload implements Payload {
|
||||
/** @var int */
|
||||
private $commentId;
|
||||
|
||||
/** @var WordPress */
|
||||
protected $wp;
|
||||
|
||||
public function __construct(
|
||||
int $commentId,
|
||||
WordPress $wp
|
||||
) {
|
||||
$this->commentId = $commentId;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function getCommentId(): int {
|
||||
return $this->commentId;
|
||||
}
|
||||
|
||||
public function getComment(): ?\WP_Comment {
|
||||
$comment = $this->wp->getComment($this->commentId);
|
||||
return $comment instanceof \WP_Comment ? $comment : null;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WordPress\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\WordPress;
|
||||
|
||||
class PostPayload implements Payload {
|
||||
/** @var int */
|
||||
private $postId;
|
||||
|
||||
/** @var WordPress */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
int $postId,
|
||||
WordPress $wp
|
||||
) {
|
||||
$this->postId = $postId;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function getPostId(): int {
|
||||
return $this->postId;
|
||||
}
|
||||
|
||||
public function getPost(): ?\WP_Post {
|
||||
$post = $this->wp->getPost($this->postId);
|
||||
return $post instanceof \WP_Post ? $post : null;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WordPress\Payloads;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use WP_User;
|
||||
|
||||
class UserPayload implements Payload {
|
||||
/** @var WP_User */
|
||||
private $user;
|
||||
|
||||
public function __construct(
|
||||
WP_User $user
|
||||
) {
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->user->ID;
|
||||
}
|
||||
|
||||
public function getUser(): WP_User {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string {
|
||||
return $this->user->user_email ?: null;
|
||||
}
|
||||
|
||||
public function exists(): bool {
|
||||
return $this->user->exists();
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getRoles(): array {
|
||||
return $this->user->roles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user