2554 lines
89 KiB
PHP
Executable File
2554 lines
89 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Mautic\LeadBundle\Model;
|
|
|
|
use Doctrine\DBAL\Exception as DBALException;
|
|
use Doctrine\ORM\EntityManager;
|
|
use Doctrine\ORM\NonUniqueResultException;
|
|
use Doctrine\ORM\Tools\Pagination\Paginator;
|
|
use Illuminate\Support\Collection;
|
|
use Mautic\CategoryBundle\Entity\Category;
|
|
use Mautic\CategoryBundle\Model\CategoryModel;
|
|
use Mautic\ChannelBundle\Helper\ChannelListHelper;
|
|
use Mautic\CoreBundle\Cache\ResultCacheOptions;
|
|
use Mautic\CoreBundle\Entity\IpAddress;
|
|
use Mautic\CoreBundle\Form\RequestTrait;
|
|
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
|
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
|
use Mautic\CoreBundle\Helper\Chart\PieChart;
|
|
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
|
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
|
use Mautic\CoreBundle\Helper\InputHelper;
|
|
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
|
use Mautic\CoreBundle\Helper\PathsHelper;
|
|
use Mautic\CoreBundle\Helper\UserHelper;
|
|
use Mautic\CoreBundle\Model\FormModel;
|
|
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
|
use Mautic\CoreBundle\Translation\Translator;
|
|
use Mautic\EmailBundle\Entity\Stat;
|
|
use Mautic\EmailBundle\Entity\StatRepository;
|
|
use Mautic\EmailBundle\Helper\EmailValidator;
|
|
use Mautic\LeadBundle\DataObject\LeadManipulator;
|
|
use Mautic\LeadBundle\Entity\Company;
|
|
use Mautic\LeadBundle\Entity\CompanyLead;
|
|
use Mautic\LeadBundle\Entity\DoNotContact as DNC;
|
|
use Mautic\LeadBundle\Entity\FrequencyRule;
|
|
use Mautic\LeadBundle\Entity\Lead;
|
|
use Mautic\LeadBundle\Entity\LeadCategory;
|
|
use Mautic\LeadBundle\Entity\LeadEventLog;
|
|
use Mautic\LeadBundle\Entity\LeadField;
|
|
use Mautic\LeadBundle\Entity\LeadList;
|
|
use Mautic\LeadBundle\Entity\LeadRepository;
|
|
use Mautic\LeadBundle\Entity\OperatorListTrait;
|
|
use Mautic\LeadBundle\Entity\PointsChangeLog;
|
|
use Mautic\LeadBundle\Entity\StagesChangeLog;
|
|
use Mautic\LeadBundle\Entity\Tag;
|
|
use Mautic\LeadBundle\Entity\UtmTag;
|
|
use Mautic\LeadBundle\Event\CategoryChangeEvent;
|
|
use Mautic\LeadBundle\Event\DoNotContactAddEvent;
|
|
use Mautic\LeadBundle\Event\DoNotContactRemoveEvent;
|
|
use Mautic\LeadBundle\Event\LeadEvent;
|
|
use Mautic\LeadBundle\Event\LeadTimelineEvent;
|
|
use Mautic\LeadBundle\Event\SaveBatchLeadsEvent;
|
|
use Mautic\LeadBundle\Exception\ImportFailedException;
|
|
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
|
|
use Mautic\LeadBundle\Form\Type\LeadType;
|
|
use Mautic\LeadBundle\Helper\CustomFieldValueHelper;
|
|
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper;
|
|
use Mautic\LeadBundle\LeadEvents;
|
|
use Mautic\LeadBundle\Tracker\ContactTracker;
|
|
use Mautic\LeadBundle\Tracker\DeviceTracker;
|
|
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
|
use Mautic\PointBundle\Entity\GroupContactScore;
|
|
use Mautic\PointBundle\Entity\GroupContactScoreRepository;
|
|
use Mautic\StageBundle\Entity\Stage;
|
|
use Mautic\UserBundle\Entity\User;
|
|
use Mautic\UserBundle\Security\Provider\UserProvider;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
use Symfony\Component\Form\FormFactoryInterface;
|
|
use Symfony\Component\HttpFoundation\RequestStack;
|
|
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
|
use Symfony\Component\Intl\Countries;
|
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
use Symfony\Contracts\EventDispatcher\Event;
|
|
|
|
/**
|
|
* @extends FormModel<Lead>
|
|
*/
|
|
class LeadModel extends FormModel
|
|
{
|
|
use DefaultValueTrait;
|
|
use OperatorListTrait;
|
|
use RequestTrait;
|
|
|
|
public const CHANNEL_FEATURE = 'contact_preference';
|
|
|
|
/**
|
|
* @var FieldModel
|
|
*/
|
|
protected $leadFieldModel;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $leadFields = [];
|
|
|
|
protected $leadTrackingId;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected $leadTrackingCookieGenerated = false;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $availableLeadFields = [];
|
|
|
|
private bool $repoSetup = false;
|
|
|
|
private array $flattenedFields = [];
|
|
|
|
private array $fieldsByGroup = [];
|
|
|
|
public function __construct(
|
|
protected RequestStack $requestStack,
|
|
protected IpLookupHelper $ipLookupHelper,
|
|
protected PathsHelper $pathsHelper,
|
|
protected IntegrationHelper $integrationHelper,
|
|
FieldModel $leadFieldModel,
|
|
protected FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
|
|
protected ListModel $leadListModel,
|
|
protected FormFactoryInterface $formFactory,
|
|
protected CompanyModel $companyModel,
|
|
protected CategoryModel $categoryModel,
|
|
protected ChannelListHelper $channelListHelper,
|
|
CoreParametersHelper $coreParametersHelper,
|
|
protected EmailValidator $emailValidator,
|
|
protected UserProvider $userProvider,
|
|
private ContactTracker $contactTracker,
|
|
private DeviceTracker $deviceTracker,
|
|
private IpAddressModel $ipAddressModel,
|
|
EntityManager $em,
|
|
CorePermissions $security,
|
|
EventDispatcherInterface $dispatcher,
|
|
UrlGeneratorInterface $router,
|
|
Translator $translator,
|
|
UserHelper $userHelper,
|
|
LoggerInterface $mauticLogger,
|
|
) {
|
|
$this->leadFieldModel = $leadFieldModel;
|
|
|
|
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
|
}
|
|
|
|
/**
|
|
* @return LeadRepository
|
|
*/
|
|
public function getRepository()
|
|
{
|
|
/** @var LeadRepository $repo */
|
|
$repo = $this->em->getRepository(Lead::class);
|
|
$repo->setDispatcher($this->dispatcher);
|
|
|
|
if (!$this->repoSetup) {
|
|
$this->repoSetup = true;
|
|
|
|
// set the point trigger model in order to get the color code for the lead
|
|
$fields = $this->leadFieldModel->getFieldList(true, false);
|
|
|
|
$socialFields = (!empty($fields['social'])) ? array_keys($fields['social']) : [];
|
|
$repo->setAvailableSocialFields($socialFields);
|
|
|
|
$searchFields = [];
|
|
foreach ($fields as $groupFields) {
|
|
$searchFields = array_merge($searchFields, array_keys($groupFields));
|
|
}
|
|
$repo->setAvailableSearchFields($searchFields);
|
|
}
|
|
|
|
return $repo;
|
|
}
|
|
|
|
/**
|
|
* Get the tags repository.
|
|
*
|
|
* @return \Mautic\LeadBundle\Entity\TagRepository
|
|
*/
|
|
public function getTagRepository()
|
|
{
|
|
return $this->em->getRepository(Tag::class);
|
|
}
|
|
|
|
/**
|
|
* @return \Mautic\LeadBundle\Entity\PointsChangeLogRepository
|
|
*/
|
|
public function getPointLogRepository()
|
|
{
|
|
return $this->em->getRepository(PointsChangeLog::class);
|
|
}
|
|
|
|
/**
|
|
* Get the tags repository.
|
|
*
|
|
* @return \Mautic\LeadBundle\Entity\UtmTagRepository
|
|
*/
|
|
public function getUtmTagRepository()
|
|
{
|
|
return $this->em->getRepository(UtmTag::class);
|
|
}
|
|
|
|
/**
|
|
* Get the tags repository.
|
|
*
|
|
* @return \Mautic\LeadBundle\Entity\LeadDeviceRepository
|
|
*/
|
|
public function getDeviceRepository()
|
|
{
|
|
return $this->em->getRepository(\Mautic\LeadBundle\Entity\LeadDevice::class);
|
|
}
|
|
|
|
/**
|
|
* Get the lead event log repository.
|
|
*
|
|
* @return \Mautic\LeadBundle\Entity\LeadEventLogRepository
|
|
*/
|
|
public function getEventLogRepository()
|
|
{
|
|
return $this->em->getRepository(LeadEventLog::class);
|
|
}
|
|
|
|
/**
|
|
* Get the frequency rules repository.
|
|
*
|
|
* @return \Mautic\LeadBundle\Entity\FrequencyRuleRepository
|
|
*/
|
|
public function getFrequencyRuleRepository()
|
|
{
|
|
return $this->em->getRepository(FrequencyRule::class);
|
|
}
|
|
|
|
/**
|
|
* Get the Stages change log repository.
|
|
*
|
|
* @return \Mautic\LeadBundle\Entity\StagesChangeLogRepository
|
|
*/
|
|
public function getStagesChangeLogRepository()
|
|
{
|
|
return $this->em->getRepository(StagesChangeLog::class);
|
|
}
|
|
|
|
/**
|
|
* Get the lead categories repository.
|
|
*
|
|
* @return \Mautic\LeadBundle\Entity\LeadCategoryRepository
|
|
*/
|
|
public function getLeadCategoryRepository()
|
|
{
|
|
return $this->em->getRepository(LeadCategory::class);
|
|
}
|
|
|
|
/**
|
|
* @return \Mautic\LeadBundle\Entity\MergeRecordRepository
|
|
*/
|
|
public function getMergeRecordRepository()
|
|
{
|
|
return $this->em->getRepository(\Mautic\LeadBundle\Entity\MergeRecord::class);
|
|
}
|
|
|
|
/**
|
|
* @return \Mautic\LeadBundle\Entity\LeadListRepository
|
|
*/
|
|
public function getLeadListRepository()
|
|
{
|
|
return $this->em->getRepository(LeadList::class);
|
|
}
|
|
|
|
public function getGroupContactScoreRepository(): GroupContactScoreRepository
|
|
{
|
|
return $this->em->getRepository(GroupContactScore::class);
|
|
}
|
|
|
|
public function getPermissionBase(): string
|
|
{
|
|
return 'lead:leads';
|
|
}
|
|
|
|
public function getNameGetter(): string
|
|
{
|
|
return 'getPrimaryIdentifier';
|
|
}
|
|
|
|
/**
|
|
* @param Lead $entity
|
|
* @param string|null $action
|
|
* @param array $options
|
|
*
|
|
* @return \Symfony\Component\Form\FormInterface<Lead>
|
|
*
|
|
* @throws MethodNotAllowedHttpException
|
|
*/
|
|
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
|
{
|
|
if (!$entity instanceof Lead) {
|
|
throw new MethodNotAllowedHttpException(['Lead'], 'Entity must be of class Lead()');
|
|
}
|
|
if (!empty($action)) {
|
|
$options['action'] = $action;
|
|
}
|
|
|
|
return $formFactory->create(LeadType::class, $entity, $options);
|
|
}
|
|
|
|
/**
|
|
* Get a specific entity or generate a new one if id is empty.
|
|
*/
|
|
public function getEntity($id = null): ?Lead
|
|
{
|
|
if (null === $id) {
|
|
return new Lead();
|
|
}
|
|
|
|
$entity = parent::getEntity($id);
|
|
|
|
if (null === $entity) {
|
|
// Check if this contact was merged into another and if so, return the new contact
|
|
if ($entity = $this->getMergeRecordRepository()->findMergedContact($id)) {
|
|
// Hydrate fields with custom field data
|
|
$fields = $this->getRepository()->getFieldValues($entity->getId());
|
|
$entity->setFields($fields);
|
|
}
|
|
}
|
|
|
|
return $entity;
|
|
}
|
|
|
|
/**
|
|
* @param array<mixed> $args
|
|
*
|
|
* @return array|Paginator|mixed
|
|
*/
|
|
public function getEntities(array $args = [])
|
|
{
|
|
$entities = parent::getEntities($args);
|
|
$contactIds = $this->getContactIdsFromArgs($args);
|
|
for ($i = 0; $i < count($contactIds); ++$i) {
|
|
$contactId = (int) $contactIds[$i];
|
|
if (empty($entities[$contactId])) {
|
|
if ($entity = $this->getMergeRecordRepository()->findMergedContact($contactId)) {
|
|
$entity->setPreviousId($contactId);
|
|
|
|
if (isset($entities[$entity->getId()])) {
|
|
// The entity is already in the array, so skip the field hydration.
|
|
continue;
|
|
}
|
|
|
|
// Hydrate fields with custom field data
|
|
$fields = $this->getRepository()->getFieldValues($entity->getId());
|
|
$entity->setFields($fields);
|
|
|
|
// Add the entity to the array to the right place.
|
|
$entities = array_slice($entities, $i, 0, true) + [$entity->getId() => $entity] + $entities;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $entities;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*
|
|
* @throws MethodNotAllowedHttpException
|
|
*/
|
|
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
|
{
|
|
if (!$entity instanceof Lead) {
|
|
throw new MethodNotAllowedHttpException(['Lead'], 'Entity must be of class Lead()');
|
|
}
|
|
|
|
switch ($action) {
|
|
case 'pre_save':
|
|
$name = LeadEvents::LEAD_PRE_SAVE;
|
|
break;
|
|
case 'post_save':
|
|
$name = LeadEvents::LEAD_POST_SAVE;
|
|
break;
|
|
case 'pre_delete':
|
|
$name = LeadEvents::LEAD_PRE_DELETE;
|
|
break;
|
|
case 'post_delete':
|
|
$name = LeadEvents::LEAD_POST_DELETE;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
if ($this->dispatcher->hasListeners($name)) {
|
|
if (empty($event)) {
|
|
$event = new LeadEvent($entity, $isNew);
|
|
$event->setEntityManager($this->em);
|
|
}
|
|
$this->dispatcher->dispatch($event, $name);
|
|
|
|
return $event;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
protected function dispatchEventFromBatch(string $action, object &$entity, bool $isNew = false, ?Event $event = null): ?Event
|
|
{
|
|
if (empty($event)) {
|
|
$event = new LeadEvent($entity, $isNew);
|
|
$event->setAlreadyProcessedInBatch(true);
|
|
$event->setEntityManager($this->em);
|
|
}
|
|
|
|
return $this->dispatchEvent($action, $entity, $isNew, $event);
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
protected function dispatchBatchEvent(string $action, array &$entitiesBatchParams, ?Event $event = null): ?Event
|
|
{
|
|
if (count($entitiesBatchParams) < 1) {
|
|
throw new MethodNotAllowedHttpException(['Lead'], 'For the batch operation the array must be used');
|
|
}
|
|
foreach ($entitiesBatchParams as $entityParam) {
|
|
if (!$entityParam['entity'] instanceof Lead) {
|
|
throw new MethodNotAllowedHttpException(['Lead'], 'Entity must be of class Lead()');
|
|
}
|
|
}
|
|
|
|
switch ($action) {
|
|
case 'pre_batch_save':
|
|
$name = LeadEvents::LEAD_PRE_BATCH_SAVE;
|
|
break;
|
|
case 'post_batch_save':
|
|
$name = LeadEvents::LEAD_POST_BATCH_SAVE;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
if ($this->dispatcher->hasListeners($name)) {
|
|
$leadEvents = [];
|
|
if (empty($event)) {
|
|
foreach ($entitiesBatchParams as $entityParam) {
|
|
if (!$leadEvent = $entityParam['event']) {
|
|
$leadEvent = new LeadEvent($entityParam['entity'], $entityParam['isNew']);
|
|
}
|
|
$leadEvent->setEntityManager($this->em);
|
|
$leadEvents[] = $leadEvent;
|
|
}
|
|
$event = new SaveBatchLeadsEvent($leadEvents);
|
|
}
|
|
$this->dispatcher->dispatch($event, $name);
|
|
|
|
return $event;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param Lead $entity
|
|
* @param bool $unlock
|
|
*/
|
|
public function saveEntity($entity, $unlock = true): void
|
|
{
|
|
$companyFieldMatches = [];
|
|
$fields = $entity->getFields();
|
|
$company = null;
|
|
|
|
// check to see if we can glean information from ip address
|
|
if (!$entity->imported && count($ips = $entity->getIpAddresses())) {
|
|
$details = $ips->first()->getIpDetails();
|
|
// Only update with IP details if none of the following are set to prevent wrong combinations
|
|
if (empty($fields['core']['city']['value']) && empty($fields['core']['state']['value']) && empty($fields['core']['country']['value']) && empty($fields['core']['zipcode']['value'])) {
|
|
if ($this->coreParametersHelper->get('anonymize_ip') && $this->ipLookupHelper->getRealIp()) {
|
|
$details = $this->ipLookupHelper->getIpDetails($this->ipLookupHelper->getRealIp());
|
|
}
|
|
|
|
if (!empty($details['city'])) {
|
|
$entity->addUpdatedField('city', $details['city']);
|
|
$companyFieldMatches['city'] = $details['city'];
|
|
}
|
|
|
|
if (!empty($details['region'])) {
|
|
$entity->addUpdatedField('state', $details['region']);
|
|
$companyFieldMatches['state'] = $details['region'];
|
|
}
|
|
|
|
if (!empty($details['country'])) {
|
|
$entity->addUpdatedField('country', $details['country']);
|
|
$companyFieldMatches['country'] = $details['country'];
|
|
}
|
|
|
|
if (!empty($details['zipcode'])) {
|
|
$entity->addUpdatedField('zipcode', $details['zipcode']);
|
|
}
|
|
}
|
|
|
|
if (!$entity->getCompany() && !empty($details['organization']) && $this->coreParametersHelper->get('ip_lookup_create_organization', false)) {
|
|
$entity->addUpdatedField('company', $details['organization']);
|
|
}
|
|
}
|
|
|
|
$updatedFields = $entity->getUpdatedFields();
|
|
$changeLogEntity = null;
|
|
if (isset($updatedFields['company'])) {
|
|
$companyFieldMatches['company'] = $updatedFields['company'];
|
|
[$company, $leadAdded, $companyEntity] = IdentifyCompanyHelper::identifyLeadsCompany($companyFieldMatches, $entity, $this->companyModel);
|
|
if ($leadAdded) {
|
|
$changeLogEntity = $entity->addCompanyChangeLogEntry('form', 'Identify Company', 'Lead added to the company, '.$company['companyname'], $company['id']);
|
|
}
|
|
}
|
|
|
|
$this->validateSelectFields($entity, $fields);
|
|
|
|
$this->processManipulator($entity);
|
|
|
|
$this->setEntityDefaultValues($entity);
|
|
|
|
$this->ipAddressModel->saveIpAddressesReferencesForContact($entity);
|
|
|
|
parent::saveEntity($entity, $unlock);
|
|
|
|
if (!empty($company)) {
|
|
// Save after the lead in for new leads created through the API and maybe other places
|
|
$this->companyModel->addLeadToCompany($companyEntity, $entity);
|
|
$this->setPrimaryCompany($companyEntity->getId(), $entity->getId());
|
|
} elseif (array_key_exists('company', $updatedFields) && empty($updatedFields['company'])) {
|
|
$this->companyModel->getCompanyLeadRepository()->removeContactPrimaryCompany($entity->getId());
|
|
}
|
|
|
|
if (null !== $changeLogEntity) {
|
|
$this->em->detach($changeLogEntity);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param object $entity
|
|
*/
|
|
public function deleteEntity($entity): void
|
|
{
|
|
// Delete custom avatar if one exists
|
|
$imageDir = $this->pathsHelper->getSystemPath('images', true);
|
|
$avatar = $imageDir.'/lead_avatars/avatar'.$entity->getId();
|
|
|
|
if (file_exists($avatar)) {
|
|
unlink($avatar);
|
|
}
|
|
|
|
parent::deleteEntity($entity);
|
|
}
|
|
|
|
/**
|
|
* Populates custom field values for updating the lead. Also retrieves social media data.
|
|
*
|
|
* @param bool|false $overwriteWithBlank
|
|
* @param bool|true $fetchSocialProfiles
|
|
* @param bool|false $bindWithForm Send $data through the Lead form and only use valid data (should be used with request data)
|
|
*
|
|
* @throws ImportFailedException
|
|
*/
|
|
public function setFieldValues(Lead $lead, array $data, $overwriteWithBlank = false, $fetchSocialProfiles = true, $bindWithForm = false): void
|
|
{
|
|
if ($fetchSocialProfiles) {
|
|
// @todo - add a catch to NOT do social gleaning if a lead is created via a form, etc as we do not want the user to experience the wait
|
|
// generate the social cache
|
|
[$socialCache, $socialFeatureSettings] = $this->integrationHelper->getUserProfiles(
|
|
$lead,
|
|
$data,
|
|
true,
|
|
null,
|
|
false,
|
|
true
|
|
);
|
|
|
|
// set the social cache while we have it
|
|
if (!empty($socialCache)) {
|
|
$lead->setSocialCache($socialCache);
|
|
}
|
|
}
|
|
|
|
if (isset($data['stage'])) {
|
|
$stagesChangeLogRepo = $this->getStagesChangeLogRepository();
|
|
$currentLeadStageId = $stagesChangeLogRepo->getCurrentLeadStage($lead->getId());
|
|
$currentLeadStageName = null;
|
|
if ($currentLeadStageId) {
|
|
/** @var Stage|null $currentStage */
|
|
$currentStage = $this->em->getRepository(Stage::class)->findByIdOrName($currentLeadStageId);
|
|
if ($currentStage) {
|
|
$currentLeadStageName = $currentStage->getName();
|
|
}
|
|
}
|
|
|
|
$newLeadStageIdOrName = is_object($data['stage']) ? $data['stage']->getId() : $data['stage'];
|
|
if ((int) $newLeadStageIdOrName !== $currentLeadStageId && $newLeadStageIdOrName !== $currentLeadStageName) {
|
|
/** @var Stage|null $newStage */
|
|
$newStage = $this->em->getRepository(Stage::class)->findByIdOrName($newLeadStageIdOrName);
|
|
if ($newStage) {
|
|
$lead->stageChangeLogEntry(
|
|
$newStage,
|
|
$newStage->getId().':'.$newStage->getName(),
|
|
$this->translator->trans('mautic.stage.event.changed')
|
|
);
|
|
} else {
|
|
throw new ImportFailedException($this->translator->trans('mautic.lead.import.stage.not.exists', ['id' => $newLeadStageIdOrName]));
|
|
}
|
|
}
|
|
}
|
|
|
|
// save the field values
|
|
$fieldValues = $lead->getFields();
|
|
|
|
if (empty($fieldValues) || $bindWithForm) {
|
|
// Lead is new or they haven't been populated so let's build the fields now
|
|
if (empty($this->flattenedFields)) {
|
|
/** @var Paginator<mixed[]> $paginator */
|
|
$paginator = $this->leadFieldModel->getEntities(
|
|
[
|
|
'filter' => ['isPublished' => true, 'object' => 'lead'],
|
|
'hydration_mode' => 'HYDRATE_ARRAY',
|
|
'result_cache' => new ResultCacheOptions(LeadField::CACHE_NAMESPACE),
|
|
]
|
|
);
|
|
$this->flattenedFields = iterator_to_array($paginator->getIterator());
|
|
$this->fieldsByGroup = $this->organizeFieldsByGroup($this->flattenedFields);
|
|
}
|
|
|
|
if (empty($fieldValues)) {
|
|
$fieldValues = $this->fieldsByGroup;
|
|
}
|
|
}
|
|
|
|
if ($bindWithForm) {
|
|
// Cleanup the field values
|
|
$form = $this->createForm(
|
|
new Lead(), // use empty lead to prevent binding errors
|
|
$this->formFactory,
|
|
null,
|
|
['fields' => $this->flattenedFields, 'csrf_protection' => false, 'allow_extra_fields' => true]
|
|
);
|
|
|
|
// Unset stage and owner from the form because it's already been handled
|
|
unset($data['stage'], $data['owner'], $data['tags']);
|
|
// Prepare special fields
|
|
$this->prepareParametersFromRequest($form, $data, $lead, [], $this->fieldsByGroup);
|
|
// Submit the data
|
|
$form->submit($data);
|
|
|
|
if ($form->getErrors()->count()) {
|
|
$this->logger->debug('LEAD: form validation failed with an error of '.$form->getErrors());
|
|
}
|
|
foreach ($form as $field => $formField) {
|
|
if (isset($data[$field])) {
|
|
if ($formField->getErrors()->count()) {
|
|
$this->logger->debug('LEAD: '.$field.' failed form validation with an error of '.$formField->getErrors());
|
|
// Don't save bad data
|
|
unset($data[$field]);
|
|
} else {
|
|
$data[$field] = $formField->getData();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// update existing values
|
|
foreach ($fieldValues as $group => &$groupFields) {
|
|
if ('all' === $group) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($groupFields as $alias => &$field) {
|
|
if (!isset($field['value'])) {
|
|
$field['value'] = null;
|
|
}
|
|
|
|
// Only update fields that are part of the passed $data array
|
|
if (array_key_exists($alias, $data)) {
|
|
if (!$bindWithForm) {
|
|
$this->cleanFields($data, $field);
|
|
}
|
|
$curValue = $field['value'];
|
|
$newValue = $data[$alias] ?? '';
|
|
|
|
if (is_array($newValue)) {
|
|
$newValue = implode('|', $newValue);
|
|
}
|
|
|
|
$isEmpty = (null == $newValue || '' == $newValue);
|
|
if ($curValue !== $newValue && (!$isEmpty || ($isEmpty && $overwriteWithBlank))) {
|
|
$field['value'] = $newValue;
|
|
$lead->addUpdatedField($alias, $newValue, $curValue);
|
|
}
|
|
|
|
// if empty, check for social media data to plug the hole
|
|
if (empty($newValue) && !empty($socialCache)) {
|
|
foreach ($socialCache as $service => $details) {
|
|
// check to see if a field has been assigned
|
|
|
|
if (!empty($socialFeatureSettings[$service]['leadFields'])
|
|
&& in_array($field['alias'], $socialFeatureSettings[$service]['leadFields'])
|
|
) {
|
|
// check to see if the data is available
|
|
$key = array_search($field['alias'], $socialFeatureSettings[$service]['leadFields']);
|
|
if (isset($details['profile'][$key])) {
|
|
// Found!!
|
|
$field['value'] = $details['profile'][$key];
|
|
$lead->addUpdatedField($alias, $details['profile'][$key]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$lead->setFields($fieldValues);
|
|
}
|
|
|
|
/**
|
|
* Disassociates a user from leads.
|
|
*/
|
|
public function disassociateOwner($userId): void
|
|
{
|
|
$leads = $this->getRepository()->findByOwner($userId);
|
|
foreach ($leads as $lead) {
|
|
$lead->setOwner(null);
|
|
$this->saveEntity($lead);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get list of entities for autopopulate fields.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getLookupResults($type, $filter = '', $limit = 10, $start = 0)
|
|
{
|
|
$results = [];
|
|
|
|
switch ($type) {
|
|
case 'user':
|
|
$results = $this->em->getRepository(User::class)->getUserList($filter, $limit, $start, ['lead' => 'leads']);
|
|
break;
|
|
case 'contact':
|
|
$fetchResults = $this->getEntities(['start' => $start, 'limit' => $limit, 'filter' => $filter]);
|
|
|
|
$results = [];
|
|
|
|
/** @var Lead $fetchResult */
|
|
foreach ($fetchResults as $fetchResult) {
|
|
$results[] = [
|
|
'value' => $fetchResult->getName() ?: $fetchResult->getEmail(),
|
|
'id' => $fetchResult->getId(),
|
|
];
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Obtain an array of users for api lead edits.
|
|
*
|
|
* @return array<mixed>
|
|
*/
|
|
public function getOwnerList()
|
|
{
|
|
return $this->em->getRepository(User::class)->getUserList('', 0);
|
|
}
|
|
|
|
/**
|
|
* Obtains a list of leads based off IP.
|
|
*
|
|
* @return array<mixed>
|
|
*/
|
|
public function getLeadsByIp($ip)
|
|
{
|
|
return $this->getRepository()->getLeadsByIp($ip);
|
|
}
|
|
|
|
/**
|
|
* Obtains a list of leads based a list of IDs.
|
|
*
|
|
* @return array<mixed>
|
|
*/
|
|
public function getLeadsByIds(array $ids)
|
|
{
|
|
return $this->getEntities([
|
|
'filter' => [
|
|
'force' => [
|
|
[
|
|
'column' => 'l.id',
|
|
'expr' => 'in',
|
|
'value' => $ids,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function canEditContact(Lead $contact): bool
|
|
{
|
|
return $this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $contact->getPermissionUser());
|
|
}
|
|
|
|
/**
|
|
* Gets the details of a lead if not already set.
|
|
*
|
|
* @return array<mixed>
|
|
*/
|
|
public function getLeadDetails($lead)
|
|
{
|
|
if ($lead instanceof Lead) {
|
|
$fields = $lead->getFields();
|
|
if (!empty($fields)) {
|
|
return $fields;
|
|
}
|
|
}
|
|
|
|
$leadId = ($lead instanceof Lead) ? $lead->getId() : (int) $lead;
|
|
|
|
return $this->getRepository()->getFieldValues($leadId);
|
|
}
|
|
|
|
/**
|
|
* Reorganizes a field list to be keyed by field's group then alias.
|
|
*/
|
|
public function organizeFieldsByGroup($fields): array
|
|
{
|
|
$array = [];
|
|
|
|
foreach ($fields as $field) {
|
|
if ($field instanceof LeadField) {
|
|
$alias = $field->getAlias();
|
|
if ($field->isPublished() and 'Lead' === $field->getObject()) {
|
|
$group = $field->getGroup();
|
|
$array[$group][$alias]['id'] = $field->getId();
|
|
$array[$group][$alias]['group'] = $group;
|
|
$array[$group][$alias]['label'] = $field->getLabel();
|
|
$array[$group][$alias]['alias'] = $alias;
|
|
$array[$group][$alias]['type'] = $field->getType();
|
|
$array[$group][$alias]['properties'] = $field->getProperties();
|
|
}
|
|
} else {
|
|
$alias = $field['alias'];
|
|
if ($field['isPublished'] and 'lead' === $field['object']) {
|
|
$group = $field['group'];
|
|
$array[$group][$alias]['id'] = $field['id'];
|
|
$array[$group][$alias]['group'] = $group;
|
|
$array[$group][$alias]['label'] = $field['label'];
|
|
$array[$group][$alias]['alias'] = $alias;
|
|
$array[$group][$alias]['type'] = $field['type'];
|
|
$array[$group][$alias]['properties'] = $field['properties'] ?? [];
|
|
}
|
|
}
|
|
}
|
|
|
|
// make sure each group key is present
|
|
$groups = ['core', 'social', 'personal', 'professional'];
|
|
foreach ($groups as $g) {
|
|
if (!isset($array[$g])) {
|
|
$array[$g] = [];
|
|
}
|
|
}
|
|
|
|
return $array;
|
|
}
|
|
|
|
/**
|
|
* Returns flat array for single lead.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getLead($leadId)
|
|
{
|
|
return $this->getRepository()->getLead($leadId);
|
|
}
|
|
|
|
/**
|
|
* @param bool $returnWithQueryFields
|
|
*
|
|
* @return array|Lead
|
|
*/
|
|
public function checkForDuplicateContact(array $queryFields, $returnWithQueryFields = false, $onlyPubliclyUpdateable = false)
|
|
{
|
|
// Search for lead by request and/or update lead fields if some data were sent in the URL query
|
|
if (empty($this->availableLeadFields)) {
|
|
$filter = ['isPublished' => true, 'object' => 'lead'];
|
|
|
|
if ($onlyPubliclyUpdateable) {
|
|
$filter['isPubliclyUpdatable'] = true;
|
|
}
|
|
|
|
$this->availableLeadFields = $this->leadFieldModel->getFieldList(
|
|
false,
|
|
false,
|
|
$filter
|
|
);
|
|
}
|
|
|
|
$lead = new Lead();
|
|
$uniqueFields = $this->fieldsWithUniqueIdentifier->getFieldsWithUniqueIdentifier();
|
|
$uniqueFieldData = [];
|
|
$inQuery = array_intersect_key($queryFields, $this->availableLeadFields);
|
|
$values = $onlyPubliclyUpdateable ? $inQuery : $queryFields;
|
|
|
|
// Run values through setFieldValues to clean them first
|
|
$this->setFieldValues($lead, $values, false, false);
|
|
$cleanFields = $lead->getFields();
|
|
|
|
foreach ($inQuery as $k => $v) {
|
|
if (empty($queryFields[$k])) {
|
|
unset($inQuery[$k]);
|
|
}
|
|
}
|
|
|
|
foreach ($cleanFields as $group) {
|
|
foreach ($group as $key => $field) {
|
|
if (array_key_exists($key, $uniqueFields) && !empty($field['value'])) {
|
|
$uniqueFieldData[$key] = $field['value'];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for leads using unique identifier
|
|
if (count($uniqueFieldData)) {
|
|
$existingLeads = $this->getRepository()->getLeadsByUniqueFields($uniqueFieldData);
|
|
|
|
if (!empty($existingLeads)) {
|
|
$this->logger->debug("LEAD: Existing contact ID# {$existingLeads[0]->getId()} found through query identifiers.");
|
|
$lead = $existingLeads[0];
|
|
}
|
|
}
|
|
|
|
return $returnWithQueryFields ? [$lead, $inQuery] : $lead;
|
|
}
|
|
|
|
/**
|
|
* Get a list of segments this lead belongs to.
|
|
*
|
|
* @param bool $forLists
|
|
* @param bool $arrayHydration
|
|
* @param bool $isPublic
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function getLists(Lead $lead, $forLists = false, $arrayHydration = false, $isPublic = false, $isPreferenceCenter = false)
|
|
{
|
|
$repo = $this->em->getRepository(LeadList::class);
|
|
|
|
return $repo->getLeadLists($lead->getId(), $forLists, $arrayHydration, $isPublic, $isPreferenceCenter);
|
|
}
|
|
|
|
/**
|
|
* Get a list of companies this contact belongs to.
|
|
*
|
|
* @return array<mixed>
|
|
*/
|
|
public function getCompanies(Lead $lead)
|
|
{
|
|
$repo = $this->em->getRepository(CompanyLead::class);
|
|
|
|
return $repo->getCompaniesByLeadId($lead->getId());
|
|
}
|
|
|
|
/**
|
|
* Add lead to lists.
|
|
*
|
|
* @param array|Lead|int $lead
|
|
* @param array|LeadList $lists
|
|
* @param bool $manuallyAdded
|
|
*/
|
|
public function addToLists($lead, $lists, $manuallyAdded = true): void
|
|
{
|
|
$this->leadListModel->addLead($lead, $lists, $manuallyAdded);
|
|
}
|
|
|
|
/**
|
|
* Remove lead from lists.
|
|
*
|
|
* @param bool $manuallyRemoved
|
|
*/
|
|
public function removeFromLists($lead, $lists, $manuallyRemoved = true): void
|
|
{
|
|
$this->leadListModel->removeLead($lead, $lists, $manuallyRemoved);
|
|
}
|
|
|
|
/**
|
|
* Add lead to Stage.
|
|
*
|
|
* @param array|Lead $lead
|
|
* @param array|Stage $stage
|
|
* @param bool $manuallyAdded
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function addToStages($lead, $stage, $manuallyAdded = true)
|
|
{
|
|
if (!$lead instanceof Lead) {
|
|
$leadId = (is_array($lead) && isset($lead['id'])) ? $lead['id'] : $lead;
|
|
$lead = $this->em->getReference(Lead::class, $leadId);
|
|
}
|
|
$lead->setStage($stage);
|
|
$lead->stageChangeLogEntry(
|
|
$stage,
|
|
$stage->getId().': '.$stage->getName(),
|
|
$this->translator->trans('mautic.stage.event.added.batch')
|
|
);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Remove lead from Stage.
|
|
*
|
|
* @param bool $manuallyRemoved
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function removeFromStages($lead, $stage, $manuallyRemoved = true)
|
|
{
|
|
$lead->setStage(null);
|
|
$lead->stageChangeLogEntry(
|
|
$stage,
|
|
$stage->getId().': '.$stage->getName(),
|
|
$this->translator->trans('mautic.stage.event.removed.batch')
|
|
);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param string $channel
|
|
*
|
|
* @return array<mixed>
|
|
*/
|
|
public function getFrequencyRules(Lead $lead, $channel = null)
|
|
{
|
|
if (is_array($channel)) {
|
|
$channel = key($channel);
|
|
}
|
|
|
|
/** @var \Mautic\LeadBundle\Entity\FrequencyRuleRepository $frequencyRuleRepo */
|
|
$frequencyRuleRepo = $this->em->getRepository(FrequencyRule::class);
|
|
$frequencyRules = $frequencyRuleRepo->getFrequencyRules($channel, $lead->getId());
|
|
|
|
if (empty($frequencyRules)) {
|
|
return [];
|
|
}
|
|
|
|
return $frequencyRules;
|
|
}
|
|
|
|
/**
|
|
* Set frequency rules for lead per channel.
|
|
*
|
|
* @param array<mixed> $data
|
|
* @param array<LeadList> $leadLists
|
|
*
|
|
* @return bool Returns true
|
|
*/
|
|
public function setFrequencyRules(Lead $lead, $data, $leadLists, $persist = true): bool
|
|
{
|
|
// One query to get all the lead's current frequency rules and go ahead and create entities for them
|
|
$frequencyRules = $lead->getFrequencyRules()->toArray();
|
|
$entities = [];
|
|
$channels = $this->getPreferenceChannels();
|
|
|
|
foreach ($channels as $ch) {
|
|
if (empty($data['lead_channels']['preferred_channel'])) {
|
|
$data['lead_channels']['preferred_channel'] = $ch;
|
|
}
|
|
|
|
$frequencyRule = $frequencyRules[$ch] ?? new FrequencyRule();
|
|
$frequencyRule->setChannel($ch);
|
|
$frequencyRule->setLead($lead);
|
|
$frequencyRule->setDateAdded(new \DateTime());
|
|
|
|
if (!empty($data['lead_channels']['frequency_number_'.$ch]) && !empty($data['lead_channels']['frequency_time_'.$ch])) {
|
|
$frequencyRule->setFrequencyNumber($data['lead_channels']['frequency_number_'.$ch]);
|
|
$frequencyRule->setFrequencyTime($data['lead_channels']['frequency_time_'.$ch]);
|
|
} else {
|
|
$frequencyRule->setFrequencyNumber(null);
|
|
$frequencyRule->setFrequencyTime(null);
|
|
}
|
|
|
|
$frequencyRule->setPauseFromDate(!empty($data['lead_channels']['contact_pause_start_date_'.$ch]) ? $data['lead_channels']['contact_pause_start_date_'.$ch] : null);
|
|
$frequencyRule->setPauseToDate(!empty($data['lead_channels']['contact_pause_end_date_'.$ch]) ? $data['lead_channels']['contact_pause_end_date_'.$ch] : null);
|
|
|
|
$frequencyRule->setLead($lead);
|
|
$frequencyRule->setPreferredChannel($data['lead_channels']['preferred_channel'] === $ch);
|
|
|
|
if ($persist) {
|
|
$entities[$ch] = $frequencyRule;
|
|
} else {
|
|
$lead->addFrequencyRule($frequencyRule);
|
|
}
|
|
}
|
|
|
|
if (!empty($entities)) {
|
|
$this->em->getRepository(FrequencyRule::class)->saveEntities($entities);
|
|
}
|
|
|
|
foreach ($data['lead_lists'] as $leadList) {
|
|
if (!isset($leadLists[$leadList])) {
|
|
$this->addToLists($lead, [$leadList]);
|
|
}
|
|
}
|
|
// Delete lists that were removed
|
|
$deletedLists = array_diff(array_keys($leadLists), $data['lead_lists']);
|
|
if (!empty($deletedLists)) {
|
|
$this->removeFromLists($lead, $deletedLists);
|
|
}
|
|
|
|
if (!empty($data['global_categories'])) {
|
|
$this->addToCategory($lead, $data['global_categories']);
|
|
}
|
|
$leadCategories = $this->getLeadCategories($lead);
|
|
|
|
// Update categories relations as removed those are removed.
|
|
$unsubscribedCategories = array_diff($leadCategories, $data['global_categories']);
|
|
|
|
if (!empty($unsubscribedCategories)) {
|
|
$this->unsubscribeCategories($unsubscribedCategories);
|
|
}
|
|
|
|
// Add non associated categories relations as removed.
|
|
$nonAssociatedCategories = $this->getLeadCategoryRepository()->getNonAssociatedCategoryIdsForAContact($lead, ['global', 'email']);
|
|
|
|
$unsubscribeNewCategories = array_diff($nonAssociatedCategories, $data['global_categories']);
|
|
if (!empty($unsubscribeNewCategories)) {
|
|
$this->addToCategory($lead, $unsubscribeNewCategories, false);
|
|
}
|
|
|
|
// Delete channels that were removed
|
|
$deleted = array_diff_key($frequencyRules, $entities);
|
|
if (!empty($deleted)) {
|
|
$this->em->getRepository(FrequencyRule::class)->deleteEntities($deleted);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param array<int|Category> $categories
|
|
*
|
|
* @return mixed[]
|
|
*/
|
|
public function addToCategory(Lead $lead, array $categories, bool $subscribedFlag = true): array
|
|
{
|
|
$results = [];
|
|
foreach ($categories as $category) {
|
|
if (!$category instanceof Category) {
|
|
$category = $this->categoryModel->getEntity($category);
|
|
}
|
|
|
|
$dispatchEvent = false;
|
|
|
|
/** @var ?LeadCategory $leadCategory */
|
|
$leadCategory = $this->getLeadCategoryRepository()->findOneBy(['lead' => $lead, 'category' => $category]);
|
|
if (is_null($leadCategory)) {
|
|
$dispatchEvent = true;
|
|
|
|
$newLeadCategory = new LeadCategory();
|
|
$newLeadCategory->setLead($lead);
|
|
$newLeadCategory->setCategory($category);
|
|
$newLeadCategory->setDateAdded(new \DateTime());
|
|
$newLeadCategory->setManuallyAdded($subscribedFlag);
|
|
$newLeadCategory->setManuallyRemoved(!$subscribedFlag);
|
|
$results[$category->getId()] = $newLeadCategory;
|
|
} elseif (true === $leadCategory->getManuallyRemoved()) {
|
|
$dispatchEvent = true;
|
|
|
|
$leadCategory->setManuallyAdded($subscribedFlag);
|
|
$leadCategory->setManuallyRemoved(!$subscribedFlag);
|
|
$leadCategory->setDateAdded(new \DateTime());
|
|
$results[$category->getId()] = $leadCategory;
|
|
}
|
|
|
|
if ($dispatchEvent) {
|
|
if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) {
|
|
$this->dispatcher->dispatch(new CategoryChangeEvent($lead, $category), LeadEvents::LEAD_CATEGORY_CHANGE);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($results)) {
|
|
$this->getLeadCategoryRepository()->saveEntities($results);
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* @param mixed[] $categories
|
|
*/
|
|
private function unsubscribeCategories(array $categories): void
|
|
{
|
|
$unsubscribedCats = [];
|
|
foreach ($categories as $key => $category) {
|
|
/** @var LeadCategory $category */
|
|
$category = $this->getLeadCategoryRepository()->getEntity($key);
|
|
$category->setManuallyRemoved(true);
|
|
$category->setManuallyAdded(false);
|
|
$category->setDateAdded(new \DateTime());
|
|
|
|
$unsubscribedCats[] = $category;
|
|
|
|
if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) {
|
|
$this->dispatcher->dispatch(new CategoryChangeEvent($category->getLead(), $category->getCategory(), false), LeadEvents::LEAD_CATEGORY_CHANGE);
|
|
}
|
|
}
|
|
|
|
if (!empty($unsubscribedCats)) {
|
|
$this->getLeadCategoryRepository()->saveEntities($unsubscribedCats);
|
|
}
|
|
}
|
|
|
|
public function removeFromCategories($categories): void
|
|
{
|
|
$deleteCats = [];
|
|
if (is_array($categories)) {
|
|
foreach ($categories as $key => $category) {
|
|
/** @var LeadCategory $category */
|
|
$category = $this->getLeadCategoryRepository()->getEntity($key);
|
|
$deleteCats[] = $category;
|
|
|
|
if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) {
|
|
$this->dispatcher->dispatch(new CategoryChangeEvent($category->getLead(), $category->getCategory(), false), LeadEvents::LEAD_CATEGORY_CHANGE);
|
|
}
|
|
}
|
|
} elseif ($categories instanceof LeadCategory) {
|
|
$deleteCats[] = $categories;
|
|
|
|
if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) {
|
|
$this->dispatcher->dispatch(new CategoryChangeEvent($categories->getLead(), $categories->getCategory(), false), LeadEvents::LEAD_CATEGORY_CHANGE);
|
|
}
|
|
}
|
|
|
|
if (!empty($deleteCats)) {
|
|
$this->getLeadCategoryRepository()->deleteEntities($deleteCats);
|
|
}
|
|
}
|
|
|
|
public function getLeadCategories(Lead $lead): array
|
|
{
|
|
$leadCategories = $this->getLeadCategoryRepository()->getLeadCategories($lead);
|
|
$leadCategoryList = [];
|
|
foreach ($leadCategories as $category) {
|
|
$leadCategoryList[$category['id']] = $category['category_id'];
|
|
}
|
|
|
|
return $leadCategoryList;
|
|
}
|
|
|
|
/**
|
|
* @param string[] $types
|
|
*
|
|
* @return array<int, int>
|
|
*/
|
|
public function getSubscribedAndNewCategoryIds(Lead $lead, array $types): array
|
|
{
|
|
return $this->getLeadCategoryRepository()->getSubscribedAndNewCategoryIds($lead, $types);
|
|
}
|
|
|
|
/**
|
|
* @return mixed[]
|
|
*/
|
|
public function getUnsubscribedLeadCategoriesIds(Lead $lead): array
|
|
{
|
|
$leadCategories = $this->getLeadCategoryRepository()->getUnsubscribedLeadCategories($lead);
|
|
$leadCategoryList = [];
|
|
foreach ($leadCategories as $category) {
|
|
$leadCategoryList[$category['id']] = $category['category_id'];
|
|
}
|
|
|
|
return $leadCategoryList;
|
|
}
|
|
|
|
/**
|
|
* @param array<string> $fields
|
|
* @param array<mixed> $data
|
|
* @param int|string|null $owner
|
|
* @param LeadList|mixed[]|string|null $list
|
|
* @param string[]|string|null $tags
|
|
* @param ?int $importId
|
|
*/
|
|
public function import(array $fields, array $data, $owner = null, $list = null, $tags = null, bool $persist = true, ?LeadEventLog $eventLog = null, $importId = null, bool $skipIfExists = false): bool
|
|
{
|
|
$fields = array_flip($fields);
|
|
|
|
// Extract company data and import separately
|
|
// Modifies the data array
|
|
$company = null;
|
|
[$companyFields, $companyData] = $this->companyModel->extractCompanyDataFromImport($fields, $data);
|
|
$fieldData = $this->getCleanedFieldData($fields, $data);
|
|
|
|
if (array_key_exists('id', $fieldData)) {
|
|
$lead = $this->getEntity($fieldData['id']);
|
|
}
|
|
|
|
$lead ??= $this->checkForDuplicateContact($fieldData);
|
|
$merged = (bool) $lead->getId();
|
|
|
|
if ($merged) {
|
|
$granted = $this->security->hasEntityAccess(
|
|
'lead:leads:editown',
|
|
'lead:leads:editother',
|
|
$lead->getPermissionUser()
|
|
);
|
|
} else {
|
|
$granted = $this->security->isGranted('lead:leads:create');
|
|
}
|
|
|
|
if (!$granted) {
|
|
throw new \Exception($this->translator->trans('mautic.lead.import.error.unauthorized', ['%username%' => $this->userHelper->getUser()->getUsername()]));
|
|
}
|
|
|
|
if (!empty($fields['dateAdded']) && !empty($data[$fields['dateAdded']])) {
|
|
$dateAdded = new DateTimeHelper($data[$fields['dateAdded']]);
|
|
$lead->setDateAdded($dateAdded->getUtcDateTime());
|
|
}
|
|
unset($fieldData['dateAdded']);
|
|
|
|
if (!empty($fields['dateModified']) && !empty($data[$fields['dateModified']])) {
|
|
$dateModified = new DateTimeHelper($data[$fields['dateModified']]);
|
|
$lead->setDateModified($dateModified->getUtcDateTime());
|
|
}
|
|
unset($fieldData['dateModified']);
|
|
|
|
if (!empty($fields['lastActive']) && !empty($data[$fields['lastActive']])) {
|
|
$lastActive = new DateTimeHelper($data[$fields['lastActive']]);
|
|
$lead->setLastActive($lastActive->getUtcDateTime());
|
|
}
|
|
unset($fieldData['lastActive']);
|
|
|
|
if (!empty($fields['dateIdentified']) && !empty($data[$fields['dateIdentified']])) {
|
|
$dateIdentified = new DateTimeHelper($data[$fields['dateIdentified']]);
|
|
$lead->setDateIdentified($dateIdentified->getUtcDateTime());
|
|
}
|
|
unset($fieldData['dateIdentified']);
|
|
|
|
if (!empty($fields['createdByUser']) && !empty($data[$fields['createdByUser']])) {
|
|
$userRepo = $this->em->getRepository(User::class);
|
|
$createdByUser = $userRepo->findByIdentifier($data[$fields['createdByUser']]);
|
|
if (null !== $createdByUser) {
|
|
$lead->setCreatedBy($createdByUser);
|
|
}
|
|
}
|
|
unset($fieldData['createdByUser']);
|
|
|
|
if (!empty($fields['modifiedByUser']) && !empty($data[$fields['modifiedByUser']])) {
|
|
$userRepo = $this->em->getRepository(User::class);
|
|
$modifiedByUser = $userRepo->findByIdentifier($data[$fields['modifiedByUser']]);
|
|
if (null !== $modifiedByUser) {
|
|
$lead->setModifiedBy($modifiedByUser);
|
|
}
|
|
}
|
|
unset($fieldData['modifiedByUser']);
|
|
|
|
if (!empty($fields['ip']) && !empty($data[$fields['ip']])) {
|
|
$addresses = explode(',', $data[$fields['ip']]);
|
|
foreach ($addresses as $address) {
|
|
$address = trim($address);
|
|
if (!$ipAddress = $this->ipAddressModel->findOneByIpAddress($address)) {
|
|
$ipAddress = new IpAddress();
|
|
$ipAddress->setIpAddress($address);
|
|
}
|
|
$lead->addIpAddress($ipAddress);
|
|
}
|
|
}
|
|
unset($fieldData['ip']);
|
|
|
|
if (!empty($fields['points']) && !empty($data[$fields['points']]) && null === $lead->getId()) {
|
|
// Add points only for new leads
|
|
$lead->setPoints($data[$fields['points']]);
|
|
|
|
// add a lead point change log
|
|
$log = new PointsChangeLog();
|
|
$log->setDelta($data[$fields['points']]);
|
|
$log->setLead($lead);
|
|
$log->setType('lead');
|
|
$log->setEventName($this->translator->trans('mautic.lead.import.event.name'));
|
|
$log->setActionName($this->translator->trans('mautic.lead.import.action.name', [
|
|
'%name%' => $this->userHelper->getUser()->getUserIdentifier(),
|
|
]));
|
|
$log->setIpAddress($this->ipLookupHelper->getIpAddress());
|
|
$log->setDateAdded(new \DateTime());
|
|
$lead->addPointsChangeLog($log);
|
|
}
|
|
|
|
if (!empty($fields['stage']) && !empty($data[$fields['stage']])) {
|
|
static $stages = [];
|
|
$stageName = $data[$fields['stage']];
|
|
if (!array_key_exists($stageName, $stages)) {
|
|
// Set stage for contact
|
|
$stage = $this->em->getRepository(Stage::class)->getStageByName($stageName);
|
|
|
|
if (empty($stage)) {
|
|
$stage = new Stage();
|
|
$stage->setName($stageName);
|
|
$stages[$stageName] = $stage;
|
|
}
|
|
} else {
|
|
$stage = $stages[$stageName];
|
|
}
|
|
|
|
$lead->setStage($stage);
|
|
|
|
// add a contact stage change log
|
|
$log = new StagesChangeLog();
|
|
$log->setStage($stage);
|
|
$log->setEventName($stage->getId().':'.$stage->getName());
|
|
$log->setLead($lead);
|
|
$log->setActionName(
|
|
$this->translator->trans(
|
|
'mautic.stage.import.action.name',
|
|
[
|
|
'%name%' => $this->userHelper->getUser()->getUserIdentifier(),
|
|
]
|
|
)
|
|
);
|
|
$log->setDateAdded(new \DateTime());
|
|
$lead->stageChangeLog($log);
|
|
}
|
|
unset($fieldData['stage']);
|
|
|
|
// Set unsubscribe status
|
|
if (!empty($fields['doNotEmail']) && isset($data[$fields['doNotEmail']]) && (!empty($fields['email']) && !empty($data[$fields['email']]))) {
|
|
$doNotEmail = filter_var($data[$fields['doNotEmail']], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
if (null !== $doNotEmail) {
|
|
$reason = $this->translator->trans('mautic.lead.import.by.user', [
|
|
'%user%' => $this->userHelper->getUser()->getUserIdentifier(),
|
|
]);
|
|
|
|
// The email must be set for successful unsubscribtion
|
|
$lead->addUpdatedField('email', $data[$fields['email']]);
|
|
if ($doNotEmail) {
|
|
$event = new DoNotContactAddEvent($lead, 'email', $reason, DNC::MANUAL);
|
|
$this->dispatcher->dispatch($event, DoNotContactAddEvent::ADD_DONOT_CONTACT);
|
|
} else {
|
|
$event = new DoNotContactRemoveEvent($lead, 'email');
|
|
$this->dispatcher->dispatch($event, DoNotContactRemoveEvent::REMOVE_DONOT_CONTACT);
|
|
}
|
|
}
|
|
}
|
|
|
|
unset($fieldData['doNotEmail']);
|
|
|
|
if (!empty($fields['ownerusername']) && !empty($data[$fields['ownerusername']])) {
|
|
try {
|
|
$newOwner = $this->userProvider->loadUserByIdentifier($data[$fields['ownerusername']]);
|
|
$lead->setOwner($newOwner);
|
|
// reset default import owner if exists owner for contact
|
|
$owner = null;
|
|
} catch (NonUniqueResultException) {
|
|
// user not found
|
|
}
|
|
}
|
|
unset($fieldData['ownerusername']);
|
|
|
|
if (!empty($fields['tags']) && !empty($data[$fields['tags']])) {
|
|
$leadTags = explode('|', $data[$fields['tags']]);
|
|
$this->modifyTags($lead, $leadTags, null, false);
|
|
}
|
|
unset($fieldData['tags']);
|
|
|
|
if (null !== $owner) {
|
|
$lead->setOwner($this->em->getReference(User::class, $owner));
|
|
}
|
|
|
|
if (null !== $tags) {
|
|
$this->modifyTags($lead, $tags, null, false);
|
|
}
|
|
|
|
if (empty($this->leadFields)) {
|
|
$this->leadFields = $this->leadFieldModel->getEntities(
|
|
[
|
|
'filter' => [
|
|
'force' => [
|
|
[
|
|
'column' => 'f.isPublished',
|
|
'expr' => 'eq',
|
|
'value' => true,
|
|
],
|
|
[
|
|
'column' => 'f.object',
|
|
'expr' => 'eq',
|
|
'value' => 'lead',
|
|
],
|
|
],
|
|
],
|
|
'hydration_mode' => 'HYDRATE_ARRAY',
|
|
'result_cache' => new ResultCacheOptions(LeadField::CACHE_NAMESPACE),
|
|
]
|
|
);
|
|
}
|
|
|
|
$fieldErrors = [];
|
|
|
|
foreach ($this->leadFields as $leadField) {
|
|
// Skip If value already exists
|
|
if ($skipIfExists && !$lead->isNew() && !$this->isValueAlreadyExists($lead, $leadField)) {
|
|
unset($fieldData[$leadField['alias']]);
|
|
continue;
|
|
}
|
|
|
|
if ('company' === $leadField['alias'] && !empty($companyData)) {
|
|
$company = $this->companyModel->importCompany(array_flip($companyFields), $companyData);
|
|
}
|
|
|
|
if (isset($fieldData[$leadField['alias']])) {
|
|
if ('NULL' === $fieldData[$leadField['alias']]) {
|
|
$fieldData[$leadField['alias']] = null;
|
|
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$this->cleanFields($fieldData, $leadField);
|
|
} catch (\Exception $exception) {
|
|
$fieldErrors[] = $leadField['alias'].': '.$exception->getMessage();
|
|
}
|
|
|
|
if ('email' === $leadField['type'] && !empty($fieldData[$leadField['alias']])) {
|
|
try {
|
|
$this->emailValidator->validate($fieldData[$leadField['alias']], false);
|
|
} catch (\Exception $exception) {
|
|
$fieldErrors[] = $leadField['alias'].': '.$exception->getMessage();
|
|
}
|
|
}
|
|
|
|
// Skip if the value is in the CSV row
|
|
continue;
|
|
} elseif ($lead->isNew() && !empty($leadField['defaultValue'])) {
|
|
// Fill in the default value if any
|
|
$fieldData[$leadField['alias']] = ('multiselect' === $leadField['type']) ? [$leadField['defaultValue']] : $leadField['defaultValue'];
|
|
}
|
|
}
|
|
|
|
if ($fieldErrors) {
|
|
$fieldErrors = implode("\n", $fieldErrors);
|
|
|
|
throw new \Exception($fieldErrors);
|
|
}
|
|
|
|
// All clear
|
|
foreach ($fieldData as $field => $value) {
|
|
$lead->addUpdatedField($field, $value);
|
|
}
|
|
|
|
$lead->imported = true;
|
|
|
|
if ($eventLog) {
|
|
$action = $merged ? 'updated' : 'inserted';
|
|
$eventLog->setAction($action);
|
|
}
|
|
|
|
if ($persist) {
|
|
$lead->setManipulator(new LeadManipulator(
|
|
'lead',
|
|
'import',
|
|
$importId,
|
|
$this->userHelper->getUser()->getName()
|
|
));
|
|
$this->saveEntity($lead);
|
|
|
|
if (null !== $list) {
|
|
$this->addToLists($lead, [$list]);
|
|
}
|
|
|
|
if (null !== $company) {
|
|
$this->companyModel->addLeadToCompany($company, $lead);
|
|
$this->setPrimaryCompany($company->getId(), $lead->getId());
|
|
}
|
|
|
|
if ($eventLog) {
|
|
$lead->addEventLog($eventLog);
|
|
}
|
|
}
|
|
|
|
return $merged;
|
|
}
|
|
|
|
/**
|
|
* Update a leads tags.
|
|
*
|
|
* @param bool|false $removeOrphans
|
|
*/
|
|
public function setTags(Lead $lead, array $tags, $removeOrphans = false): void
|
|
{
|
|
/** @var Tag[] $currentTags */
|
|
$currentTags = $lead->getTags();
|
|
$leadModified = $tagsDeleted = false;
|
|
|
|
foreach ($currentTags as $tag) {
|
|
if (!in_array($tag->getId(), $tags)) {
|
|
// Tag has been removed
|
|
$lead->removeTag($tag);
|
|
$leadModified = $tagsDeleted = true;
|
|
} else {
|
|
// Remove tag so that what's left are new tags
|
|
$key = array_search($tag->getId(), $tags);
|
|
unset($tags[$key]);
|
|
}
|
|
}
|
|
|
|
if (!empty($tags)) {
|
|
foreach ($tags as $tag) {
|
|
if (is_numeric($tag)) {
|
|
// Existing tag being added to this lead
|
|
$lead->addTag(
|
|
$this->em->getReference(Tag::class, $tag)
|
|
);
|
|
} else {
|
|
$lead->addTag(
|
|
$this->getTagRepository()->getTagByNameOrCreateNewOne($tag)
|
|
);
|
|
}
|
|
}
|
|
$leadModified = true;
|
|
}
|
|
|
|
if ($leadModified) {
|
|
$this->saveEntity($lead);
|
|
|
|
// Delete orphaned tags
|
|
if ($tagsDeleted && $removeOrphans) {
|
|
$this->getTagRepository()->deleteOrphans();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update a leads UTM tags.
|
|
*/
|
|
public function setUtmTags(Lead $lead, UtmTag $utmTags): void
|
|
{
|
|
$lead->setUtmTags($utmTags);
|
|
|
|
$this->saveEntity($lead);
|
|
}
|
|
|
|
/**
|
|
* Add leads UTM tags via API.
|
|
*
|
|
* @param array $params
|
|
*/
|
|
public function addUTMTags(Lead $lead, $params): void
|
|
{
|
|
// known "synonym" fields expected
|
|
$synonyms = ['useragent' => 'user_agent',
|
|
'remotehost' => 'remote_host', ];
|
|
|
|
// convert 'query' option to an array if necessary
|
|
if (isset($params['query']) && !is_array($params['query'])) {
|
|
// assume it's a query string; convert it to array
|
|
parse_str($params['query'], $queryResult);
|
|
if (!empty($queryResult)) {
|
|
$params['query'] = $queryResult;
|
|
} else {
|
|
// Something wrong with, remove it
|
|
unset($params['query']);
|
|
}
|
|
}
|
|
|
|
// Fix up known synonym/mismatch field names
|
|
foreach ($synonyms as $expected => $replace) {
|
|
if (array_key_exists($expected, $params) && !isset($params[$replace])) {
|
|
// add expected key name
|
|
$params[$replace] = $params[$expected];
|
|
}
|
|
}
|
|
|
|
// see if active date set, so we can use it
|
|
$updateLastActive = false;
|
|
$lastActive = new \DateTime();
|
|
// should be: yyyy-mm-ddT00:00:00+00:00
|
|
if (isset($params['lastActive'])) {
|
|
$lastActive = new \DateTime($params['lastActive']);
|
|
$updateLastActive = true;
|
|
}
|
|
$params['date_added'] = $lastActive;
|
|
|
|
// New utmTag
|
|
$utmTags = new UtmTag();
|
|
|
|
// get available fields and their setter.
|
|
$fields = $utmTags->getFieldSetterList();
|
|
|
|
// cycle through calling appropriate setter
|
|
foreach ($fields as $q => $setter) {
|
|
if (isset($params[$q])) {
|
|
$utmTags->$setter($params[$q]);
|
|
}
|
|
}
|
|
|
|
// create device
|
|
if (!empty($params['useragent'])) {
|
|
$this->deviceTracker->createDeviceFromUserAgent($lead, $params['useragent']);
|
|
}
|
|
|
|
// add the lead
|
|
$utmTags->setLead($lead);
|
|
if ($updateLastActive) {
|
|
$lead->setLastActive($lastActive);
|
|
}
|
|
|
|
$this->setUtmTags($lead, $utmTags);
|
|
}
|
|
|
|
/**
|
|
* Removes a UtmTag set from a Lead.
|
|
*
|
|
* @param int $utmId
|
|
*/
|
|
public function removeUtmTags(Lead $lead, $utmId): bool
|
|
{
|
|
/** @var UtmTag $utmTag */
|
|
foreach ($lead->getUtmTags() as $utmTag) {
|
|
if ($utmTag->getId() === $utmId) {
|
|
$lead->removeUtmTagEntry($utmTag);
|
|
$this->saveEntity($lead);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Modify tags with support to remove via a prefixed minus sign.
|
|
*
|
|
* @param bool $persist True if tags modified
|
|
* @param string[]|string $tags can be CSV string
|
|
* @param string[] $removeTags
|
|
*/
|
|
public function modifyTags(Lead $lead, $tags, ?array $removeTags = null, bool $persist = true): bool
|
|
{
|
|
$tagsModified = false;
|
|
$leadTags = $lead->getTags();
|
|
|
|
if (!$leadTags->isEmpty()) {
|
|
$this->logger->debug('CONTACT: Contact currently has tags '.implode(', ', $leadTags->getKeys()));
|
|
} else {
|
|
$this->logger->debug('CONTACT: Contact currently does not have any tags');
|
|
}
|
|
|
|
if (!is_array($tags)) {
|
|
$tags = explode(',', $tags);
|
|
}
|
|
|
|
if (empty($tags) && empty($removeTags)) {
|
|
return false;
|
|
}
|
|
|
|
$this->logger->debug('CONTACT: Adding '.implode(', ', $tags).' to contact ID# '.$lead->getId());
|
|
|
|
array_walk($tags, function (&$val): void {
|
|
$val = html_entity_decode(trim($val), ENT_QUOTES);
|
|
$val = InputHelper::_($val, 'string');
|
|
});
|
|
// Remove any tags that became empty after filtering
|
|
$tags = array_filter($tags, fn ($tag) => strlen($tag) > 0);
|
|
|
|
// See which tags already exist
|
|
$foundTags = $this->getTagRepository()->getTagsByName($tags);
|
|
foreach ($tags as $tag) {
|
|
if (str_starts_with($tag, '-')) {
|
|
// Tag to be removed
|
|
$tag = substr($tag, 1);
|
|
|
|
if (array_key_exists($tag, $foundTags) && $leadTags->contains($foundTags[$tag])) {
|
|
$tagsModified = true;
|
|
$lead->removeTag($foundTags[$tag]);
|
|
|
|
$this->logger->debug('CONTACT: Removed '.$tag);
|
|
}
|
|
} else {
|
|
$tagToBeAdded = null;
|
|
|
|
if (!array_key_exists($tag, $foundTags)) {
|
|
$tagToBeAdded = new Tag($tag, false);
|
|
} elseif (!$leadTags->contains($foundTags[$tag])) {
|
|
$tagToBeAdded = $foundTags[$tag];
|
|
}
|
|
|
|
if ($tagToBeAdded) {
|
|
$lead->addTag($tagToBeAdded);
|
|
$tagsModified = true;
|
|
$this->logger->debug('CONTACT: Added '.$tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($removeTags)) {
|
|
$this->logger->debug('CONTACT: Removing '.implode(', ', $removeTags).' for contact ID# '.$lead->getId());
|
|
|
|
array_walk($removeTags, function (&$val): void {
|
|
$val = html_entity_decode(trim($val), ENT_QUOTES);
|
|
$val = InputHelper::_($val, 'string');
|
|
});
|
|
// Remove any tags that became empty after filtering
|
|
$removeTags = array_filter($removeTags, fn ($tag) => strlen($tag) > 0);
|
|
|
|
// See which tags really exist
|
|
$foundRemoveTags = $this->getTagRepository()->getTagsByName($removeTags);
|
|
|
|
foreach ($removeTags as $tag) {
|
|
// Tag to be removed
|
|
if (array_key_exists($tag, $foundRemoveTags) && $leadTags->contains($foundRemoveTags[$tag])) {
|
|
$lead->removeTag($foundRemoveTags[$tag]);
|
|
$tagsModified = true;
|
|
|
|
$this->logger->debug('CONTACT: Removed '.$tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($persist) {
|
|
$this->saveEntity($lead);
|
|
}
|
|
|
|
return $tagsModified;
|
|
}
|
|
|
|
/**
|
|
* Modify companies for lead.
|
|
*
|
|
* @param int[] $companies
|
|
*/
|
|
public function modifyCompanies(Lead $lead, array $companies): void
|
|
{
|
|
// See which companies belong to the lead already
|
|
$leadCompanies = $this->companyModel->getCompanyLeadRepository()->getCompaniesByLeadId($lead->getId());
|
|
|
|
$requestedCompanies = new Collection($companies);
|
|
$currentCompanies = (new Collection($leadCompanies))->keyBy('company_id');
|
|
|
|
// Add companies that are not in the array of found companies
|
|
$addCompanies = $requestedCompanies->reject(
|
|
// Reject if the lead is already in the given company
|
|
fn ($companyId) => $currentCompanies->has($companyId)
|
|
);
|
|
if ($addCompanies->count()) {
|
|
$this->companyModel->addLeadToCompany($addCompanies->toArray(), $lead);
|
|
}
|
|
|
|
// Remove companies that are not in the array of given companies
|
|
$removeCompanies = $currentCompanies->reject(
|
|
fn (array $company) =>
|
|
// Reject if the found company is still in the list of companies given
|
|
$requestedCompanies->contains($company['company_id'])
|
|
);
|
|
if ($removeCompanies->count()) {
|
|
$this->companyModel->removeLeadFromCompany($removeCompanies->keys()->toArray(), $lead);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get array of available lead tags.
|
|
*
|
|
* @return mixed[]
|
|
*/
|
|
public function getTagList(): array
|
|
{
|
|
return $this->getTagRepository()->getSimpleList(null, [], 'tag', 'id');
|
|
}
|
|
|
|
/**
|
|
* Get bar chart data of contacts.
|
|
*
|
|
* @param string $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
|
|
* @param \DateTime $dateFrom
|
|
* @param \DateTime $dateTo
|
|
* @param string $dateFormat
|
|
* @param array $filter
|
|
* @param bool $canViewOthers
|
|
*/
|
|
public function getLeadsLineChartData($unit, $dateFrom, $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
|
|
{
|
|
$flag = null;
|
|
$topLists = null;
|
|
$allLeadsT = $this->translator->trans('mautic.lead.all.leads');
|
|
$identifiedT = $this->translator->trans('mautic.lead.identified');
|
|
$anonymousT = $this->translator->trans('mautic.lead.lead.anonymous');
|
|
|
|
if (isset($filter['flag'])) {
|
|
$flag = $filter['flag'];
|
|
unset($filter['flag']);
|
|
}
|
|
|
|
if (!$canViewOthers) {
|
|
$filter['owner_id'] = $this->userHelper->getUser()->getId();
|
|
}
|
|
|
|
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
|
|
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
|
$anonymousFilter = $filter;
|
|
$anonymousFilter['date_identified'] = [
|
|
'expression' => 'isNull',
|
|
];
|
|
$identifiedFilter = $filter;
|
|
$identifiedFilter['date_identified'] = [
|
|
'expression' => 'isNotNull',
|
|
];
|
|
|
|
if ('top' == $flag) {
|
|
$topLists = $this->leadListModel->getTopLists(6, $dateFrom, $dateTo);
|
|
foreach ($topLists as $list) {
|
|
$filter['leadlist_id'] = [
|
|
'value' => $list['id'],
|
|
'list_column_name' => 't.id',
|
|
];
|
|
$all = $query->fetchTimeData('leads', 'date_added', $filter);
|
|
$chart->setDataset($list['name'].': '.$allLeadsT, $all);
|
|
}
|
|
} elseif ('topIdentifiedVsAnonymous' == $flag) {
|
|
$topLists = $this->leadListModel->getTopLists(3, $dateFrom, $dateTo);
|
|
foreach ($topLists as $list) {
|
|
$anonymousFilter['leadlist_id'] = [
|
|
'value' => $list['id'],
|
|
'list_column_name' => 't.id',
|
|
];
|
|
$identifiedFilter['leadlist_id'] = [
|
|
'value' => $list['id'],
|
|
'list_column_name' => 't.id',
|
|
];
|
|
$identified = $query->fetchTimeData('leads', 'date_added', $identifiedFilter);
|
|
$anonymous = $query->fetchTimeData('leads', 'date_added', $anonymousFilter);
|
|
$chart->setDataset($list['name'].': '.$identifiedT, $identified);
|
|
$chart->setDataset($list['name'].': '.$anonymousT, $anonymous);
|
|
}
|
|
} elseif ('identified' == $flag) {
|
|
$identified = $query->fetchTimeData('leads', 'date_added', $identifiedFilter);
|
|
$chart->setDataset($identifiedT, $identified);
|
|
} elseif ('anonymous' == $flag) {
|
|
$anonymous = $query->fetchTimeData('leads', 'date_added', $anonymousFilter);
|
|
$chart->setDataset($anonymousT, $anonymous);
|
|
} elseif ('identifiedVsAnonymous' == $flag) {
|
|
$identified = $query->fetchTimeData('leads', 'date_added', $identifiedFilter);
|
|
$anonymous = $query->fetchTimeData('leads', 'date_added', $anonymousFilter);
|
|
$chart->setDataset($identifiedT, $identified);
|
|
$chart->setDataset($anonymousT, $anonymous);
|
|
} else {
|
|
$all = $query->fetchTimeData('leads', 'date_added', $filter);
|
|
$chart->setDataset($allLeadsT, $all);
|
|
}
|
|
|
|
return $chart->render();
|
|
}
|
|
|
|
/**
|
|
* Get pie chart data of dwell times.
|
|
*
|
|
* @param string $dateFrom
|
|
* @param string $dateTo
|
|
* @param array $filters
|
|
* @param bool $canViewOthers
|
|
*/
|
|
public function getAnonymousVsIdentifiedPieChartData($dateFrom, $dateTo, $filters = [], $canViewOthers = true): array
|
|
{
|
|
$chart = new PieChart();
|
|
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
|
|
|
if (!$canViewOthers) {
|
|
$filter['owner_id'] = $this->userHelper->getUser()->getId();
|
|
}
|
|
|
|
$identified = $query->count('leads', 'date_identified', 'date_added', $filters);
|
|
$all = $query->count('leads', 'id', 'date_added', $filters);
|
|
$chart->setDataset($this->translator->trans('mautic.lead.identified'), $identified);
|
|
$chart->setDataset($this->translator->trans('mautic.lead.lead.anonymous'), $all - $identified);
|
|
|
|
return $chart->render();
|
|
}
|
|
|
|
/**
|
|
* Get leads count per country name.
|
|
* Can't use entity, because country is a custom field.
|
|
*
|
|
* @param \DateTime $dateFrom
|
|
* @param \DateTime $dateTo
|
|
* @param mixed[] $filters
|
|
* @param bool $canViewOthers
|
|
*/
|
|
public function getLeadMapData($dateFrom, $dateTo, $filters = [], $canViewOthers = true): array
|
|
{
|
|
if (!$canViewOthers) {
|
|
$filter['owner_id'] = $this->userHelper->getUser()->getId();
|
|
}
|
|
|
|
$q = $this->em->getConnection()->createQueryBuilder();
|
|
$q->select('COUNT(t.id) as quantity, t.country')
|
|
->from(MAUTIC_TABLE_PREFIX.'leads', 't')
|
|
->groupBy('t.country')
|
|
->where($q->expr()->isNotNull('t.country'));
|
|
|
|
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
|
$chartQuery->applyFilters($q, $filters);
|
|
$chartQuery->applyDateFilters($q, 'date_added');
|
|
|
|
$results = $q->executeQuery()->fetchAllAssociative();
|
|
$countries = array_flip(Countries::getNames('en'));
|
|
$mapData = [];
|
|
|
|
// Convert country names to 2-char code
|
|
foreach ($results as $leadCountry) {
|
|
if (isset($countries[$leadCountry['country']])) {
|
|
$mapData[$countries[$leadCountry['country']]] = $leadCountry['quantity'];
|
|
}
|
|
}
|
|
|
|
return $mapData;
|
|
}
|
|
|
|
/**
|
|
* @param string[] $aliases
|
|
*
|
|
* @return mixed[]
|
|
*
|
|
* @throws DBALException
|
|
*/
|
|
public function getCustomLeadFieldLength(array $aliases): array
|
|
{
|
|
$columns = [];
|
|
foreach ($aliases as $alias) {
|
|
$columns[] = sprintf('max(CHAR_LENGTH(`%s`)) `%s`', $alias, $alias);
|
|
}
|
|
|
|
$query = $this->em->getConnection()->createQueryBuilder();
|
|
$query->select(implode(', ', $columns))
|
|
->from(MAUTIC_TABLE_PREFIX.'leads');
|
|
|
|
return $query->executeQuery()->fetchAssociative();
|
|
}
|
|
|
|
/**
|
|
* Get a list of top (by leads owned) users.
|
|
*
|
|
* @param int $limit
|
|
* @param string $dateFrom
|
|
* @param string $dateTo
|
|
* @param array $filters
|
|
*/
|
|
public function getTopOwners($limit = 10, $dateFrom = null, $dateTo = null, $filters = []): array
|
|
{
|
|
$q = $this->em->getConnection()->createQueryBuilder();
|
|
$q->select('COUNT(t.id) AS leads, t.owner_id, u.first_name, u.last_name')
|
|
->from(MAUTIC_TABLE_PREFIX.'leads', 't')
|
|
->join('t', MAUTIC_TABLE_PREFIX.'users', 'u', 'u.id = t.owner_id')
|
|
->where($q->expr()->isNotNull('t.owner_id'))
|
|
->orderBy('leads', 'DESC')
|
|
->groupBy('t.owner_id, u.first_name, u.last_name')
|
|
->setMaxResults($limit);
|
|
|
|
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
|
$chartQuery->applyFilters($q, $filters);
|
|
$chartQuery->applyDateFilters($q, 'date_added');
|
|
|
|
return $q->executeQuery()->fetchAllAssociative();
|
|
}
|
|
|
|
/**
|
|
* Get a list of top (by leads owned) users.
|
|
*
|
|
* @param int $limit
|
|
* @param string $dateFrom
|
|
* @param string $dateTo
|
|
* @param array $filters
|
|
*/
|
|
public function getTopCreators($limit = 10, $dateFrom = null, $dateTo = null, $filters = []): array
|
|
{
|
|
$q = $this->em->getConnection()->createQueryBuilder();
|
|
$q->select('COUNT(t.id) AS leads, t.created_by, t.created_by_user')
|
|
->from(MAUTIC_TABLE_PREFIX.'leads', 't')
|
|
->where($q->expr()->isNotNull('t.created_by'))
|
|
->andWhere($q->expr()->isNotNull('t.created_by_user'))
|
|
->orderBy('leads', 'DESC')
|
|
->groupBy('t.created_by, t.created_by_user')
|
|
->setMaxResults($limit);
|
|
|
|
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
|
$chartQuery->applyFilters($q, $filters);
|
|
$chartQuery->applyDateFilters($q, 'date_added');
|
|
|
|
return $q->executeQuery()->fetchAllAssociative();
|
|
}
|
|
|
|
/**
|
|
* Get a list of leads in a date range.
|
|
*
|
|
* @param int $limit
|
|
* @param array $filters
|
|
* @param array $options
|
|
*/
|
|
public function getLeadList($limit = 10, ?\DateTime $dateFrom = null, ?\DateTime $dateTo = null, $filters = [], $options = []): array
|
|
{
|
|
if (!empty($options['canViewOthers'])) {
|
|
$filter['owner_id'] = $this->userHelper->getUser()->getId();
|
|
}
|
|
|
|
$q = $this->em->getConnection()->createQueryBuilder();
|
|
$q->select('t.id, t.firstname, t.lastname, t.email, t.date_added, t.date_modified')
|
|
->from(MAUTIC_TABLE_PREFIX.'leads', 't')
|
|
->setMaxResults($limit);
|
|
|
|
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
|
$chartQuery->applyFilters($q, $filters);
|
|
$chartQuery->applyDateFilters($q, 'date_added');
|
|
|
|
if (empty($options['includeAnonymous'])) {
|
|
$q->andWhere($q->expr()->isNotNull('t.date_identified'));
|
|
}
|
|
|
|
$results = $q->executeQuery()->fetchAllAssociative();
|
|
|
|
foreach ($results as &$result) {
|
|
if ($result['firstname'] || $result['lastname']) {
|
|
$result['name'] = trim($result['firstname'].' '.$result['lastname']);
|
|
} elseif ($result['email']) {
|
|
$result['name'] = $result['email'];
|
|
} else {
|
|
$result['name'] = 'anonymous';
|
|
}
|
|
unset($result['firstname']);
|
|
unset($result['lastname']);
|
|
unset($result['email']);
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* @param array<mixed, mixed>|null $filters
|
|
*/
|
|
public function getEngagements(?Lead $lead = null, ?array $filters = null, ?array $orderBy = null, int $page = 1, int $limit = 25, bool $forTimeline = true): array
|
|
{
|
|
$event = $this->dispatcher->dispatch(
|
|
new LeadTimelineEvent($lead, $filters, $orderBy, $page, $limit, $forTimeline, $this->coreParametersHelper->get('site_url')),
|
|
LeadEvents::TIMELINE_ON_GENERATE
|
|
);
|
|
|
|
$payload = [
|
|
'events' => $event->getEvents(),
|
|
'filters' => $filters,
|
|
'order' => $orderBy,
|
|
'types' => $event->getEventTypes(),
|
|
'total' => $event->getEventCounter()['total'],
|
|
'page' => $page,
|
|
'limit' => $limit,
|
|
'maxPages' => $event->getMaxPage(),
|
|
];
|
|
|
|
return ($forTimeline) ? $payload : [$payload, $event->getSerializerGroups()];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getEngagementTypes()
|
|
{
|
|
$event = new LeadTimelineEvent();
|
|
$event->fetchTypesOnly();
|
|
|
|
$this->dispatcher->dispatch($event, LeadEvents::TIMELINE_ON_GENERATE);
|
|
|
|
return $event->getEventTypes();
|
|
}
|
|
|
|
/**
|
|
* Get engagement counts by time unit.
|
|
*
|
|
* @param string $unit
|
|
*/
|
|
public function getEngagementCount(Lead $lead, ?\DateTime $dateFrom = null, ?\DateTime $dateTo = null, $unit = 'm', ?ChartQuery $chartQuery = null): array
|
|
{
|
|
$event = new LeadTimelineEvent($lead);
|
|
$event->setCountOnly($dateFrom, $dateTo, $unit, $chartQuery);
|
|
|
|
$this->dispatcher->dispatch($event, LeadEvents::TIMELINE_ON_GENERATE);
|
|
|
|
return $event->getEventCounter();
|
|
}
|
|
|
|
public function addToCompany(Lead $lead, $company): bool
|
|
{
|
|
// check if lead is in company already
|
|
if (!$company instanceof Company) {
|
|
$company = $this->companyModel->getEntity($company);
|
|
}
|
|
|
|
// company does not exist anymore
|
|
if (null === $company) {
|
|
return false;
|
|
}
|
|
|
|
$companyLead = $this->companyModel->getCompanyLeadRepository()->getCompaniesByLeadId($lead->getId(), $company->getId());
|
|
|
|
if (empty($companyLead)) {
|
|
$this->companyModel->addLeadToCompany($company, $lead);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get contact channels.
|
|
*/
|
|
public function getContactChannels(Lead $lead): array
|
|
{
|
|
$allChannels = $this->getPreferenceChannels();
|
|
|
|
$channels = [];
|
|
foreach ($allChannels as $channel) {
|
|
if (DNC::IS_CONTACTABLE === $this->isContactable($lead, $channel)) {
|
|
$channels[$channel] = $channel;
|
|
}
|
|
}
|
|
|
|
return $channels;
|
|
}
|
|
|
|
/**
|
|
* Get contact channels.
|
|
*/
|
|
public function getDoNotContactChannels(Lead $lead): array
|
|
{
|
|
$allChannels = $this->getPreferenceChannels();
|
|
|
|
$channels = [];
|
|
foreach ($allChannels as $channel) {
|
|
if (DNC::IS_CONTACTABLE !== $this->isContactable($lead, $channel)) {
|
|
$channels[$channel] = $channel;
|
|
}
|
|
}
|
|
|
|
return $channels;
|
|
}
|
|
|
|
public function getPreferenceChannels(): array
|
|
{
|
|
return $this->channelListHelper->getFeatureChannels(self::CHANNEL_FEATURE, true);
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getPreferredChannel(Lead $lead)
|
|
{
|
|
$preferredChannel = $this->getFrequencyRuleRepository()->getPreferredChannel($lead->getId());
|
|
if (!empty($preferredChannel)) {
|
|
return $preferredChannel[0];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @return mixed[]
|
|
*/
|
|
public function setPrimaryCompany($companyId, $leadId)
|
|
{
|
|
$companyArray = [];
|
|
$oldPrimaryCompany = $newPrimaryCompany = false;
|
|
|
|
$lead = $this->getEntity($leadId);
|
|
|
|
$companyLeads = $this->companyModel->getCompanyLeadRepository()->getEntitiesByLead($lead);
|
|
|
|
/** @var CompanyLead $companyLead */
|
|
foreach ($companyLeads as $companyLead) {
|
|
$company = $companyLead->getCompany();
|
|
|
|
if ($companyLead) {
|
|
if ($companyLead->getPrimary() && !$oldPrimaryCompany) {
|
|
$oldPrimaryCompany = $companyLead->getCompany()->getId();
|
|
}
|
|
if ($company->getId() === (int) $companyId) {
|
|
$companyLead->setPrimary(true);
|
|
$newPrimaryCompany = $companyId;
|
|
$lead->addUpdatedField('company', $company->getName());
|
|
} else {
|
|
$companyLead->setPrimary(false);
|
|
}
|
|
$companyArray[] = $companyLead;
|
|
}
|
|
}
|
|
|
|
if (!$newPrimaryCompany) {
|
|
$latestCompany = $this->companyModel->getCompanyLeadRepository()->getLatestCompanyForLead($leadId);
|
|
if (!empty($latestCompany)) {
|
|
$lead->addUpdatedField('company', $latestCompany['companyname'])
|
|
->setDateModified(new \DateTime());
|
|
}
|
|
}
|
|
|
|
if (!empty($companyArray)) {
|
|
$this->em->getRepository(Lead::class)->saveEntity($lead);
|
|
$this->companyModel->getCompanyLeadRepository()->saveEntities($companyArray, false);
|
|
}
|
|
|
|
// Clear CompanyLead entities from Doctrine memory
|
|
$this->companyModel->getCompanyLeadRepository()->detachEntities($companyLeads);
|
|
|
|
return ['oldPrimary' => $oldPrimaryCompany, 'newPrimary' => $companyId];
|
|
}
|
|
|
|
public function scoreContactsCompany(Lead $lead, $score): bool
|
|
{
|
|
$success = false;
|
|
$entities = [];
|
|
$contactCompanies = $this->companyModel->getCompanyLeadRepository()->getCompaniesByLeadId($lead->getId());
|
|
|
|
foreach ($contactCompanies as $contactCompany) {
|
|
$company = $this->companyModel->getEntity($contactCompany['company_id']);
|
|
$oldScore = $company->getScore();
|
|
$newScore = $score + $oldScore;
|
|
$company->setScore($newScore);
|
|
$entities[] = $company;
|
|
$success = true;
|
|
}
|
|
|
|
if (!empty($entities)) {
|
|
$this->companyModel->getRepository()->saveEntities($entities);
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
public function updateLeadOwner(Lead $lead, $ownerId): void
|
|
{
|
|
$owner = $this->em->getReference(User::class, $ownerId);
|
|
$lead->setOwner($owner);
|
|
|
|
parent::saveEntity($lead);
|
|
}
|
|
|
|
private function processManipulator(Lead $lead): void
|
|
{
|
|
if ($lead->isNewlyCreated() || $lead->wasAnonymous()) {
|
|
// Only store an entry once for created and once for identified, not every time the lead is saved
|
|
$manipulator = $lead->getManipulator();
|
|
if (null !== $manipulator && !$manipulator->wasLogged()) {
|
|
$manipulationLog = new LeadEventLog();
|
|
$manipulationLog->setLead($lead)
|
|
->setBundle($manipulator->getBundleName())
|
|
->setObject($manipulator->getObjectName())
|
|
->setObjectId($manipulator->getObjectId());
|
|
if ($lead->isAnonymous()) {
|
|
$manipulationLog->setAction('created_contact');
|
|
} else {
|
|
$manipulationLog->setAction('identified_contact');
|
|
}
|
|
$description = $manipulator->getObjectDescription();
|
|
$manipulationLog->setProperties(['object_description' => $description]);
|
|
|
|
$lead->addEventLog($manipulationLog);
|
|
$manipulator->setAsLogged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param bool $persist
|
|
*/
|
|
protected function createNewContact(IpAddress $ip, $persist = true): Lead
|
|
{
|
|
// let's create a lead
|
|
$lead = new Lead();
|
|
$lead->addIpAddress($ip);
|
|
$lead->setNewlyCreated(true);
|
|
|
|
if ($persist && !defined('MAUTIC_NON_TRACKABLE_REQUEST')) {
|
|
// Set to prevent loops
|
|
$this->contactTracker->setTrackedContact($lead);
|
|
|
|
// Note ignoring a lead manipulator object here on purpose to not falsely record entries
|
|
$this->saveEntity($lead, false);
|
|
|
|
$fields = $this->getLeadDetails($lead);
|
|
$lead->setFields($fields);
|
|
}
|
|
|
|
if ($leadId = $lead->getId()) {
|
|
$this->logger->debug("LEAD: New lead created with ID# $leadId.");
|
|
}
|
|
|
|
return $lead;
|
|
}
|
|
|
|
/**
|
|
* @deprecated 2.12.0 to be removed in 3.0; use Mautic\LeadBundle\Model\DoNotContact instead
|
|
*
|
|
* @param string $channel
|
|
*
|
|
* @return int
|
|
*
|
|
* @see DNC This method can return boolean false, so be
|
|
* sure to always compare the return value against
|
|
* the class constants of DoNotContact
|
|
*/
|
|
public function isContactable(Lead $lead, $channel)
|
|
{
|
|
if (is_array($channel)) {
|
|
$channel = key($channel);
|
|
}
|
|
|
|
/** @var \Mautic\LeadBundle\Entity\DoNotContactRepository $dncRepo */
|
|
$dncRepo = $this->em->getRepository(DNC::class);
|
|
|
|
$dncEntries = $dncRepo->getEntriesByLeadAndChannel($lead, $channel);
|
|
|
|
// If the lead has no entries in the DNC table, we're good to go
|
|
if (empty($dncEntries)) {
|
|
return DNC::IS_CONTACTABLE;
|
|
}
|
|
|
|
foreach ($dncEntries as $dnc) {
|
|
if (DNC::IS_CONTACTABLE !== $dnc->getReason()) {
|
|
return $dnc->getReason();
|
|
}
|
|
}
|
|
|
|
return DNC::IS_CONTACTABLE;
|
|
}
|
|
|
|
public function getAvailableLeadFields(): array
|
|
{
|
|
return $this->availableLeadFields;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, int|float>
|
|
*/
|
|
public function getLeadEmailStats(Lead $lead): array
|
|
{
|
|
/** @var StatRepository $statRepository */
|
|
$statRepository = $this->em->getRepository(Stat::class);
|
|
|
|
return $statRepository->getStatsSummaryForContacts([$lead->getId()])[$lead->getId()];
|
|
}
|
|
|
|
public function removeTagFromLead(int $leadId, int $tagId): void
|
|
{
|
|
$lead = $this->getEntity($leadId);
|
|
$tag = $this->getTagRepository()->find($tagId);
|
|
|
|
if ($lead && $tag) {
|
|
$lead->removeTag($tag);
|
|
$this->saveEntity($lead);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param mixed[] $args
|
|
*
|
|
* @return int[]
|
|
*/
|
|
private function getContactIdsFromArgs(array $args): array
|
|
{
|
|
if (empty($args['filter']['force']) || !is_array($args['filter']['force'])) {
|
|
return [];
|
|
}
|
|
$idFilters = array_values(array_filter(
|
|
$args['filter']['force'],
|
|
fn ($filter) => is_array($filter) && isset($filter['column']) && 'l.id' === $filter['column']
|
|
));
|
|
|
|
if (isset($idFilters[0]['value']) && is_array($idFilters[0]['value'])) {
|
|
return $idFilters[0]['value'];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @param array<mixed>|null $fields
|
|
*/
|
|
private function validateSelectFields(Lead $entity, ?array $fields): void
|
|
{
|
|
if (is_null($fields)) {
|
|
return;
|
|
}
|
|
foreach ($fields as $group => $groupFields) {
|
|
foreach ($groupFields as $field) {
|
|
if (!is_array($field)) {
|
|
return;
|
|
} elseif ('select' !== $field['type']) {
|
|
continue;
|
|
}
|
|
$allowedValues = is_array($field['properties'])
|
|
? $field['properties']
|
|
: unserialize($field['properties']);
|
|
|
|
$flattenedAllowedValues = array_map(fn ($item): string => html_entity_decode($item['value'], ENT_QUOTES), $allowedValues['list']);
|
|
|
|
$fieldValue = $entity->getFieldValue($field['alias'], $group);
|
|
if (!empty($allowedValues['list']) && !in_array($fieldValue, $flattenedAllowedValues)) {
|
|
// if the set value of the field is not present allowed values array,
|
|
// update the field value to null
|
|
$entity->addUpdatedField($field['alias'], null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<mixed> $fields
|
|
* @param array<mixed> $data
|
|
*
|
|
* @return array<mixed>
|
|
*/
|
|
private function getCleanedFieldData(array $fields, array $data): array
|
|
{
|
|
$fieldData = [];
|
|
foreach ($fields as $leadField => $importField) {
|
|
// Prevent overwriting existing data with empty data
|
|
if (array_key_exists($importField, $data) && !is_null($data[$importField]) && '' != $data[$importField]) {
|
|
$fieldEntity = $this->leadFieldModel->getEntityByAlias($leadField);
|
|
|
|
$fieldData[$leadField] = InputHelper::_(
|
|
$data[$importField],
|
|
$fieldEntity instanceof LeadField && 'html' === $fieldEntity->getType() ? 'html' : 'string'
|
|
);
|
|
}
|
|
}
|
|
|
|
return $fieldData;
|
|
}
|
|
|
|
/**
|
|
* @param array<mixed> $leadField
|
|
*/
|
|
private function isValueAlreadyExists(?Lead $lead, array $leadField): bool
|
|
{
|
|
$leadFieldValue = $lead->getFieldValue($leadField['alias']);
|
|
|
|
if (CustomFieldValueHelper::TYPE_BOOLEAN === $leadField['type']) {
|
|
return is_null($leadFieldValue);
|
|
}
|
|
|
|
return empty($leadFieldValue);
|
|
}
|
|
}
|