252 lines
7.8 KiB
PHP
Executable File
252 lines
7.8 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Mautic\LeadBundle\Deduplicate;
|
|
|
|
use Mautic\CoreBundle\Helper\ArrayHelper;
|
|
use Mautic\LeadBundle\Deduplicate\Exception\SameContactException;
|
|
use Mautic\LeadBundle\Deduplicate\Exception\ValueNotMergeableException;
|
|
use Mautic\LeadBundle\Deduplicate\Helper\MergeValueHelper;
|
|
use Mautic\LeadBundle\Entity\Lead;
|
|
use Mautic\LeadBundle\Entity\MergeRecord;
|
|
use Mautic\LeadBundle\Entity\MergeRecordRepository;
|
|
use Mautic\LeadBundle\Event\LeadMergeEvent;
|
|
use Mautic\LeadBundle\LeadEvents;
|
|
use Mautic\LeadBundle\Model\LeadModel;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
|
|
class ContactMerger
|
|
{
|
|
/**
|
|
* @var Lead
|
|
*/
|
|
protected $winner;
|
|
|
|
/**
|
|
* @var Lead
|
|
*/
|
|
protected $loser;
|
|
|
|
public function __construct(
|
|
protected LeadModel $leadModel,
|
|
protected MergeRecordRepository $repo,
|
|
protected EventDispatcherInterface $dispatcher,
|
|
protected LoggerInterface $logger,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @throws SameContactException
|
|
*/
|
|
public function merge(Lead $winner, Lead $loser): Lead
|
|
{
|
|
if ($winner->getId() === $loser->getId()) {
|
|
throw new SameContactException();
|
|
}
|
|
|
|
$this->logger->debug('CONTACT: ID# '.$loser->getId().' will be merged into ID# '.$winner->getId());
|
|
|
|
// Dispatch pre merge event
|
|
$event = new LeadMergeEvent($winner, $loser);
|
|
$this->dispatcher->dispatch($event, LeadEvents::LEAD_PRE_MERGE);
|
|
|
|
// Merge everything
|
|
$this->updateMergeRecords($winner, $loser)
|
|
->mergeTimestamps($winner, $loser)
|
|
->mergeIpAddressHistory($winner, $loser)
|
|
->mergeFieldData($winner, $loser)
|
|
->mergeOwners($winner, $loser)
|
|
->mergePoints($winner, $loser)
|
|
->mergeTags($winner, $loser);
|
|
|
|
// Save the updated contact
|
|
$this->leadModel->saveEntity($winner, false);
|
|
|
|
// Dispatch post merge event
|
|
$this->dispatcher->dispatch($event, LeadEvents::LEAD_POST_MERGE);
|
|
|
|
// Delete the loser
|
|
$this->leadModel->deleteEntity($loser);
|
|
|
|
return $winner;
|
|
}
|
|
|
|
/**
|
|
* Merge timestamps.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function mergeTimestamps(Lead $winner, Lead $loser)
|
|
{
|
|
// The winner should keep the most recent last active timestamp of the two
|
|
if ($loser->getLastActive() > $winner->getLastActive()) {
|
|
$winner->setLastActive($loser->getLastActive());
|
|
}
|
|
|
|
/*
|
|
* The winner should keep the oldest date identified timestamp
|
|
* as long as the loser's date identified is not null.
|
|
* Alternatively, if the winner's date identified is null,
|
|
* use the loser's date identified (doesn't matter if it is null).
|
|
*/
|
|
if ((null !== $loser->getDateIdentified() && $loser->getDateIdentified() < $winner->getDateIdentified()) || null === $winner->getDateIdentified()) {
|
|
$winner->setDateIdentified($loser->getDateIdentified());
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Merge IP history into the winner.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function mergeIpAddressHistory(Lead $winner, Lead $loser)
|
|
{
|
|
$ipAddresses = $loser->getIpAddresses();
|
|
|
|
foreach ($ipAddresses as $ip) {
|
|
$winner->addIpAddress($ip);
|
|
|
|
$this->logger->debug('CONTACT: Associating '.$winner->getId().' with IP '.$ip->getIpAddress());
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Merge custom field data into winner.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function mergeFieldData(Lead $winner, Lead $loser)
|
|
{
|
|
// Use the modified date if applicable or date added if the contact has never been edited
|
|
$loserDate = $loser->getDateModified() ?: $loser->getDateAdded();
|
|
$winnerDate = $winner->getDateModified() ?: $winner->getDateAdded();
|
|
|
|
// When it comes to data, keep the newest value regardless of the winner/loser
|
|
$newest = ($loserDate > $winnerDate) ? $loser : $winner;
|
|
$oldest = ($newest->getId() === $winner->getId()) ? $loser : $winner;
|
|
|
|
// It may happen that the Lead entities doesn't have fields fill in. Fill them in if not.
|
|
if (!$newest->hasFields()) {
|
|
$newest->setFields($this->leadModel->getRepository()->getFieldValues($newest->getId()));
|
|
}
|
|
|
|
if (!$oldest->hasFields()) {
|
|
$oldest->setFields($this->leadModel->getRepository()->getFieldValues($oldest->getId()));
|
|
}
|
|
|
|
$newestFields = $newest->getProfileFields();
|
|
$oldestFields = $oldest->getProfileFields();
|
|
|
|
foreach (array_keys($newestFields) as $field) {
|
|
if (in_array($field, ['id', 'points'])) {
|
|
// Let mergePoints() take care of this
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$fromValue = empty($oldestFields[$field]) ? 'empty' : $oldestFields[$field];
|
|
$fieldDetails = $winner->getField($field);
|
|
|
|
if (false === $fieldDetails) {
|
|
throw new ValueNotMergeableException($fromValue, false);
|
|
}
|
|
|
|
$defaultValue = ArrayHelper::getValue('default_value', $fieldDetails);
|
|
$newValue = MergeValueHelper::getMergeValue(
|
|
$newestFields[$field],
|
|
$oldestFields[$field],
|
|
$winner->getFieldValue($field),
|
|
$defaultValue,
|
|
$newest->isAnonymous()
|
|
);
|
|
$winner->addUpdatedField($field, $newValue);
|
|
|
|
$this->logger->debug("CONTACT: Updated {$field} from {$fromValue} to {$newValue} for {$winner->getId()}");
|
|
} catch (ValueNotMergeableException $exception) {
|
|
$this->logger->info("CONTACT: {$field} is not mergeable for {$winner->getId()} - {$exception->getMessage()}");
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Merge owners if the winner isn't already assigned an owner.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function mergeOwners(Lead $winner, Lead $loser)
|
|
{
|
|
$oldOwner = $winner->getOwner();
|
|
$newOwner = $loser->getOwner();
|
|
|
|
if (null === $oldOwner && null !== $newOwner) {
|
|
$winner->setOwner($newOwner);
|
|
|
|
$this->logger->debug("CONTACT: New owner of {$winner->getId()} is {$newOwner->getId()}");
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Sum points from both contacts.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function mergePoints(Lead $winner, Lead $loser)
|
|
{
|
|
$winnerPoints = (int) $winner->getPoints();
|
|
$loserPoints = (int) $loser->getPoints();
|
|
$winner->adjustPoints($loserPoints);
|
|
|
|
$this->logger->debug(
|
|
'CONTACT: Adding '.$loserPoints.' points from contact ID #'.$loser->getId().' to contact ID #'.$winner->getId().' with '.$winnerPoints
|
|
.' points'
|
|
);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Merge tags from loser into winner.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function mergeTags(Lead $winner, Lead $loser)
|
|
{
|
|
$loserTags = $loser->getTags();
|
|
$addTags = $loserTags->getKeys();
|
|
|
|
$this->leadModel->modifyTags($winner, $addTags, null, false);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Merge past merge records into the winner.
|
|
*
|
|
* @return $this
|
|
*/
|
|
private function updateMergeRecords(Lead $winner, Lead $loser)
|
|
{
|
|
// Update merge records for the lead about to be deleted
|
|
$this->repo->moveMergeRecord($loser->getId(), $winner->getId());
|
|
|
|
// Create an entry this contact was merged
|
|
$mergeRecord = new MergeRecord();
|
|
$mergeRecord->setContact($winner)
|
|
->setDateAdded()
|
|
->setName($loser->getPrimaryIdentifier())
|
|
->setMergedId($loser->getId());
|
|
|
|
$this->repo->saveEntity($mergeRecord);
|
|
|
|
return $this;
|
|
}
|
|
}
|