<?php
namespace App\EventSubscriber;
use App\DBAL\Types\CardBrandType;
use App\DBAL\Types\CustomerCommentType;
use App\DBAL\Types\EventPriorityType;
use App\DBAL\Types\EventType;
use App\DBAL\Types\InterviewNoteType;
use App\DBAL\Types\MailboxType;
use App\DBAL\Types\NotificationCommandType;
use App\DBAL\Types\StatusType;
use App\DBAL\Types\TaxReturnFormType;
use App\DBAL\Types\TaxReturnType;
use App\DBAL\Types\TransactionMethodType;
use App\DBAL\Types\TransactionType;
use App\Entity\Chat;
use App\Entity\Config\InterviewNoteMethod;
use App\Entity\Customer;
use App\Entity\Event;
use App\Entity\Feed;
use App\Entity\Issue;
use App\Entity\Lead;
use App\Entity\Project;
use App\Entity\Stream;
use App\Entity\User;
use App\Events;
use App\Service\FayeClient;
use App\Service\MergeFields;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Persistence\ManagerRegistry;
use EWZ\SymfonyAdminBundle\Event\ObjectEvent;
use EWZ\SymfonyAdminBundle\Event\ReportExportedEvent;
use EWZ\SymfonyAdminBundle\Events as SABEvents;
use EWZ\SymfonyAdminBundle\Util\CommandRunner;
use EWZ\SymfonyAdminBundle\Util\StringUtil;
use Symfony\Component\Asset\Packages;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment as TwigEnvironment;
/**
* Notify user base on object changes.
*/
class NotificationSubscriber implements EventSubscriberInterface
{
/** @var TwigEnvironment */
private $twig;
/** @var KernelInterface */
private $kernel;
/** @var TranslatorInterface */
private $translator;
/** @var TokenStorageInterface */
private $tokenStorage;
/** @var RequestStack */
private $requestStack;
/** @var UrlGeneratorInterface */
private $urlGenerator;
/** @var ManagerRegistry */
private $managerRegistry;
/** @var Packages */
private $assetsManager;
/** @var MergeFields */
private $mergeFields;
/** @var FayeClient */
private $fayeClient;
/** @var string */
private $personalCheckTo;
/** @var string */
private $pendingUrl;
/** @var string */
private $customerUrl;
/**
* @param TwigEnvironment $twig
* @param KernelInterface $kernel
* @param TokenStorageInterface $tokenStorage
* @param RequestStack $requestStack
* @param UrlGeneratorInterface $urlGenerator
* @param ManagerRegistry $managerRegistry
* @param TranslatorInterface $translator
* @param Packages $assetsManager
* @param MergeFields $mergeFields
* @param FayeClient $fayeClient
* @param string $personalCheckTo
* @param string $pendingUrl
* @param string $customerUrl
*/
public function __construct(
TwigEnvironment $twig,
KernelInterface $kernel,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
UrlGeneratorInterface $urlGenerator,
ManagerRegistry $managerRegistry,
TranslatorInterface $translator,
Packages $assetsManager,
MergeFields $mergeFields,
FayeClient $fayeClient,
string $personalCheckTo,
string $pendingUrl,
string $customerUrl
) {
$this->twig = $twig;
$this->kernel = $kernel;
$this->tokenStorage = $tokenStorage;
$this->requestStack = $requestStack;
$this->urlGenerator = $urlGenerator;
$this->managerRegistry = $managerRegistry;
$this->translator = $translator;
$this->assetsManager = $assetsManager;
$this->mergeFields = $mergeFields;
$this->fayeClient = $fayeClient;
$this->personalCheckTo = $personalCheckTo;
$this->pendingUrl = $pendingUrl;
$this->customerUrl = $customerUrl;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
Events::LEAD_IMPORT_COMPLETED => 'onLeadImported',
SABEvents::REPORT_EXPORT_COMPLETED => 'onReportExported',
SABEvents::NOTIFICATION_OBJECT_CREATED => 'onObjectCreated',
Events::NOTIFICATION_USER_ASSIGNED => 'onUserAssigned',
Events::NOTIFICATION_EVENT_POKED => 'onEventPoked',
Events::NOTIFICATION_EVENT_DUE => 'onEventDue',
Events::NOTIFICATION_EVENT_UPCOMING => 'onEventUpcoming',
Events::NOTIFICATION_UPSELL_REQUEST_POKED => 'onUpsellRequestPoked',
Events::NOTIFICATION_PROJECT_POKED => 'onProjectPoked',
Events::NOTIFICATION_MESSAGE_SENT => 'onMessageSent',
Events::NOTIFICATION_CUSTOMER_QA_SIGNED => 'onCustomerQASigned',
Events::NOTIFICATION_CUSTOMER_STAGE_ITEM_COMPLETED => 'onCustomerStageItemCompleted',
Events::NOTIFICATION_CUSTOMER_STAGE_COMPLETED => 'onCustomerStageCompleted',
Events::NOTIFICATION_CUSTOMER_TRANSACTION_UPDATED => 'onCustomerTransactionUpdated',
Events::NOTIFICATION_CHAT_MESSAGE_CREATED => 'onChatMessageCreated',
Events::NOTIFICATION_CHAT_MESSAGE_REACTED => 'onChatMessageReacted',
Events::NOTIFICATION_CHAT_DELETED => 'onChatDeleted',
];
}
/**
* @param GenericEvent $event
*/
public function onLeadImported(GenericEvent $event): void
{
/** @var User $user */
$user = $event->getSubject();
$this->addNotification($user, NotificationCommandType::LEAD_IMPORTED, $user, $event->getArguments());
}
/**
* @param ReportExportedEvent $event
*/
public function onReportExported(ReportExportedEvent $event): void
{
$report = $event->getReport();
$user = $event->getUser();
$fileName = $event->getFileName();
$this->addNotification($report, NotificationCommandType::REPORT_EXPORTED, $user, [
'file_name' => $fileName,
]);
}
/**
* @param ObjectEvent $event
*/
public function onObjectCreated(ObjectEvent $event): void
{
$object = $event->getObject();
// set comment body
$commentBody = $this->translator->trans('notification.object_created', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($object)),
'%object_id%' => $object->getId(),
'%object_url%' => '#',
'%object_name%' => $this->formatObjectName($object),
'%object_extra%' => null,
'%object_properties%' => null,
'%object_body%' => null,
]);
switch (\get_class($object)) {
case Customer\Comment\ComplaintComment::class:
$commentMethod = $this->getInterviewNoteMethod('Complaint');
$objectId = $object->getObject()->getId();
// no break
case Customer\Comment\TaxReturnComment::class:
$commentMethod = $commentMethod ?? $this->getInterviewNoteMethod('Returns');
$objectId = $objectId ?? sprintf('%d %d %s %s',
$object->getObject()->getId(),
$object->getObject()->getYear(),
$object->getObject()->getType()
? TaxReturnType::getReadableValue($object->getObject()->getType())
: '—',
$object->getObject()->getForm()
? TaxReturnFormType::getReadableValue($object->getObject()->getForm())
: '—'
);
// no break
case Customer\Comment\UpsellRequestComment::class:
$commentMethod = $commentMethod ?? $this->getInterviewNoteMethod('Upsell Req.');
$objectId = $objectId ?? $object->getObject()->getId();
$objectName = $object->getType()
? CustomerCommentType::getReadableValue($object->getType())
: '—';
$commentBody = $this->translator->trans('notification.object_commented', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($objectName),
'%object_id%' => $objectId,
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $object->getObject()->getCustomer()->getId(),
'tab' => 'upsells',
'filters' => json_encode(['id' => $objectId]),
]
),
'%object_name%' => $objectName,
'%object_body%' => $object->getBody(),
]);
$this->addInterviewNote(
$object->getCustomer(),
$commentBody,
null,
$commentMethod
);
/** @var User $user */
$user = method_exists('getTargetUser', $object->getObject())
? $object->getObject()->getTargetUser()
: $object->getObject()->getUser();
if ($user
&& $user->isEnabled()
&& $user->isNotifyOnAssignedCustomer()
&& $user->hasEntityAccess('customer')
) {
$this->addNotification($object, NotificationCommandType::CREATED_COMMENT, $user);
}
break;
case Customer\TaxReturn::class:
$commentBody = $this->translator->trans('notification.object_created', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($object)),
'%object_id%' => $object->getId(),
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $object->getCustomer()->getId(),
'tab' => 'returns',
'filters' => json_encode(['id' => $object->getId()]),
]
),
'%object_name%' => $this->formatObjectName($object),
'%object_extra%' => null,
'%object_properties%' => null,
'%object_body%' => sprintf("Year: %d\nType: %s\nForm: %s",
$object->getYear(),
$object->getType()
? TaxReturnType::getReadableValue($object->getType())
: '—',
$object->getForm()
? TaxReturnFormType::getReadableValue($object->getForm())
: '—'
),
]);
$this->addInterviewNote(
$object->getCustomer(),
$commentBody,
null,
$this->getInterviewNoteMethod('Returns')
);
break;
case Customer\Complaint::class:
$commentBody = $this->translator->trans('notification.object_created', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($object)),
'%object_id%' => $object->getId(),
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $object->getCustomer()->getId(),
'tab' => 'complaints',
'filters' => json_encode(['id' => $object->getId()]),
]
),
'%object_name%' => $this->formatObjectName($object),
'%object_extra%' => null,
'%object_properties%' => null,
'%object_body%' => $object->getMessage(),
]);
$this->addInterviewNote(
$object->getCustomer(),
$commentBody,
null,
$this->getInterviewNoteMethod('Complaint')
);
break;
case Customer\GoalTime::class:
$serviceCodes = [];
foreach ($object->getServiceCodes() as $serviceCode) {
$serviceCodes[] = (string) $serviceCode;
}
$properties = \count($serviceCodes)
? $this->translator->trans('notification.customer_goal_properties', [
'%object_service_codes%' => implode(', ', $serviceCodes),
])
: null;
$commentBody = $this->translator->trans('notification.object_created', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($object)),
'%object_id%' => $object->getId(),
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $object->getCustomer()->getId(),
'tab' => 'goals',
'filters' => json_encode(['id' => $object->getId()]),
]
),
'%object_name%' => $this->formatObjectName($object),
'%object_extra%' => null,
'%object_properties%' => $properties,
'%object_body%' => $object->getMessage(),
]);
$this->addInterviewNote(
$object->getCustomer(),
$commentBody,
null,
$this->getInterviewNoteMethod('Goal')
);
break;
case Customer\FaxLog::class:
$commentBody = $this->translator->trans('notification.customer_file_faxed', [
'%actor%' => $this->getUser() ?: '—',
'%value%' => $object->getFile()->getFile(),
]);
$this->addInterviewNote(
$object->getCustomer(),
$commentBody,
InterviewNoteType::FAXED_CLIENT,
$this->getInterviewNoteMethod('Fax')
);
break;
case Customer\RatingLog::class:
$commentBody = $this->translator->trans('notification.customer_rating_log', [
'%value%' => $object->getValue() ?: 0,
'%stars%' => preg_replace(
'/[\n|\r]/',
'',
$this->twig->load('@SymfonyAdmin/form/rating.html.twig')->render([
'rating' => $object->getValue() ?: 0,
'class' => 'star-success star-1x',
])
),
'%message%' => $object->getMessage(),
]);
$this->addInterviewNote(
$object->getCustomer(),
$commentBody,
null,
$this->getInterviewNoteMethod('Rating')
);
$users = new ArrayCollection();
if ($user = $object->getCaseManagerUser()) {
$users->add($user);
}
if ($user = $object->getLeadAgentUser()) {
$users->add($user);
}
if ($user = $object->getFinancialUser()) {
$users->add($user);
}
if ($user = $object->getProcessorUser()) {
$users->add($user);
}
if ($user = $object->getResolutionSpecialistUser()) {
$users->add($user);
}
$adminUsers = $this->managerRegistry
->getRepository(User::class)
->searchAll(['allowSystemAdmin' => true]);
foreach ($adminUsers as $user) {
if (!$users->contains($user)) {
$users->add($user);
}
}
$this->addNotification($object, NotificationCommandType::CREATED_OBJECT, $users);
break;
case Customer\Request::class:
// map embed fields with customer data
$data = $this->mergeFields->mapValues($object->getCustomer(), null, true);
// embed data/values
$body = $object->getBody();
foreach ($data as $key => $value) {
$body = preg_replace(sprintf('/<span class="text-editor-merge-field-container"><span class="text-editor-merge-field is-recipient"[^>]*data-name="%s"[^>]*># %s<\/span><\/span>/isU', $key, $key), str_replace('$', '\$', $value), $body);
}
$commentBody = $this->translator->trans('notification.object_created', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($object)),
'%object_id%' => $object->getId(),
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $object->getCustomer()->getId(),
'tab' => 'requests',
'filters' => json_encode(['id' => $object->getId()]),
]
),
'%object_name%' => $this->formatObjectName($object),
'%object_extra%' => null,
'%object_properties%' => null,
'%object_body%' => $body,
]);
$this->addInterviewNote(
$object->getCustomer(),
$commentBody,
InterviewNoteType::EMAILED_CLIENT,
$this->getInterviewNoteMethod('Info. Request')
);
break;
case Customer\StatusLog::class:
$properties = $this->translator->trans('notification.customer_status_properties', [
'%object_status%' => $object->getStatus(),
]);
$commentBody = $this->translator->trans('notification.object_created', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($object)),
'%object_id%' => $object->getId(),
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $object->getCustomer()->getId(),
'tab' => 'statuses',
'filters' => json_encode(['id' => $object->getId()]),
]
),
'%object_name%' => $this->formatObjectName($object),
'%object_extra%' => null,
'%object_properties%' => $properties,
'%object_body%' => $object->getMessage(),
]);
// only apply to opportunities
if (!$object->getCustomer()->isCustomer()
&& $interviewTime = $object->getCustomer()->getInterviewTime()
) {
// get default status
$defaultStatus = $this->managerRegistry
->getRepository(Customer\Status::class)
->findOneBy(['type' => StatusType::DEFAULT]);
if ($defaultStatus
&& $object->getStatus()
&& $object->getStatus()->getId() === $defaultStatus->getId()
) {
// convert seconds
$hours = intdiv($interviewTime, 3600);
$minutes = intdiv($interviewTime % 3600, 60);
// reset interview time
$object->getCustomer()->setInterviewTime(null);
}
}
$this->addInterviewNote(
$object->getCustomer(),
$commentBody,
null,
$this->getInterviewNoteMethod('Status'),
false,
false,
$hours ?? null,
$minutes ?? null
);
break;
case Customer\UpsellRequest::class:
$properties = $this->translator->trans('notification.customer_upsell_properties', [
'%object_category%' => $object->getCategory() ?: '—',
'%object_level%' => $object->getLevel() ?: '—',
]);
$commentBody = $this->translator->trans('notification.object_created', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($object)),
'%object_id%' => $object->getId(),
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $object->getCustomer()->getId(),
'tab' => 'upsells',
'filters' => json_encode(['id' => $object->getId()]),
]
),
'%object_name%' => $this->formatObjectName($object),
'%object_extra%' => null,
'%object_properties%' => $properties,
'%object_body%' => $object->getMessage(),
]);
$this->addInterviewNote(
$object->getCustomer(),
$commentBody,
null,
$this->getInterviewNoteMethod('Upsell Req.')
);
/** @var User $user */
$user = $object->getTargetUser();
if ($user
&& $user->isEnabled()
&& $user->isNotifyOnAssignedCustomer()
&& $user->hasEntityAccess('customer')
) {
$this->addNotification($object, NotificationCommandType::CREATED_OBJECT, $user);
}
break;
case Customer\File::class:
if (preg_match('/\/signatures\//', $object->getFile())) {
return;
}
/** @var User $assignedUser */
$assignedUser = $this->getAssignedUser($object->getCustomer());
if ($assignedUser
&& $assignedUser->isEnabled()
&& $assignedUser->isNotifyOnAssignedCustomer()
&& $assignedUser->hasEntityAccess('customer')
) {
$this->addNotification($object, NotificationCommandType::CREATED_OBJECT, $assignedUser);
}
break;
case Customer\Customer::class:
$this->addInterviewNote($object, $commentBody);
/** @var User $user */
$user = $object->getUser();
if ($user
&& $user->isEnabled()
&& $user->isNotifyOnAssignedCustomer()
&& $user->hasEntityAccess('customer')
) {
$this->addNotification($object, NotificationCommandType::CREATED_OBJECT, $user);
}
break;
case Lead\Lead::class:
$comment = new Lead\Comment();
$comment->setLead($object);
$comment->setUser($this->getUser());
$comment->setBody($commentBody);
$object->addComment($comment);
/** @var User $user */
$user = $object->getUser();
if ($user
&& $user->isEnabled()
&& $user->isNotifyOnAssignedLead()
&& $user->hasEntityAccess('lead')
) {
$this->addNotification($object, NotificationCommandType::CREATED_OBJECT, $user);
}
break;
case Event\Event::class:
if ($object->getCustomer()) {
$extra = null;
if ($object->getTargetUser()
&& $this->getUser()->getId() !== $object->getTargetUser()->getId()
) {
$extra = $this->translator->trans('notification.customer_event_extra', [
'%target%' => $object->getTargetUser(),
]);
}
$properties = $this->translator->trans('notification.customer_event_properties', [
'%object_priority%' => $object->getPriority()
? EventPriorityType::getReadableValue($object->getPriority())
: '—',
'%object_tag%' => $object->getTag() ?: '—',
]);
$commentBody = $this->translator->trans('notification.object_created', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($object)),
'%object_id%' => $object->getId(),
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $object->getCustomer()->getId(),
'tab' => 'events',
'filters' => json_encode(['id' => $object->getId()]),
]
),
'%object_name%' => $this->formatObjectName($object),
'%object_extra%' => $extra,
'%object_properties%' => $properties,
'%object_body%' => $object->getDescription(),
]);
$this->addInterviewNote(
$object->getCustomer(),
$commentBody,
null,
EventType::REMINDER === $object->getType()
? $this->getInterviewNoteMethod('Reminder')
: $this->getInterviewNoteMethod('Task')
);
}
/** @var User $user */
$user = $object->getTargetUser();
if ($user
&& $user->isEnabled()
&& $user->isNotifyOnAssignedEvent()
&& $user->hasEntityAccess('event')
) {
$this->addNotification($object, NotificationCommandType::CREATED_OBJECT, $user);
}
break;
case Event\Comment::class:
if ($object->getEvent()->getCustomer()) {
$commentBody = $this->translator->trans('notification.object_commented', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($object->getEvent())),
'%object_id%' => $object->getEvent()->getId(),
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $object->getEvent()->getCustomer()->getId(),
'tab' => 'events',
'filters' => json_encode(['id' => $object->getEvent()->getId()]),
]
),
'%object_name%' => $this->formatObjectName($object->getEvent()),
'%object_body%' => $object->getBody(),
]);
$this->addInterviewNote(
$object->getEvent()->getCustomer(),
$commentBody,
null,
$this->getInterviewNoteMethod('Task')
);
}
$event = $object->getEvent();
$users = new ArrayCollection([
$event->getUser(),
]);
if ($event->getTargetUser()) {
$users->add($event->getTargetUser());
}
foreach ($event->getComments() as $comment) {
$user = $comment->getUser();
if ($user && !$users->contains($user)) {
$users->add($user);
}
}
foreach ($users as $user) {
if (!$user->isEnabled()
|| !$user->isNotifyOnAssignedEvent()
|| !$user->hasEntityAccess('event')
) {
$users->removeElement($user);
}
}
$this->addNotification($event, NotificationCommandType::CREATED_COMMENT, $users, [
'comment_id' => $object->getId(),
]);
break;
case Feed\Comment::class:
$feed = $object->getFeed();
$users = new ArrayCollection();
$isMentioned = false;
if (false !== preg_match_all('/<span class="text-editor-merge-field-container">\s*<span class="text-editor-merge-field is-mention"[^>]*data-name="([^"]+)"[^>]*>@\s*([^<]+)<\/span><\/span>/isU', $object->getBody(), $matches)) {
$matchedUsers = !empty($matches[1])
? $this->managerRegistry
->getRepository(User::class)
->searchAll(['username' => $matches[1]])
: [];
foreach ($matchedUsers as $user) {
if (!$users->contains($user)) {
$users->add($user);
}
}
if ($users->count()) {
$isMentioned = true;
}
}
if (!$isMentioned) {
$users = $feed->getUsers();
}
foreach ($users as $user) {
if (!$user->isEnabled()
|| !$user->isNotifyOnAssignedFeed()
|| !$user->hasEntityAccess('feed')
) {
$users->removeElement($user);
}
}
if (1 === $feed->getComments()->count()) {
$this->addNotification($feed, NotificationCommandType::CREATED_OBJECT, $users);
} else {
$this->addNotification($feed, NotificationCommandType::CREATED_COMMENT, $users, [
'comment_id' => $object->getId(),
'mentioned' => $isMentioned,
]);
}
return;
case Project\Project::class:
$users = $object->getUsers();
foreach ($users as $user) {
if (!$user->isEnabled()
|| !$user->isNotifyOnAssignedProject()
|| !$user->hasEntityAccess('project')
) {
$users->removeElement($user);
}
}
$this->addNotification($object, NotificationCommandType::CREATED_OBJECT, $users);
return;
case Project\Comment::class:
$project = $object->getProject();
$item = $object->getItem();
$users = new ArrayCollection();
$isMentioned = false;
if (false !== preg_match_all('/<span class="text-editor-merge-field-container">\s*<span class="text-editor-merge-field is-mention"[^>]*data-name="([^"]+)"[^>]*>@\s*([^<]+)<\/span><\/span>/isU', $object->getBody(), $matches)) {
$matchedUsers = !empty($matches[1])
? $this->managerRegistry
->getRepository(User::class)
->searchAll(['username' => $matches[1]])
: [];
foreach ($matchedUsers as $user) {
if (!$users->contains($user)) {
$users->add($user);
}
}
if ($users->count()) {
$isMentioned = true;
}
}
if (!$isMentioned) {
$users = $item ? $item->getUsers() : $project->getUsers();
}
foreach ($users as $user) {
if (!$user->isEnabled()
|| !$user->isNotifyOnAssignedProject()
|| !$user->hasEntityAccess('project')
) {
$users->removeElement($user);
}
}
if ($item) {
$this->addNotification($project, NotificationCommandType::CREATED_COMMENT, $users, [
'item_id' => $item->getId(),
'comment_id' => $object->getId(),
'mentioned' => $isMentioned,
]);
} else {
$this->addNotification($project, NotificationCommandType::CREATED_COMMENT, $users, [
'parent_id' => $object->getParent() ? $object->getParent()->getId() : null,
'comment_id' => $object->getId(),
'mentioned' => $isMentioned,
]);
}
return;
case Stream\Comment::class:
$stream = $object->getStream();
$users = new ArrayCollection();
$isMentioned = false;
if (false !== preg_match_all('/<span class="text-editor-merge-field-container">\s*<span class="text-editor-merge-field is-mention"[^>]*data-name="([^"]+)"[^>]*>@\s*([^<]+)<\/span><\/span>/isU', $object->getBody(), $matches)) {
$matchedUsers = !empty($matches[1])
? $this->managerRegistry
->getRepository(User::class)
->searchAll(['username' => $matches[1]])
: [];
foreach ($matchedUsers as $user) {
if (!$users->contains($user)) {
$users->add($user);
}
}
if ($users->count()) {
$isMentioned = true;
}
}
if (!$isMentioned) {
$users = $stream->getParticipantUsers();
}
foreach ($users as $user) {
if (!$user->isEnabled()
|| !$user->isNotifyOnAssignedStream()
|| !$user->hasEntityAccess('stream')
) {
$users->removeElement($user);
}
}
if (1 === $stream->getComments()->count() && 0 === $object->getChildren()->count()) {
$this->addNotification($stream, NotificationCommandType::CREATED_OBJECT, $users);
} else {
$this->addNotification($stream, NotificationCommandType::CREATED_COMMENT, $users, [
'parent_id' => $object->getParent() ? $object->getParent()->getId() : null,
'comment_id' => $object->getId(),
'mentioned' => $isMentioned,
]);
}
return;
case Issue\Comment::class:
$issue = $object->getIssue();
$users = new ArrayCollection();
foreach ($issue->getParticipants() as $participant) {
$user = $participant->getUser();
if ($user && !$users->contains($user)) {
$users->add($user);
}
}
foreach ($users as $user) {
if (!$user->isEnabled()
|| !$user->isNotifyOnAssignedIssue()
|| !$user->hasEntityAccess('issue')
) {
$users->removeElement($user);
}
}
if (1 === $issue->getComments()->count()) {
$this->addNotification($issue, NotificationCommandType::CREATED_OBJECT, $users);
} else {
$this->addNotification($issue, NotificationCommandType::CREATED_COMMENT, $users, [
'comment_id' => $object->getId(),
]);
}
return;
default:
return;
}
$repository = $this->managerRegistry->getRepository(\get_class($object));
$repository->update($object);
}
/**
* @param ObjectEvent $event
*/
public function onUserAssigned(ObjectEvent $event): void
{
$object = $event->getObject();
$method = 'getUser';
switch (\get_class($object)) {
case Customer\Customer::class:
if ($event->hasChangedField('caseManagerUser')) {
$method = 'getCaseManagerUser';
} elseif ($event->hasChangedField('leadAgentUser')) {
$method = 'getLeadAgentUser';
} elseif ($event->hasChangedField('financialUser')) {
$method = 'getFinancialUser';
} elseif ($event->hasChangedField('serviceUser')) {
$method = 'getServiceUser';
} elseif ($event->hasChangedField('processorUser')) {
$method = 'getProcessorUser';
} elseif ($event->hasChangedField('resolutionSpecialistUser')) {
$method = 'getResolutionSpecialistUser';
}
break;
case Customer\UpsellRequest::class:
case Event\Event::class:
$method = 'getTargetUser';
break;
}
/** @var User $user */
if (!method_exists($object, $method) || !$user = $object->$method()) {
return;
}
// set comment body
$commentBody = $this->translator->trans('notification.user_assigned', [
'%actor%' => $this->getUser() ?: '—',
'%target%' => $user,
]);
switch (\get_class($object)) {
case Customer\Customer::class:
break;
case Event\Event::class:
case Lead\Lead::class:
$class = sprintf('App\Entity\%s\Comment', $this->getObjectName($object));
$method = sprintf('set%s', $this->getObjectName($object));
$comment = new $class();
$comment->$method($object);
$comment->setUser($this->getUser());
$comment->setBody($commentBody);
$object->addComment($comment);
break;
case Customer\UpsellRequest::class:
$comment = new Customer\Comment\UpsellRequestComment($object);
$comment->setUser($this->getUser());
$comment->setBody($commentBody);
$object->addComment($comment);
break;
default:
return;
}
$repository = $this->managerRegistry->getRepository(\get_class($object));
$repository->update($object);
$method = sprintf('isNotifyOnAssigned%s', $this->getObjectName($object));
if ($user && (Customer\UpsellRequest::class === \get_class($object) || $user->$method())) {
$command = sprintf('App\DBAL\Types\NotificationCommandType::ASSIGNED_%s', strtoupper(StringUtil::tableize($this->getObjectName($object))));
$this->addNotification($object, \constant($command), $user);
}
}
/**
* @param ObjectEvent $event
*/
public function onEventPoked(ObjectEvent $event): void
{
/** @var Event\Event $event */
$event = $event->getObject();
/** @var User $user */
$user = $event->getTargetUser();
if ($user
&& $user->isEnabled()
&& $user->isNotifyOnAssignedEvent()
&& $user->hasEntityAccess('event')
) {
$this->addNotification($event, NotificationCommandType::EVENT_POKED, $user);
}
}
/**
* @param ObjectEvent $event
*/
public function onEventDue(ObjectEvent $event): void
{
/** @var Event\Event $event */
$event = $event->getObject();
/** @var User $user */
$user = $event->getTargetUser();
if ($user
&& $user->isEnabled()
&& $user->isNotifyWhenEventIsDue()
&& $user->hasEntityAccess('event')
) {
$this->addNotification($event, NotificationCommandType::EVENT_IS_DUE, $user);
}
}
/**
* @param ObjectEvent $event
*/
public function onEventUpcoming(ObjectEvent $event): void
{
/** @var Event\Event $event */
$event = $event->getObject();
/** @var User $user */
$user = $event->getTargetUser();
if ($user
&& $user->isEnabled()
&& $user->isNotifyUpcomingEvent()
&& $user->hasEntityAccess('event')
) {
$this->addNotification($event, NotificationCommandType::EVENT_REMINDER, $user);
}
}
/**
* @param ObjectEvent $event
*/
public function onUpsellRequestPoked(ObjectEvent $event): void
{
/** @var Customer\UpsellRequest $upsellRequest */
$upsellRequest = $event->getObject();
/** @var User $user */
$user = $upsellRequest->getTargetUser();
if ($user
&& $user->isEnabled()
&& $user->isNotifyOnAssignedCustomer()
&& $user->hasEntityAccess('customer')
) {
$this->addNotification($upsellRequest, NotificationCommandType::UPSELL_REQUEST_POKED, $user);
}
}
/**
* @param ObjectEvent $event
*/
public function onProjectPoked(ObjectEvent $event): void
{
/** @var Project\Item $item */
$item = $event->getObject();
$users = $item->getUsers();
foreach ($users as $user) {
if (!$user->isEnabled()
|| !$user->isNotifyOnAssignedProject()
|| !$user->hasEntityAccess('project')
) {
$users->removeElement($user);
}
}
$this->addNotification($item->getProject(), NotificationCommandType::PROJECT_POKED, $users, [
'item_id' => $item->getId(),
]);
}
/**
* @param ObjectEvent $event
*/
public function onMessageSent(ObjectEvent $event): void
{
try {
/** @var SessionInterface $session */
$session = $this->requestStack->getSession();
if ($session->get('ignore_create_note')) {
$session->remove('ignore_create_note');
return;
}
} catch (SessionNotFoundException $e) {
// do nothing
}
/** @var Customer\Message $message */
$message = $event->getObject();
// map embed fields with customer data
$data = $this->mergeFields->mapValues($message->getCustomer(), null, true);
// embed data/values
$body = $message->getBody();
foreach ($data as $key => $value) {
$body = preg_replace(sprintf('/<span class="text-editor-merge-field-container"><span class="text-editor-merge-field is-recipient"[^>]*data-name="%s"[^>]*># %s<\/span><\/span>/isU', $key, $key), str_replace('$', '\$', $value), $body);
}
$commentBody = $this->translator->trans('notification.object_created', [
'%actor%' => $this->getUser() ?: $this->getCustomerUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($message)),
'%object_id%' => $message->getId(),
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $message->getCustomer()->getId(),
'tab' => 'messages',
'filters' => json_encode([
'id' => $message->getId(),
'mailbox' => $message->getMailbox(),
]),
]
),
'%object_name%' => $this->formatObjectName($message),
'%object_extra%' => null,
'%object_properties%' => null,
'%object_body%' => $body,
]);
$hasEmails = false;
$hasPhones = false;
foreach ($message->getContacts() as $contact) {
if ($message->isEmailContact($contact)) {
$hasEmails = true;
}
if ($message->isPhoneContact($contact)) {
$hasPhones = true;
}
}
if ($hasEmails) {
$this->addInterviewNote(
$message->getCustomer(),
$commentBody,
InterviewNoteType::EMAILED_CLIENT,
$this->getInterviewNoteMethod('Email'),
$this->getUser() && $this->getUser()->isAllowSalesItems() && !$this->getUser()->isAllowServiceItems(),
MailboxType::SENT === $message->getMailbox()
);
}
if ($hasPhones) {
$this->addInterviewNote(
$message->getCustomer(),
$commentBody,
InterviewNoteType::TEXTED_CLIENT,
$this->getInterviewNoteMethod('Text'),
$this->getUser() && $this->getUser()->isAllowSalesItems() && !$this->getUser()->isAllowServiceItems(),
MailboxType::SENT === $message->getMailbox()
);
}
if ($hasEmails || $hasPhones) {
$repository = $this->managerRegistry->getRepository(\get_class($message));
$repository->update($message);
}
if (MailboxType::INBOX !== $message->getMailbox()) {
return;
}
/** @var User $assignedUser */
$assignedUser = $this->getAssignedUser($message->getCustomer());
if ($assignedUser
&& $assignedUser->isEnabled()
&& $assignedUser->isNotifyOnAssignedCustomer()
&& $assignedUser->hasEntityAccess('customer')
) {
$this->addNotification($message, NotificationCommandType::MESSAGE_SENT, $assignedUser);
}
}
/**
* @param ObjectEvent $event
*/
public function onCustomerQASigned(ObjectEvent $event): void
{
/** @var Customer $customer */
$customer = $event->getObject();
/** @var User|null $caseManagerUser */
$caseManagerUser = $customer->getCaseManagerUser();
if ($caseManagerUser
&& (
($event->hasChangedField('pqaReceivedAt') && $customer->getPqaReceivedAt())
|| ($event->hasChangedField('bqaReceivedAt') && $customer->getBqaReceivedAt())
)
) {
$this->addNotification($customer, NotificationCommandType::QA_SIGNED, $caseManagerUser);
}
}
/**
* @param ObjectEvent $event
*/
public function onCustomerStageItemCompleted(ObjectEvent $event): void
{
/** @var Customer\Stage $stage */
$stage = $event->getObject();
$commentBody = $this->translator->trans('notification.customer_stage_item_completed', [
'%actor%' => $this->getUser() ?: '—',
'%value%' => $stage->getStageItem(),
]);
$this->addInterviewNote(
$stage->getCustomer(),
$commentBody,
null,
$this->getInterviewNoteMethod('Stage')
);
}
/**
* @param ObjectEvent $event
*/
public function onCustomerStageCompleted(ObjectEvent $event): void
{
/** @var Customer\Customer $customer */
$customer = $event->getObject();
$commentBody = $this->translator->trans('notification.customer_stage_completed', [
'%actor%' => $this->getUser() ?: '—',
'%value%' => $customer->getStage(),
]);
$this->addInterviewNote(
$customer,
$commentBody,
null,
$this->getInterviewNoteMethod('Stage')
);
}
/**
* @param ObjectEvent $event
*/
public function onCustomerTransactionUpdated(ObjectEvent $event): void
{
/** @var Customer\Transaction $transaction */
$transaction = $event->getObject();
if (TransactionType::PAID !== $transaction->getType()) {
return;
}
$extra = $this->translator->trans('notification.customer_payment_extra', [
'%amount%' => number_format($transaction->getAmount()),
]);
switch ($transaction->getMethod()) {
case TransactionMethodType::CREDIT_CARD:
$paymentDetails = json_encode([
'cardBrand' => $transaction->getCardBrand()
? CardBrandType::getReadableValue($transaction->getCardBrand())
: '-',
'cardOwner' => $transaction->getCardOwner() ?: '-',
'cardNumber' => $transaction->getCardNumber() ?: '-',
'cardExpMonth' => $transaction->getCardExpMonth() ?: '-',
'cardExpYear' => $transaction->getCardExpYear() ?: '-',
'cardCvc' => $transaction->getCardCvc() ?: '-',
]);
break;
case TransactionMethodType::ECHECK:
$paymentDetails = json_encode([
'eCheckOwner' => $transaction->getECheckOwner() ?: '-',
'eCheckBankName' => $transaction->getECheckBankName() ?: '-',
'eCheckAccountNumber' => $transaction->getECheckAccountNumber() ?: '-',
'eCheckRouting' => $transaction->getECheckRouting() ?: '-',
'eCheckNumber' => $transaction->getECheckNumber() ?: '-',
'eCheckPhoneNumber' => $transaction->getECheckPhoneNumber() ?: '-',
]);
break;
case TransactionMethodType::CASHIER_CHECK:
$paymentDetails = json_encode([
'cashierCheckNumber' => $transaction->getCashierCheckNumber() ?: '-',
]);
break;
case TransactionMethodType::LOAN:
$paymentDetails = json_encode([
'loanMerchantNumber' => $transaction->getLoanMerchantNumber() ?: '-',
]);
break;
case TransactionMethodType::PERSONAL_CHECK:
$paymentDetails = json_encode([
'payableTo' => $this->personalCheckTo ?: '-',
]);
break;
case TransactionMethodType::CASH_ON_DELIVERY:
break;
}
$properties = $this->translator->trans('notification.customer_payment_properties', [
'%object_method%' => $transaction->getMethod()
? TransactionMethodType::getReadableValue($transaction->getMethod())
: '—',
'%object_details%' => $paymentDetails ?? '—',
]);
$body = $transaction->getMessage();
if ($transaction->getRanAt()) {
$ranMessage = $this->translator->trans('notification.customer_payment_ran_message', [
'%object_ran_by%' => $transaction->getRanBy(),
'%object_ran_message%' => $transaction->getRanMessage(),
]);
$body .= sprintf("\n\n%s", $ranMessage);
}
$commentBody = $this->translator->trans('notification.object_created', [
'%actor%' => $this->getUser() ?: '—',
'%article%' => $this->getIndefiniteArticle($this->formatObjectName($transaction)),
'%object_id%' => $transaction->getId(),
'%object_url%' => $this->urlGenerator->generate(
'admin_customers_view',
[
'id' => $transaction->getCustomer()->getId(),
'tab' => 'transactions',
'filters' => json_encode(['id' => $transaction->getId()]),
]
),
'%object_name%' => $this->formatObjectName($transaction),
'%object_extra%' => $extra,
'%object_properties%' => $properties,
'%object_body%' => $body,
]);
$this->addInterviewNote(
$transaction->getCustomer(),
$commentBody,
null,
$this->getInterviewNoteMethod('Payment'),
true
);
// require when updating
$repository = $this->managerRegistry->getRepository(\get_class($transaction));
$repository->update($transaction);
}
/**
* @param GenericEvent $event
*/
public function onChatMessageCreated(GenericEvent $event): void
{
/** @var Chat\Message $message */
$newMessage = $event->getSubject();
/** @var Chat\Chat $chat */
$chat = $newMessage->getChat();
/** @var bool $ignoreInsert */
$ignoreInsert = $event->getArguments()['ignoreInsert'] ?? false;
/** @var bool $autoOpen */
$autoOpen = $event->getArguments()['autoOpen'] ?? false;
$photoIds = [];
$photoUsers = [];
foreach (array_reverse($chat->getMessages()->toArray()) as $message) {
if ($message->getUser()->getId() !== $this->getUser()->getId()
&& !\in_array($message->getUser()->getId(), $photoIds)
) {
$photoIds[] = $message->getUser()->getId();
$photoUsers[] = [
'user_id' => $message->getUser()->getId(),
'user_name' => (string) $message->getUser(),
'user_photo' => $message->getUser()->getPhoto(),
];
}
if (6 === \count($photoIds)) {
break;
}
}
$participants = [];
foreach ($chat->getParticipants() as $participant) {
$participants[$participant->getUser()->getId()] = (string) $participant->getUser();
if ($participant->getUser()->getId() !== $this->getUser()->getId()
&& !\in_array($participant->getUser()->getId(), $photoIds)
&& \count($photoIds) < 6
) {
$photoIds[] = $participant->getUser()->getId();
$photoUsers[] = [
'user_id' => $participant->getUser()->getId(),
'user_name' => (string) $participant->getUser(),
'user_photo' => $participant->getUser()->getPhoto(),
];
}
}
$files = [];
foreach ($newMessage->getFiles() as $file) {
$files[] = [
'name' => (string) $file,
'file' => $file->getFile(),
];
}
$data = [
'ignore_insert' => $ignoreInsert,
'auto_open' => false,
'chat_id' => $chat->getId(),
'creator_id' => $chat->getUser()->getId(),
'message_id' => $newMessage->getId(),
'participants' => $participants,
'photos' => $photoUsers,
'user_id' => $newMessage->getUser()->getId(),
'user_name' => (string) $newMessage->getUser(),
'user_photo' => $newMessage->getUser()->getPhoto(),
'body' => (string) $newMessage,
'files' => $files,
'created_at' => $newMessage->getCreatedAt()
? $newMessage->getCreatedAt()->format($this->getUser()->getTimeFormat())
: null,
];
foreach ($chat->getParticipants() as $participant) {
$config = $data;
$config['unread'] = $participant->getUnread();
if ($participant->getUser()->getId() === $this->getUser()->getId()) {
// automatically open chat for creator on first message
if ($autoOpen && 1 === $chat->getMessages()->count()) {
$config['auto_open'] = true;
}
}
$this->fayeClient->send(sprintf('/%d/chat/message', $participant->getUser()->getId()), $config);
}
}
/**
* @param GenericEvent $event
*/
public function onChatMessageReacted(GenericEvent $event): void
{
/** @var Chat\Message $message */
$message = $event->getSubject();
/** @var Chat $chat */
$chat = $message->getChat();
$config = array_merge(
$event->getArguments(),
[
'chat_id' => $chat->getId(),
'message_id' => $message->getId(),
'user_id' => $this->getUser()->getId(),
'read_url' => $this->urlGenerator->generate(
'admin_api_chat_read',
[
'id' => $chat->getId(),
],
UrlGeneratorInterface::ABSOLUTE_URL
),
]
);
foreach ($chat->getParticipants() as $participant) {
$this->fayeClient->send(sprintf('/%s/chat/reaction', $participant->getUser()->getId()), $config);
}
}
/**
* @param GenericEvent $event
*/
public function onChatDeleted(GenericEvent $event): void
{
/** @var int $chatId */
$chatId = $event->getSubject();
/** @var int|null $userId */
$userId = $event->getArguments()['userId'] ?? null;
$config = [
'chat_id' => $chatId,
'user_id' => $userId,
];
$this->fayeClient->send(sprintf('/all/chat/delete', $userId), $config);
}
/**
* @return User|null
*/
private function getUser(): ?User
{
if ($token = $this->tokenStorage->getToken()) {
$user = $token->getUser();
if ($user instanceof User) {
return $user;
}
}
return null;
}
/**
* @return Customer\Customer|null
*/
private function getCustomerUser(): ?Customer\Customer
{
if ($token = $this->tokenStorage->getToken()) {
$user = $token->getUser();
if ($user instanceof Customer\Customer) {
return $user;
}
}
return null;
}
/**
* @param mixed $object
* @param int $offset
* @param int|null $length
*
* @return string
*/
private function getObjectName($object, int $offset = -1, int $length = null): string
{
return implode('', \array_slice(explode('\\', \get_class($object)), $offset, $length));
}
/**
* @param mixed $object
* @param bool $lowerCase
*
* @return string
*/
private function formatObjectName($object, bool $lowerCase = true): string
{
$objectName = $this->getObjectName($object);
if ('StatusLog' === $objectName) {
$objectName = 'Status';
}
$tableized = StringUtil::tableize($objectName);
$clean = str_replace('_', ' ', $tableized);
return $lowerCase ? strtolower($clean) : ucwords($clean);
}
/**
* @param Customer\Customer $customer
*
* @return User|null
*/
private function getAssignedUser(Customer\Customer $customer): ?User
{
if (!$customer->getConvertedAt()) {
return $customer->getCaseManagerUser();
}
return $customer->getUser();
}
/**
* @param string $name
*
* @return InterviewNoteMethod|null
*/
private function getInterviewNoteMethod(string $name): ?InterviewNoteMethod
{
return $this->managerRegistry->getRepository(InterviewNoteMethod::class)->findOneBy(['name' => $name]);
}
/**
* @param Customer\Customer $customer
* @param string $body
* @param string|null $type
* @param string|null $section
* @param InterviewNoteMethod|null $method
* @param bool $isSales
* @param int|null $hours
* @param int|null $minutes
*/
private function addInterviewNote(
Customer\Customer $customer,
string $body,
string $type = null,
InterviewNoteMethod $method = null,
bool $isSales = false,
bool $isSent = false,
int $hours = null,
int $minutes = null
): void {
$body = trim(preg_replace('/\n{2,}/', "\n\n", $body, 1));
if (0 === \strlen($body)) {
$body = null;
}
$interviewNote = new Customer\InterviewNote();
$interviewNote->setCustomer($customer);
$interviewNote->setUser($this->getUser());
$interviewNote->setBody($body);
$interviewNote->setType($type);
$interviewNote->setMethod($method);
$interviewNote->setSales($isSales);
$interviewNote->setSent($isSent);
$interviewNote->setHours($hours ?: 0);
$interviewNote->setMinutes($minutes ?: 5);
if ($this->getCustomerUser() || (!$this->getCustomerUser() && !$this->getUser())) {
$interviewNote->setClient(true);
}
$customer->addInterviewNote($interviewNote);
}
/**
* @param mixed $object
* @param string $command
* @param User|array|null $user
* @param array $params
* @param bool $alert
*/
private function addNotification($object, string $command, $users = null, array $params = [], bool $alert = true): void
{
// ignore when no user is defined
if (empty($users)) {
return;
}
if (!\is_array($users) && !$users instanceof Collection) {
$users = [$users];
}
$userIds = [];
foreach ($users as $user) {
$userIds[] = $user->getId();
}
// @hack
$_SERVER['argv'] = [
sprintf('%s/bin/console', $this->kernel->getProjectDir()),
];
CommandRunner::runCommand(
'app:notification:create',
array_merge(
[
$object->getId(),
str_replace('\\', '\\\\', \get_class($object)),
implode(',', $userIds),
$command,
\count($params)
? urlencode(http_build_query($params))
: '-',
$alert,
$this->getUser()
? $this->getUser()->getId()
: null,
],
['--env' => $this->kernel->getEnvironment()]
)
);
}
/**
* @param string $work
*
* @return string
*/
private function getIndefiniteArticle(string $word): string
{
return preg_match('/^[aeiouAEIOU]/', $word) ? 'an' : 'a';
}
}