Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
Mautic.messagesOnLoad = function(container) {
|
||||
mQuery(container + ' .sortable-panel-wrapper .modal').each(function() {
|
||||
// Move modals outside of the wrapper
|
||||
mQuery(this).closest('.panel').append(mQuery(this));
|
||||
});
|
||||
};
|
||||
|
||||
Mautic.toggleChannelFormDisplay = function (el, channel) {
|
||||
Mautic.toggleTabPublished(el);
|
||||
|
||||
if (mQuery(el).val() === "1" && mQuery(el).prop('checked')) {
|
||||
mQuery(el).closest('.tab-pane').find('.message_channel_properties_' + channel).removeClass('hide')
|
||||
} else {
|
||||
mQuery(el).closest('.tab-pane').find('.message_channel_properties_' + channel).addClass('hide');
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.cancelQueuedMessageEvent = function (channelId) {
|
||||
Mautic.ajaxActionRequest('channel:cancelQueuedMessageEvent',
|
||||
{
|
||||
channelId: channelId
|
||||
}, function (response) {
|
||||
if (response.success) {
|
||||
mQuery('#queued-message-'+channelId).addClass('disabled');
|
||||
mQuery('#queued-status-'+channelId).html(Mautic.translate('mautic.message.queue.status.cancelled'));
|
||||
}
|
||||
}, false
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle;
|
||||
|
||||
final class ChannelEvents
|
||||
{
|
||||
/**
|
||||
* The mautic.add_channel event registers communication channels.
|
||||
*
|
||||
* The event listener receives a Mautic\ChannelBundle\Event\ChannelEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ADD_CHANNEL = 'mautic.add_channel';
|
||||
|
||||
/**
|
||||
* The mautic.channel_broadcast event is dispatched by the mautic:send:broadcast command to process communication to pending contacts.
|
||||
*
|
||||
* The event listener receives a Mautic\ChannelBundle\Event\ChannelBroadcastEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CHANNEL_BROADCAST = 'mautic.channel_broadcast';
|
||||
|
||||
/**
|
||||
* The mautic.message_queued event is dispatched to save a message to the queue.
|
||||
*
|
||||
* The event listener receives a Mautic\ChannelBundle\Event\MessageQueueEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const MESSAGE_QUEUED = 'mautic.message_queued';
|
||||
|
||||
/**
|
||||
* The mautic.process_message_queue event is dispatched to be processed by a listener.
|
||||
*
|
||||
* The event listener receives a Mautic\ChannelBundle\Event\MessageQueueProcessEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PROCESS_MESSAGE_QUEUE = 'mautic.process_message_queue';
|
||||
|
||||
/**
|
||||
* The mautic.process_message_queue_batch event is dispatched to process a batch of messages by channel and channel ID.
|
||||
*
|
||||
* The event listener receives a Mautic\ChannelBundle\Event\MessageQueueBatchProcessEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PROCESS_MESSAGE_QUEUE_BATCH = 'mautic.process_message_queue_batch';
|
||||
|
||||
/**
|
||||
* The mautic.channel.on_campaign_batch_action event is dispatched when the campaign action triggers.
|
||||
*
|
||||
* The event listener receives a Mautic\CampaignBundle\Event\PendingEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_CAMPAIGN_BATCH_ACTION = 'mautic.channel.on_campaign_batch_action';
|
||||
|
||||
/**
|
||||
* The mautic.message_pre_save event is dispatched right before a form is persisted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\ChannelEvent\Event\MessageEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const MESSAGE_PRE_SAVE = 'mautic.message_pre_save';
|
||||
|
||||
/**
|
||||
* The mautic.message_post_save event is dispatched right after a form is persisted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\ChannelEvent\Event\MessageEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const MESSAGE_POST_SAVE = 'mautic.message_post_save';
|
||||
|
||||
/**
|
||||
* The mautic.message_pre_delete event is dispatched before a form is deleted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\ChannelEvent\Event\MessageEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const MESSAGE_PRE_DELETE = 'mautic.message_pre_delete';
|
||||
|
||||
/**
|
||||
* The mautic.message_post_delete event is dispatched after a form is deleted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\ChannelEvent\Event\MessageEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const MESSAGE_POST_DELETE = 'mautic.message_post_delete';
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Command;
|
||||
|
||||
use Mautic\ChannelBundle\Model\MessageQueueModel;
|
||||
use Mautic\CoreBundle\Command\ModeratedCommand;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'mautic:messages:send',
|
||||
description: 'Process sending of messages queue.',
|
||||
aliases: [
|
||||
'mautic:campaigns:messagequeue',
|
||||
'mautic:campaigns:messages',
|
||||
]
|
||||
)]
|
||||
class ProcessMarketingMessagesQueueCommand extends ModeratedCommand
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private MessageQueueModel $messageQueueModel,
|
||||
PathsHelper $pathsHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($pathsHelper, $coreParametersHelper);
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->addOption(
|
||||
'--channel',
|
||||
'-c',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Channel to use for sending messages i.e. email, sms.',
|
||||
null
|
||||
)
|
||||
->addOption('--channel-id', '-i', InputOption::VALUE_REQUIRED, 'The ID of the message i.e. email ID, sms ID.')
|
||||
->addOption('--message-id', '-m', InputOption::VALUE_REQUIRED, 'ID of a specific queued message')
|
||||
->addOption(
|
||||
'--limit',
|
||||
'-l',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Maximum number of messages to process',
|
||||
null
|
||||
)
|
||||
->addOption(
|
||||
'--batch',
|
||||
'-b',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Number of messages to process in each batch',
|
||||
50
|
||||
);
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$processed = 0;
|
||||
$channel = $input->getOption('channel');
|
||||
$channelId = $input->getOption('channel-id');
|
||||
$messageId = $input->getOption('message-id');
|
||||
$limit = $input->getOption('limit') ? (int) $input->getOption('limit') : null;
|
||||
$batch = (int) $input->getOption('batch');
|
||||
$key = $channel.$channelId.$messageId;
|
||||
|
||||
if (!$this->checkRunStatus($input, $output, $key)) {
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
|
||||
$output->writeln('<info>'.$this->translator->trans('mautic.campaign.command.process.messages').'</info>');
|
||||
|
||||
if ($messageId) {
|
||||
if ($message = $this->messageQueueModel->getEntity($messageId)) {
|
||||
$processed = intval($this->messageQueueModel->processMessageQueue($message));
|
||||
}
|
||||
} else {
|
||||
// Process messages in batches until the limit is reached or no more messages
|
||||
do {
|
||||
$remainingBatch = $limit ? min($batch, $limit - $processed) : $batch;
|
||||
$batchProcessed = $this->messageQueueModel->sendMessages($channel, $channelId, $remainingBatch);
|
||||
$processed += $batchProcessed;
|
||||
|
||||
// Continue only if messages were processed and limit not reached
|
||||
} while ($batchProcessed > 0 && (!$limit || $processed < $limit));
|
||||
}
|
||||
|
||||
$output->writeln('<comment>'.$this->translator->trans('mautic.campaign.command.messages.sent', ['%events%' => $processed]).'</comment>'."\n");
|
||||
|
||||
$this->completeRun();
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Command;
|
||||
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Event\ChannelBroadcastEvent;
|
||||
use Mautic\CoreBundle\Command\ModeratedCommand;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* CLI Command to send a scheduled broadcast.
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'mautic:broadcasts:send',
|
||||
description: 'Process contacts pending to receive a channel broadcast.'
|
||||
)]
|
||||
class SendChannelBroadcastCommand extends ModeratedCommand
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
PathsHelper $pathsHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($pathsHelper, $coreParametersHelper);
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>%command.name%</info> command is send a channel broadcast to pending contacts.
|
||||
|
||||
<info>php %command.full_name% --channel=email --id=3</info>
|
||||
EOT
|
||||
)
|
||||
->setDefinition(
|
||||
[
|
||||
new InputOption(
|
||||
'channel', 'c', InputOption::VALUE_OPTIONAL,
|
||||
'A specific channel to process broadcasts for pending contacts.'
|
||||
),
|
||||
new InputOption(
|
||||
'id', 'i', InputOption::VALUE_OPTIONAL,
|
||||
'The ID for a specifc channel to process broadcasts for pending contacts.'
|
||||
),
|
||||
new InputOption(
|
||||
'min-contact-id', null, InputOption::VALUE_OPTIONAL,
|
||||
'Min contact ID to filter recipients.'
|
||||
),
|
||||
new InputOption(
|
||||
'max-contact-id', null, InputOption::VALUE_OPTIONAL,
|
||||
'Max contact ID to filter recipients.'
|
||||
),
|
||||
new InputOption(
|
||||
'limit', 'l', InputOption::VALUE_OPTIONAL,
|
||||
'Limit how many contacts to load from database to process.'
|
||||
),
|
||||
new InputOption(
|
||||
'batch', 'b', InputOption::VALUE_OPTIONAL,
|
||||
'Limit how many messages to send at once.'
|
||||
),
|
||||
]
|
||||
)->addOption(
|
||||
'--thread-id',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'The number of this current process if running multiple in parallel.'
|
||||
)
|
||||
->addOption(
|
||||
'--max-threads',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'The maximum number of processes you intend to run in parallel.'
|
||||
);
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$channel = $input->getOption('channel');
|
||||
$channelId = $input->getOption('id');
|
||||
$limit = $input->getOption('limit');
|
||||
$batch = $input->getOption('batch');
|
||||
$minContactId = $input->getOption('min-contact-id');
|
||||
$maxContactId = $input->getOption('max-contact-id');
|
||||
$threadId = $input->getOption('thread-id');
|
||||
$maxThreads = $input->getOption('max-threads');
|
||||
$key = sprintf('%s-%s-%s-%s', $channel, $channelId, $threadId, $maxThreads);
|
||||
|
||||
if (is_numeric($limit)) {
|
||||
$limit = (int) $limit;
|
||||
}
|
||||
|
||||
if (is_numeric($batch)) {
|
||||
$batch = (int) $batch;
|
||||
}
|
||||
|
||||
if (is_numeric($minContactId)) {
|
||||
$minContactId = (int) $minContactId;
|
||||
}
|
||||
|
||||
if (is_numeric($maxContactId)) {
|
||||
$maxContactId = (int) $maxContactId;
|
||||
}
|
||||
|
||||
if (is_numeric($threadId)) {
|
||||
$threadId = (int) $threadId;
|
||||
}
|
||||
|
||||
if (is_numeric($maxThreads)) {
|
||||
$maxThreads = (int) $maxThreads;
|
||||
}
|
||||
|
||||
if ($threadId && $maxThreads) {
|
||||
if ((int) $threadId > (int) $maxThreads) {
|
||||
$output->writeln('--thread-id cannot be larger than --max-thread');
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->checkRunStatus($input, $output, $key)) {
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
|
||||
$event = new ChannelBroadcastEvent($channel, $channelId, $output);
|
||||
|
||||
if ($limit) {
|
||||
$event->setLimit((int) $limit);
|
||||
}
|
||||
|
||||
if ($batch) {
|
||||
$event->setBatch((int) $batch);
|
||||
}
|
||||
|
||||
if ($minContactId) {
|
||||
$event->setMinContactIdFilter((int) $minContactId);
|
||||
}
|
||||
|
||||
if ($maxContactId) {
|
||||
$event->setMaxContactIdFilter((int) $maxContactId);
|
||||
}
|
||||
|
||||
if ($threadId) {
|
||||
$event->setThreadId((int) $threadId);
|
||||
}
|
||||
|
||||
if ($maxThreads) {
|
||||
$event->setMaxThreads((int) $maxThreads);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, ChannelEvents::CHANNEL_BROADCAST);
|
||||
|
||||
$results = $event->getResults();
|
||||
|
||||
$rows = [];
|
||||
foreach ($results as $channel => $counts) {
|
||||
$rows[] = [$channel, $counts['success'], $counts['failed']];
|
||||
}
|
||||
|
||||
// Put a blank line after anything the event spits out
|
||||
$output->writeln('');
|
||||
$output->writeln('');
|
||||
|
||||
$table = new Table($output);
|
||||
$table
|
||||
->setHeaders([$this->translator->trans('mautic.core.channel'), $this->translator->trans('mautic.core.channel.broadcast_success_count'), $this->translator->trans('mautic.core.channel.broadcast_failed_count')])
|
||||
->setRows($rows);
|
||||
$table->render();
|
||||
|
||||
$this->completeRun();
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'mautic_message_index' => [
|
||||
'path' => '/messages/{page}',
|
||||
'controller' => 'Mautic\ChannelBundle\Controller\MessageController::indexAction',
|
||||
],
|
||||
'mautic_message_contacts' => [
|
||||
'path' => '/messages/contacts/{objectId}/{channel}/{page}',
|
||||
'controller' => 'Mautic\ChannelBundle\Controller\MessageController::contactsAction',
|
||||
],
|
||||
'mautic_message_action' => [
|
||||
'path' => '/messages/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\ChannelBundle\Controller\MessageController::executeAction',
|
||||
],
|
||||
'mautic_channel_batch_contact_set' => [
|
||||
'path' => '/channels/batch/contact/set',
|
||||
'controller' => 'Mautic\ChannelBundle\Controller\BatchContactController::setAction',
|
||||
],
|
||||
'mautic_channel_batch_contact_view' => [
|
||||
'path' => '/channels/batch/contact/view',
|
||||
'controller' => 'Mautic\ChannelBundle\Controller\BatchContactController::indexAction',
|
||||
],
|
||||
],
|
||||
'api' => [
|
||||
'mautic_api_messagetandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'messages',
|
||||
'path' => '/messages',
|
||||
'controller' => Mautic\ChannelBundle\Controller\Api\MessageApiController::class,
|
||||
],
|
||||
],
|
||||
'public' => [
|
||||
],
|
||||
],
|
||||
|
||||
'menu' => [
|
||||
'main' => [
|
||||
'mautic.channel.messages' => [
|
||||
'route' => 'mautic_message_index',
|
||||
'access' => ['channel:messages:viewown', 'channel:messages:viewother'],
|
||||
'parent' => 'mautic.core.channels',
|
||||
'priority' => 110,
|
||||
],
|
||||
],
|
||||
'admin' => [
|
||||
],
|
||||
'profile' => [
|
||||
],
|
||||
'extra' => [
|
||||
],
|
||||
],
|
||||
|
||||
'categories' => [
|
||||
'messages' => [
|
||||
'class' => Mautic\ChannelBundle\Entity\Message::class,
|
||||
],
|
||||
],
|
||||
|
||||
'services' => [
|
||||
'helpers' => [
|
||||
'mautic.channel.helper.channel_list' => [
|
||||
'class' => Mautic\ChannelBundle\Helper\ChannelListHelper::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
'translator',
|
||||
],
|
||||
'alias' => 'channel',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'parameters' => [
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
'PreferenceBuilder/ChannelPreferences.php',
|
||||
'PreferenceBuilder/PreferenceBuilder.php',
|
||||
];
|
||||
|
||||
$services->load('Mautic\\ChannelBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->load('Mautic\\ChannelBundle\\Entity\\', '../Entity/*Repository.php')
|
||||
->tag(Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG);
|
||||
$services->alias('mautic.channel.model.message', Mautic\ChannelBundle\Model\MessageModel::class);
|
||||
$services->alias('mautic.channel.model.queue', Mautic\ChannelBundle\Model\MessageQueueModel::class);
|
||||
$services->alias('mautic.channel.model.channel.action', Mautic\ChannelBundle\Model\ChannelActionModel::class);
|
||||
$services->alias('mautic.channel.model.frequency.action', Mautic\ChannelBundle\Model\FrequencyActionModel::class);
|
||||
$services->alias('mautic.channel.repository.message_queue', Mautic\ChannelBundle\Entity\MessageQueueRepository::class);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Controller;
|
||||
|
||||
use Mautic\ChannelBundle\Model\MessageQueueModel;
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\CoreBundle\Controller\AjaxLookupControllerTrait;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
use AjaxLookupControllerTrait;
|
||||
|
||||
public function cancelQueuedMessageEventAction(Request $request): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$messageQueueId = (int) $request->request->get('channelId');
|
||||
$queueModel = $this->getModel('channel.queue');
|
||||
\assert($queueModel instanceof MessageQueueModel);
|
||||
$queuedMessage = $queueModel->getEntity($messageQueueId);
|
||||
if ($queuedMessage) {
|
||||
$queuedMessage->setStatus('cancelled');
|
||||
$queueModel->saveEntity($queuedMessage);
|
||||
$dataArray = ['success' => 1];
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Entity\Message;
|
||||
use Mautic\ChannelBundle\Event\ChannelEvent;
|
||||
use Mautic\ChannelBundle\Model\MessageModel;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Message>
|
||||
*/
|
||||
class MessageApiController extends CommonApiController
|
||||
{
|
||||
/**
|
||||
* @var MessageModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(
|
||||
CorePermissions $security,
|
||||
Translator $translator,
|
||||
EntityResultHelper $entityResultHelper,
|
||||
RouterInterface $router,
|
||||
FormFactoryInterface $formFactory,
|
||||
AppVersion $appVersion,
|
||||
private RequestStack $requestStack,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
$messageModel = $modelFactory->getModel('channel.message');
|
||||
\assert($messageModel instanceof MessageModel);
|
||||
$this->model = $messageModel;
|
||||
$this->entityClass = Message::class;
|
||||
$this->entityNameOne = 'message';
|
||||
$this->entityNameMulti = 'messages';
|
||||
$this->serializerGroups = ['messageDetails', 'messageChannelList', 'categoryList', 'publishDetails'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
protected function prepareParametersFromRequest(FormInterface $form, array &$params, ?object $entity = null, array $masks = [], array $fields = []): void
|
||||
{
|
||||
parent::prepareParametersFromRequest($form, $params, $entity, $masks);
|
||||
|
||||
if ('PATCH' === $this->requestStack->getCurrentRequest()->getMethod() && !isset($params['channels'])) {
|
||||
return;
|
||||
} elseif (!isset($params['channels'])) {
|
||||
$params['channels'] = [];
|
||||
}
|
||||
|
||||
$channels = $this->model->getChannels();
|
||||
|
||||
foreach ($channels as $channelType => $channel) {
|
||||
if (!isset($params['channels'][$channelType])) {
|
||||
$params['channels'][$channelType] = ['isEnabled' => 0];
|
||||
} else {
|
||||
$params['channels'][$channelType]['isEnabled'] = (int) $params['channels'][$channelType]['isEnabled'];
|
||||
}
|
||||
$params['channels'][$channelType]['channel'] = $channelType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and set channel names to the response.
|
||||
*/
|
||||
protected function preSerializeEntity(object $entity, string $action = 'view'): void
|
||||
{
|
||||
$event = $this->dispatcher->dispatch(new ChannelEvent(), ChannelEvents::ADD_CHANNEL);
|
||||
|
||||
foreach ($entity->getChannels() as $channel) {
|
||||
$repository = $event->getRepositoryName($channel->getChannel());
|
||||
$nameColumn = $event->getNameColumn($channel->getChannel());
|
||||
$name = $this->model->getChannelName($channel->getChannelId(), $repository, $nameColumn);
|
||||
$channel->setChannelName($name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ChannelBundle\Model\ChannelActionModel;
|
||||
use Mautic\ChannelBundle\Model\FrequencyActionModel;
|
||||
use Mautic\CoreBundle\Controller\AbstractFormController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Form\Type\ContactChannelsType;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class BatchContactController extends AbstractFormController
|
||||
{
|
||||
public function __construct(
|
||||
private ChannelActionModel $channelActionModel,
|
||||
private FrequencyActionModel $frequencyActionModel,
|
||||
private LeadModel $contactModel,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the batch action.
|
||||
*/
|
||||
public function setAction(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->get('contact_channels', []);
|
||||
$ids = empty($params['ids']) ? [] : json_decode($params['ids']);
|
||||
|
||||
if ($ids && is_array($ids)) {
|
||||
$subscribedChannels = $params['subscribed_channels'] ?? [];
|
||||
$preferredChannel = $params['preferred_channel'] ?? null;
|
||||
|
||||
$this->channelActionModel->update($ids, $subscribedChannels);
|
||||
$this->frequencyActionModel->update($ids, $params, $preferredChannel);
|
||||
|
||||
$this->addFlashMessage('mautic.lead.batch_leads_affected', [
|
||||
'%count%' => count($ids),
|
||||
]);
|
||||
} else {
|
||||
$this->addFlashMessage('mautic.core.error.ids.missing');
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View for batch action.
|
||||
*/
|
||||
public function indexAction(): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$route = $this->generateUrl('mautic_channel_batch_contact_set');
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $this->createForm(ContactChannelsType::class, [], [
|
||||
'action' => $route,
|
||||
'channels' => $this->contactModel->getPreferenceChannels(),
|
||||
'public_view' => false,
|
||||
'save_button' => true,
|
||||
])->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Batch/channel.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contact_index',
|
||||
'mauticContent' => 'leadBatch',
|
||||
'route' => $route,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ChannelBundle\Entity\Channel;
|
||||
use Mautic\ChannelBundle\Model\MessageModel;
|
||||
use Mautic\CoreBundle\Controller\AbstractStandardFormController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\FormBundle\Helper\FormFieldHelper;
|
||||
use Mautic\LeadBundle\Controller\EntityContactsTrait;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class MessageController extends AbstractStandardFormController
|
||||
{
|
||||
use EntityContactsTrait;
|
||||
|
||||
public function __construct(
|
||||
FormFactoryInterface $formFactory,
|
||||
FormFieldHelper $fieldHelper,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
private RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
parent::__construct($formFactory, $fieldHelper, $doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function batchDeleteAction(Request $request)
|
||||
{
|
||||
return $this->batchDeleteStandard($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\CoreBundle\Controller\Response|\Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function cloneAction(Request $request, $objectId)
|
||||
{
|
||||
return $this->cloneStandard($request, $objectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $ignorePost
|
||||
*
|
||||
* @return \Mautic\CoreBundle\Controller\Response|\Symfony\Component\HttpFoundation\JsonResponse
|
||||
*/
|
||||
public function editAction(Request $request, $objectId, $ignorePost = false)
|
||||
{
|
||||
return $this->editStandard($request, $objectId, $ignorePost);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*/
|
||||
public function indexAction(Request $request, $page = 1): Response
|
||||
{
|
||||
return $this->indexStandard($request, $page);
|
||||
}
|
||||
|
||||
public function newAction(Request $request): Response
|
||||
{
|
||||
return $this->newStandard($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|\Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function viewAction(Request $request, $objectId)
|
||||
{
|
||||
return $this->viewStandard($request, $objectId, 'message', 'channel');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function getViewArguments(array $args, $action): array
|
||||
{
|
||||
/** @var MessageModel $model */
|
||||
$model = $this->getModel($this->getModelName());
|
||||
$viewParameters = [];
|
||||
switch ($action) {
|
||||
case 'index':
|
||||
$viewParameters = [
|
||||
'headerTitle' => $this->translator->trans('mautic.channel.messages'),
|
||||
'listHeaders' => [
|
||||
[
|
||||
'text' => 'mautic.core.channels',
|
||||
'class' => 'visible-md visible-lg',
|
||||
],
|
||||
],
|
||||
'listItemTemplate' => '@MauticChannel/Message/list_item.html.twig',
|
||||
'enableCloneButton' => true,
|
||||
];
|
||||
|
||||
break;
|
||||
case 'view':
|
||||
$message = $args['viewParameters']['item'];
|
||||
|
||||
// Init the date range filter form
|
||||
$returnUrl = $this->generateUrl(
|
||||
'mautic_message_action',
|
||||
[
|
||||
'objectAction' => 'view',
|
||||
'objectId' => $message->getId(),
|
||||
]
|
||||
);
|
||||
|
||||
[$dateFrom, $dateTo] = $this->getViewDateRange($this->requestStack->getCurrentRequest(), $message->getId(), $returnUrl, 'local', $dateRangeForm);
|
||||
$chart = new LineChart(null, $dateFrom, $dateTo);
|
||||
|
||||
/** @var Channel[] $channels */
|
||||
$channels = $model->getChannels();
|
||||
$messageChannels = $message->getChannels();
|
||||
$chart->setDataset(
|
||||
$this->translator->trans('mautic.core.all'),
|
||||
$model->getLeadStatsPost($message->getId(), $dateFrom, $dateTo)
|
||||
);
|
||||
|
||||
$messagedLeads = [
|
||||
'all' => $this->forward(
|
||||
'Mautic\ChannelBundle\Controller\MessageController::contactsAction',
|
||||
[
|
||||
'objectId' => $message->getId(),
|
||||
'page' => $this->requestStack->getCurrentRequest()->getSession()->get('mautic.'.$this->getSessionBase('all').'.contact.page', 1),
|
||||
'ignoreAjax' => true,
|
||||
'channel' => 'all',
|
||||
]
|
||||
)->getContent(),
|
||||
];
|
||||
|
||||
foreach ($messageChannels as $channel) {
|
||||
if ($channel->isEnabled() && isset($channels[$channel->getChannel()])) {
|
||||
$chart->setDataset(
|
||||
$channels[$channel->getChannel()]['label'],
|
||||
$model->getLeadStatsPost($message->getId(), $dateFrom, $dateTo, $channel->getChannel())
|
||||
);
|
||||
|
||||
$messagedLeads[$channel->getChannel()] = $this->forward(
|
||||
'Mautic\ChannelBundle\Controller\MessageController::contactsAction',
|
||||
[
|
||||
'objectId' => $message->getId(),
|
||||
'page' => $this->requestStack->getCurrentRequest()->getSession()->get(
|
||||
'mautic.'.$this->getSessionBase($channel->getChannel()).'.contact.page',
|
||||
1
|
||||
),
|
||||
'ignoreAjax' => true,
|
||||
'channel' => $channel->getChannel(),
|
||||
]
|
||||
)->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$viewParameters = [
|
||||
'channels' => $channels,
|
||||
'channelContents' => $model->getMessageChannels($message->getId()),
|
||||
'dateRangeForm' => $dateRangeForm->createView(),
|
||||
'eventCounts' => $chart->render(),
|
||||
'messagedLeads' => $messagedLeads,
|
||||
];
|
||||
break;
|
||||
case 'new':
|
||||
case 'edit':
|
||||
$viewParameters = [
|
||||
'channels' => $model->getChannels(),
|
||||
];
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$args['viewParameters'] = array_merge($args['viewParameters'], $viewParameters);
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
return $this->deleteStandard($request, $objectId);
|
||||
}
|
||||
|
||||
protected function getTemplateBase(): string
|
||||
{
|
||||
return '@MauticChannel/Message';
|
||||
}
|
||||
|
||||
protected function getFormView(FormInterface $form, $view): FormView
|
||||
{
|
||||
return $form->createView();
|
||||
}
|
||||
|
||||
protected function getJsLoadMethodPrefix(): string
|
||||
{
|
||||
return 'messages';
|
||||
}
|
||||
|
||||
protected function getModelName(): string
|
||||
{
|
||||
return 'channel.message';
|
||||
}
|
||||
|
||||
protected function getRouteBase(): string
|
||||
{
|
||||
return 'message';
|
||||
}
|
||||
|
||||
/***
|
||||
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getSessionBase($objectId = null): string
|
||||
{
|
||||
return 'message'.(($objectId) ? '.'.$objectId : '');
|
||||
}
|
||||
|
||||
protected function getTranslationBase(): string
|
||||
{
|
||||
return 'mautic.channel.message';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function contactsAction(
|
||||
Request $request,
|
||||
PageHelperFactoryInterface $pageHelperFactory,
|
||||
$objectId,
|
||||
$channel,
|
||||
$page = 1,
|
||||
) {
|
||||
$filter = [];
|
||||
if ('all' !== $channel) {
|
||||
$returnUrl = $this->generateUrl(
|
||||
'mautic_message_action',
|
||||
[
|
||||
'objectAction' => 'view',
|
||||
'objectId' => $objectId,
|
||||
]
|
||||
);
|
||||
[$dateFrom, $dateTo] = $this->getViewDateRange($request, $objectId, $returnUrl, 'UTC');
|
||||
|
||||
$filter = [
|
||||
'channel' => $channel,
|
||||
[
|
||||
'col' => 'entity.date_triggered',
|
||||
'expr' => 'between',
|
||||
'val' => [
|
||||
$dateFrom->format('Y-m-d H:i:s'),
|
||||
$dateTo->format('Y-m-d H:i:s'),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->generateContactsGrid(
|
||||
$request,
|
||||
$pageHelperFactory,
|
||||
$objectId,
|
||||
$page,
|
||||
'channel:messages:view',
|
||||
'message.'.$channel,
|
||||
'campaign_lead_event_log',
|
||||
$channel,
|
||||
null,
|
||||
$filter,
|
||||
[
|
||||
[
|
||||
'type' => 'join',
|
||||
'from_alias' => 'entity',
|
||||
'table' => 'campaign_events',
|
||||
'alias' => 'event',
|
||||
'condition' => "entity.event_id = event.id and event.channel = 'channel.message' and event.channel_id = ".(int) $objectId,
|
||||
],
|
||||
],
|
||||
null,
|
||||
[
|
||||
'channel' => $channel ?: 'all',
|
||||
],
|
||||
'.message-'.$channel
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticChannelExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\CommonEntity;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('channel:messages:viewown')"),
|
||||
new Post(security: "is_granted('channel:messages:create')"),
|
||||
new Get(security: "is_granted('channel:messages:viewown')"),
|
||||
new Put(security: "is_granted('channel:messages:editown')"),
|
||||
new Patch(security: "is_granted('channel:messages:editother')"),
|
||||
new Delete(security: "is_granted('channel:messages:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['channel:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['message'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['channel:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Channel extends CommonEntity implements UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['channel:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['channel:read', 'channel:write', 'message:read'])]
|
||||
private $channel;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
#[Groups(['channel:read', 'channel:write'])]
|
||||
private $channelId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['channel:read', 'message:read'])]
|
||||
private $channelName;
|
||||
|
||||
/**
|
||||
* @var Message
|
||||
*/
|
||||
#[Groups(['channel:read', 'channel:write'])]
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[Groups(['channel:read', 'channel:write'])]
|
||||
private $properties = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['channel:read', 'channel:write', 'message:read'])]
|
||||
private $isEnabled = false;
|
||||
|
||||
public static function loadMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('message_channels')
|
||||
->addIndex(['channel', 'channel_id'], 'channel_entity_index')
|
||||
->addIndex(['channel', 'is_enabled'], 'channel_enabled_index')
|
||||
->addUniqueConstraint(['message_id', 'channel'], 'channel_index');
|
||||
|
||||
$builder
|
||||
->addId()
|
||||
->addField('channel', 'string')
|
||||
->addNamedField('channelId', 'integer', 'channel_id', true)
|
||||
->addField('properties', Types::JSON)
|
||||
->createField('isEnabled', 'boolean')
|
||||
->columnName('is_enabled')
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('message', Message::class)
|
||||
->addJoinColumn('message_id', 'id', false, false, 'CASCADE')
|
||||
->inversedBy('channels')
|
||||
->build();
|
||||
|
||||
static::addUuidField($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('messageChannel')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'channel',
|
||||
'channelId',
|
||||
'channelName',
|
||||
'isEnabled',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'properties',
|
||||
'message',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*
|
||||
* @return Channel
|
||||
*/
|
||||
public function setChannel($channel)
|
||||
{
|
||||
$this->channel = $channel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getChannelId()
|
||||
{
|
||||
return $this->channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $channelId
|
||||
*
|
||||
* @return Channel
|
||||
*/
|
||||
public function setChannelId($channelId)
|
||||
{
|
||||
if (empty($channelId)) {
|
||||
$channelId = null;
|
||||
}
|
||||
|
||||
$this->channelId = $channelId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getChannelName()
|
||||
{
|
||||
return $this->channelName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channelName
|
||||
*
|
||||
* @return Channel
|
||||
*/
|
||||
public function setChannelName($channelName)
|
||||
{
|
||||
$this->channelName = $channelName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Message
|
||||
*/
|
||||
public function getMessage()
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Channel
|
||||
*/
|
||||
public function setMessage(Message $message)
|
||||
{
|
||||
$this->message = $message;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getProperties()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Channel
|
||||
*/
|
||||
public function setProperties(array $properties)
|
||||
{
|
||||
$this->properties = $properties;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return $this->isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isEnabled
|
||||
*
|
||||
* @return Channel
|
||||
*/
|
||||
public function setIsEnabled($isEnabled)
|
||||
{
|
||||
$this->isEnabled = $isEnabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\ProjectBundle\Entity\ProjectTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata as ValidationClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('channel:messages:viewown')"),
|
||||
new Post(security: "is_granted('channel:messages:create')"),
|
||||
new Get(security: "is_granted('channel:messages:viewown')"),
|
||||
new Put(security: "is_granted('channel:messages:editown')"),
|
||||
new Patch(security: "is_granted('channel:messages:editother')"),
|
||||
new Delete(security: "is_granted('channel:messages:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['message:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category', 'channels'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['message:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Message extends FormEntity implements UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
use ProjectTrait;
|
||||
|
||||
/**
|
||||
* @var ?int
|
||||
*/
|
||||
#[Groups(['message:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['message:read', 'message:write', 'channel:read'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
#[Groups(['message:read', 'message:write'])]
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var ?\DateTimeInterface
|
||||
*/
|
||||
#[Groups(['message:read', 'message:write'])]
|
||||
private $publishUp;
|
||||
|
||||
/**
|
||||
* @var ?\DateTimeInterface
|
||||
*/
|
||||
#[Groups(['message:read', 'message:write'])]
|
||||
private $publishDown;
|
||||
|
||||
/**
|
||||
* @var ?Category
|
||||
*/
|
||||
#[Groups(['message:read', 'message:write'])]
|
||||
private $category;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int,Channel>
|
||||
*/
|
||||
#[Groups(['message:read', 'message:write'])]
|
||||
private $channels;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
}
|
||||
|
||||
public static function loadMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('messages')
|
||||
->setCustomRepositoryClass(MessageRepository::class)
|
||||
->addIndex(['date_added'], 'date_message_added');
|
||||
|
||||
$builder
|
||||
->addIdColumns()
|
||||
->addPublishDates()
|
||||
->addCategory();
|
||||
|
||||
$builder->createOneToMany('channels', Channel::class)
|
||||
->setIndexBy('channel')
|
||||
->orphanRemoval()
|
||||
->mappedBy('message')
|
||||
->cascadeMerge()
|
||||
->cascadePersist()
|
||||
->cascadeDetach()
|
||||
->build();
|
||||
|
||||
static::addUuidField($builder);
|
||||
self::addProjectsField($builder, 'message_projects_xref', 'message_id');
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ValidationClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('name', new NotBlank([
|
||||
'message' => 'mautic.core.name.required',
|
||||
]));
|
||||
}
|
||||
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('message')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'publishUp',
|
||||
'publishDown',
|
||||
'channels',
|
||||
'category',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'message');
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->channels = new ArrayCollection();
|
||||
$this->initializeProjects();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $name
|
||||
*
|
||||
* @return Message
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $description
|
||||
*
|
||||
* @return Message
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->isChanged('description', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?\DateTimeInterface
|
||||
*/
|
||||
public function getPublishUp()
|
||||
{
|
||||
return $this->publishUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?\DateTime $publishUp
|
||||
*
|
||||
* @return Message
|
||||
*/
|
||||
public function setPublishUp($publishUp)
|
||||
{
|
||||
$this->isChanged('publishUp', $publishUp);
|
||||
$this->publishUp = $publishUp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?\DateTimeInterface
|
||||
*/
|
||||
public function getPublishDown()
|
||||
{
|
||||
return $this->publishDown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?\DateTime $publishDown
|
||||
*
|
||||
* @return Message
|
||||
*/
|
||||
public function setPublishDown($publishDown)
|
||||
{
|
||||
$this->isChanged('publishDown', $publishDown);
|
||||
$this->publishDown = $publishDown;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?Category
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?Category $category
|
||||
*
|
||||
* @return Message
|
||||
*/
|
||||
public function setCategory($category)
|
||||
{
|
||||
$this->isChanged('category', $category);
|
||||
$this->category = $category;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection<int,Channel>
|
||||
*/
|
||||
public function getChannels()
|
||||
{
|
||||
return $this->channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArrayCollection<int,Channel> $channels
|
||||
*
|
||||
* @return Message
|
||||
*/
|
||||
public function setChannels($channels)
|
||||
{
|
||||
$this->isChanged('channels', $channels);
|
||||
$this->channels = $channels;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addChannel(Channel $channel): void
|
||||
{
|
||||
if (!$this->channels->contains($channel)) {
|
||||
$channel->setMessage($this);
|
||||
$this->isChanged('channels', $channel);
|
||||
|
||||
$this->channels[$channel->getChannel()] = $channel;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeChannel(Channel $channel): void
|
||||
{
|
||||
if ($channel->getId()) {
|
||||
$this->isChanged('channels', $channel->getId());
|
||||
}
|
||||
$this->channels->removeElement($channel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class MessageQueue
|
||||
{
|
||||
public const STATUS_RESCHEDULED = 'rescheduled';
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
|
||||
public const STATUS_SENT = 'sent';
|
||||
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
public const PRIORITY_NORMAL = 2;
|
||||
|
||||
public const PRIORITY_HIGH = 1;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $channel;
|
||||
|
||||
private $channelId;
|
||||
|
||||
/**
|
||||
* @var Event|null
|
||||
*/
|
||||
private $event;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $priority = 2;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $maxAttempts = 3;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $attempts = 0;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $success = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $status = self::STATUS_PENDING;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
**/
|
||||
private $datePublished;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
private $scheduledDate;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
private $lastAttempt;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
private $dateSent;
|
||||
|
||||
private $options = [];
|
||||
|
||||
/**
|
||||
* Used by listeners to note if the message had been processed in bulk.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $processed = false;
|
||||
|
||||
/**
|
||||
* Used by listeners to tell the event dispatcher the message needs to be retried in 15 minutes.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $failed = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $metadataUpdated = false;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('message_queue')
|
||||
->setCustomRepositoryClass(MessageQueueRepository::class)
|
||||
->addIndex(['status'], 'message_status_search')
|
||||
->addIndex(['date_sent'], 'message_date_sent')
|
||||
->addIndex(['scheduled_date'], 'message_scheduled_date')
|
||||
->addIndex(['priority'], 'message_priority')
|
||||
->addIndex(['success'], 'message_success')
|
||||
->addIndex(['channel', 'channel_id'], 'message_channel_search')
|
||||
->addIndex(['date_published'], 'message_queue_date_published');
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->addField('channel', 'string');
|
||||
$builder->addNamedField('channelId', 'integer', 'channel_id');
|
||||
|
||||
$builder->createManyToOne('event', Event::class)
|
||||
->addJoinColumn('event_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false);
|
||||
|
||||
$builder->createField('priority', 'smallint')
|
||||
->columnName('priority')
|
||||
->build();
|
||||
|
||||
$builder->createField('maxAttempts', 'smallint')
|
||||
->columnName('max_attempts')
|
||||
->build();
|
||||
|
||||
$builder->createField('attempts', 'smallint')
|
||||
->columnName('attempts')
|
||||
->build();
|
||||
|
||||
$builder->createField('success', 'boolean')
|
||||
->columnName('success')
|
||||
->build();
|
||||
|
||||
$builder->createField('status', 'string')
|
||||
->columnName('status')
|
||||
->build();
|
||||
|
||||
$builder->createField('datePublished', 'datetime')
|
||||
->columnName('date_published')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('scheduledDate', 'datetime')
|
||||
->columnName('scheduled_date')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('lastAttempt', 'datetime')
|
||||
->columnName('last_attempt')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('dateSent', 'datetime')
|
||||
->columnName('date_sent')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('options', 'array')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getAttempts()
|
||||
{
|
||||
return $this->attempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $attempts
|
||||
*/
|
||||
public function setAttempts($attempts): void
|
||||
{
|
||||
$this->attempts = $attempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
*/
|
||||
public function setOptions($options): void
|
||||
{
|
||||
$this->options[] = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*/
|
||||
public function setChannel($channel): void
|
||||
{
|
||||
$this->channel = $channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getChannelId()
|
||||
{
|
||||
return $this->channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $channelId
|
||||
*
|
||||
* @return MessageQueue
|
||||
*/
|
||||
public function setChannelId($channelId)
|
||||
{
|
||||
$this->channelId = $channelId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Event|null
|
||||
*/
|
||||
public function getEvent()
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MessageQueue
|
||||
*/
|
||||
public function setEvent(Event $event)
|
||||
{
|
||||
$this->event = $event;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDatePublished()
|
||||
{
|
||||
return $this->datePublished;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $datePublished
|
||||
*/
|
||||
public function setDatePublished($datePublished): void
|
||||
{
|
||||
$this->datePublished = $datePublished;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateSent()
|
||||
{
|
||||
return $this->dateSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateSent
|
||||
*/
|
||||
public function setDateSent($dateSent): void
|
||||
{
|
||||
$this->dateSent = $dateSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getLastAttempt()
|
||||
{
|
||||
return $this->lastAttempt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $lastAttempt
|
||||
*/
|
||||
public function setLastAttempt($lastAttempt): void
|
||||
{
|
||||
$this->lastAttempt = $lastAttempt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
public function setLead(Lead $lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMaxAttempts()
|
||||
{
|
||||
return $this->maxAttempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $maxAttempts
|
||||
*/
|
||||
public function setMaxAttempts($maxAttempts): void
|
||||
{
|
||||
$this->maxAttempts = $maxAttempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority()
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $priority
|
||||
*/
|
||||
public function setPriority($priority): void
|
||||
{
|
||||
$this->priority = $priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getScheduledDate()
|
||||
{
|
||||
return $this->scheduledDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $scheduledDate
|
||||
*/
|
||||
public function setScheduledDate($scheduledDate): void
|
||||
{
|
||||
$this->scheduledDate = $scheduledDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getStatus()
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $status
|
||||
*/
|
||||
public function setStatus($status): void
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getSuccess()
|
||||
{
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSuccess()
|
||||
{
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $success
|
||||
*/
|
||||
public function setSuccess($success = true): void
|
||||
{
|
||||
$this->success = $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFailed()
|
||||
{
|
||||
return $this->failed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $failed
|
||||
*
|
||||
* @return MessageQueue
|
||||
*/
|
||||
public function setFailed($failed = true)
|
||||
{
|
||||
$this->failed = $failed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isProcessed()
|
||||
{
|
||||
return $this->processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $processed
|
||||
*
|
||||
* @return MessageQueue
|
||||
*/
|
||||
public function setProcessed($processed = true)
|
||||
{
|
||||
$this->processed = $processed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function getMetadata()
|
||||
{
|
||||
return $this->options['metadata'] ?? [];
|
||||
}
|
||||
|
||||
public function setMetadata(array $metadata = []): void
|
||||
{
|
||||
$this->metadataUpdated = true;
|
||||
$this->options['metadata'] = $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasMetadataUpdated()
|
||||
{
|
||||
return $this->metadataUpdated;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\LeadBundle\Entity\TimelineTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<MessageQueue>
|
||||
*/
|
||||
class MessageQueueRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
public function findMessage($channel, $channelId, $leadId)
|
||||
{
|
||||
$results = $this->createQueryBuilder('mq')
|
||||
->where('IDENTITY(mq.lead) = :leadId')
|
||||
->andWhere('mq.channel = :channel')
|
||||
->andWhere('mq.channelId = :channelId')
|
||||
->setParameter('leadId', $leadId)
|
||||
->setParameter('channel', $channel)
|
||||
->setParameter('channelId', $channelId)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
return ($results) ? $results[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, MessageQueue>
|
||||
*/
|
||||
public function getQueuedMessages($limit, $processStarted, $channel = null, $channelId = null)
|
||||
{
|
||||
$q = $this->createQueryBuilder('mq');
|
||||
|
||||
$q->where($q->expr()->eq('mq.success', ':success'))
|
||||
->andWhere($q->expr()->lt('mq.attempts', 'mq.maxAttempts'))
|
||||
->andWhere('mq.lastAttempt is null or mq.lastAttempt < :processStarted')
|
||||
->andWhere('mq.scheduledDate <= :processStarted')
|
||||
->setParameter('success', false, 'boolean')
|
||||
->setParameter('processStarted', $processStarted)
|
||||
->indexBy('mq', 'mq.id');
|
||||
|
||||
$q->orderBy('mq.priority, mq.scheduledDate', Order::Ascending->value);
|
||||
|
||||
if ($limit) {
|
||||
$q->setMaxResults((int) $limit);
|
||||
}
|
||||
|
||||
if ($channel) {
|
||||
$q->andWhere($q->expr()->eq('mq.channel', ':channel'))
|
||||
->setParameter('channel', $channel);
|
||||
|
||||
if ($channelId) {
|
||||
$q->andWhere($q->expr()->eq('mq.channelId', (int) $channelId));
|
||||
}
|
||||
}
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function getQueuedChannelCount($channel, ?array $ids = null): int
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->eq($this->getTableAlias().'.channel', ':channel'),
|
||||
$q->expr()->neq($this->getTableAlias().'.status', ':status')
|
||||
);
|
||||
|
||||
if (!empty($ids)) {
|
||||
$expr = $expr->with(
|
||||
$q->expr()->in($this->getTableAlias().'.channel_id', $ids)
|
||||
);
|
||||
}
|
||||
|
||||
return (int) $q->select('count(*)')
|
||||
->from(MAUTIC_TABLE_PREFIX.'message_queue', $this->getTableAlias())
|
||||
->where($expr)
|
||||
->setParameter('channel', $channel)
|
||||
->setParameter('status', MessageQueue::STATUS_SENT)
|
||||
->setParameter('ids', $ids, ArrayParameterType::INTEGER)
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lead's point log.
|
||||
*
|
||||
* @param int|null $leadId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLeadTimelineEvents($leadId = null, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->from(MAUTIC_TABLE_PREFIX.'message_queue', 'mq')
|
||||
->select('mq.id, mq.lead_id, mq.channel as channelName, mq.channel_id as channelId,
|
||||
mq.priority as priority, mq.attempts, mq.success, mq.status, mq.date_published as dateAdded,
|
||||
mq.scheduled_date as scheduledDate, mq.last_attempt as lastAttempt, mq.date_sent as dateSent');
|
||||
|
||||
if ($leadId) {
|
||||
$query->where('mq.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere(
|
||||
$query->expr()->like('mq.channel', ':search')
|
||||
)->setParameter('search', '%'.$options['search'].'%');
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($query, $options, 'mq.channel', 'mq.date_published', [], ['dateAdded'], null, 'mq.id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Entity;
|
||||
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Message>
|
||||
*/
|
||||
class MessageRepository extends CommonRepository
|
||||
{
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @return \Doctrine\ORM\Tools\Pagination\Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$qb = $this->createQueryBuilder($this->getTableAlias());
|
||||
// Because of this inner join pagination is not working properly. Removing this doesn't seem to break any feature.
|
||||
// $qb->join(Channel::class, 'channel', 'WITH', 'channel.message = '.$this->getTableAlias().'.id');
|
||||
$qb->leftJoin(Category::class, 'cat', 'WITH', 'cat.id = '.$this->getTableAlias().'.category');
|
||||
$qb->groupBy($this->getTableAlias().'.id');
|
||||
|
||||
$args['qb'] = $qb;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'm';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $search
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMessageList($search = '', $limit = 10, $start = 0)
|
||||
{
|
||||
$alias = $this->getTableAlias();
|
||||
$q = $this->createQueryBuilder($this->getTableAlias());
|
||||
$q->select('partial '.$alias.'.{id, name, description}');
|
||||
|
||||
if (!empty($search)) {
|
||||
if (is_array($search)) {
|
||||
$search = array_map('intval', $search);
|
||||
$q->andWhere($q->expr()->in($alias.'.id', ':search'))
|
||||
->setParameter('search', $search);
|
||||
} else {
|
||||
$q->andWhere($q->expr()->like($alias.'.name', ':search'))
|
||||
->setParameter('search', "%{$search}%");
|
||||
}
|
||||
}
|
||||
$q->andWhere($q->expr()->eq($alias.'.isPublished', true));
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
public function getMessageChannels($messageId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->from(MAUTIC_TABLE_PREFIX.'message_channels', 'mc')
|
||||
->select('id, channel, channel_id, properties')
|
||||
->where($q->expr()->eq('message_id', ':messageId'))
|
||||
->setParameter('messageId', $messageId)
|
||||
->andWhere($q->expr()->eq('is_enabled', true));
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$channels = [];
|
||||
foreach ($results as $result) {
|
||||
$result['properties'] = json_decode($result['properties'], true);
|
||||
$channels[$result['channel']] = $result;
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getChannelMessageByChannelId($channelId)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->from(MAUTIC_TABLE_PREFIX.'message_channels', 'mc')
|
||||
->select('id, channel, channel_id, properties, message_id')
|
||||
->where($q->expr()->eq('id', ':channelId'))
|
||||
->setParameter('channelId', $channelId)
|
||||
->andWhere($q->expr()->eq('is_enabled', true));
|
||||
|
||||
return $q->executeQuery()->fetchAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $filter
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
return match ($filter->command) {
|
||||
$this->translator->trans('mautic.project.searchcommand.name'),
|
||||
$this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US') => $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'message_id',
|
||||
'message_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
),
|
||||
default => $this->addStandardSearchCommandWhereClause($q, $filter),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
return array_merge([
|
||||
'mautic.core.searchcommand.ispublished',
|
||||
'mautic.core.searchcommand.isunpublished',
|
||||
'mautic.core.searchcommand.ismine',
|
||||
'mautic.core.searchcommand.isuncategorized',
|
||||
'mautic.project.searchcommand.name',
|
||||
], parent::getSearchCommands());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Event;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class ChannelBroadcastEvent extends Event
|
||||
{
|
||||
/**
|
||||
* Number of contacts successfully processed and/or failed per channel.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $results = [];
|
||||
|
||||
/**
|
||||
* Min contact ID filter can be used for process parallelization.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $minContactIdFilter;
|
||||
|
||||
/**
|
||||
* Max contact ID filter can be used for process parallelization.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $maxContactIdFilter;
|
||||
|
||||
/**
|
||||
* How many contacts to load from the database.
|
||||
*/
|
||||
private int $limit = 100;
|
||||
|
||||
/**
|
||||
* How big batches to use to actually send.
|
||||
*/
|
||||
private int $batch = 50;
|
||||
|
||||
private ?int $maxThreads = null;
|
||||
|
||||
private ?int $threadId = null;
|
||||
|
||||
public function __construct(
|
||||
/**
|
||||
* Specific channel.
|
||||
*/
|
||||
protected ?string $channel,
|
||||
/**
|
||||
* Specific ID of a specific channel.
|
||||
*/
|
||||
protected string|int|null $id,
|
||||
protected OutputInterface $output,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channelLabel
|
||||
* @param int $successCount
|
||||
* @param int $failedCount
|
||||
*/
|
||||
public function setResults($channelLabel, $successCount, $failedCount = 0, array $failedRecipientsByList = []): void
|
||||
{
|
||||
$this->results[$channelLabel] = [
|
||||
'success' => (int) $successCount,
|
||||
'failed' => (int) $failedCount,
|
||||
'failedRecipientsByList' => $failedRecipientsByList,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getResults()
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
public function checkContext($channel): bool
|
||||
{
|
||||
if ($this->channel && $this->channel !== $channel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OutputInterface
|
||||
*/
|
||||
public function getOutput()
|
||||
{
|
||||
return $this->output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $minContactIdFilter
|
||||
*/
|
||||
public function setMinContactIdFilter($minContactIdFilter): void
|
||||
{
|
||||
$this->minContactIdFilter = $minContactIdFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getMinContactIdFilter()
|
||||
{
|
||||
return $this->minContactIdFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $maxContactIdFilter
|
||||
*/
|
||||
public function setMaxContactIdFilter($maxContactIdFilter): void
|
||||
{
|
||||
$this->maxContactIdFilter = $maxContactIdFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getMaxContactIdFilter()
|
||||
{
|
||||
return $this->maxContactIdFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $limit
|
||||
*/
|
||||
public function setLimit($limit): void
|
||||
{
|
||||
$this->limit = $limit;
|
||||
}
|
||||
|
||||
public function getLimit(): int
|
||||
{
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $batch
|
||||
*/
|
||||
public function setBatch($batch): void
|
||||
{
|
||||
$this->batch = $batch;
|
||||
}
|
||||
|
||||
public function getBatch(): int
|
||||
{
|
||||
return $this->batch;
|
||||
}
|
||||
|
||||
public function getMaxThreads(): ?int
|
||||
{
|
||||
return $this->maxThreads;
|
||||
}
|
||||
|
||||
public function setMaxThreads(?int $maxThreads): void
|
||||
{
|
||||
$this->maxThreads = $maxThreads;
|
||||
}
|
||||
|
||||
public function getThreadId(): ?int
|
||||
{
|
||||
return $this->threadId;
|
||||
}
|
||||
|
||||
public function setThreadId(?int $threadId): void
|
||||
{
|
||||
$this->threadId = $threadId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Event;
|
||||
|
||||
use Mautic\ChannelBundle\Model\MessageModel;
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
|
||||
class ChannelEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $channels = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $featureChannels = [];
|
||||
|
||||
/**
|
||||
* Adds a submit action to the list of available actions.
|
||||
*
|
||||
* @param string $channel a unique identifier; it is recommended that it be namespaced if there are multiple entities in a channel i.e. something.something
|
||||
* @param array $config Should be keyed by the feature it supports that contains an array of feature configuration options. i.e.
|
||||
* $config = [
|
||||
* MessageModel::CHANNEL_FEATURE => [
|
||||
* 'lookupFormType' => (optional) Form type class/alias for the channel lookup list,
|
||||
* 'propertiesFormType' => (optional) Form type class/alias for the channel properties if a lookup list is not used,
|
||||
*
|
||||
* 'channelTemplate' => (optional) template to inject UI/DOM into the bottom of the channel's tab
|
||||
* 'formTheme' => (optional) theme directory for custom form types
|
||||
*
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addChannel($channel, array $config = [])
|
||||
{
|
||||
$this->channels[$channel] = $config;
|
||||
|
||||
foreach ($config as $feature => $featureConfig) {
|
||||
$this->featureChannels[$feature][$channel] = $featureConfig;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns registered channels with their configs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getChannelConfigs()
|
||||
{
|
||||
return $this->channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns repository name for the provided channel. Defaults to classic naming convention.
|
||||
*
|
||||
* @param string $channel
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRepositoryName($channel)
|
||||
{
|
||||
if (isset($this->channels[$channel][MessageModel::CHANNEL_FEATURE]['repository'])) {
|
||||
return $this->channels[$channel][MessageModel::CHANNEL_FEATURE]['repository'];
|
||||
}
|
||||
|
||||
// if not defined, try the classic naming convention
|
||||
$channel = ucfirst($channel);
|
||||
$class = "\Mautic\\{$channel}Bundle\Entity\\{$channel}";
|
||||
\assert(class_exists($class));
|
||||
|
||||
return $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the column holding the channel name for the provided channel. Defaults to 'name'.
|
||||
*
|
||||
* @param string $channel
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNameColumn($channel)
|
||||
{
|
||||
return $this->channels[$channel][MessageModel::CHANNEL_FEATURE]['nameColumn'] ?? 'name';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFeatureChannels()
|
||||
{
|
||||
return $this->featureChannels;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Event;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\Message;
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
|
||||
class MessageEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @param bool $isNew
|
||||
*/
|
||||
public function __construct(Message $message, $isNew = false)
|
||||
{
|
||||
$this->entity = $message;
|
||||
$this->isNew = $isNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Message
|
||||
*/
|
||||
public function getMessage()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Event;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class MessageQueueBatchProcessEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @param MessageQueue[] $messages
|
||||
*/
|
||||
public function __construct(
|
||||
private array $messages,
|
||||
private $channel,
|
||||
private $channelId,
|
||||
) {
|
||||
}
|
||||
|
||||
public function checkContext($channel): bool
|
||||
{
|
||||
return $channel === $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMessages()
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getChannelId()
|
||||
{
|
||||
return $this->channelId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Event;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
|
||||
class MessageQueueEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @param bool $isNew
|
||||
*/
|
||||
public function __construct(MessageQueue $entity, $isNew = false)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
$this->isNew = $isNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MessageQueue
|
||||
*/
|
||||
public function getMessageQueue()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MessageQueue $entity
|
||||
*/
|
||||
public function setMessageQueue($entity): void
|
||||
{
|
||||
$this->entity = $entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Event;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
|
||||
class MessageQueueProcessEvent extends CommonEvent
|
||||
{
|
||||
public function __construct(MessageQueue $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MessageQueue
|
||||
*/
|
||||
public function getMessageQueue()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
public function checkContext($channel): bool
|
||||
{
|
||||
return $channel === $this->entity->getChannel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\Event\CustomButtonEvent;
|
||||
use Mautic\CoreBundle\Twig\Helper\ButtonHelper;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ButtonSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private RouterInterface $router,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::VIEW_INJECT_CUSTOM_BUTTONS => ['injectContactBulkButtons', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function injectContactBulkButtons(CustomButtonEvent $event): void
|
||||
{
|
||||
if (str_starts_with($event->getRoute(), 'mautic_contact_')) {
|
||||
$event->addButton(
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'btn btn-ghost btn-sm btn-nospin',
|
||||
'data-toggle' => 'ajaxmodal',
|
||||
'data-target' => '#MauticSharedModal',
|
||||
'href' => $this->router->generate('mautic_channel_batch_contact_view'),
|
||||
'data-header' => $this->translator->trans('mautic.lead.batch.channels'),
|
||||
],
|
||||
'btnText' => $this->translator->trans('mautic.lead.batch.channels'),
|
||||
'iconClass' => 'ri-remote-control-line',
|
||||
],
|
||||
ButtonHelper::LOCATION_BULK_ACTIONS
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\EventListener;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
|
||||
use Mautic\CampaignBundle\Event\PendingEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\EventCollector;
|
||||
use Mautic\CampaignBundle\Executioner\Dispatcher\ActionDispatcher;
|
||||
use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException;
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Form\Type\MessageSendType;
|
||||
use Mautic\ChannelBundle\Model\MessageModel;
|
||||
use Mautic\ChannelBundle\PreferenceBuilder\PreferenceBuilder;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class CampaignSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private ?Event $pseudoEvent = null;
|
||||
|
||||
private ?ArrayCollection $mmLogs = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $messageChannels = [];
|
||||
|
||||
public function __construct(
|
||||
private MessageModel $messageModel,
|
||||
private ActionDispatcher $actionDispatcher,
|
||||
private EventCollector $eventCollector,
|
||||
private LoggerInterface $logger,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0],
|
||||
ChannelEvents::ON_CAMPAIGN_BATCH_ACTION => ['onCampaignTriggerAction', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onCampaignBuild(CampaignBuilderEvent $event): void
|
||||
{
|
||||
$channels = $this->messageModel->getChannels();
|
||||
$decisions = [];
|
||||
foreach ($channels as $channel) {
|
||||
if (isset($channel['campaignDecisionsSupported'])) {
|
||||
$decisions = $decisions + $channel['campaignDecisionsSupported'];
|
||||
}
|
||||
}
|
||||
|
||||
$action = [
|
||||
'label' => 'mautic.channel.message.send.marketing.message',
|
||||
'description' => 'mautic.channel.message.send.marketing.message.descr',
|
||||
'batchEventName' => ChannelEvents::ON_CAMPAIGN_BATCH_ACTION,
|
||||
'formType' => MessageSendType::class,
|
||||
'channel' => 'channel.message',
|
||||
'channelIdField' => 'marketingMessage',
|
||||
'connectionRestrictions' => [
|
||||
'target' => [
|
||||
'decision' => $decisions,
|
||||
],
|
||||
],
|
||||
'timelineTemplate' => '@MauticChannel/SubscribedEvents/Timeline/index.html.twig',
|
||||
'timelineTemplateVars' => [
|
||||
'messageSettings' => $channels,
|
||||
],
|
||||
];
|
||||
$event->addAction('message.send', $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException
|
||||
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function onCampaignTriggerAction(PendingEvent $pendingEvent): void
|
||||
{
|
||||
$this->pseudoEvent = clone $pendingEvent->getEvent();
|
||||
$this->pseudoEvent->setCampaign($pendingEvent->getEvent()->getCampaign());
|
||||
|
||||
$this->mmLogs = $pendingEvent->getPending();
|
||||
$campaignEvent = $pendingEvent->getEvent();
|
||||
$properties = $campaignEvent->getProperties();
|
||||
$messageSettings = $this->messageModel->getChannels();
|
||||
$id = (int) $properties['marketingMessage'];
|
||||
|
||||
// Set channel for the event logs
|
||||
$pendingEvent->setChannel('channel.message', $id);
|
||||
|
||||
if (!isset($this->messageChannels[$id])) {
|
||||
$this->messageChannels[$id] = $this->messageModel->getMessageChannels($id);
|
||||
}
|
||||
|
||||
// organize into preferred channels
|
||||
$preferenceBuilder = new PreferenceBuilder($this->mmLogs, $this->pseudoEvent, $this->messageChannels[$id], $this->logger);
|
||||
|
||||
// Loop until we have no more channels
|
||||
$priority = 1;
|
||||
$channelPreferences = $preferenceBuilder->getChannelPreferences();
|
||||
|
||||
while ($priority <= count($this->messageChannels[$id])) {
|
||||
foreach ($channelPreferences as $channel => $preferences) {
|
||||
if (!isset($messageSettings[$channel]['campaignAction'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$channelLogs = $preferences->getLogsByPriority($priority);
|
||||
if (!$channelLogs->count()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Marketing messages mimick campaign actions so create a pseudo event
|
||||
$this->pseudoEvent->setEventType(Event::TYPE_ACTION)
|
||||
->setType($messageSettings[$channel]['campaignAction']);
|
||||
|
||||
$successfullyExecuted = $this->sendChannelMessage($channelLogs, $channel, $this->messageChannels[$id][$channel]);
|
||||
|
||||
$this->passExecutedLogs($pendingEvent, $successfullyExecuted, $preferenceBuilder);
|
||||
}
|
||||
++$priority;
|
||||
}
|
||||
|
||||
// Remove logs from failures if they are also in successful logs
|
||||
// This handles Marketing Messages with multiple channels where one channel fails but another succeeds.
|
||||
$this->removeSuccessfulFromFailures($pendingEvent);
|
||||
|
||||
$pendingEvent->failRemainingPending($this->translator->trans('mautic.channel.message.failed'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*
|
||||
* @return bool|ArrayCollection
|
||||
*
|
||||
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException
|
||||
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
private function sendChannelMessage(ArrayCollection $logs, $channel, array $messageChannel)
|
||||
{
|
||||
/** @var ActionAccessor $config */
|
||||
$config = $this->eventCollector->getEventConfig($this->pseudoEvent);
|
||||
|
||||
// Set the property set as the channel ID with the message ID
|
||||
if ($channelIdField = $config->getChannelIdField()) {
|
||||
$messageChannel['properties'][$channelIdField] = $messageChannel['channel_id'];
|
||||
}
|
||||
|
||||
$this->pseudoEvent->setProperties($messageChannel['properties']);
|
||||
|
||||
// Dispatch the mimicked campaign action
|
||||
$pendingEvent = new PendingEvent($config, $this->pseudoEvent, $logs);
|
||||
$pendingEvent->setChannel('campaign.event', $messageChannel['channel_id']);
|
||||
|
||||
$this->actionDispatcher->dispatchEvent(
|
||||
$config,
|
||||
$this->pseudoEvent,
|
||||
$logs,
|
||||
$pendingEvent
|
||||
);
|
||||
|
||||
// Record the channel metadata mainly for debugging
|
||||
$this->recordChannelMetadata($pendingEvent, $channel);
|
||||
|
||||
// Remove pseudo failures so we can try the next channel
|
||||
$success = $pendingEvent->getSuccessful();
|
||||
$this->removePsuedoFailures($success);
|
||||
|
||||
unset($pendingEvent);
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
private function passExecutedLogs(PendingEvent $pendingEvent, ArrayCollection $logs, PreferenceBuilder $channelPreferences): void
|
||||
{
|
||||
/** @var LeadEventLog $log */
|
||||
foreach ($logs as $log) {
|
||||
// Remove those successfully executed from being processed again for lower priorities
|
||||
$channelPreferences->removeLogFromAllChannels($log);
|
||||
|
||||
// Find the Marketing Message log and pass it
|
||||
$mmLog = $pendingEvent->findLogByContactId($log->getLead()->getId());
|
||||
|
||||
// Pass these for the MM campaign event
|
||||
$pendingEvent->pass($mmLog);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArrayCollection<int,LeadEventLog> $success
|
||||
*/
|
||||
private function removePsuedoFailures(ArrayCollection $success): void
|
||||
{
|
||||
foreach ($success as $key => $log) {
|
||||
if (!empty($log->getMetadata()['failed'])) {
|
||||
$success->remove($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function removeSuccessfulFromFailures(PendingEvent $pendingEvent): void
|
||||
{
|
||||
$successfulKeys = $pendingEvent->getSuccessful()->getKeys();
|
||||
foreach ($successfulKeys as $key) {
|
||||
if ($pendingEvent->getFailures()->containsKey($key)) {
|
||||
$pendingEvent->getFailures()->remove($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function recordChannelMetadata(PendingEvent $pendingEvent, string $channel): void
|
||||
{
|
||||
/** @var LeadEventLog $log */
|
||||
foreach ($this->mmLogs as $log) {
|
||||
try {
|
||||
$channelLog = $pendingEvent->findLogByContactId($log->getLead()->getId());
|
||||
|
||||
if ($metadata = $channelLog->getMetadata()) {
|
||||
$log->appendToMetadata([$channel => $metadata]);
|
||||
}
|
||||
} catch (NoContactsFoundException) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\EventListener;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\MessageQueueRepository;
|
||||
use Mautic\LeadBundle\Event\LeadTimelineEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class LeadSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private RouterInterface $router,
|
||||
private MessageQueueRepository $messageQueueRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LeadEvents::TIMELINE_ON_GENERATE => ['onTimelineGenerate', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile events for the lead timeline.
|
||||
*/
|
||||
public function onTimelineGenerate(LeadTimelineEvent $event): void
|
||||
{
|
||||
$eventTypeKey = 'message.queue';
|
||||
$eventTypeName = $this->translator->trans('mautic.message.queue');
|
||||
|
||||
$event->addEventType($eventTypeKey, $eventTypeName);
|
||||
$event->addSerializerGroup('messageQueueList');
|
||||
|
||||
$label = $this->translator->trans('mautic.queued.channel');
|
||||
|
||||
// Decide if those events are filtered
|
||||
if (!$event->isApplicable($eventTypeKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$logs = $this->messageQueueRepository->getLeadTimelineEvents($event->getLeadId(), $event->getQueryOptions());
|
||||
|
||||
// Add to counter
|
||||
$event->addToCounter($eventTypeKey, $logs);
|
||||
|
||||
if (!$event->isEngagementCount()) {
|
||||
// Add the logs to the event array
|
||||
foreach ($logs['results'] as $log) {
|
||||
$eventName = [
|
||||
'label' => $label.$log['channelName'].' '.$log['channelId'],
|
||||
'href' => $this->router->generate('mautic_'.$log['channelName'].'_action', ['objectAction' => 'view', 'objectId' => $log['channelId']]),
|
||||
];
|
||||
$event->addEvent(
|
||||
[
|
||||
'eventId' => $eventTypeKey.$log['id'],
|
||||
'event' => $eventTypeKey,
|
||||
'eventLabel' => $eventName,
|
||||
'eventType' => $eventTypeName,
|
||||
'timestamp' => $log['dateAdded'],
|
||||
'extra' => [
|
||||
'log' => $log,
|
||||
],
|
||||
'contentTemplate' => '@MauticChannel/SubscribedEvents/Timeline/queued_messages.html.twig',
|
||||
'icon' => 'ri-question-answer-line',
|
||||
'contactId' => $log['lead_id'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\EventListener;
|
||||
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Event\MessageEvent;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class MessageSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AuditLogModel $auditLogModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ChannelEvents::MESSAGE_POST_SAVE => ['onPostSave', 0],
|
||||
ChannelEvents::MESSAGE_POST_DELETE => ['onDelete', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the audit log.
|
||||
*/
|
||||
public function onPostSave(MessageEvent $event): void
|
||||
{
|
||||
$entity = $event->getMessage();
|
||||
if ($details = $event->getChanges()) {
|
||||
$log = [
|
||||
'bundle' => 'channel',
|
||||
'object' => 'message',
|
||||
'objectId' => $entity->getId(),
|
||||
'action' => ($event->isNew()) ? 'create' : 'update',
|
||||
'details' => $details,
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a delete entry to the audit log.
|
||||
*/
|
||||
public function onDelete(MessageEvent $event): void
|
||||
{
|
||||
$entity = $event->getMessage();
|
||||
$log = [
|
||||
'bundle' => 'channel',
|
||||
'object' => 'message',
|
||||
'objectId' => $entity->deletedId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name' => $entity->getName()],
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Model\CompanyReportData;
|
||||
use Mautic\ReportBundle\Event\ReportBuilderEvent;
|
||||
use Mautic\ReportBundle\Event\ReportDataEvent;
|
||||
use Mautic\ReportBundle\Event\ReportGeneratorEvent;
|
||||
use Mautic\ReportBundle\ReportEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class ReportSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public const CONTEXT_MESSAGE_CHANNEL = 'message.channel';
|
||||
|
||||
public function __construct(
|
||||
private CompanyReportData $companyReportData,
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ReportEvents::REPORT_ON_BUILD => ['onReportBuilder', 0],
|
||||
ReportEvents::REPORT_ON_GENERATE => ['onReportGenerate', 0],
|
||||
ReportEvents::REPORT_ON_DISPLAY => ['onReportDisplay', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add available tables and columns to the report builder lookup.
|
||||
*/
|
||||
public function onReportBuilder(ReportBuilderEvent $event): void
|
||||
{
|
||||
if (!$event->checkContext([self::CONTEXT_MESSAGE_CHANNEL])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// message queue
|
||||
$prefix = 'mq.';
|
||||
$columns = [
|
||||
$prefix.'channel' => [
|
||||
'label' => 'mautic.message.queue.report.channel',
|
||||
'type' => 'html',
|
||||
],
|
||||
$prefix.'channel_id' => [
|
||||
'label' => 'mautic.message.queue.report.channel_id',
|
||||
'type' => 'int',
|
||||
],
|
||||
$prefix.'priority' => [
|
||||
'label' => 'mautic.message.queue.report.priority',
|
||||
'type' => 'string',
|
||||
],
|
||||
$prefix.'max_attempts' => [
|
||||
'label' => 'mautic.message.queue.report.max_attempts',
|
||||
'type' => 'int',
|
||||
],
|
||||
$prefix.'attempts' => [
|
||||
'label' => 'mautic.message.queue.report.attempts',
|
||||
'type' => 'int',
|
||||
],
|
||||
$prefix.'success' => [
|
||||
'label' => 'mautic.message.queue.report.success',
|
||||
'type' => 'boolean',
|
||||
],
|
||||
$prefix.'status' => [
|
||||
'label' => 'mautic.message.queue.report.status',
|
||||
'type' => 'string',
|
||||
],
|
||||
$prefix.'last_attempt' => [
|
||||
'label' => 'mautic.message.queue.report.last_attempt',
|
||||
'type' => 'datetime',
|
||||
],
|
||||
$prefix.'date_sent' => [
|
||||
'label' => 'mautic.message.queue.report.date_sent',
|
||||
'type' => 'datetime',
|
||||
],
|
||||
$prefix.'scheduled_date' => [
|
||||
'label' => 'mautic.message.queue.report.scheduled_date',
|
||||
'type' => 'datetime',
|
||||
],
|
||||
$prefix.'date_published' => [
|
||||
'label' => 'mautic.message.queue.report.date_published',
|
||||
'type' => 'datetime',
|
||||
],
|
||||
];
|
||||
|
||||
$companyColumns = $this->companyReportData->getCompanyData();
|
||||
|
||||
$columns = array_merge(
|
||||
$columns,
|
||||
$event->getLeadColumns(),
|
||||
$companyColumns
|
||||
);
|
||||
|
||||
$event->addTable(
|
||||
self::CONTEXT_MESSAGE_CHANNEL,
|
||||
[
|
||||
'display_name' => 'mautic.message.queue',
|
||||
'columns' => $columns,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the QueryBuilder object to generate reports from.
|
||||
*/
|
||||
public function onReportGenerate(ReportGeneratorEvent $event): void
|
||||
{
|
||||
if (!$event->checkContext([self::CONTEXT_MESSAGE_CHANNEL])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder = $event->getQueryBuilder();
|
||||
$queryBuilder->from(MAUTIC_TABLE_PREFIX.'message_queue', 'mq')
|
||||
->leftJoin('mq', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = mq.lead_id');
|
||||
|
||||
if ($this->companyReportData->eventHasCompanyColumns($event)) {
|
||||
$event->addCompanyLeftJoin($queryBuilder);
|
||||
}
|
||||
|
||||
$event->setQueryBuilder($queryBuilder);
|
||||
}
|
||||
|
||||
public function onReportDisplay(ReportDataEvent $event): void
|
||||
{
|
||||
$data = $event->getData();
|
||||
if ($event->checkContext([self::CONTEXT_MESSAGE_CHANNEL])) {
|
||||
if (isset($data[0]['channel']) && isset($data[0]['channel_id'])) {
|
||||
foreach ($data as &$row) {
|
||||
$href = $this->router->generate('mautic_'.$row['channel'].'_action', ['objectAction' => 'view', 'objectId' => $row['channel_id']]);
|
||||
if (isset($row['channel'])) {
|
||||
$row['channel'] = '<a href="'.$href.'">'.$row['channel'].'</a>';
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$event->setData($data);
|
||||
unset($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\EventListener;
|
||||
|
||||
use Mautic\ChannelBundle\Model\MessageModel;
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\DTO\GlobalSearchFilterDTO;
|
||||
use Mautic\CoreBundle\Event\GlobalSearchEvent;
|
||||
use Mautic\CoreBundle\Service\GlobalSearch;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class SearchSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private MessageModel $model,
|
||||
private GlobalSearch $globalSearch,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::GLOBAL_SEARCH => ['onGlobalSearch', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onGlobalSearch(GlobalSearchEvent $event): void
|
||||
{
|
||||
$results = $this->globalSearch->performSearch(
|
||||
new GlobalSearchFilterDTO($event->getSearchString()),
|
||||
$this->model,
|
||||
'@MauticChannel/SubscribedEvents/Search/global.html.twig'
|
||||
);
|
||||
|
||||
if (!empty($results)) {
|
||||
$event->addResults('mautic.messages.header', $results);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Form\Type;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\Channel;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<Channel>
|
||||
*/
|
||||
class ChannelType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$formModifier = function (FormEvent $event) use ($options): void {
|
||||
$form = $event->getForm();
|
||||
|
||||
$data = $event->getData();
|
||||
if (is_array($data)) {
|
||||
$channelName = $data['channel'];
|
||||
$enabled = $data['isEnabled'];
|
||||
} elseif ($data instanceof Channel) {
|
||||
$channelName = $data->getChannel();
|
||||
$enabled = $data->isEnabled();
|
||||
} else {
|
||||
$channelName = $data;
|
||||
$enabled = false;
|
||||
}
|
||||
|
||||
if (!$data || !$channelName || !isset($options['channels'][$channelName])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$channelConfig = $options['channels'][$channelName];
|
||||
$form->add(
|
||||
'channel',
|
||||
HiddenType::class
|
||||
);
|
||||
|
||||
$form->add(
|
||||
'isEnabled',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.channel.message.form.enabled',
|
||||
'attr' => [
|
||||
'onchange' => 'Mautic.toggleChannelFormDisplay(this, \''.$channelName.'\')',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if (isset($channelConfig['lookupFormType'])) {
|
||||
$form->add(
|
||||
'channelId',
|
||||
$channelConfig['lookupFormType'],
|
||||
[
|
||||
'multiple' => false,
|
||||
'label' => 'mautic.channel.message.form.message',
|
||||
'constraints' => ($enabled) ? [
|
||||
new NotBlank(
|
||||
[
|
||||
'message' => 'mautic.core.value.required',
|
||||
]
|
||||
),
|
||||
] : [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($channelConfig['propertiesFormType'])) {
|
||||
$propertiesOptions = [
|
||||
'label' => false,
|
||||
];
|
||||
if (!$enabled) {
|
||||
// Disable validation
|
||||
$propertiesOptions['validation_groups'] = false;
|
||||
}
|
||||
|
||||
$form->add(
|
||||
'properties',
|
||||
$channelConfig['propertiesFormType'],
|
||||
$propertiesOptions
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, $formModifier);
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, $formModifier);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['channels'] = $options['channels'];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setRequired(['channels']);
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'data_class' => Channel::class,
|
||||
'label' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\EntityLookupType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class MessageListType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'required' => false,
|
||||
'modal_route' => 'mautic_message_action',
|
||||
'model' => 'channel.message',
|
||||
'multiple' => true,
|
||||
'ajax_lookup_action' => function (Options $options): string {
|
||||
$query = [
|
||||
'is_published' => $options['is_published'],
|
||||
];
|
||||
|
||||
return 'channel:getLookupChoiceList&'.http_build_query($query);
|
||||
},
|
||||
'model_lookup_method' => 'getLookupResults',
|
||||
'lookup_arguments' => fn (Options $options): array => [
|
||||
'type' => 'channel.message',
|
||||
'filter' => '$data',
|
||||
'limit' => 0,
|
||||
'start' => 0,
|
||||
'options' => [
|
||||
'is_published' => $options['is_published'],
|
||||
],
|
||||
],
|
||||
'is_published' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return EntityLookupType::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Form\Type;
|
||||
|
||||
use Mautic\ChannelBundle\Model\MessageModel;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class MessageSendType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
protected RouterInterface $router,
|
||||
protected MessageModel $messageModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'marketingMessage',
|
||||
MessageListType::class,
|
||||
[
|
||||
'label' => 'mautic.channel.send.selectmessages',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'multiple' => false,
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(
|
||||
['message' => 'mautic.channel.choosemessage.notblank']
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['update_select'])) {
|
||||
$windowUrl = $this->router->generate(
|
||||
'mautic_message_action',
|
||||
[
|
||||
'objectAction' => 'new',
|
||||
'contentOnly' => 1,
|
||||
'updateSelect' => $options['update_select'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'newMarketingMessageButton',
|
||||
ButtonType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'btn btn-primary btn-nospin',
|
||||
'onclick' => 'Mautic.loadNewWindow({windowUrl: \''.$windowUrl.'\'})',
|
||||
'icon' => 'ri-add-line',
|
||||
],
|
||||
'label' => 'mautic.channel.create.new.message',
|
||||
]
|
||||
);
|
||||
|
||||
// create button edit email
|
||||
$windowUrlEdit = $this->router->generate(
|
||||
'mautic_message_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => 'messageId',
|
||||
'contentOnly' => 1,
|
||||
'updateSelect' => $options['update_select'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'editMessageButton',
|
||||
ButtonType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'btn btn-primary btn-nospin',
|
||||
'onclick' => 'Mautic.loadNewWindow({windowUrl: \''.$windowUrlEdit.'\'})',
|
||||
'disabled' => !isset($options['data']['message']),
|
||||
'icon' => 'ri-edit-line',
|
||||
],
|
||||
'label' => 'mautic.channel.send.edit.message',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefined(['update_select']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Form\Type;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\Channel;
|
||||
use Mautic\ChannelBundle\Entity\Message;
|
||||
use Mautic\ChannelBundle\Model\MessageModel;
|
||||
use Mautic\CoreBundle\Form\Type\AbstractFormStandardType;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\ProjectBundle\Form\Type\ProjectType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
|
||||
class MessageType extends AbstractFormStandardType
|
||||
{
|
||||
public function __construct(
|
||||
protected MessageModel $model,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
// Add standard fields
|
||||
$options = array_merge($options, ['model_name' => 'channel.message', 'permission_base' => 'channel:messages']);
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
// Ensure that all channels exist
|
||||
/** @var Message $message */
|
||||
$message = $options['data'];
|
||||
$channels = $this->model->getChannels();
|
||||
$messageChannels = $message->getChannels();
|
||||
|
||||
foreach ($channels as $channelType => $channel) {
|
||||
if (!isset($messageChannels[$channelType])) {
|
||||
$message->addChannel(
|
||||
(new Channel())
|
||||
->setChannel($channelType)
|
||||
->setMessage($message)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$builder->add(
|
||||
'channels',
|
||||
CollectionType::class,
|
||||
[
|
||||
'label' => false,
|
||||
'allow_add' => true,
|
||||
'allow_delete' => false,
|
||||
'prototype' => false,
|
||||
'entry_type' => ChannelType::class,
|
||||
'by_reference' => false,
|
||||
'entry_options' => [
|
||||
'channels' => $channels,
|
||||
],
|
||||
'constraints' => [
|
||||
new Valid(),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('projects', ProjectType::class);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'data_class' => Message::class,
|
||||
'category_bundle' => 'messages',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Helper;
|
||||
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Event\ChannelEvent;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class ChannelListHelper
|
||||
{
|
||||
/**
|
||||
* @var array<string,string>
|
||||
*/
|
||||
private array $channels = [];
|
||||
|
||||
/**
|
||||
* @var array<string,string[]>
|
||||
*/
|
||||
private array $featureChannels = [];
|
||||
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private Translator $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contact channels.
|
||||
*/
|
||||
public function getChannelList(): array
|
||||
{
|
||||
$channels = [];
|
||||
foreach ($this->getChannels() as $channel => $details) {
|
||||
$channelName = isset($details['label']) ? $this->translator->trans($details['label']) : $this->getChannelLabel($channel);
|
||||
$channels[$channelName] = $channel;
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $listOnly
|
||||
*/
|
||||
public function getFeatureChannels($features, $listOnly = false): array
|
||||
{
|
||||
$this->setupChannels();
|
||||
|
||||
if (!is_array($features)) {
|
||||
$features = [$features];
|
||||
}
|
||||
$channels = [];
|
||||
foreach ($features as $feature) {
|
||||
$featureChannels = $this->featureChannels[$feature] ?? [];
|
||||
$returnChannels = [];
|
||||
foreach ($featureChannels as $channel => $details) {
|
||||
if (!isset($details['label'])) {
|
||||
$featureChannels[$channel]['label'] = $this->getChannelLabel($channel);
|
||||
}
|
||||
|
||||
if ($listOnly) {
|
||||
$returnChannels[$featureChannels[$channel]['label']] = $channel;
|
||||
} else {
|
||||
$returnChannels[$channel] = $featureChannels[$channel];
|
||||
}
|
||||
}
|
||||
unset($featureChannels);
|
||||
$channels[$feature] = $returnChannels;
|
||||
}
|
||||
|
||||
if (1 === count($features)) {
|
||||
$channels = $channels[$features[0]];
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getChannels()
|
||||
{
|
||||
$this->setupChannels();
|
||||
|
||||
return $this->channels;
|
||||
}
|
||||
|
||||
public function getChannelLabel($channel): string
|
||||
{
|
||||
return match (true) {
|
||||
$this->translator->hasId('mautic.channel.'.$channel) => $this->translator->trans('mautic.channel.'.$channel),
|
||||
$this->translator->hasId('mautic.'.$channel.'.'.$channel) => $this->translator->trans('mautic.'.$channel.'.'.$channel),
|
||||
default => ucfirst($channel),
|
||||
};
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'chanel';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup channels.
|
||||
*
|
||||
* Done this way to avoid a circular dependency error with LeadModel
|
||||
*/
|
||||
private function setupChannels(): void
|
||||
{
|
||||
if (!empty($this->channels)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = $this->dispatcher->dispatch(new ChannelEvent(), ChannelEvents::ADD_CHANNEL);
|
||||
$this->channels = $event->getChannelConfigs();
|
||||
$this->featureChannels = $event->getFeatureChannels();
|
||||
unset($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class MauticChannelBundle extends Bundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Model;
|
||||
|
||||
use Mautic\LeadBundle\Entity\DoNotContact as DNC;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\DoNotContact;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ChannelActionModel
|
||||
{
|
||||
public function __construct(
|
||||
private LeadModel $contactModel,
|
||||
private DoNotContact $doNotContact,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update channels and frequency rules.
|
||||
*/
|
||||
public function update(array $contactIds, array $subscribedChannels): void
|
||||
{
|
||||
$contacts = $this->contactModel->getLeadsByIds($contactIds);
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
if (!$this->contactModel->canEditContact($contact)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->addChannels($contact, $subscribedChannels);
|
||||
$this->removeChannels($contact, $subscribedChannels);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add contact's channels.
|
||||
* Only resubscribe if the contact did not opt out themselves.
|
||||
*/
|
||||
private function addChannels(Lead $contact, array $subscribedChannels): void
|
||||
{
|
||||
$contactChannels = $this->contactModel->getContactChannels($contact);
|
||||
|
||||
foreach ($subscribedChannels as $subscribedChannel) {
|
||||
if (!array_key_exists($subscribedChannel, $contactChannels)) {
|
||||
$contactable = $this->doNotContact->isContactable($contact, $subscribedChannel);
|
||||
if (DNC::UNSUBSCRIBED !== $contactable) {
|
||||
$this->doNotContact->removeDncForContact($contact->getId(), $subscribedChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove contact's channels.
|
||||
*/
|
||||
private function removeChannels(Lead $contact, array $subscribedChannels): void
|
||||
{
|
||||
$allChannels = $this->contactModel->getPreferenceChannels();
|
||||
$dncChannels = array_diff($allChannels, $subscribedChannels);
|
||||
|
||||
foreach ($dncChannels as $channel) {
|
||||
$this->doNotContact->addDncForContact(
|
||||
$contact->getId(),
|
||||
$channel,
|
||||
DNC::MANUAL,
|
||||
$this->translator->trans('mautic.lead.event.donotcontact_manual')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Model;
|
||||
|
||||
use Mautic\LeadBundle\Entity\FrequencyRule;
|
||||
use Mautic\LeadBundle\Entity\FrequencyRuleRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
|
||||
class FrequencyActionModel
|
||||
{
|
||||
public function __construct(
|
||||
private LeadModel $contactModel,
|
||||
private FrequencyRuleRepository $frequencyRuleRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update channels.
|
||||
*
|
||||
* @param string $preferredChannel
|
||||
*/
|
||||
public function update(array $contactIds, array $params, $preferredChannel): void
|
||||
{
|
||||
$contacts = $this->contactModel->getLeadsByIds($contactIds);
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
if (!$this->contactModel->canEditContact($contact)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->updateFrequencyRules($contact, $params, $preferredChannel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $preferredChannel
|
||||
*/
|
||||
private function updateFrequencyRules(Lead $contact, array $params, $preferredChannel): void
|
||||
{
|
||||
$frequencyRules = $contact->getFrequencyRules()->toArray();
|
||||
$channels = $this->contactModel->getPreferenceChannels();
|
||||
|
||||
foreach ($channels as $channel) {
|
||||
if (is_null($preferredChannel)) {
|
||||
$preferredChannel = $channel;
|
||||
}
|
||||
|
||||
$frequencyRule = $frequencyRules[$channel] ?? new FrequencyRule();
|
||||
$frequencyRule->setChannel($channel);
|
||||
$frequencyRule->setLead($contact);
|
||||
|
||||
if (!$frequencyRule->getDateAdded()) {
|
||||
$frequencyRule->setDateAdded(new \DateTime());
|
||||
}
|
||||
|
||||
if (!empty($params['frequency_number_'.$channel]) && !empty($params['frequency_time_'.$channel])) {
|
||||
$frequencyRule->setFrequencyNumber($params['frequency_number_'.$channel]);
|
||||
$frequencyRule->setFrequencyTime($params['frequency_time_'.$channel]);
|
||||
} else {
|
||||
$frequencyRule->setFrequencyNumber(null);
|
||||
$frequencyRule->setFrequencyTime(null);
|
||||
}
|
||||
|
||||
if (!empty($params['contact_pause_start_date_'.$channel])) {
|
||||
$frequencyRule->setPauseFromDate(new \DateTime($params['contact_pause_start_date_'.$channel]));
|
||||
}
|
||||
|
||||
if (!empty($params['contact_pause_end_date_'.$channel])) {
|
||||
$frequencyRule->setPauseToDate(new \DateTime($params['contact_pause_end_date_'.$channel]));
|
||||
}
|
||||
|
||||
$frequencyRule->setPreferredChannel($preferredChannel === $channel);
|
||||
|
||||
$contact->addFrequencyRule($frequencyRule);
|
||||
|
||||
$this->frequencyRuleRepository->saveEntity($frequencyRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Entity\Message;
|
||||
use Mautic\ChannelBundle\Entity\MessageRepository;
|
||||
use Mautic\ChannelBundle\Event\MessageEvent;
|
||||
use Mautic\ChannelBundle\Form\Type\MessageType;
|
||||
use Mautic\ChannelBundle\Helper\ChannelListHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\AjaxLookupModelInterface;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends FormModel<Message>
|
||||
*
|
||||
* @implements AjaxLookupModelInterface<Message>
|
||||
*/
|
||||
class MessageModel extends FormModel implements AjaxLookupModelInterface, GlobalSearchInterface
|
||||
{
|
||||
public const CHANNEL_FEATURE = 'marketing_messages';
|
||||
|
||||
protected static $channels;
|
||||
|
||||
public function __construct(
|
||||
protected ChannelListHelper $channelListHelper,
|
||||
protected CampaignModel $campaignModel,
|
||||
EntityManager $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Message $entity
|
||||
* @param bool $unlock
|
||||
*/
|
||||
public function saveEntity($entity, $unlock = true): void
|
||||
{
|
||||
$isNew = $entity->isNew();
|
||||
|
||||
parent::saveEntity($entity, $unlock);
|
||||
|
||||
if (!$isNew) {
|
||||
// Update the channels
|
||||
$channels = $entity->getChannels();
|
||||
foreach ($channels as $channel) {
|
||||
$channel->setMessage($entity);
|
||||
}
|
||||
$this->getRepository()->saveEntities($channels);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'channel:messages';
|
||||
}
|
||||
|
||||
public function getRepository(): ?MessageRepository
|
||||
{
|
||||
return $this->em->getRepository(Message::class);
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?Message
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Message();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
* @param array $options
|
||||
*
|
||||
* @return \Symfony\Component\Form\FormInterface<mixed>
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(MessageType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getChannels()
|
||||
{
|
||||
if (!self::$channels) {
|
||||
$channels = $this->channelListHelper->getFeatureChannels(self::CHANNEL_FEATURE);
|
||||
|
||||
// Validate channel configs
|
||||
foreach ($channels as $channel => $config) {
|
||||
if (!isset($config['lookupFormType']) && !isset($config['propertiesFormType'])) {
|
||||
throw new \InvalidArgumentException('lookupFormType and/or propertiesFormType are required for channel '.$channel);
|
||||
}
|
||||
|
||||
$label = match (true) {
|
||||
$this->translator->hasId('mautic.channel.'.$channel) => $this->translator->trans('mautic.channel.'.$channel),
|
||||
$this->translator->hasId('mautic.'.$channel) => $this->translator->trans('mautic.'.$channel),
|
||||
$this->translator->hasId('mautic.'.$channel.'.'.$channel) => $this->translator->trans('mautic.'.$channel.'.'.$channel),
|
||||
default => ucfirst($channel),
|
||||
};
|
||||
$config['label'] = $label;
|
||||
|
||||
$channels[$channel] = $config;
|
||||
}
|
||||
|
||||
self::$channels = $channels;
|
||||
}
|
||||
|
||||
return self::$channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filter
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param array $options
|
||||
*/
|
||||
public function getLookupResults($type, $filter = '', $limit = 10, $start = 0, $options = []): array
|
||||
{
|
||||
$results = [];
|
||||
switch ($type) {
|
||||
case 'channel.message':
|
||||
$entities = $this->getRepository()->getMessageList(
|
||||
$filter,
|
||||
$limit,
|
||||
$start
|
||||
);
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$results[] = [
|
||||
'label' => $entity['name'],
|
||||
'value' => $entity['id'],
|
||||
];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getMessageChannels($messageId): array
|
||||
{
|
||||
return $this->getRepository()->getMessageChannels($messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getChannelMessageByChannelId($channelId)
|
||||
{
|
||||
return $this->getRepository()->getChannelMessageByChannelId($channelId);
|
||||
}
|
||||
|
||||
public function getLeadStatsPost($messageId, $dateFrom = null, $dateTo = null, $channel = null): array
|
||||
{
|
||||
$eventLog = $this->campaignModel->getCampaignLeadEventLogRepository();
|
||||
|
||||
return $eventLog->getChartQuery(
|
||||
[
|
||||
'type' => 'message.send',
|
||||
'dateFrom' => $dateFrom,
|
||||
'dateTo' => $dateTo,
|
||||
'channel' => 'channel.message',
|
||||
'channelId' => $messageId,
|
||||
'logChannel' => $channel,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMarketingMessagesEventLogs($messageId, $dateFrom = null, $dateTo = null)
|
||||
{
|
||||
$eventLog = $this->campaignModel->getCampaignLeadEventLogRepository();
|
||||
|
||||
return $eventLog->getEventLogs(['type' => 'message.send', 'dateFrom' => $dateFrom, 'dateTo' => $dateTo, 'channel' => 'message', 'channelId' => $messageId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channel name from the database.
|
||||
*
|
||||
* @template T of object
|
||||
*
|
||||
* @param int $id
|
||||
* @param class-string<T> $entityName
|
||||
* @param string $nameColumn
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getChannelName($id, $entityName, $nameColumn = 'name')
|
||||
{
|
||||
if (!$id || !$entityName || !$nameColumn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$repo = $this->em->getRepository($entityName);
|
||||
$qb = $repo->createQueryBuilder('e')
|
||||
->select('e.'.$nameColumn)
|
||||
->where('e.id = :id')
|
||||
->setParameter('id', (int) $id);
|
||||
$result = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
return $result[$nameColumn] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Message) {
|
||||
throw new MethodNotAllowedHttpException(['Message']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = ChannelEvents::MESSAGE_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = ChannelEvents::MESSAGE_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = ChannelEvents::MESSAGE_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = ChannelEvents::MESSAGE_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new MessageEvent($entity, $isNew);
|
||||
}
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Mautic\ChannelBundle\Event\MessageQueueBatchProcessEvent;
|
||||
use Mautic\ChannelBundle\Event\MessageQueueEvent;
|
||||
use Mautic\ChannelBundle\Event\MessageQueueProcessEvent;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends FormModel<MessageQueue>
|
||||
*/
|
||||
class MessageQueueModel extends FormModel
|
||||
{
|
||||
/**
|
||||
* @var string A default message reschedule interval
|
||||
*/
|
||||
public const DEFAULT_RESCHEDULE_INTERVAL = 'PT15M';
|
||||
|
||||
public function __construct(
|
||||
protected LeadModel $leadModel,
|
||||
protected CompanyModel $companyModel,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EntityManagerInterface $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\ChannelBundle\Entity\MessageQueueRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(MessageQueue::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $attempts
|
||||
* @param int $priority
|
||||
* @param mixed $messageQueue
|
||||
* @param string $statTableName
|
||||
* @param string $statContactColumn
|
||||
* @param string $statSentColumn
|
||||
*/
|
||||
public function processFrequencyRules(
|
||||
array &$leads,
|
||||
$channel,
|
||||
$channelId,
|
||||
$campaignEventId = null,
|
||||
$attempts = 3,
|
||||
$priority = MessageQueue::PRIORITY_NORMAL,
|
||||
$messageQueue = null,
|
||||
$statTableName = 'email_stats',
|
||||
$statContactColumn = 'lead_id',
|
||||
$statSentColumn = 'date_sent',
|
||||
): array {
|
||||
$leadIds = array_keys($leads);
|
||||
$leadIds = array_combine($leadIds, $leadIds);
|
||||
|
||||
/** @var \Mautic\LeadBundle\Entity\FrequencyRuleRepository $frequencyRulesRepo */
|
||||
$frequencyRulesRepo = $this->em->getRepository(\Mautic\LeadBundle\Entity\FrequencyRule::class);
|
||||
$defaultFrequencyNumber = $this->coreParametersHelper->get($channel.'_frequency_number');
|
||||
$defaultFrequencyTime = $this->coreParametersHelper->get($channel.'_frequency_time');
|
||||
|
||||
$dontSendTo = $frequencyRulesRepo->getAppliedFrequencyRules(
|
||||
$channel,
|
||||
$leadIds,
|
||||
$defaultFrequencyNumber,
|
||||
$defaultFrequencyTime,
|
||||
$statTableName,
|
||||
$statContactColumn,
|
||||
$statSentColumn
|
||||
);
|
||||
|
||||
$queuedContacts = [];
|
||||
foreach ($dontSendTo as $frequencyRuleMet) {
|
||||
// We only deal with date intervals here (no time intervals) so it's safe to use 'P'
|
||||
$scheduleInterval = new \DateInterval('P1'.substr($frequencyRuleMet['frequency_time'], 0, 1));
|
||||
if ($messageQueue && isset($messageQueue[$frequencyRuleMet['lead_id']])) {
|
||||
$this->reschedule($messageQueue[$frequencyRuleMet['lead_id']], $scheduleInterval);
|
||||
} else {
|
||||
// Queue this message to be processed by frequency and priority
|
||||
$this->queue(
|
||||
[$leads[$frequencyRuleMet['lead_id']]],
|
||||
$channel,
|
||||
$channelId,
|
||||
$scheduleInterval,
|
||||
$attempts,
|
||||
$priority,
|
||||
$campaignEventId
|
||||
);
|
||||
}
|
||||
$queuedContacts[$frequencyRuleMet['lead_id']] = $frequencyRuleMet['lead_id'];
|
||||
unset($leads[$frequencyRuleMet['lead_id']]);
|
||||
}
|
||||
|
||||
return $queuedContacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds messages to the queue.
|
||||
*
|
||||
* @param array $leads
|
||||
* @param string $channel
|
||||
* @param int $channelId
|
||||
* @param int $maxAttempts
|
||||
* @param int $priority
|
||||
* @param int|null $campaignEventId
|
||||
* @param array $options
|
||||
*/
|
||||
public function queue(
|
||||
$leads,
|
||||
$channel,
|
||||
$channelId,
|
||||
\DateInterval $scheduledInterval,
|
||||
$maxAttempts = 1,
|
||||
$priority = 1,
|
||||
$campaignEventId = null,
|
||||
$options = [],
|
||||
): bool {
|
||||
$messageQueues = [];
|
||||
|
||||
$scheduledDate = (new \DateTime())->add($scheduledInterval);
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$leadId = (is_array($lead)) ? $lead['id'] : $lead->getId();
|
||||
if (!empty($this->getRepository()->findMessage($channel, $channelId, $leadId))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$messageQueue = new MessageQueue();
|
||||
if ($campaignEventId) {
|
||||
$messageQueue->setEvent($this->em->getReference(\Mautic\CampaignBundle\Entity\Event::class, $campaignEventId));
|
||||
}
|
||||
$messageQueue->setChannel($channel);
|
||||
$messageQueue->setChannelId($channelId);
|
||||
$messageQueue->setDatePublished(new \DateTime());
|
||||
$messageQueue->setMaxAttempts($maxAttempts);
|
||||
$messageQueue->setLead(
|
||||
($lead instanceof Lead) ? $lead : $this->em->getReference(Lead::class, $leadId)
|
||||
);
|
||||
$messageQueue->setPriority($priority);
|
||||
$messageQueue->setScheduledDate($scheduledDate);
|
||||
$messageQueue->setOptions($options);
|
||||
|
||||
$messageQueues[] = $messageQueue;
|
||||
}
|
||||
|
||||
if ($messageQueues) {
|
||||
$this->saveEntities($messageQueues);
|
||||
$messageQueueRepository = $this->getRepository();
|
||||
$messageQueueRepository->detachEntities($messageQueues);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sendMessages($channel = null, $channelId = null, int $limit = 50): int
|
||||
{
|
||||
// Note when the process started for batch purposes
|
||||
$processStarted = new \DateTime();
|
||||
$counter = 0;
|
||||
|
||||
foreach ($this->getRepository()->getQueuedMessages($limit, $processStarted, $channel, $channelId) as $queue) {
|
||||
$counter += $this->processMessageQueue($queue);
|
||||
$event = $queue->getEvent();
|
||||
|
||||
if ($event) {
|
||||
$this->em->detach($event);
|
||||
}
|
||||
$this->em->detach($queue);
|
||||
}
|
||||
|
||||
return $counter;
|
||||
}
|
||||
|
||||
public function processMessageQueue($queue): int
|
||||
{
|
||||
if (!is_array($queue)) {
|
||||
if (!$queue instanceof MessageQueue) {
|
||||
throw new \InvalidArgumentException('$queue must be an instance of '.MessageQueue::class);
|
||||
}
|
||||
|
||||
$queue = [$queue->getId() => $queue];
|
||||
}
|
||||
|
||||
$counter = 0;
|
||||
$contacts = [];
|
||||
$byChannel = [];
|
||||
|
||||
// Lead entities will not have profile fields populated due to the custom field use - therefore to optimize resources,
|
||||
// get a list of leads to fetch details all at once along with company details for dynamic email content, etc
|
||||
/** @var MessageQueue $message */
|
||||
foreach ($queue as $message) {
|
||||
if ($message->getLead()) {
|
||||
$contacts[$message->getId()] = $message->getLead()->getId();
|
||||
}
|
||||
}
|
||||
if (!empty($contacts)) {
|
||||
$contactData = $this->leadModel->getRepository()->getContacts($contacts);
|
||||
foreach ($contacts as $messageId => $contactId) {
|
||||
$queue[$messageId]->getLead()->setFields($contactData[$contactId]);
|
||||
}
|
||||
}
|
||||
// Group queue by channel and channel ID - this make it possible for processing listeners to batch process such as
|
||||
// sending emails in batches to 3rd party transactional services via HTTP APIs
|
||||
foreach ($queue as $key => $message) {
|
||||
if (MessageQueue::STATUS_SENT == $message->getStatus()) {
|
||||
unset($queue[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$messageChannel = $message->getChannel();
|
||||
$messageChannelId = $message->getChannelId();
|
||||
if (!$messageChannelId) {
|
||||
$messageChannelId = 0;
|
||||
}
|
||||
|
||||
if (!isset($byChannel[$messageChannel])) {
|
||||
$byChannel[$messageChannel] = [];
|
||||
}
|
||||
if (!isset($byChannel[$messageChannel][$messageChannelId])) {
|
||||
$byChannel[$messageChannel][$messageChannelId] = [];
|
||||
}
|
||||
|
||||
$byChannel[$messageChannel][$messageChannelId][] = $message;
|
||||
}
|
||||
|
||||
// First try to batch process each channel
|
||||
foreach ($byChannel as $messageChannel => $channelMessages) {
|
||||
foreach ($channelMessages as $messageChannelId => $messages) {
|
||||
$event = new MessageQueueBatchProcessEvent($messages, $messageChannel, $messageChannelId);
|
||||
$ignore = null;
|
||||
$this->dispatchEvent('process_batch_message_queue', $ignore, false, $event);
|
||||
}
|
||||
}
|
||||
unset($byChannel);
|
||||
|
||||
// Now check to see if the message was processed by the listener and if not
|
||||
// send it through a single process event listener
|
||||
foreach ($queue as $message) {
|
||||
if (!$message->isProcessed()) {
|
||||
$event = new MessageQueueProcessEvent($message);
|
||||
$this->dispatchEvent('process_message_queue', $message, false, $event);
|
||||
}
|
||||
|
||||
if ($message->isSuccess()) {
|
||||
++$counter;
|
||||
$message->setSuccess();
|
||||
$message->setLastAttempt(new \DateTime());
|
||||
$message->setDateSent(new \DateTime());
|
||||
$message->setStatus(MessageQueue::STATUS_SENT);
|
||||
} elseif ($message->isFailed()) {
|
||||
// Failure such as email delivery issue or something so retry in a short time
|
||||
$this->reschedule($message, new \DateInterval(self::DEFAULT_RESCHEDULE_INTERVAL));
|
||||
} // otherwise assume the listener did something such as rescheduling the message
|
||||
}
|
||||
|
||||
// add listener
|
||||
$this->saveEntities($queue);
|
||||
|
||||
return $counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $persist
|
||||
*/
|
||||
public function reschedule($message, \DateInterval $rescheduleInterval, $leadId = null, $channel = null, $channelId = null, $persist = false): void
|
||||
{
|
||||
if (!$message instanceof MessageQueue && $leadId && $channel && $channelId) {
|
||||
$message = $this->getRepository()->findMessage($channel, $channelId, $leadId);
|
||||
$persist = true;
|
||||
}
|
||||
|
||||
if (!$message) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message->setAttempts($message->getAttempts() + 1);
|
||||
$message->setLastAttempt(new \DateTime());
|
||||
$rescheduleTo = clone $message->getScheduledDate();
|
||||
|
||||
$rescheduleTo->add($rescheduleInterval);
|
||||
$message->setScheduledDate($rescheduleTo);
|
||||
$message->setStatus(MessageQueue::STATUS_RESCHEDULED);
|
||||
|
||||
if ($persist) {
|
||||
$this->saveEntity($message);
|
||||
}
|
||||
|
||||
// Mark as processed for listeners
|
||||
$message->setProcessed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $channelIds
|
||||
*/
|
||||
public function getQueuedChannelCount($channel, $channelIds = []): int
|
||||
{
|
||||
return $this->getRepository()->getQueuedChannelCount($channel, $channelIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?object $entity
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
switch ($action) {
|
||||
case 'process_message_queue':
|
||||
$name = ChannelEvents::PROCESS_MESSAGE_QUEUE;
|
||||
break;
|
||||
case 'process_batch_message_queue':
|
||||
$name = ChannelEvents::PROCESS_MESSAGE_QUEUE_BATCH;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = ChannelEvents::MESSAGE_QUEUED;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new MessageQueueEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->em);
|
||||
}
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\PreferenceBuilder;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
|
||||
class ChannelPreferences
|
||||
{
|
||||
/**
|
||||
* @var ArrayCollection[]
|
||||
*/
|
||||
private array $organizedByPriority = [];
|
||||
|
||||
public function __construct(
|
||||
private Event $event,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $priority
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addPriority($priority)
|
||||
{
|
||||
$priority = (int) $priority;
|
||||
|
||||
if (!isset($this->organizedByPriority[$priority])) {
|
||||
$this->organizedByPriority[$priority] = new ArrayCollection();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $priority
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addLog(LeadEventLog $log, $priority)
|
||||
{
|
||||
$priority = (int) $priority;
|
||||
|
||||
$this->addPriority($priority);
|
||||
|
||||
// We have to clone the log to not affect the original assocaited with the MM event itself
|
||||
|
||||
// Clone to remove from Doctrine's ORM memory since we're having to apply a pseudo event
|
||||
$log = clone $log;
|
||||
$log->setEvent($this->event);
|
||||
|
||||
$this->organizedByPriority[$priority]->set($log->getId(), $log);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a log from all prioritized groups.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removeLog(LeadEventLog $log)
|
||||
{
|
||||
foreach ($this->organizedByPriority as $logs) {
|
||||
/** @var ArrayCollection<int, LeadEventLog> $logs */
|
||||
$logs->remove($log->getId());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $priority
|
||||
*
|
||||
* @return ArrayCollection|LeadEventLog[]
|
||||
*/
|
||||
public function getLogsByPriority($priority)
|
||||
{
|
||||
$priority = (int) $priority;
|
||||
|
||||
return $this->organizedByPriority[$priority] ?? new ArrayCollection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\PreferenceBuilder;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class PreferenceBuilder
|
||||
{
|
||||
/**
|
||||
* @var ChannelPreferences[]
|
||||
*/
|
||||
private array $channels = [];
|
||||
|
||||
public function __construct(
|
||||
ArrayCollection $logs,
|
||||
private Event $event,
|
||||
array $channels,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
$this->buildRules($logs, $channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ChannelPreferences[]
|
||||
*/
|
||||
public function getChannelPreferences()
|
||||
{
|
||||
return $this->channels;
|
||||
}
|
||||
|
||||
public function removeLogFromAllChannels(LeadEventLog $log): void
|
||||
{
|
||||
foreach ($this->channels as $channelPreferences) {
|
||||
$channelPreferences->removeLog($log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
* @param int $priority
|
||||
*/
|
||||
private function addChannelRule($channel, array $rule, LeadEventLog $log, $priority): void
|
||||
{
|
||||
$channelPreferences = $this->getChannelPreferenceObject($channel, $priority);
|
||||
|
||||
if (DoNotContact::IS_CONTACTABLE !== $rule['dnc']) {
|
||||
$log->appendToMetadata(
|
||||
[
|
||||
$channel => [
|
||||
'failed' => 1,
|
||||
'dnc' => $rule['dnc'],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->debug("MARKETING MESSAGE: Set $channel as priority $priority for contact ID #".$log->getLead()->getId());
|
||||
|
||||
$channelPreferences->addLog($log, $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*
|
||||
* @return ChannelPreferences
|
||||
*/
|
||||
private function getChannelPreferenceObject($channel, $priority)
|
||||
{
|
||||
if (!isset($this->channels[$channel])) {
|
||||
$this->channels[$channel] = new ChannelPreferences($this->event);
|
||||
}
|
||||
|
||||
$this->channels[$channel]->addPriority($priority);
|
||||
|
||||
return $this->channels[$channel];
|
||||
}
|
||||
|
||||
private function buildRules(ArrayCollection $logs, array $channels): void
|
||||
{
|
||||
/** @var LeadEventLog $log */
|
||||
foreach ($logs as $log) {
|
||||
$channelRules = $log->getLead()->getChannelRules();
|
||||
$allChannels = $channels;
|
||||
$priority = 1;
|
||||
|
||||
// Build priority based on channel rules
|
||||
foreach ($channelRules as $channel => $rule) {
|
||||
$this->addChannelRule($channel, $rule, $log, $priority);
|
||||
++$priority;
|
||||
unset($allChannels[$channel]);
|
||||
}
|
||||
|
||||
// Add the rest of the channels as least priority
|
||||
foreach ($allChannels as $channel => $messageSettings) {
|
||||
$this->addChannelRule($channel, ['dnc' => DoNotContact::IS_CONTACTABLE], $log, $priority);
|
||||
++$priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block mauicContent %}message
|
||||
{% endblock %}
|
||||
|
||||
{% block headerTitle %}
|
||||
{{ item.getName() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block preHeader %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig',
|
||||
{
|
||||
'item' : item,
|
||||
'templateButtons' : {
|
||||
'close' : securityHasEntityAccess(permissions['channel:messages:viewown'], permissions['channel:messages:viewother'], item.getCreatedBy()),
|
||||
},
|
||||
'routeBase' : 'message',
|
||||
'targetLabel' : 'mautic.channel.messages'|trans
|
||||
}
|
||||
) -}}
|
||||
{{ include('@MauticCore/Modules/category--inline.html.twig', {'category': item.category}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig', {
|
||||
'item': item,
|
||||
'templateButtons': {
|
||||
'edit': securityHasEntityAccess(permissions['channel:messages:editown'], permissions['channel:messages:editother'], item.getCreatedBy()),
|
||||
'clone': permissions['channel:messages:create'],
|
||||
'delete': securityHasEntityAccess(permissions['channel:messages:deleteown'], permissions['channel:messages:deleteother'], item.getCreatedBy()),
|
||||
},
|
||||
'routeBase': 'message'
|
||||
}) -}}
|
||||
{% endblock %}
|
||||
|
||||
{% block publishStatus %}
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {'entity' : item}) -}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- left section -->
|
||||
<div class="col-md-9 height-auto">
|
||||
<div>
|
||||
<!-- form detail header -->
|
||||
{% include '@MauticCore/Helper/description--expanded.html.twig' with {'description': item.description} %}
|
||||
<!--/ form detail header -->
|
||||
<!-- form detail collapseable -->
|
||||
<div class="collapse pr-md pl-md" id="focus-details">
|
||||
<div class="pr-md pl-md pb-md">
|
||||
<div class="panel shd-none mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<tbody>
|
||||
{{- include('@MauticCore/Helper/details.html.twig', {'entity' : item}) -}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ form detail collapseable -->
|
||||
</div>
|
||||
|
||||
<!--/ form detail collapseable toggler -->
|
||||
<div>
|
||||
<!-- form detail collapseable toggler -->
|
||||
<div class="hr-expand nm">
|
||||
<span data-toggle="tooltip" title="{% trans %}mautic.core.details{% endtrans %}">
|
||||
<a href="javascript:void(0)" class="arrow text-secondary collapsed" data-toggle="collapse" data-target="#focus-details"><span class="caret"></span> {% trans %}mautic.core.details{% endtrans %}</a>
|
||||
</span>
|
||||
</div>
|
||||
<!-- stats -->
|
||||
<div class="pa-md">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel">
|
||||
<div class="panel-body box-layout">
|
||||
<div class="col-md-6 va-m">
|
||||
<h5 class="text-white dark-md fw-sb mb-xs">
|
||||
<div><i class="ri-line-chart-fill pull-left"></i>
|
||||
<span class="pull-left"> {% trans %}mautic.messages.processed.messages{% endtrans %}</span></div>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-md-9 va-m">
|
||||
{{- include('@MauticCore/Helper/graph_dateselect.html.twig', {'dateRangeForm' : dateRangeForm, 'class' : 'pull-right'}) -}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex fd-column pt-0 pl-15 pb-15 pr-15 min-h-256">
|
||||
{{- include('@MauticCore/Helper/chart.html.twig', {'chartData' : eventCounts, 'chartType' : 'line', 'chartHeight' : 300}) -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ stats -->
|
||||
{{ customContent('details.stats.graph.below', _context) }}
|
||||
|
||||
<!-- tabs controls -->
|
||||
<ul class="nav nav-tabs nav-tabs-contained">
|
||||
{% set active = 'active' %}
|
||||
{% for channel, contacts in messagedLeads %}
|
||||
<li class="{{ active }}">
|
||||
<a href="#contacts-{{ channel }}" role="tab" data-toggle="tab">
|
||||
{{ ('all' is not same as(channel)) ? channels[channel]['label'] : 'mautic.lead.leads'|trans() }}
|
||||
</a>
|
||||
</li>
|
||||
{% set active = '' %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!--/ tabs controls -->
|
||||
</div>
|
||||
|
||||
<!-- start: tab-content -->
|
||||
<div class="tab-content pa-md">
|
||||
{% set active = ' active in' %}
|
||||
{% for channel, contacts in messagedLeads %}
|
||||
<div class="tab-pane bdr-w-0 page-list{{ active }}" id="contacts-{{ channel }}">
|
||||
{% set message = ('all' is same as(channel)) ? 'mautic.channel.message.all_contacts' : 'mautic.channel.message.channel_contacts' %}
|
||||
<div class="alert alert-info"><strong>{{ message|trans }}</strong></div>
|
||||
<div class="message-{{ channel }}">
|
||||
{{ contacts|raw }}
|
||||
</div>
|
||||
</div>
|
||||
{% set active = '' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- right section -->
|
||||
<div class="col-md-3 bdr-l height-auto">
|
||||
<!-- recent activity -->
|
||||
{{- include('@MauticCore/Helper/recentactivity.html.twig', {'logs' : logs}) -}}
|
||||
{% block rightFormContent %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,87 @@
|
||||
{% extends "@MauticCore/FormTheme/form_tabbed.html.twig" %}
|
||||
{% form_theme form _self %}
|
||||
{% block channel_row %}
|
||||
{% if form.children.channel is defined %}
|
||||
{% set channel = form.children.channel.vars.data %}
|
||||
{% set enabled = form.children.isEnabled.vars.data %}
|
||||
{% set channelContent = customContent('channel.right', _context) %}
|
||||
{% set leftCol = channelContent ? 6 : 12 %}
|
||||
{% set enableCol = channelContent ? '' : 'col-md-2' %}
|
||||
{% set propsCol = channelContent ? '' : 'col-md-10' %}
|
||||
|
||||
{{ form_row(form.children.channel) }}
|
||||
{{ form_errors(form) }}
|
||||
<div class="row">
|
||||
<div class="col-md-{{ leftCol }}">
|
||||
<div class="{{ enableCol }}">
|
||||
{{ form_row(form.children.isEnabled) }}
|
||||
</div>
|
||||
<div class="{{ propsCol }}">
|
||||
<div class="message_channel_properties_{{ channel }}{% if not enabled %} hide{% endif %}">
|
||||
{% if form.children.channelId is defined %}
|
||||
{{ form_row(form.children.channelId) }}
|
||||
{% endif %}
|
||||
|
||||
{% if form.children.properties is defined and form.children.properties is not empty %}
|
||||
{{ form_row(form.children.properties) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if channelContent %}
|
||||
<div class="col-md-6">
|
||||
{{ channelContent }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{# active, id, name, content #}
|
||||
{% set tabs, active = [], true %}
|
||||
{% set _channel = '' %}
|
||||
{% for channel, config in channels %}
|
||||
{% if form.channels[channel] is defined %}
|
||||
{% set tab = {
|
||||
'active' : active,
|
||||
'id' : 'channel_' ~ channel,
|
||||
'name' : config.label,
|
||||
'content' : form_row(form.channels[channel]),
|
||||
'containerAttr' : {
|
||||
'style' : 'min-height: 200px;',
|
||||
},
|
||||
} %}
|
||||
{% if formContainsErrors(form.channels[channel]) %}
|
||||
{% set tab = tab|merge({'class': 'text-danger', 'icon': 'ri-alert-fill'}) %}
|
||||
{% elseif form.channels[channel].isEnabled.vars.data %}
|
||||
{% set tab = tab|merge({'published': true}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set tabs, active = tabs|merge([tab]), false %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set formTabs = tabs %}
|
||||
|
||||
{% block aboveTabsContent %}
|
||||
<div class="pa-md row">
|
||||
<div class="col-md-12">
|
||||
{{ form_row(form.name) }}
|
||||
{{ form_row(form.description) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block rightFormContent %}
|
||||
{{ form_row(form.category) }}
|
||||
{{ form_row(form.projects) }}
|
||||
{{ form_row(form.isPublished, {
|
||||
'attr': {
|
||||
'data-none': 'mautic.core.form.unavailable_regardless_of_scheduling',
|
||||
'data-start': 'mautic.core.form.available_on_scheduled_date',
|
||||
'data-both': 'mautic.core.form.available_during_scheduled_period',
|
||||
'data-end': 'mautic.core.form.available_until_scheduled_end'
|
||||
}
|
||||
}) }}
|
||||
{{ form_row(form.publishUp, {'label': 'mautic.core.form.available.available_from'}) }}
|
||||
{{ form_row(form.publishDown, {'label': 'mautic.core.form.available.unavailable_from'}) }}
|
||||
{% endblock %}
|
||||
{% block _content %}{% endblock %}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% set messageChannels, channels = item.getChannels(), [] %}
|
||||
{% if messageChannels %}
|
||||
{% for channelName, channel in messageChannels %}
|
||||
{% if channel.isEnabled() %}
|
||||
{% set channels = channels|merge({0: translatorHasId('mautic.channel.' ~ channelName) ? ('mautic.channel.' ~ channelName)|trans : channelName|title, }) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<td class="visible-md visible-lg">
|
||||
{% for channel in channels %}
|
||||
<span size="sm" class="label label-gray">{{ channel }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% if showMore is defined %}
|
||||
<a href="{{ url('mautic_message_index', {'search': searchString}) }}" data-toggle="ajax">
|
||||
<span>{{ 'mautic.core.search.more'|trans({'%count%': remaining}) }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url('mautic_message_action', {'objectAction': 'view', 'objectId': item.id}) }}" data-toggle="ajax">
|
||||
<span class="fw-sb">{{ item.name }}</span>
|
||||
<span class="ml-4 mr-sm">#{{ item.id }}</span>
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': item,
|
||||
'status': 'available',
|
||||
'simplified': 'true'
|
||||
}) -}}
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% set extra = event.extra %}
|
||||
{% set log = extra.log %}
|
||||
{% set eventType = log.type %}
|
||||
{% set eventSettings = extra.campaignEventSettings %}
|
||||
{% set messageSettings = eventSettings.action[eventType].timelineTemplateVars.messageSettings %}
|
||||
{% set template = '@MauticCampaign/SubscribedEvents/Timeline/index.html.twig' %}
|
||||
{% set channelEvent = event %}
|
||||
{% set channelEvent = channelEvent|merge({extra: {log: log}}) %}
|
||||
{% set vars = {'event' : channelEvent} %}
|
||||
|
||||
{% set counter = extra.log.metadata|length %}
|
||||
{% for channel, results in extra.log.metadata %}
|
||||
{% if messageSettings[channel] is defined %}
|
||||
<h4>{{ messageSettings[channel]['label'] }}</h4>
|
||||
{% if log.metadata[channel].dnc is defined %}
|
||||
{{ getChannelDncText(log.metadata[channel].dnc) }}
|
||||
{% endif %}
|
||||
{# Successful send through this channel #}
|
||||
{% if messageSettings[channel].campaignAction is not empty %}
|
||||
{% set eventType = messageSettings[channel]['campaignAction'] %}
|
||||
|
||||
{% if eventSettings.action[eventType].timelineTemplate is defined and eventSettings.action[eventType].timelineTemplate is not empty %}
|
||||
{% set template = eventSettings.action[eventType].timelineTemplate %}
|
||||
{% endif %}
|
||||
{% if eventSettings.action[eventType].timelineTemplateVars is defined and eventSettings.action[eventType].timelineTemplateVars is not empty %}
|
||||
{% set extra = vars.event.extra|merge(eventSettings.action[eventType].timelineTemplateVars) %}
|
||||
{% set vars = {
|
||||
'event': {
|
||||
'extra' : extra
|
||||
}
|
||||
} %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% set counter = counter - 1 %}
|
||||
{% if counter > 0 %}
|
||||
<hr/>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{- include(template, vars) -}}
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% set item = event.extra.log is not empty ? event.extra.log : null %}
|
||||
{% if item is not empty %}
|
||||
<table class="table table-hover table-sm table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}mautic.queued.message.timeline.channel{% endtrans %}</th>
|
||||
<th>{% trans %}mautic.queued.message.timeline.attempts{% endtrans %}</th>
|
||||
<th>{% trans %}mautic.queued.message.timeline.date.added{% endtrans %}</th>
|
||||
<th>{% trans %}mautic.queued.message.timeline.rescheduled{% endtrans %}</th>
|
||||
<th>{% trans %}mautic.queued.message.timeline.status{% endtrans %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ getChannelLabel(item.channelName) }}</th>
|
||||
<td>{{ item.attempts }}</td>
|
||||
<td>{% if item.dateAdded is not empty %}{{ dateToFullConcat(item.dateAdded, 'UTC') }}{% endif %}</td>
|
||||
<td>{% if item.scheduledDate is not empty %}{{ dateToFullConcat(item.scheduledDate, 'UTC') }}{% endif %}</td>
|
||||
<td id="queued-status-{{ item.id }}">
|
||||
{{ ('mautic.message.queue.status.' ~ item.status) | trans({},'javascript') }}
|
||||
</td>
|
||||
<td>
|
||||
{% if ('pending' == item.status) %}
|
||||
<button type="button" id="queued-message-{{ item.id }}" class="btn btn-ghost btn-nospin" onclick="Mautic.cancelQueuedMessageEvent({{ item.id }})" data-toggle="tooltip" title="{% trans %}mautic.queued.message.event.cancel{% endtrans %}">
|
||||
<i class="ri-close-line text-danger"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Security\Permissions;
|
||||
|
||||
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ChannelPermissions extends AbstractPermissions
|
||||
{
|
||||
public function __construct($params)
|
||||
{
|
||||
parent::__construct($params);
|
||||
|
||||
$this->addStandardPermissions('categories');
|
||||
$this->addExtendedPermissions('messages');
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'channel';
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
|
||||
{
|
||||
$this->addStandardFormFields($this->getName(), 'categories', $builder, $data);
|
||||
$this->addExtendedFormFields($this->getName(), 'messages', $builder, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Command;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\EmailBundle\Entity\StatRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class ProcessMarketingMessagesQueueCommandFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
|
||||
public function testIdleCommand(): void
|
||||
{
|
||||
$commandTester = $this->testSymfonyCommand('mautic:messages:send');
|
||||
Assert::assertSame(0, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testCommandWithEmailQueue(): void
|
||||
{
|
||||
$email = $this->createEmail('Test Email');
|
||||
$this->em->flush();
|
||||
|
||||
$scheduledDate = new \DateTime('-10 minutes');
|
||||
$datePublished = new \DateTime('-1 day');
|
||||
|
||||
// Create 60 different leads and message queue items
|
||||
$leads = [];
|
||||
$messages = [];
|
||||
for ($i = 0; $i < 60; ++$i) {
|
||||
$leads[$i] = $this->createLead("John{$i}", "Doe{$i}", "jd{$i}@example.com");
|
||||
$this->em->persist($leads[$i]);
|
||||
}
|
||||
$this->em->flush();
|
||||
|
||||
// Create a message for each lead
|
||||
foreach ($leads as $lead) {
|
||||
$messages[] = $this->createMessageQueue($email, $lead, $scheduledDate, $datePublished);
|
||||
}
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$this->em->persist($message);
|
||||
}
|
||||
$this->em->flush();
|
||||
|
||||
$commandTester = $this->testSymfonyCommand('mautic:messages:send');
|
||||
Assert::assertSame(0, $commandTester->getStatusCode());
|
||||
Assert::assertStringContainsString('Messages sent: 60', $commandTester->getDisplay());
|
||||
|
||||
// Verify that stats were created for a sample of leads
|
||||
$this->assertEmailStatCreated($email, $leads[0]);
|
||||
$this->assertEmailStatCreated($email, $leads[29]);
|
||||
$this->assertEmailStatCreated($email, $leads[59]);
|
||||
}
|
||||
|
||||
public function testCommandWithLimitParameter(): void
|
||||
{
|
||||
$lead = $this->createLead('John', 'Doe', 'jd@example.com');
|
||||
$email1 = $this->createEmail('Test Email 1');
|
||||
$email2 = $this->createEmail('Test Email 2');
|
||||
$email3 = $this->createEmail('Test Email 3');
|
||||
$this->em->flush();
|
||||
|
||||
$scheduledDate = new \DateTime('-10 minutes');
|
||||
$datePublished = new \DateTime('-1 day');
|
||||
|
||||
$messages = [
|
||||
$this->createMessageQueue($email1, $lead, $scheduledDate, $datePublished),
|
||||
$this->createMessageQueue($email2, $lead, $scheduledDate, $datePublished),
|
||||
$this->createMessageQueue($email3, $lead, $scheduledDate, $datePublished),
|
||||
];
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$this->em->persist($message);
|
||||
}
|
||||
$this->em->flush();
|
||||
|
||||
$commandTester = $this->testSymfonyCommand('mautic:messages:send', ['--limit' => 2]);
|
||||
Assert::assertSame(0, $commandTester->getStatusCode());
|
||||
Assert::assertStringContainsString('Messages sent: 2', $commandTester->getDisplay());
|
||||
}
|
||||
|
||||
private function createMessageQueue(Email $email, Lead $lead, \DateTime $scheduledDate, \DateTime $datePublished): MessageQueue
|
||||
{
|
||||
$message = new MessageQueue();
|
||||
$message->setScheduledDate($scheduledDate);
|
||||
$message->setDatePublished($datePublished);
|
||||
$message->setChannel('email');
|
||||
$message->setChannelId($email->getId());
|
||||
$message->setLead($lead);
|
||||
$message->setPriority(MessageQueue::PRIORITY_NORMAL);
|
||||
$message->setMaxAttempts(3);
|
||||
$message->setAttempts(0);
|
||||
$message->setStatus(MessageQueue::STATUS_PENDING);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
private function assertEmailStatCreated(Email $email, Lead $lead): void
|
||||
{
|
||||
/** @var StatRepository $emailStatRepository */
|
||||
$emailStatRepository = $this->em->getRepository(Stat::class);
|
||||
|
||||
/** @var Stat|null $emailStat */
|
||||
$emailStat = $emailStatRepository->findOneBy([
|
||||
'email' => $email->getId(),
|
||||
'lead' => $lead->getId(),
|
||||
]);
|
||||
|
||||
Assert::assertNotNull($emailStat, "Email stat not created for email ID {$email->getId()} and lead ID {$lead->getId()}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class SendChannelBroadcastCommandTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testBroadcastCommand(): void
|
||||
{
|
||||
$commandTester = $this->testSymfonyCommand('mautic:broadcasts:send');
|
||||
Assert::assertSame(0, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testBroadcastCommandWithLimit(): void
|
||||
{
|
||||
$commandTester = $this->testSymfonyCommand('mautic:broadcasts:send', ['--limit' => 1]);
|
||||
Assert::assertSame(0, $commandTester->getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\Channel;
|
||||
use Mautic\ChannelBundle\Entity\Message;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class MessageApiControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testCreateMessage(): void
|
||||
{
|
||||
$payloadJson = <<<'JSON'
|
||||
{
|
||||
"name": "API message",
|
||||
"description": "Marketing message created via API functional test",
|
||||
"channels": {
|
||||
"email": {
|
||||
"channel": "email",
|
||||
"channelId": 12,
|
||||
"isEnabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON;
|
||||
|
||||
$payloadArray = json_decode($payloadJson, true);
|
||||
|
||||
$this->client->request('POST', '/api/messages/new', $payloadArray);
|
||||
$responseJson = $this->client->getResponse()->getContent();
|
||||
self::assertResponseStatusCodeSame(201, $responseJson);
|
||||
$this->assertMessagePayload($payloadArray, json_decode($responseJson, true)['message'], $responseJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $payload
|
||||
* @param mixed[] $expectedResponsePayload
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('patchProvider')]
|
||||
public function testEditMessageWithPatch(array $payload, array $expectedResponsePayload): void
|
||||
{
|
||||
$channel = new Channel();
|
||||
$channel->setChannel('email');
|
||||
$channel->setChannelId(12);
|
||||
$channel->setIsEnabled(true);
|
||||
|
||||
$message = new Message();
|
||||
$message->setName('API message');
|
||||
$message->addChannel($channel);
|
||||
|
||||
$this->em->persist($channel);
|
||||
$this->em->persist($message);
|
||||
$this->em->flush();
|
||||
$this->em->detach($channel);
|
||||
$this->em->detach($message);
|
||||
|
||||
$patchPayload = ['id' => $message->getId()] + $payload;
|
||||
$this->client->request('PATCH', "/api/messages/{$message->getId()}/edit", $patchPayload);
|
||||
$responseJson = $this->client->getResponse()->getContent();
|
||||
self::assertResponseIsSuccessful($responseJson);
|
||||
$this->assertMessagePayload(
|
||||
['id' => $message->getId()] + $expectedResponsePayload,
|
||||
json_decode($responseJson, true)['message'],
|
||||
$responseJson
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: the ID is added to the payload automatically in the test.
|
||||
*
|
||||
* @return iterable<mixed[]>
|
||||
*/
|
||||
public static function patchProvider(): iterable
|
||||
{
|
||||
yield [
|
||||
[
|
||||
'name' => 'API message (updated)',
|
||||
],
|
||||
[
|
||||
'name' => 'API message (updated)',
|
||||
'description' => null,
|
||||
'channels' => [
|
||||
'email' => [
|
||||
'channel' => 'email',
|
||||
'channelId' => 12,
|
||||
'isEnabled' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
yield [
|
||||
[
|
||||
'description' => 'Description (updated)',
|
||||
'channels' => [
|
||||
'email' => [
|
||||
'channel' => 'email',
|
||||
'channelId' => 13,
|
||||
'isEnabled' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'API message',
|
||||
'description' => 'Description (updated)',
|
||||
'channels' => [
|
||||
'email' => [
|
||||
'channel' => 'email',
|
||||
'channelId' => 13,
|
||||
'isEnabled' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testEditMessagesWithPatch(): void
|
||||
{
|
||||
$channel1 = new Channel();
|
||||
$channel1->setChannel('email');
|
||||
$channel1->setChannelId(12);
|
||||
$channel1->setIsEnabled(true);
|
||||
|
||||
$message1 = new Message();
|
||||
$message1->setName('API message 1');
|
||||
$message1->addChannel($channel1);
|
||||
|
||||
$channel2 = new Channel();
|
||||
$channel2->setChannel('email');
|
||||
$channel2->setChannelId(13);
|
||||
$channel2->setIsEnabled(true);
|
||||
|
||||
$message2 = new Message();
|
||||
$message2->setName('API message 2');
|
||||
$message2->addChannel($channel2);
|
||||
|
||||
$this->em->persist($channel1);
|
||||
$this->em->persist($channel2);
|
||||
$this->em->persist($message1);
|
||||
$this->em->persist($message2);
|
||||
$this->em->flush();
|
||||
$this->em->detach($channel1);
|
||||
$this->em->detach($channel2);
|
||||
$this->em->detach($message1);
|
||||
$this->em->detach($message2);
|
||||
|
||||
$patchPayload = [
|
||||
['id' => $message1->getId(), 'name' => 'API message 1 (updated)'],
|
||||
['id' => $message2->getId(), 'channels' => ['email' => ['channelId' => 14, 'isEnabled' => false]]],
|
||||
];
|
||||
$this->client->request('PATCH', '/api/messages/batch/edit', $patchPayload);
|
||||
$responseJson = $this->client->getResponse()->getContent();
|
||||
self::assertResponseIsSuccessful($responseJson);
|
||||
$responseArray = json_decode($responseJson, true);
|
||||
$this->assertMessagePayload(
|
||||
[
|
||||
'id' => $message1->getId(),
|
||||
'name' => 'API message 1 (updated)',
|
||||
'description' => null,
|
||||
'channels' => [
|
||||
'email' => [
|
||||
'channel' => 'email',
|
||||
'channelId' => 12,
|
||||
'isEnabled' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
$responseArray['messages'][0],
|
||||
$responseJson
|
||||
);
|
||||
$this->assertMessagePayload(
|
||||
[
|
||||
'id' => $message2->getId(),
|
||||
'name' => 'API message 2',
|
||||
'description' => null,
|
||||
'channels' => [
|
||||
'email' => [
|
||||
'channel' => 'email',
|
||||
'channelId' => 14,
|
||||
'isEnabled' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
$responseArray['messages'][1],
|
||||
$responseJson
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $expectedPayload
|
||||
* @param mixed[] $actualPayload
|
||||
*/
|
||||
private function assertMessagePayload(array $expectedPayload, array $actualPayload, string $deliveredPayloadJson): void
|
||||
{
|
||||
Assert::assertSame($expectedPayload['name'], $actualPayload['name'], $deliveredPayloadJson);
|
||||
Assert::assertSame($expectedPayload['description'], $actualPayload['description'], $deliveredPayloadJson);
|
||||
Assert::assertCount(count($expectedPayload['channels']), $actualPayload['channels'], $deliveredPayloadJson);
|
||||
Assert::assertGreaterThan(0, $actualPayload['id'], $deliveredPayloadJson);
|
||||
|
||||
Assert::assertSame($expectedPayload['channels']['email']['channel'], $actualPayload['channels'][0]['channel'], $deliveredPayloadJson);
|
||||
Assert::assertSame($expectedPayload['channels']['email']['channelId'], $actualPayload['channels'][0]['channelId'], $deliveredPayloadJson);
|
||||
Assert::assertSame($expectedPayload['channels']['email']['isEnabled'], $actualPayload['channels'][0]['isEnabled'], $deliveredPayloadJson);
|
||||
Assert::assertGreaterThan(0, $actualPayload['channels'][0]['id'], $deliveredPayloadJson);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Controller;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\Message;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class MessageControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testFormWithProject(): void
|
||||
{
|
||||
$message = new Message();
|
||||
$message->setName('Test message');
|
||||
$this->em->persist($message);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
$this->em->persist($project);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/messages/edit/'.$message->getId());
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['message[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedMessage = $this->em->find(Message::class, $message->getId());
|
||||
Assert::assertSame($project->getId(), $savedMessage->getProjects()->first()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class MessageControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testMMUiWorkflow(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/messages/new');
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form([
|
||||
'message[name]' => 'Test message',
|
||||
'message[description]' => 'Test message description',
|
||||
]);
|
||||
|
||||
$this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Controller;
|
||||
|
||||
use Mautic\ChannelBundle\Entity\Channel;
|
||||
use Mautic\ChannelBundle\Entity\Message;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class MessageProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
|
||||
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
|
||||
{
|
||||
$projectOne = $this->createProject('Project One');
|
||||
$projectTwo = $this->createProject('Project Two');
|
||||
$projectThree = $this->createProject('Project Three');
|
||||
|
||||
$messageAlpha = $this->createMessage('Message Alpha');
|
||||
$messageBeta = $this->createMessage('Message Beta');
|
||||
$this->createMessage('Message Gamma');
|
||||
$this->createMessage('Message Delta');
|
||||
|
||||
$messageAlpha->addProject($projectOne);
|
||||
$messageAlpha->addProject($projectTwo);
|
||||
$messageBeta->addProject($projectTwo);
|
||||
$messageBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/messages', '/s/messages']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
|
||||
*/
|
||||
public static function searchDataProvider(): \Generator
|
||||
{
|
||||
yield 'search by one project' => [
|
||||
'searchTerm' => 'project:"Project Two"',
|
||||
'expectedEntities' => ['Message Alpha', 'Message Beta'],
|
||||
'unexpectedEntities' => ['Message Gamma', 'Message Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND message name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Message Beta'],
|
||||
'unexpectedEntities' => ['Message Alpha', 'Message Gamma', 'Message Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR message name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Message Alpha', 'Message Beta', 'Message Gamma'],
|
||||
'unexpectedEntities' => ['Message Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Message Gamma', 'Message Delta'],
|
||||
'unexpectedEntities' => ['Message Alpha', 'Message Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Message Beta'],
|
||||
'unexpectedEntities' => ['Message Alpha', 'Message Gamma', 'Message Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Message Gamma', 'Message Delta'],
|
||||
'unexpectedEntities' => ['Message Alpha', 'Message Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Message Alpha', 'Message Beta'],
|
||||
'unexpectedEntities' => ['Message Gamma', 'Message Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Message Alpha', 'Message Gamma', 'Message Delta'],
|
||||
'unexpectedEntities' => ['Message Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createMessage(string $name): Message
|
||||
{
|
||||
$message = new Message();
|
||||
$message->setName($name);
|
||||
$message->addChannel((new Channel())
|
||||
->setChannel('email')
|
||||
->setMessage($message));
|
||||
$this->em->persist($message);
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\ChannelBundle\Entity\Message;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MessageTest extends TestCase
|
||||
{
|
||||
public function testMessageUpdatesReflectsInChanges(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$category->setTitle('New Category');
|
||||
$category->setAlias('category');
|
||||
$category->setBundle('bundle');
|
||||
|
||||
$message = new Message();
|
||||
$message->setName('New Message');
|
||||
$message->setDescription('random text string for description');
|
||||
$message->setCategory($category);
|
||||
$message->setPublishDown(new \DateTime());
|
||||
$message->setPublishUp(new \DateTime());
|
||||
|
||||
$this->assertIsArray($message->getChanges());
|
||||
$this->assertNotEmpty($message->getChanges());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Event;
|
||||
|
||||
use Mautic\ChannelBundle\Event\ChannelBroadcastEvent;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ChannelBroadcastEventTest extends TestCase
|
||||
{
|
||||
private string $channel;
|
||||
|
||||
private int $channelId;
|
||||
|
||||
private OutputInterface $output;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->channel = 'email';
|
||||
$this->channelId = 1;
|
||||
$this->output = new BufferedOutput();
|
||||
}
|
||||
|
||||
public function testConstructorAndGetters(): void
|
||||
{
|
||||
$event = new ChannelBroadcastEvent($this->channel, $this->channelId, $this->output);
|
||||
|
||||
$this->assertSame($this->channel, $event->getChannel());
|
||||
$this->assertSame($this->channelId, $event->getId());
|
||||
$this->assertSame($this->output, $event->getOutput());
|
||||
}
|
||||
|
||||
public function testResults(): void
|
||||
{
|
||||
$event = new ChannelBroadcastEvent($this->channel, $this->channelId, $this->output);
|
||||
$successCount = 10;
|
||||
$failedCount = 2;
|
||||
$failedRecipientsByList = ['list1' => ['user1@example.com', 'user2@example.com']];
|
||||
|
||||
$event->setResults($this->channel, $successCount, $failedCount, $failedRecipientsByList);
|
||||
|
||||
$this->assertSame([
|
||||
$this->channel => [
|
||||
'success' => $successCount,
|
||||
'failed' => $failedCount,
|
||||
'failedRecipientsByList' => $failedRecipientsByList,
|
||||
],
|
||||
], $event->getResults());
|
||||
}
|
||||
|
||||
public function testCheckContext(): void
|
||||
{
|
||||
$event = new ChannelBroadcastEvent($this->channel, $this->channelId, $this->output);
|
||||
|
||||
$this->assertTrue($event->checkContext('email'));
|
||||
$this->assertFalse($event->checkContext('sms'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\EventListener;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
|
||||
use Mautic\CampaignBundle\Event\PendingEvent;
|
||||
use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor;
|
||||
use Mautic\CampaignBundle\EventCollector\EventCollector;
|
||||
use Mautic\CampaignBundle\Executioner\Dispatcher\ActionDispatcher;
|
||||
use Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher;
|
||||
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\EventListener\CampaignSubscriber;
|
||||
use Mautic\ChannelBundle\Form\Type\MessageSendType;
|
||||
use Mautic\ChannelBundle\Model\MessageModel;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\EmailEvents;
|
||||
use Mautic\EmailBundle\Form\Type\EmailListType;
|
||||
use Mautic\EmailBundle\Form\Type\EmailSendType;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\SmsBundle\Form\Type\SmsSendType;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
|
||||
class CampaignSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private EventDispatcher $dispatcher;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|MessageModel
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $messageModel;
|
||||
|
||||
private ActionDispatcher $eventDispatcher;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|EventCollector
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $eventCollector;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|Translator
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $translator;
|
||||
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|EventScheduler
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $scheduler;
|
||||
|
||||
private LegacyEventDispatcher $legacyDispatcher;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dispatcher = new EventDispatcher();
|
||||
|
||||
$this->messageModel = $this->createMock(MessageModel::class);
|
||||
|
||||
$this->messageModel->method('getChannels')
|
||||
->willReturn(
|
||||
[
|
||||
'email' => [
|
||||
'campaignAction' => 'email.send',
|
||||
'campaignDecisionsSupported' => [
|
||||
'email.open',
|
||||
'page.pagehit',
|
||||
'asset.download',
|
||||
'form.submit',
|
||||
],
|
||||
'lookupFormType' => EmailListType::class,
|
||||
],
|
||||
'sms' => [
|
||||
'campaignAction' => 'sms.send_text_sms',
|
||||
'campaignDecisionsSupported' => [
|
||||
'page.pagehit',
|
||||
'asset.download',
|
||||
'form.submit',
|
||||
],
|
||||
'lookupFormType' => 'sms_list',
|
||||
'repository' => \Mautic\SmsBundle\Entity\Sms::class,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->messageModel->method('getMessageChannels')
|
||||
->willReturn(
|
||||
[
|
||||
'email' => [
|
||||
'id' => 2,
|
||||
'channel' => 'email',
|
||||
'channel_id' => 2,
|
||||
'properties' => [],
|
||||
],
|
||||
'sms' => [
|
||||
'id' => 1,
|
||||
'channel' => 'sms',
|
||||
'channel_id' => 1,
|
||||
'properties' => [],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->scheduler = $this->createMock(EventScheduler::class);
|
||||
|
||||
$contactTracker = $this->createMock(ContactTracker::class);
|
||||
|
||||
$this->legacyDispatcher = new LegacyEventDispatcher(
|
||||
$this->dispatcher,
|
||||
$this->scheduler,
|
||||
new NullLogger(),
|
||||
$contactTracker
|
||||
);
|
||||
|
||||
$this->eventDispatcher = new ActionDispatcher(
|
||||
$this->dispatcher,
|
||||
new NullLogger(),
|
||||
$this->scheduler,
|
||||
$this->legacyDispatcher
|
||||
);
|
||||
|
||||
$this->eventCollector = $this->createMock(EventCollector::class);
|
||||
|
||||
$this->eventCollector->method('getEventConfig')
|
||||
->willReturnCallback(
|
||||
function (Event $event) {
|
||||
switch ($event->getType()) {
|
||||
case 'email.send':
|
||||
return new ActionAccessor(
|
||||
[
|
||||
'label' => 'mautic.email.campaign.event.send',
|
||||
'description' => 'mautic.email.campaign.event.send_descr',
|
||||
'batchEventName' => EmailEvents::ON_CAMPAIGN_BATCH_ACTION,
|
||||
'formType' => EmailSendType::class,
|
||||
'formTypeOptions' => ['update_select' => 'campaignevent_properties_email', 'with_email_types' => true],
|
||||
'formTheme' => 'MauticEmailBundle:FormTheme\EmailSendList',
|
||||
'channel' => 'email',
|
||||
'channelIdField' => 'email',
|
||||
]
|
||||
);
|
||||
|
||||
case 'sms.send_text_sms':
|
||||
return new ActionAccessor(
|
||||
[
|
||||
'label' => 'mautic.campaign.sms.send_text_sms',
|
||||
'description' => 'mautic.campaign.sms.send_text_sms.tooltip',
|
||||
'eventName' => SmsEvents::ON_CAMPAIGN_TRIGGER_ACTION,
|
||||
'formType' => SmsSendType::class,
|
||||
'formTypeOptions' => ['update_select' => 'campaignevent_properties_sms'],
|
||||
'formTheme' => 'MauticSmsBundle:FormTheme\SmsSendList',
|
||||
'timelineTemplate' => '@MauticSms/SubscribedEvents/Timeline/index.html.twig',
|
||||
'channel' => 'sms',
|
||||
'channelIdField' => 'sms',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
|
||||
$campaignSubscriber = new CampaignSubscriber(
|
||||
$this->messageModel,
|
||||
$this->eventDispatcher,
|
||||
$this->eventCollector,
|
||||
new NullLogger(),
|
||||
$this->translator
|
||||
);
|
||||
|
||||
$this->dispatcher->addSubscriber($campaignSubscriber);
|
||||
$this->dispatcher->addListener(EmailEvents::ON_CAMPAIGN_BATCH_ACTION, [$this, 'sendMarketingMessageEmail']);
|
||||
$this->dispatcher->addListener(SmsEvents::ON_CAMPAIGN_TRIGGER_ACTION, [$this, 'sendMarketingMessageSms']);
|
||||
}
|
||||
|
||||
public function testCorrectChannelIsUsed(): void
|
||||
{
|
||||
$event = $this->getEvent();
|
||||
$config = new ActionAccessor(
|
||||
[
|
||||
'label' => 'mautic.channel.message.send.marketing.message',
|
||||
'description' => 'mautic.channel.message.send.marketing.message.descr',
|
||||
'batchEventName' => ChannelEvents::ON_CAMPAIGN_BATCH_ACTION,
|
||||
'formType' => MessageSendType::class,
|
||||
'formTheme' => 'MauticChannelBundle:FormTheme\MessageSend',
|
||||
'channel' => 'channel.message',
|
||||
'channelIdField' => 'marketingMessage',
|
||||
'connectionRestrictions' => [
|
||||
'target' => [
|
||||
'decision' => [
|
||||
'email.open',
|
||||
'page.pagehit',
|
||||
'asset.download',
|
||||
'form.submit',
|
||||
],
|
||||
],
|
||||
],
|
||||
'timelineTemplate' => '@MauticChannel/SubscribedEvents/Timeline/index.html.twig',
|
||||
'timelineTemplateVars' => [
|
||||
'messageSettings' => [],
|
||||
],
|
||||
]
|
||||
);
|
||||
$logs = $this->getLogs();
|
||||
|
||||
$pendingEvent = new PendingEvent($config, $event, $logs);
|
||||
|
||||
$this->dispatcher->dispatch($pendingEvent, ChannelEvents::ON_CAMPAIGN_BATCH_ACTION);
|
||||
|
||||
$this->assertCount(0, $pendingEvent->getFailures());
|
||||
|
||||
$successful = $pendingEvent->getSuccessful();
|
||||
|
||||
// SMS should be noted as DNC
|
||||
$this->assertFalse(empty($successful->get(2)->getMetadata()['sms']['dnc']));
|
||||
|
||||
// Nothing recorded for success
|
||||
$this->assertTrue(empty($successful->get(1)->getMetadata()));
|
||||
}
|
||||
|
||||
public function sendMarketingMessageEmail(PendingEvent $event): void
|
||||
{
|
||||
$contacts = $event->getContacts();
|
||||
$logs = $event->getPending();
|
||||
$this->assertCount(1, $logs);
|
||||
|
||||
if (1 === $contacts->first()->getId()) {
|
||||
// Processing priority 1 for contact 1, let's fail this one so that SMS is used
|
||||
$event->fail($logs->first(), 'just because');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (2 === $contacts->first()->getId()) {
|
||||
// Processing priority 1 for contact 2 so let's pass it
|
||||
$event->pass($logs->first());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BC support for old campaign.
|
||||
*/
|
||||
public function sendMarketingMessageSms(CampaignExecutionEvent $event): void
|
||||
{
|
||||
$lead = $event->getLead();
|
||||
if (1 === $lead->getId()) {
|
||||
$event->setResult(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (2 === $lead->getId()) {
|
||||
$this->fail('Lead ID 2 is unsubscribed from SMS so this shouldn not have happened.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Event|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private function getEvent()
|
||||
{
|
||||
$event = $this->getMockBuilder(Event::class)
|
||||
->onlyMethods(['getId'])
|
||||
->getMock();
|
||||
$event->method('getId')
|
||||
->willReturn(1);
|
||||
$event->setEventType(Event::TYPE_ACTION);
|
||||
$event->setType('message.send');
|
||||
$event->setChannel('channel.message');
|
||||
$event->setChannelId('1');
|
||||
$event->setProperties(
|
||||
[
|
||||
'canvasSettings' => [
|
||||
'droppedX' => '337',
|
||||
'droppedY' => '155',
|
||||
],
|
||||
'name' => '',
|
||||
'triggerMode' => 'immediate',
|
||||
'triggerDate' => null,
|
||||
'triggerInterval' => '1',
|
||||
'triggerIntervalUnit' => 'd',
|
||||
'anchor' => 'leadsource',
|
||||
'properties' => [
|
||||
'marketingMessage' => '1',
|
||||
],
|
||||
'type' => 'message.send',
|
||||
'eventType' => 'action',
|
||||
'anchorEventType' => 'source',
|
||||
'campaignId' => '1',
|
||||
'_token' => 'q7FpcDX7iye6fBuBzsqMvQWKqW75lcD77jSmuNAEDXg',
|
||||
'buttons' => [
|
||||
'save' => '',
|
||||
],
|
||||
'marketingMessage' => '1',
|
||||
]
|
||||
);
|
||||
$campaign = $this->createMock(Campaign::class);
|
||||
$campaign->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection
|
||||
*/
|
||||
private function getLogs()
|
||||
{
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->method('getId')
|
||||
->willReturn(1);
|
||||
$lead->expects($this->once())
|
||||
->method('getChannelRules')
|
||||
->willReturn(
|
||||
[
|
||||
'sms' => [
|
||||
'dnc' => DoNotContact::IS_CONTACTABLE,
|
||||
],
|
||||
'email' => [
|
||||
'dnc' => DoNotContact::IS_CONTACTABLE,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$log = $this->getMockBuilder(LeadEventLog::class)
|
||||
->onlyMethods(['getLead', 'getId'])
|
||||
->getMock();
|
||||
$log->method('getLead')
|
||||
->willReturn($lead);
|
||||
$log->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$lead2 = $this->createMock(Lead::class);
|
||||
$lead2->method('getId')
|
||||
->willReturn(2);
|
||||
$lead2->expects($this->once())
|
||||
->method('getChannelRules')
|
||||
->willReturn(
|
||||
[
|
||||
'email' => [
|
||||
'dnc' => DoNotContact::IS_CONTACTABLE,
|
||||
],
|
||||
'sms' => [
|
||||
'dnc' => DoNotContact::UNSUBSCRIBED,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$log2 = $this->getMockBuilder(LeadEventLog::class)
|
||||
->onlyMethods(['getLead', 'getId'])
|
||||
->getMock();
|
||||
$log2->method('getLead')
|
||||
->willReturn($lead2);
|
||||
$log2->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
return new ArrayCollection([1 => $log, 2 => $log2]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Model;
|
||||
|
||||
use Mautic\ChannelBundle\Model\ChannelActionModel;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact as DNC;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\DoNotContact;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ChannelActionModelTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private \PHPUnit\Framework\MockObject\MockObject $contactMock5;
|
||||
|
||||
private \PHPUnit\Framework\MockObject\MockObject $contactMock6;
|
||||
|
||||
private \PHPUnit\Framework\MockObject\MockObject $contactModelMock;
|
||||
|
||||
private \PHPUnit\Framework\MockObject\MockObject $doNotContactMock;
|
||||
|
||||
private \PHPUnit\Framework\MockObject\MockObject $translatorMock;
|
||||
|
||||
private ChannelActionModel $actionModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->contactMock5 = $this->createMock(Lead::class);
|
||||
$this->contactMock6 = $this->createMock(Lead::class);
|
||||
$this->contactModelMock = $this->createMock(LeadModel::class);
|
||||
$this->doNotContactMock = $this->createMock(DoNotContact::class);
|
||||
$this->translatorMock = $this->createMock(TranslatorInterface::class);
|
||||
$this->actionModel = new ChannelActionModel(
|
||||
$this->contactModelMock,
|
||||
$this->doNotContactMock,
|
||||
$this->translatorMock
|
||||
);
|
||||
|
||||
$this->contactMock5->method('getId')->willReturn(5);
|
||||
}
|
||||
|
||||
public function testUpdateEntityAccess(): void
|
||||
{
|
||||
$contacts = [5, 6];
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getLeadsByIds')
|
||||
->with($contacts)
|
||||
->willReturn([$this->contactMock5, $this->contactMock6]);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->contactModelMock->expects($matcher)
|
||||
->method('canEditContact')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($this->contactMock5, $parameters[0]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($this->contactMock6, $parameters[0]);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$this->contactModelMock->expects($this->never())
|
||||
->method('getContactChannels');
|
||||
|
||||
$this->actionModel->update($contacts, []);
|
||||
}
|
||||
|
||||
public function testSubscribeContactToEmailChannel(): void
|
||||
{
|
||||
$contacts = [5];
|
||||
$subscribedChannels = ['email', 'sms']; // Subscribe contact to these channels
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getLeadsByIds')
|
||||
->with($contacts)
|
||||
->willReturn([$this->contactMock5]);
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('canEditContact')
|
||||
->with($this->contactMock5)
|
||||
->willReturn(true);
|
||||
|
||||
// Contact is already subscribed to the SMS channel but not to email
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getContactChannels')
|
||||
->with($this->contactMock5)
|
||||
->willReturn(['sms' => 'sms']);
|
||||
|
||||
$this->doNotContactMock->expects($this->once())
|
||||
->method('isContactable')
|
||||
->with($this->contactMock5, 'email')
|
||||
->willReturn(DNC::IS_CONTACTABLE);
|
||||
|
||||
$this->doNotContactMock->expects($this->once())
|
||||
->method('removeDncForContact')
|
||||
->with(5, 'email');
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getPreferenceChannels')
|
||||
->willReturn(['Email' => 'email', 'Text Message' => 'sms']);
|
||||
|
||||
$this->doNotContactMock->expects($this->never())
|
||||
->method('addDncForContact');
|
||||
|
||||
$this->actionModel->update($contacts, $subscribedChannels);
|
||||
}
|
||||
|
||||
public function testSubscribeContactWhoUnsubscribedToEmailChannel(): void
|
||||
{
|
||||
$contacts = [5];
|
||||
$subscribedChannels = ['email', 'sms']; // Subscribe contact to these channels
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getLeadsByIds')
|
||||
->with($contacts)
|
||||
->willReturn([$this->contactMock5]);
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('canEditContact')
|
||||
->with($this->contactMock5)
|
||||
->willReturn(true);
|
||||
|
||||
// Contact is already subscribed to the SMS channel but not to email
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getContactChannels')
|
||||
->with($this->contactMock5)
|
||||
->willReturn(['sms' => 'sms']);
|
||||
|
||||
$this->doNotContactMock->expects($this->once())
|
||||
->method('isContactable')
|
||||
->with($this->contactMock5, 'email')
|
||||
->willReturn(DNC::UNSUBSCRIBED);
|
||||
|
||||
$this->doNotContactMock->expects($this->never())
|
||||
->method('removeDncForContact');
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getPreferenceChannels')
|
||||
->willReturn(['Email' => 'email', 'Text Message' => 'sms']);
|
||||
|
||||
$this->doNotContactMock->expects($this->never())
|
||||
->method('addDncForContact');
|
||||
|
||||
$this->actionModel->update($contacts, $subscribedChannels);
|
||||
}
|
||||
|
||||
public function testUnsubscribeContactFromSmsChannel(): void
|
||||
{
|
||||
$contacts = [5];
|
||||
$subscribedChannels = []; // Unsubscribe contact from missing
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getLeadsByIds')
|
||||
->with($contacts)
|
||||
->willReturn([$this->contactMock5]);
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('canEditContact')
|
||||
->with($this->contactMock5)
|
||||
->willReturn(true);
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getContactChannels')
|
||||
->with($this->contactMock5)
|
||||
->willReturn(['sms' => 'sms']);
|
||||
|
||||
$this->doNotContactMock->expects($this->never())
|
||||
->method('isContactable');
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getPreferenceChannels')
|
||||
->willReturn(['Email' => 'email', 'Text Message' => 'sms']);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->doNotContactMock->expects($matcher)
|
||||
->method('addDncForContact')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(5, $parameters[0]);
|
||||
$this->assertSame('email', $parameters[1]);
|
||||
$this->assertSame(DNC::MANUAL, $parameters[2]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(5, $parameters[0]);
|
||||
$this->assertSame('sms', $parameters[1]);
|
||||
$this->assertSame(DNC::MANUAL, $parameters[2]);
|
||||
}
|
||||
});
|
||||
|
||||
$this->actionModel->update($contacts, $subscribedChannels);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Model;
|
||||
|
||||
use Doctrine\Common\Collections\AbstractLazyCollection;
|
||||
use Mautic\ChannelBundle\Model\FrequencyActionModel;
|
||||
use Mautic\LeadBundle\Entity\FrequencyRule;
|
||||
use Mautic\LeadBundle\Entity\FrequencyRuleRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class FrequencyActionModelTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|Lead
|
||||
*/
|
||||
private MockObject $contactMock5;
|
||||
|
||||
/**
|
||||
* @var MockObject|LeadModel
|
||||
*/
|
||||
private MockObject $contactModelMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|FrequencyRuleRepository
|
||||
*/
|
||||
private MockObject $frequencyRepoMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|FrequencyRule
|
||||
*/
|
||||
private MockObject $frequencyRuleEmailMock;
|
||||
|
||||
/**
|
||||
* @var MockObject|FrequencyRule
|
||||
*/
|
||||
private MockObject $frequencyRuleSmsMock;
|
||||
|
||||
private FrequencyActionModel $actionModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->contactMock5 = $this->createMock(Lead::class);
|
||||
$this->contactModelMock = $this->createMock(LeadModel::class);
|
||||
$this->frequencyRepoMock = $this->createMock(FrequencyRuleRepository::class);
|
||||
$this->frequencyRuleEmailMock = $this->createMock(FrequencyRule::class);
|
||||
$this->frequencyRuleSmsMock = $this->createMock(FrequencyRule::class);
|
||||
$collectionMock = $this->createMock(AbstractLazyCollection::class);
|
||||
$this->actionModel = new FrequencyActionModel(
|
||||
$this->contactModelMock,
|
||||
$this->frequencyRepoMock
|
||||
);
|
||||
|
||||
$collectionMock->method('toArray')
|
||||
->willReturn([
|
||||
'email' => $this->frequencyRuleEmailMock,
|
||||
'sms' => $this->frequencyRuleSmsMock,
|
||||
]);
|
||||
|
||||
$this->contactMock5->method('getFrequencyRules')->willReturn($collectionMock);
|
||||
}
|
||||
|
||||
public function testUpdateWhenEntityAccess(): void
|
||||
{
|
||||
$contacts = [5];
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getLeadsByIds')
|
||||
->with($contacts)
|
||||
->willReturn([$this->contactMock5]);
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('canEditContact')
|
||||
->with($this->contactMock5)
|
||||
->willReturn(false);
|
||||
|
||||
$this->contactModelMock->expects($this->never())
|
||||
->method('getPreferenceChannels');
|
||||
|
||||
$this->actionModel->update($contacts, [], '');
|
||||
}
|
||||
|
||||
public function testUpdate(): void
|
||||
{
|
||||
$contacts = [5];
|
||||
$params = [
|
||||
'subscribed_channels' => ['email', 'sms'],
|
||||
'frequency_number_email' => '2',
|
||||
'frequency_time_email' => 'WEEK',
|
||||
'preferred_channel' => 'email',
|
||||
'contact_pause_start_date_email' => '2018-05-13',
|
||||
'contact_pause_end_date_email' => '2018-05-26',
|
||||
'frequency_number_sms' => '',
|
||||
'frequency_time_sms' => '',
|
||||
'contact_pause_start_date_sms' => '',
|
||||
'contact_pause_end_date_sms' => '',
|
||||
];
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getLeadsByIds')
|
||||
->with($contacts)
|
||||
->willReturn([$this->contactMock5]);
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('canEditContact')
|
||||
->with($this->contactMock5)
|
||||
->willReturn(true);
|
||||
|
||||
$this->contactModelMock->expects($this->once())
|
||||
->method('getPreferenceChannels')
|
||||
->willReturn([
|
||||
'Email' => 'email',
|
||||
'Text Message' => 'sms',
|
||||
]);
|
||||
|
||||
$this->frequencyRuleEmailMock->expects($this->once())
|
||||
->method('setChannel')
|
||||
->with('email');
|
||||
|
||||
$this->frequencyRuleEmailMock->expects($this->once())
|
||||
->method('setLead')
|
||||
->with($this->contactMock5);
|
||||
|
||||
$this->frequencyRuleEmailMock->expects($this->once())
|
||||
->method('setDateAdded');
|
||||
|
||||
$this->frequencyRuleEmailMock->expects($this->once())
|
||||
->method('setFrequencyNumber')
|
||||
->with('2');
|
||||
|
||||
$this->frequencyRuleEmailMock->expects($this->once())
|
||||
->method('setFrequencyTime')
|
||||
->with('WEEK');
|
||||
|
||||
$this->frequencyRuleEmailMock->expects($this->once())
|
||||
->method('setPauseFromDate')
|
||||
->with(new \DateTime('2018-05-13T00:00:00.000000+0000'));
|
||||
|
||||
$this->frequencyRuleEmailMock->expects($this->once())
|
||||
->method('setPauseToDate')
|
||||
->with(new \DateTime('2018-05-26T00:00:00.000000+0000'));
|
||||
|
||||
$this->frequencyRuleEmailMock->expects($this->once())
|
||||
->method('setPreferredChannel')
|
||||
->with(true);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->contactMock5->expects($matcher)
|
||||
->method('addFrequencyRule')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals($this->frequencyRuleEmailMock, $parameters[0]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals($this->frequencyRuleEmailMock, $parameters[0]);
|
||||
}
|
||||
});
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->frequencyRepoMock->expects($matcher)
|
||||
->method('saveEntity')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($this->frequencyRuleEmailMock, $parameters[0]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($this->frequencyRuleSmsMock, $parameters[0]);
|
||||
}
|
||||
});
|
||||
|
||||
$this->frequencyRuleSmsMock->expects($this->once())
|
||||
->method('setChannel')
|
||||
->with('sms');
|
||||
|
||||
$this->frequencyRuleSmsMock->expects($this->once())
|
||||
->method('setLead')
|
||||
->with($this->contactMock5);
|
||||
|
||||
$this->frequencyRuleSmsMock->expects($this->once())
|
||||
->method('setDateAdded');
|
||||
|
||||
$this->frequencyRuleSmsMock->expects($this->once())
|
||||
->method('setFrequencyNumber')
|
||||
->with(null);
|
||||
|
||||
$this->frequencyRuleSmsMock->expects($this->once())
|
||||
->method('setFrequencyTime')
|
||||
->with(null);
|
||||
|
||||
$this->frequencyRuleSmsMock->expects($this->never())
|
||||
->method('setPauseFromDate');
|
||||
|
||||
$this->frequencyRuleSmsMock->expects($this->never())
|
||||
->method('setPauseToDate');
|
||||
|
||||
$this->frequencyRuleSmsMock->expects($this->once())
|
||||
->method('setPreferredChannel')
|
||||
->with(false);
|
||||
|
||||
$this->actionModel->update($contacts, $params, 'email');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Mautic\ChannelBundle\Entity\MessageQueueRepository;
|
||||
use Mautic\ChannelBundle\Model\MessageQueueModel;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class MessageQueueModelTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const DATE = '2019-07-07 15:00:00';
|
||||
|
||||
/**
|
||||
* @var MessageQueueModel
|
||||
*/
|
||||
protected $messageQueue;
|
||||
|
||||
/**
|
||||
* @var MessageQueue
|
||||
*/
|
||||
protected $message;
|
||||
|
||||
/** @var MockObject|LeadModel */
|
||||
protected $leadModel;
|
||||
|
||||
/** @var MockObject|CompanyModel */
|
||||
protected $companyModel;
|
||||
|
||||
/** @var MockObject|EntityManagerInterface */
|
||||
protected $entityManager;
|
||||
|
||||
/** @var MockObject|MessageQueueRepository */
|
||||
protected $messageQueueRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->leadModel = $this->createMock(LeadModel::class);
|
||||
$this->companyModel = $this->createMock(CompanyModel::class);
|
||||
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$this->messageQueueRepository = $this->createMock(MessageQueueRepository::class);
|
||||
$coreHelper = $this->createMock(CoreParametersHelper::class);
|
||||
|
||||
$this->messageQueue = new MessageQueueModel(
|
||||
$this->leadModel,
|
||||
$this->companyModel,
|
||||
$coreHelper,
|
||||
$this->entityManager,
|
||||
$this->createMock(CorePermissions::class),
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(UrlGeneratorInterface::class),
|
||||
$this->createMock(Translator::class),
|
||||
$this->createMock(UserHelper::class),
|
||||
$this->createMock(LoggerInterface::class)
|
||||
);
|
||||
|
||||
$this->entityManager->method('getRepository')->willReturn($this->messageQueueRepository);
|
||||
|
||||
$message = new MessageQueue();
|
||||
$scheduleDate = new \DateTime(self::DATE);
|
||||
$message->setScheduledDate($scheduleDate);
|
||||
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function testRescheduleMessageIntervalDay(): void
|
||||
{
|
||||
$interval = new \DateInterval('P2D');
|
||||
$this->prepareRescheduleMessageIntervalTest($interval);
|
||||
}
|
||||
|
||||
public function testRescheduleMessageIntervalWeek(): void
|
||||
{
|
||||
$interval = new \DateInterval('P4W');
|
||||
$this->prepareRescheduleMessageIntervalTest($interval);
|
||||
}
|
||||
|
||||
public function testRescheduleMessageIntervalMonth(): void
|
||||
{
|
||||
$interval = new \DateInterval('P8M');
|
||||
$this->prepareRescheduleMessageIntervalTest($interval);
|
||||
}
|
||||
|
||||
public function testRescheduleMessageNoInterval(): void
|
||||
{
|
||||
$interval = new \DateInterval('PT0S');
|
||||
$this->prepareRescheduleMessageIntervalTest($interval);
|
||||
}
|
||||
|
||||
protected function prepareRescheduleMessageIntervalTest(\DateInterval $interval)
|
||||
{
|
||||
$oldScheduleDate = $this->message->getScheduledDate();
|
||||
$this->messageQueue->reschedule($this->message, $interval);
|
||||
$scheduleDate = $this->message->getScheduledDate();
|
||||
/** @var \DateTime $oldScheduleDate */
|
||||
$oldScheduleDate->add($interval);
|
||||
|
||||
$this->assertEquals($oldScheduleDate, $scheduleDate);
|
||||
$this->assertNotSame($oldScheduleDate, $scheduleDate);
|
||||
}
|
||||
|
||||
public function testSendMessagesWithNullEvent(): void
|
||||
{
|
||||
$queue = $this->message;
|
||||
$lead = new Lead();
|
||||
$lead->setId(1);
|
||||
$queue->setLead($lead);
|
||||
|
||||
$contactData = [
|
||||
1 => [
|
||||
'firstname' => 'John',
|
||||
'email' => 'john.doe@example.com',
|
||||
],
|
||||
];
|
||||
|
||||
$leadRepository = $this->createMock(LeadRepository::class);
|
||||
$this->leadModel->method('getRepository')->willReturn($leadRepository);
|
||||
$leadRepository->method('getContacts')->willReturn($contactData);
|
||||
|
||||
$this->entityManager->expects($this->exactly(1))
|
||||
->method('detach');
|
||||
|
||||
$this->messageQueueRepository->method('getQueuedMessages')
|
||||
->willReturn([$queue]);
|
||||
|
||||
$this->messageQueue->sendMessages('email', 1);
|
||||
}
|
||||
|
||||
public function testProcessMessageQueueLeadFieldsShouldNotContainCompany(): void
|
||||
{
|
||||
$queue = $this->message;
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setId(1);
|
||||
$queue->setLead($lead);
|
||||
|
||||
$contactData = [
|
||||
1 => [
|
||||
'firstname' => 'John',
|
||||
'email' => 'john.doe@example.com',
|
||||
],
|
||||
];
|
||||
|
||||
$leadRepository = $this->createMock(LeadRepository::class);
|
||||
$this->leadModel->method('getRepository')->willReturn($leadRepository);
|
||||
$leadRepository->method('getContacts')->willReturn($contactData);
|
||||
|
||||
$this->messageQueue->processMessageQueue($queue);
|
||||
$this->assertArrayNotHasKey('companies', $queue->getLead()->getFields());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\PreferenceBuilder;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\ChannelBundle\PreferenceBuilder\ChannelPreferences;
|
||||
|
||||
class ChannelPreferencesTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testLogsAreOrganizedByPriority(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$event = new Event();
|
||||
$event->setCampaign($campaign);
|
||||
|
||||
$channelPreferences = $this->getChannelPreference('email', $event);
|
||||
|
||||
$log1 = new LeadEventLog();
|
||||
$log1->setEvent($event);
|
||||
$log1->setCampaign($campaign);
|
||||
$log1->setMetadata(['log' => 1]);
|
||||
$channelPreferences->addLog($log1, 1);
|
||||
|
||||
$log2 = new LeadEventLog();
|
||||
$log2->setEvent($event);
|
||||
$log2->setCampaign($campaign);
|
||||
$log2->setMetadata(['log' => 2]);
|
||||
$channelPreferences->addLog($log2, 2);
|
||||
|
||||
$organized = $channelPreferences->getLogsByPriority(1);
|
||||
$this->assertEquals($organized->first()->getMetadata()['log'], 1);
|
||||
|
||||
$organized = $channelPreferences->getLogsByPriority(2);
|
||||
$this->assertEquals($organized->first()->getMetadata()['log'], 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ChannelPreferences
|
||||
*/
|
||||
private function getChannelPreference($channel, Event $event)
|
||||
{
|
||||
return new ChannelPreferences($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ChannelBundle\Tests\PreferenceBuilder;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\ChannelBundle\PreferenceBuilder\ChannelPreferences;
|
||||
use Mautic\ChannelBundle\PreferenceBuilder\PreferenceBuilder;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class PreferenceBuilderTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testChannelsArePrioritized(): void
|
||||
{
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->once())
|
||||
->method('getChannelRules')
|
||||
->willReturn(
|
||||
[
|
||||
'sms' => [
|
||||
'dnc' => DoNotContact::IS_CONTACTABLE,
|
||||
],
|
||||
'email' => [
|
||||
'dnc' => DoNotContact::IS_CONTACTABLE,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->method('getLead')
|
||||
->willReturn($lead);
|
||||
$log->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$lead2 = $this->createMock(Lead::class);
|
||||
$lead2->expects($this->once())
|
||||
->method('getChannelRules')
|
||||
->willReturn(
|
||||
[
|
||||
'email' => [
|
||||
'dnc' => DoNotContact::IS_CONTACTABLE,
|
||||
],
|
||||
'sms' => [
|
||||
'dnc' => DoNotContact::UNSUBSCRIBED,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$log2 = $this->createMock(LeadEventLog::class);
|
||||
$log2->method('getLead')
|
||||
->willReturn($lead2);
|
||||
$log2->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$logs = new ArrayCollection([$log, $log2]);
|
||||
|
||||
$event = new Event();
|
||||
|
||||
$builder = new PreferenceBuilder($logs, $event, ['email' => [], 'sms' => [], 'push' => []], new NullLogger());
|
||||
|
||||
$preferences = $builder->getChannelPreferences();
|
||||
|
||||
$this->assertCount(3, $preferences);
|
||||
$this->assertTrue(isset($preferences['email']));
|
||||
$this->assertTrue(isset($preferences['sms']));
|
||||
$this->assertTrue(isset($preferences['push']));
|
||||
|
||||
/** @var ChannelPreferences $emailLogs */
|
||||
$email = $preferences['email'];
|
||||
|
||||
// First priority
|
||||
$emailLogs = $email->getLogsByPriority(1);
|
||||
$this->assertCount(1, $emailLogs);
|
||||
$this->assertEquals(2, $emailLogs->first()->getId());
|
||||
|
||||
// Second priority
|
||||
$emailLogs = $email->getLogsByPriority(2);
|
||||
$this->assertCount(1, $emailLogs);
|
||||
$this->assertEquals(1, $emailLogs->first()->getId());
|
||||
|
||||
// First priority for SMS which should just be one
|
||||
/** @var ChannelPreferences $smsLogs */
|
||||
$sms = $preferences['sms'];
|
||||
$smsLogs = $sms->getLogsByPriority(1);
|
||||
$this->assertCount(1, $smsLogs);
|
||||
$this->assertEquals(1, $smsLogs->first()->getId());
|
||||
|
||||
// None for second priority because of DNC
|
||||
$smsLogs = $sms->getLogsByPriority(2);
|
||||
$this->assertCount(0, $smsLogs);
|
||||
|
||||
// No one had push enabled but it should be defined
|
||||
$push = $preferences['push'];
|
||||
$pushLogs = $push->getLogsByPriority(1);
|
||||
$this->assertCount(0, $pushLogs);
|
||||
}
|
||||
|
||||
public function testLogIsRemovedFromAllChannels(): void
|
||||
{
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects($this->once())
|
||||
->method('getChannelRules')
|
||||
->willReturn(
|
||||
[
|
||||
'sms' => [
|
||||
'dnc' => DoNotContact::IS_CONTACTABLE,
|
||||
],
|
||||
'email' => [
|
||||
'dnc' => DoNotContact::IS_CONTACTABLE,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$log = $this->createMock(LeadEventLog::class);
|
||||
$log->method('getLead')
|
||||
->willReturn($lead);
|
||||
$log->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$logs = new ArrayCollection([$log]);
|
||||
|
||||
$event = new Event();
|
||||
$builder = new PreferenceBuilder($logs, $event, ['email' => [], 'sms' => [], 'push' => []], new NullLogger());
|
||||
|
||||
$preferences = $builder->getChannelPreferences();
|
||||
/** @var ChannelPreferences $sms */
|
||||
$sms = $preferences['sms'];
|
||||
$smsLogs = $sms->getLogsByPriority(1);
|
||||
$this->assertCount(1, $smsLogs);
|
||||
|
||||
/** @var ChannelPreferences $email */
|
||||
$email = $preferences['email'];
|
||||
$emailLogs = $email->getLogsByPriority(2);
|
||||
$this->assertCount(1, $emailLogs);
|
||||
|
||||
$builder->removeLogFromAllChannels($log);
|
||||
|
||||
$preferences = $builder->getChannelPreferences();
|
||||
/** @var ChannelPreferences $sms */
|
||||
$sms = $preferences['sms'];
|
||||
$smsLogs = $sms->getLogsByPriority(1);
|
||||
$this->assertCount(0, $smsLogs);
|
||||
|
||||
/** @var ChannelPreferences $email */
|
||||
$email = $preferences['email'];
|
||||
$emailLogs = $email->getLogsByPriority(2);
|
||||
$this->assertCount(0, $emailLogs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
mautic.message.queue.status.cancelled="Cancelled"
|
||||
mautic.message.queue.status.rescheduled="Rescheduled"
|
||||
mautic.message.queue.status.pending="Pending"
|
||||
mautic.message.queue.status.sent="Sent"
|
||||
@@ -0,0 +1,50 @@
|
||||
mautic.channel.messages="Marketing Messages"
|
||||
mautic.channel.message.all_contacts="This message has been sent to the following contacts."
|
||||
mautic.channel.message.channel_contacts="This channel was used for the following contacts during the timeframe selected above:"
|
||||
mautic.channel.message.header.new="New Marketing Message"
|
||||
mautic.channel.message.failed="No channel was successful in sending the message."
|
||||
mautic.channel.message.form.message="Message"
|
||||
mautic.channel.message.form.enabled="Enabled?"
|
||||
mautic.channel.message.send.attempts="Attempts"
|
||||
mautic.channel.message.send.attempts.tooltip="Number of attempts if an message has failed"
|
||||
mautic.channel.message.send.priority="Priority"
|
||||
mautic.channel.message.send.priority.high="High"
|
||||
mautic.channel.message.send.priority.normal="Normal"
|
||||
mautic.channel.message.send.priority.tooltip="Messages with priority set as high will be handled before rescheduled messages"
|
||||
mautic.channel.permissions.header="Channel Permissions"
|
||||
mautic.channel.permissions.messages="Marketing Messages - Users have access to"
|
||||
mautic.channel.form.additem="Nothing found here! Change that by adding a new item."
|
||||
mautic.channel.campaign.event.send="Send a marketing message"
|
||||
mautic.channel.campaign.event.send_descr="Select from the list of marketing messages to send in this campaign"
|
||||
mautic.channel.send.selectmessages="Select a marketing message"
|
||||
mautic.channel.choose.messages_descr="Marketing messages"
|
||||
mautic.email.send.edit.message="Edit Message"
|
||||
mautic.channel.message.send.marketing.message="Send marketing message"
|
||||
mautic.channel.message.send.marketing.message.descr="Send a message through the configured channels within the marketing message selected."
|
||||
mautic.messages.processed.messages="Messages Sent by Channel"
|
||||
mautic.channel.message.form.confirmdelete="Delete this message?"
|
||||
mautic.queued.channel="Message Queued for channel - "
|
||||
mautic.message.queue="Message Queued"
|
||||
mautic.queued.message.timeline.status="Status"
|
||||
mautic.queued.message.event.cancel="Cancel"
|
||||
mautic.queued.message.timeline.attempts="Attempts"
|
||||
mautic.queued.message.timeline.channel="Channel"
|
||||
mautic.queued.message.timeline.date.added="Date Added"
|
||||
mautic.queued.message.timeline.rescheduled="Rescheduled Date"
|
||||
mautic.message.form.confirmdelete="Delete the marketing message, %name%?"
|
||||
mautic.message.queue.report.channel="Message channel"
|
||||
mautic.message.queue.report.channel_id="Message ID"
|
||||
mautic.message.queue.report.priority="Priority"
|
||||
mautic.message.queue.report.max_attempts="Max attempts"
|
||||
mautic.message.queue.report.attempts="Total attempts"
|
||||
mautic.message.queue.report.success="Success"
|
||||
mautic.message.queue.report.status="Status"
|
||||
mautic.message.queue.report.last_attempt="Last attempt date"
|
||||
mautic.message.queue.report.date_sent="Date sent"
|
||||
mautic.message.queue.report.scheduled_date="Scheduled Date"
|
||||
mautic.message.queue.report.date_published="Date published"
|
||||
mautic.report.group.message.channel="Channels Messages"
|
||||
mautic.message.queue="Message Queue"
|
||||
mautic.messages.messages="Marketing Messages"
|
||||
mautic.messages.header="Marketing Messages"
|
||||
mautic.report.source.message.channel="Message Queue"
|
||||
@@ -0,0 +1 @@
|
||||
mautic.channel.choosemessage.notblank="A message is required."
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ChannelBundle\Twig;
|
||||
|
||||
use Mautic\ChannelBundle\Helper\ChannelListHelper;
|
||||
use Mautic\LeadBundle\Exception\UnknownDncReasonException;
|
||||
use Mautic\LeadBundle\Twig\Helper\DncReasonHelper;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class ChannelExtension extends AbstractExtension
|
||||
{
|
||||
public function __construct(
|
||||
private DncReasonHelper $dncReasonHelper,
|
||||
private ChannelListHelper $channelListHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TwigFunction[]
|
||||
*/
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('getChannelDncText', [$this, 'getChannelDncText']),
|
||||
new TwigFunction('getChannelLabel', [$this, 'getChannelLabel']),
|
||||
];
|
||||
}
|
||||
|
||||
public function getChannelDncText(int $reasonId): string
|
||||
{
|
||||
try {
|
||||
return $this->dncReasonHelper->toText($reasonId);
|
||||
} catch (UnknownDncReasonException $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function getChannelLabel(string $channel): string
|
||||
{
|
||||
return $this->channelListHelper->getChannelLabel($channel);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user