diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 0b72ee7c1f4..2d20671b35d 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -308,6 +308,7 @@ 'translator', 'doctrine.orm.entity_manager', 'mautic.stage.model.stage', + 'mautic.helper.core_parameters', ], ], 'mautic.form.type.email.utm_tags' => [ diff --git a/app/bundles/EmailBundle/Entity/Email.php b/app/bundles/EmailBundle/Entity/Email.php index 38cf6505019..6923feb426a 100644 --- a/app/bundles/EmailBundle/Entity/Email.php +++ b/app/bundles/EmailBundle/Entity/Email.php @@ -64,6 +64,11 @@ class Email extends FormEntity implements VariantEntityInterface, TranslationEnt */ private $subject; + /** + * @var bool + */ + private $useOwnerAsMailer; + /** * @var string */ @@ -265,6 +270,7 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) $builder->addNullableField('fromName', Type::STRING, 'from_name'); $builder->addNullableField('replyToAddress', Type::STRING, 'reply_to_address'); $builder->addNullableField('bccAddress', Type::STRING, 'bcc_address'); + $builder->addNullableField('useOwnerAsMailer', Type::BOOLEAN, 'use_owner_as_mailer'); $builder->addNullableField('template', Type::STRING); $builder->addNullableField('content', Type::TARRAY); $builder->addNullableField('utmTags', Type::TARRAY, 'utm_tags'); @@ -436,6 +442,7 @@ public static function loadApiMetadata(ApiMetadataDriver $metadata) 'fromName', 'replyToAddress', 'bccAddress', + 'useOwnerAsMailer', 'utmTags', 'customHtml', 'plainText', @@ -678,6 +685,26 @@ public function setSubject($subject) return $this; } + /** + * @return bool + */ + public function getUseOwnerAsMailer() + { + return $this->useOwnerAsMailer; + } + + /** + * @param bool $useOwnerAsMailer + * + * @return $this + */ + public function setUseOwnerAsMailer($useOwnerAsMailer) + { + $this->useOwnerAsMailer = $useOwnerAsMailer; + + return $this; + } + /** * @return mixed */ diff --git a/app/bundles/EmailBundle/Form/Type/EmailType.php b/app/bundles/EmailBundle/Form/Type/EmailType.php index 29efbc7288a..5d90f66af91 100644 --- a/app/bundles/EmailBundle/Form/Type/EmailType.php +++ b/app/bundles/EmailBundle/Form/Type/EmailType.php @@ -23,6 +23,7 @@ use Mautic\CoreBundle\Form\Type\SortableListType; use Mautic\CoreBundle\Form\Type\ThemeListType; use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType; +use Mautic\CoreBundle\Helper\CoreParametersHelper; use Mautic\EmailBundle\Entity\Email; use Mautic\FormBundle\Form\Type\FormListType; use Mautic\LeadBundle\Form\Type\LeadListType; @@ -62,14 +63,21 @@ class EmailType extends AbstractType */ private $stageModel; + /** + * @var CoreParametersHelper + */ + private $coreParametersHelper; + public function __construct( TranslatorInterface $translator, EntityManager $entityManager, - StageModel $stageModel + StageModel $stageModel, + CoreParametersHelper $coreParametersHelper ) { - $this->translator = $translator; - $this->em = $entityManager; - $this->stageModel = $stageModel; + $this->translator = $translator; + $this->em = $entityManager; + $this->stageModel = $stageModel; + $this->coreParametersHelper = $coreParametersHelper; } public function buildForm(FormBuilderInterface $builder, array $options) @@ -160,6 +168,21 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); + $builder->add( + 'useOwnerAsMailer', + YesNoButtonGroupType::class, + [ + 'label' => 'mautic.email.use.owner.as.mailer', + 'label_attr' => ['class' => 'control-label'], + 'attr' => [ + 'class' => 'form-control', + 'tooltip' => 'mautic.email.use.owner.as.mailer.tooltip', + ], + 'data' => (bool) (is_null($options['data']->getUseOwnerAsMailer()) ? $this->coreParametersHelper->get('mailer_is_owner') : $options['data']->getUseOwnerAsMailer()), + 'required' => false, + ] + ); + $builder->add( 'utmTags', EmailUtmTagsType::class, diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index eefdafb6a42..4784d65258b 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -208,13 +208,6 @@ class MailHelper */ protected $fatal = false; - /** - * Flag whether to use only the globally set From email and name or whether to switch to mailer is owner. - * - * @var bool - */ - protected $useGlobalFrom = false; - /** * Large batch mail sends may result on timeouts with SMTP servers. This will will keep track of the number of sends and restart the connection once met. * @@ -323,18 +316,17 @@ public function getSampleMailer($cleanSlate = true) * * @param bool $dispatchSendEvent * @param bool $isQueueFlush (a tokenized/batch send via API such as Mandrill) - * @param bool $useOwnerAsMailer * * @return bool */ - public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwnerAsMailer = true) + public function send($dispatchSendEvent = false, $isQueueFlush = false) { if ($this->tokenizationEnabled && !empty($this->queuedRecipients) && !$isQueueFlush) { // This transport uses tokenization and queue()/flushQueue() was not used therefore use them in order // properly populate metadata for this transport if ($result = $this->queue($dispatchSendEvent)) { - $result = $this->flushQueue(['To', 'Cc', 'Bcc'], $useOwnerAsMailer); + $result = $this->flushQueue(['To', 'Cc', 'Bcc']); } return $result; @@ -343,15 +335,23 @@ public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwne // Set from email $ownerSignature = false; if (!$isQueueFlush) { - if ($useOwnerAsMailer) { - if ($owner = $this->getContactOwner($this->lead)) { - $this->setFrom($owner['email'], $owner['first_name'].' '.$owner['last_name'], null); - $ownerSignature = $this->getContactOwnerSignature($owner); + $emailToSend = $this->getEmail(); + if (!empty($emailToSend)) { + if ($emailToSend->getUseOwnerAsMailer()) { + $owner = $this->getContactOwner($this->lead); + if (!empty($owner)) { + $this->setFrom($owner['email'], $owner['first_name'].' '.$owner['last_name']); + $ownerSignature = $this->getContactOwnerSignature($owner); + } else { + $this->setFrom($this->systemFrom, null); + } + } elseif (!empty($emailToSend->getFromAddress())) { + $this->setFrom($emailToSend->getFromAddress(), $emailToSend->getFromName()); } else { - $this->setFrom($this->from, null, null); + $this->setFrom($this->systemFrom, null); } - } elseif (!$from = $this->message->getFrom()) { - $this->setFrom($this->from, null, null); + } else { + $this->setFrom($this->from, null); } } // from is set in flushQueue @@ -572,12 +572,11 @@ public function queue($dispatchSendEvent = false, $returnMode = self::QUEUE_RESE /** * Send batched mail to mailer. * - * @param array $resetEmailTypes Array of email types to clear after flusing the queue - * @param bool $useOwnerAsMailer + * @param array $resetEmailTypes Array of email types to clear after flusing the queue * * @return bool */ - public function flushQueue($resetEmailTypes = ['To', 'Cc', 'Bcc'], $useOwnerAsMailer = true) + public function flushQueue($resetEmailTypes = ['To', 'Cc', 'Bcc']) { // Assume true unless there was a fatal error configuring the mailer because if tokenizationEnabled is false, the send happened in queue() $flushed = empty($this->fatal); @@ -593,10 +592,18 @@ public function flushQueue($resetEmailTypes = ['To', 'Cc', 'Bcc'], $useOwnerAsMa $this->errors = []; - if (!$this->useGlobalFrom && $useOwnerAsMailer && 'default' !== $fromKey) { - $this->setFrom($metadatum['from']['email'], $metadatum['from']['first_name'].' '.$metadatum['from']['last_name'], null); + $email = $this->getEmail(); + + if (!empty($email)) { + if ($email->getUseOwnerAsMailer() && 'default' !== $fromKey) { + $this->setFrom($metadatum['from']['email'], $metadatum['from']['first_name'].' '.$metadatum['from']['last_name']); + } elseif (!empty($email->getFromAddress())) { + $this->setFrom($email->getFromAddress(), $email->getFromName()); + } else { + $this->setFrom($this->systemFrom, null); + } } else { - $this->setFrom($this->from, null, null); + $this->setFrom($this->from, null); } foreach ($metadatum['contacts'] as $email => $contact) { @@ -657,7 +664,6 @@ public function reset($cleanSlate = true) $this->internalSend = false; $this->fatal = false; $this->idHashState = true; - $this->useGlobalFrom = false; $this->checkIfTransportNeedsRestart(true); $this->logger->clear(); @@ -1235,25 +1241,15 @@ public function setReturnPath($address) * * @param string|array $fromEmail * @param string $fromName - * @param bool|null $isGlobal */ - public function setFrom($fromEmail, $fromName = null, $isGlobal = true) + public function setFrom($fromEmail, $fromName = null) { $fromName = $this->cleanName($fromName); - if (null !== $isGlobal) { - if ($isGlobal) { - if (is_array($fromEmail)) { - $this->from = $fromEmail; - } else { - $this->from = [$fromEmail => $fromName]; - } - } else { - // Reset the default to the system from - $this->from = $this->systemFrom; - } - - $this->useGlobalFrom = $isGlobal; + if (is_array($fromEmail)) { + $this->from = $fromEmail; + } else { + $this->from = [$fromEmail => $fromName]; } try { @@ -1371,7 +1367,7 @@ public function setEmail(Email $email, $allowBcc = true, $slots = [], $assetAtta $fromEmail = key($this->from); } - $this->setFrom($fromEmail, $fromName, null); + $this->setFrom($fromEmail, $fromName); $this->from = [$fromEmail => $fromName]; } else { $this->from = $this->systemFrom; @@ -2055,15 +2051,19 @@ protected function cleanName($name) protected function getContactOwner(&$contact) { $owner = false; - - if ($this->factory->getParameter('mailer_is_owner') && is_array($contact) && isset($contact['id'])) { - if (!isset($contact['owner_id'])) { - $contact['owner_id'] = 0; - } elseif (isset($contact['owner_id'])) { - if (isset(self::$leadOwners[$contact['owner_id']])) { - $owner = self::$leadOwners[$contact['owner_id']]; - } elseif ($owner = $this->factory->getModel('lead')->getRepository()->getLeadOwner($contact['owner_id'])) { - self::$leadOwners[$owner['id']] = $owner; + $email = $this->getEmail(); + + if (!empty($email)) { + if ($email->getUseOwnerAsMailer() && is_array($contact) && isset($contact['id'])) { + if (!isset($contact['owner_id'])) { + $contact['owner_id'] = 0; + } elseif (isset($contact['owner_id'])) { + $leadModel = $this->factory->getModel('lead'); + if (isset(self::$leadOwners[$contact['owner_id']])) { + $owner = self::$leadOwners[$contact['owner_id']]; + } elseif ($owner = $leadModel->getRepository()->getLeadOwner($contact['owner_id'])) { + self::$leadOwners[$owner['id']] = $owner; + } } } } diff --git a/app/bundles/EmailBundle/Tests/Form/Type/EmailTypeTest.php b/app/bundles/EmailBundle/Tests/Form/Type/EmailTypeTest.php index 54ee9c89602..3a13a27abab 100644 --- a/app/bundles/EmailBundle/Tests/Form/Type/EmailTypeTest.php +++ b/app/bundles/EmailBundle/Tests/Form/Type/EmailTypeTest.php @@ -13,6 +13,7 @@ use Doctrine\ORM\EntityManager; use Mautic\CoreBundle\Form\Type\FormButtonsType; +use Mautic\CoreBundle\Helper\CoreParametersHelper; use Mautic\EmailBundle\Entity\Email; use Mautic\EmailBundle\Form\Type\EmailType; use Mautic\StageBundle\Model\StageModel; @@ -47,18 +48,25 @@ class EmailTypeTest extends \PHPUnit\Framework\TestCase */ private $form; + /** + * @var CoreParametersHelper|MockObject + */ + private $coreParametersHelper; + protected function setUp(): void { parent::setUp(); - $this->translator = $this->createMock(TranslatorInterface::class); - $this->entityManager = $this->createMock(EntityManager::class); - $this->stageModel = $this->createMock(StageModel::class); - $this->formBuilder = $this->createMock(FormBuilderInterface::class); - $this->form = new EmailType( + $this->translator = $this->createMock(TranslatorInterface::class); + $this->entityManager = $this->createMock(EntityManager::class); + $this->stageModel = $this->createMock(StageModel::class); + $this->formBuilder = $this->createMock(FormBuilderInterface::class); + $this->coreParametersHelper = $this->createMock(CoreParametersHelper::class); + $this->form = new EmailType( $this->translator, $this->entityManager, - $this->stageModel + $this->stageModel, + $this->coreParametersHelper ); $this->formBuilder->method('create')->willReturnSelf(); @@ -70,7 +78,7 @@ public function testBuildForm() 'data' => new Email(), ]; - $this->formBuilder->expects($this->at(46)) + $this->formBuilder->expects($this->at(47)) ->method('add') ->with( 'buttons', diff --git a/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php b/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php index c3c13779570..979886fcb94 100644 --- a/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php +++ b/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php @@ -166,6 +166,7 @@ public function testQueuedEmailFromOverride() $email = new Email(); $email->setFromAddress('override@nowhere.com'); $email->setFromName('Test'); + $email->setUseOwnerAsMailer(false); $mailer->setEmail($email); @@ -233,6 +234,11 @@ public function testQueuedOwnerAsMailer() $swiftMailer = new \Swift_Mailer($transport); $mailer = new MailHelper($mockFactory, $swiftMailer, ['nobody@nowhere.com' => 'No Body']); + + $email = new Email(); + $email->setUseOwnerAsMailer(true); + $mailer->setEmail($email); + $mailer->enableQueue(); $mailer->setSubject('Hello'); @@ -291,6 +297,11 @@ public function testMailAsOwnerWithEncodedCharactersInName() $swiftMailer = new \Swift_Mailer($transport); $mailer = new MailHelper($mockFactory, $swiftMailer, ['nobody@nowhere.com' => 'No Body's Business']); + $email = new Email(); + $email->setUseOwnerAsMailer(true); + + $mailer->setEmail($email); + $mailer->enableQueue(); $mailer->enableQueue(); $mailer->setSubject('Hello'); @@ -323,6 +334,11 @@ public function testBatchIsEnabledWithBcTokenInterface() $swiftMailer = new \Swift_Mailer($transport); $mailer = new MailHelper($mockFactory, $swiftMailer, ['nobody@nowhere.com' => 'No Body']); + + $email = new Email(); + $email->setUseOwnerAsMailer(true); + + $mailer->setEmail($email); $mailer->enableQueue(); $mailer->setSubject('Hello'); @@ -372,6 +388,29 @@ public function testGlobalFromThatAllFromAddressesAreTheSame() $this->assertEquals(['override@owner.com'], array_unique($fromAddresses)); } + public function testStandardEmailFrom() + { + $mockFactory = $this->getMockFactory(true); + $transport = new BatchTransport(); + $swiftMailer = new \Swift_Mailer($transport); + $mailer = new MailHelper($mockFactory, $swiftMailer, ['nobody@nowhere.com' => 'No Body']); + $email = new Email(); + + $email->setUseOwnerAsMailer(false); + $email->setFromAddress('override@nowhere.com'); + $email->setFromName('Test'); + $mailer->setEmail($email); + + foreach ($this->contacts as $key => $contact) { + $mailer->addTo($contact['email']); + $mailer->setLead($contact); + $mailer->setBody('{signature}'); + $mailer->send(); + $from = key($mailer->message->getFrom()); + $this->assertEquals('override@nowhere.com', $from); + } + } + public function testStandardOwnerAsMailer() { $mockFactory = $this->getMockFactory(); @@ -380,6 +419,11 @@ public function testStandardOwnerAsMailer() $swiftMailer = new \Swift_Mailer($transport); $mailer = new MailHelper($mockFactory, $swiftMailer, ['nobody@nowhere.com' => 'No Body']); + + $email = new Email(); + $mailer->setEmail($email); + $email->setUseOwnerAsMailer(true); + $mailer->setBody('{signature}'); foreach ($this->contacts as $key => $contact) { diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index 951a08433bb..2a8f213b156 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -113,9 +113,9 @@ mautic.email.config.mailer.from.email.tooltip="Set the from email for email sent mautic.email.config.mailer.from.email="E-mail address to send mail from" mautic.email.config.mailer.from.name.tooltip="Set the from name for email sent by Mautic" mautic.email.config.mailer.from.name="Name to send mail as" -mautic.email.config.mailer.host.tooltip="Set the host for mail server" -mautic.email.config.mailer.host="Host" -mautic.email.config.mailer.is.owner.tooltip="If a contact owner is known, force his/her email and name as sender email and name" +mautic.email.config.mailer.host.tooltip="Set the host for SMTP server" +mautic.email.config.mailer.host="SMTP host" +mautic.email.config.mailer.is.owner.tooltip="Set the default mailer is owner setting for all new emails that are created" mautic.email.config.mailer.is.owner="Mailer is owner" mautic.email.config.mailer.password.tooltip="Set the password required to authenticate the selected mail service" mautic.email.config.mailer.password="Password for the selected mail service" @@ -254,6 +254,8 @@ mautic.email.from_email.tooltip="Set the from address for this email. This will mautic.email.from_email="From address" mautic.email.from_name.tooltip="Set the from name for the this email. This will default to the system configuration if left blank." mautic.email.from_name="From name" +mautic.email.use.owner.as.mailer.tooltip="If a contact owner is known, force his/her email and name as sender email and name." +mautic.email.use.owner.as.mailer="Use owner as mailer" mautic.email.graph.line.stats.failed="Failed" mautic.email.graph.line.stats.read="Read" mautic.email.graph.line.stats.sent="Sent" diff --git a/app/bundles/EmailBundle/Views/Email/form.html.php b/app/bundles/EmailBundle/Views/Email/form.html.php index 529fea83cb2..1c4a752a9f8 100644 --- a/app/bundles/EmailBundle/Views/Email/form.html.php +++ b/app/bundles/EmailBundle/Views/Email/form.html.php @@ -129,6 +129,9 @@
row($form['headers']); ?>
+
+ row($form['useOwnerAsMailer']); ?> +

diff --git a/app/bundles/LeadBundle/Event/CompanyMergeEvent.php b/app/bundles/LeadBundle/Event/CompanyMergeEvent.php new file mode 100644 index 00000000000..677a38fc271 --- /dev/null +++ b/app/bundles/LeadBundle/Event/CompanyMergeEvent.php @@ -0,0 +1,49 @@ +victor = $victor; + $this->loser = $loser; + } + + /** + * Returns the victor (loser merges into the victor). + * + * @return Company + */ + public function getVictor() + { + return $this->victor; + } + + /** + * Returns the loser (loser merges into the victor). + */ + public function getLoser() + { + return $this->loser; + } +} diff --git a/app/bundles/LeadBundle/LeadEvents.php b/app/bundles/LeadBundle/LeadEvents.php index 3c9d4c6ac91..f62c32850dd 100644 --- a/app/bundles/LeadBundle/LeadEvents.php +++ b/app/bundles/LeadBundle/LeadEvents.php @@ -531,6 +531,26 @@ final class LeadEvents */ const COMPANY_POST_DELETE = 'mautic.company_post_delete'; + /** + * The mautic.company_pre_merge event is dispatched before two companies are merged. + * + * The event listener receives a + * Mautic\LeadBundle\Event\CompanyMergeEvent instance. + * + * @var string + */ + const COMPANY_PRE_MERGE = 'mautic.company_pre_merge'; + + /** + * The mautic.company_post_merge event is dispatched after two companies are merged. + * + * The event listener receives a + * Mautic\LeadBundle\Event\CompanyMergeEvent instance. + * + * @var string + */ + const COMPANY_POST_MERGE = 'mautic.company_post_merge'; + /** * The mautic.list_filters_choices_on_generate event is dispatched when the choices for list filters are generated. * diff --git a/app/bundles/LeadBundle/Model/CompanyModel.php b/app/bundles/LeadBundle/Model/CompanyModel.php index 1af85a60f05..9b5ef12d9b0 100644 --- a/app/bundles/LeadBundle/Model/CompanyModel.php +++ b/app/bundles/LeadBundle/Model/CompanyModel.php @@ -24,6 +24,7 @@ use Mautic\LeadBundle\Entity\LeadEventLog; use Mautic\LeadBundle\Entity\LeadField; use Mautic\LeadBundle\Event\CompanyEvent; +use Mautic\LeadBundle\Event\CompanyMergeEvent; use Mautic\LeadBundle\Event\LeadChangeCompanyEvent; use Mautic\LeadBundle\Form\Type\CompanyType; use Mautic\LeadBundle\LeadEvents; @@ -623,6 +624,10 @@ public function companyMerge($mainCompany, $secCompany) $mainCompanyId = $mainCompany->getId(); $secCompanyId = $secCompany->getId(); + // Dispatch pre merge event + $event = new CompanyMergeEvent($mainCompany, $secCompany); + $this->dispatcher->dispatch(LeadEvents::COMPANY_PRE_MERGE, $event); + //if they are the same lead, then just return one if ($mainCompanyId === $secCompanyId) { return $mainCompany; @@ -658,6 +663,9 @@ public function companyMerge($mainCompany, $secCompany) //save the updated company $this->saveEntity($mainCompany, false); + // Dispatch post merge event + $this->dispatcher->dispatch(LeadEvents::COMPANY_POST_MERGE, $event); + //delete the old company $this->deleteEntity($secCompany); diff --git a/app/migrations/Version20180508202930.php b/app/migrations/Version20180508202930.php new file mode 100644 index 00000000000..48e1b6950a7 --- /dev/null +++ b/app/migrations/Version20180508202930.php @@ -0,0 +1,42 @@ +getTable($this->prefix.'emails')->hasColumn('use_owner_as_mailer')) { + throw new SkipMigration('Schema includes this migration'); + } + } + + public function up(Schema $schema): void + { + $this->abortIf('mysql' != $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql("ALTER TABLE {$this->prefix}emails ADD use_owner_as_mailer TINYINT(4) AFTER email_type;"); + + $ownerAsMailerConfigSetting = $this->container->getParameter('mautic.mailer_is_owner') ? 1 : 0; + + $this->addSql("UPDATE {$this->prefix}emails SET use_owner_as_mailer = {$ownerAsMailerConfigSetting};"); + } + + public function down(Schema $schema): void + { + $this->abortIf('mysql' != $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql("ALTER TABLE {$this->prefix}emails DROP use_owner_as_mailer;"); + } +} diff --git a/plugins/MauticCrmBundle/Api/PipedriveApi.php b/plugins/MauticCrmBundle/Api/PipedriveApi.php index 30199b73ecd..cec7ec0af6a 100644 --- a/plugins/MauticCrmBundle/Api/PipedriveApi.php +++ b/plugins/MauticCrmBundle/Api/PipedriveApi.php @@ -68,6 +68,15 @@ public function removeCompany($id = null) return $this->getResponseData($response); } + public function mergeCompany($id = null, $otherId = null) + { + $params = $this->getRequestParameters(['merge_with_id' => $id]); + $url = sprintf('%s/%s/%s', $this->integration->getApiUrl(), self::ORGANIZATIONS_API_ENDPOINT, $otherId); + $response = $this->transport->put($url, $params); + + return $this->getResponseData($response); + } + /** * @param $data */ @@ -101,6 +110,15 @@ public function deleteLead($id) return $this->getResponseData($response); } + public function mergeLead($id = null, $otherId = null) + { + $params = $this->getRequestParameters(['merge_with_id' => $id]); + $url = sprintf('%s/%s/%s', $this->integration->getApiUrl(), self::PERSONS_API_ENDPOINT, $otherId); + $response = $this->transport->put($url, $params); + + return $this->getResponseData($response); + } + /** * @param string $email * diff --git a/plugins/MauticCrmBundle/Command/ProcessPipedriveDeletionsCommand.php b/plugins/MauticCrmBundle/Command/ProcessPipedriveDeletionsCommand.php new file mode 100644 index 00000000000..55ceb913bd9 --- /dev/null +++ b/plugins/MauticCrmBundle/Command/ProcessPipedriveDeletionsCommand.php @@ -0,0 +1,147 @@ +setName('mautic:integration:pipedrive:process-deletions') + ->setDescription('Processes the Pipedrive deletion queue'); + + parent::configure(); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $container = $this->getContainer(); + + /** @var IntegrationHelper $integrationHelper */ + $integrationHelper = $container->get('mautic.helper.integration'); + $integrationObject = $integrationHelper->getIntegrationObject(PipedriveIntegration::INTEGRATION_NAME); + + if (!$integrationObject->getIntegrationSettings()->getIsPublished()) { + $io->note('Pipedrive integration id disabled.'); + + return; + } + + $this->em = $container->get('doctrine.orm.default_entity_manager'); + $query = $this->em->createQuery('SELECT d FROM MauticPlugin\MauticCrmBundle\Entity\PipedriveDeletion d WHERE d.deletedDate < :olderThan'); + $query->setParameter('olderThan', new \DateTime('-1 minute')); + $deletions = $query->getResult(); + + $deleted = 0; + + /** @var PipedriveDeletion $deletion */ + foreach ($deletions as $deletion) { + $integrationEntity = null; + $type = $deletion->getObjectType(); + + if ('lead' === $type) { + $integrationEntity = $this->getLeadIntegrationEntity(['integrationEntityId' => $deletion->getIntegrationEntityId()]); + } elseif ('company' === $type) { + $integrationEntity = $this->getCompanyIntegrationEntity(['integrationEntityId' => $deletion->getIntegrationEntityId()]); + } + + if (!$integrationEntity) { + $io->note('Integration entity not found, skipping.'); + continue; + } + + $entityClass = 'company' === $type ? Company::class : Lead::class; + /** @var Company|Lead $entity */ + $entity = $this->em->getRepository($entityClass)->findOneById($integrationEntity->getInternalEntityId()); + + if (!$entity) { + $name = 'company' === $type ? 'Company' : 'Lead'; + $io->note($name.' doesn\'t exist.'); + continue; + } + + // prevent listeners from exporting + $entity->setEventData('pipedrive.webhook', 1); + /** @var ModelFactory $modelFactory */ + $modelFactory = $container->get('mautic.model.factory'); + /** @var CompanyModel|LeadModel $model */ + $modelType = $type; + + if ('company' === $modelType) { + $modelType = 'lead.company'; + } + + $model = $modelFactory->getModel($modelType); + $model->deleteEntity($entity); + + if (!empty($entity->deletedId)) { + $this->em->remove($integrationEntity); + $this->em->remove($deletion); + ++$deleted; + } + } + + $io->success('Deleted '.$deleted.' items. Execution time: '.number_format(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], 3)); + } + + /** + * @return IntegrationEntity|object|null + */ + protected function getLeadIntegrationEntity(array $criteria = []) + { + $criteria['integrationEntity'] = self::PERSON_ENTITY_TYPE; + $criteria['internalEntity'] = self::LEAD_ENTITY_TYPE; + + return $this->getIntegrationEntity($criteria); + } + + /** + * @return IntegrationEntity|object|null + */ + protected function getCompanyIntegrationEntity(array $criteria = []) + { + $criteria['integrationEntity'] = self::ORGANIZATION_ENTITY_TYPE; + $criteria['internalEntity'] = self::COMPANY_ENTITY_TYPE; + + return $this->getIntegrationEntity($criteria); + } + + /** + * @return IntegrationEntity|object|null + */ + private function getIntegrationEntity(array $criteria = []) + { + $criteria['integration'] = PipedriveIntegration::INTEGRATION_NAME; + + return $this->em->getRepository(IntegrationEntity::class)->findOneBy($criteria); + } +} diff --git a/plugins/MauticCrmBundle/Config/config.php b/plugins/MauticCrmBundle/Config/config.php index 3657fb26a10..d493461f225 100644 --- a/plugins/MauticCrmBundle/Config/config.php +++ b/plugins/MauticCrmBundle/Config/config.php @@ -12,7 +12,7 @@ return [ 'name' => 'CRM', 'description' => 'Enables integration with Mautic supported CRMs.', - 'version' => '1.0', + 'version' => '1.1', 'author' => 'Mautic', 'routes' => [ 'public' => [ @@ -265,6 +265,7 @@ 'doctrine.orm.entity_manager', 'mautic.lead.model.lead', 'mautic.lead.model.company', + 'mautic.lead.merger', ], ], 'mautic_integration.pipedrive.export.company' => [ diff --git a/plugins/MauticCrmBundle/Controller/PipedriveController.php b/plugins/MauticCrmBundle/Controller/PipedriveController.php index 33b7e1cd114..8230597b913 100644 --- a/plugins/MauticCrmBundle/Controller/PipedriveController.php +++ b/plugins/MauticCrmBundle/Controller/PipedriveController.php @@ -31,10 +31,12 @@ class PipedriveController extends CommonController const LEAD_ADDED_EVENT = 'added.person'; const LEAD_UPDATE_EVENT = 'updated.person'; const LEAD_DELETE_EVENT = 'deleted.person'; + const LEAD_MERGED_EVENT = 'merged.person'; const COMPANY_ADD_EVENT = 'added.organization'; const COMPANY_UPDATE_EVENT = 'updated.organization'; const COMPANY_DELETE_EVENT = 'deleted.organization'; + const COMPANY_MERGED_EVENT = 'merged.organization'; const USER_ADD_EVENT = 'added.user'; const USER_UPDATE_EVENT = 'updated.user'; @@ -73,6 +75,10 @@ public function webhookAction(Request $request) $leadImport = $this->getLeadImport($pipedriveIntegration); $leadImport->delete($params['previous']); break; + case self::LEAD_MERGED_EVENT: + $leadImport = $this->getLeadImport($pipedriveIntegration); + $leadImport->merge($data, $data['merge_what_id']); + break; case self::COMPANY_UPDATE_EVENT: $companyImport = $this->getCompanyImport($pipedriveIntegration); $companyImport->update($data); @@ -81,6 +87,10 @@ public function webhookAction(Request $request) $companyImport = $this->getCompanyImport($pipedriveIntegration); $companyImport->delete($params['previous']); break; + case self::COMPANY_MERGED_EVENT: + $companyImport = $this->getCompanyImport($pipedriveIntegration); + $companyImport->merge($data, $data['merge_what_id']); + break; case self::USER_UPDATE_EVENT: $ownerImport = $this->getOwnerImport($pipedriveIntegration); $ownerImport->create($data[0]); diff --git a/plugins/MauticCrmBundle/Entity/PipedriveDeletion.php b/plugins/MauticCrmBundle/Entity/PipedriveDeletion.php new file mode 100644 index 00000000000..21a89db07f7 --- /dev/null +++ b/plugins/MauticCrmBundle/Entity/PipedriveDeletion.php @@ -0,0 +1,110 @@ +setTable('plugin_crm_pipedrive_deletions'); + + $builder->addId(); + $builder->addNamedField('objectType', 'string', 'object_type'); + $builder->addNamedField('integrationEntityId', 'integer', 'integration_entity_id'); + $builder->addNamedField('deletedDate', 'datetime', 'deleted_date'); + + $builder->addIndex(['deleted_date'], 'deleted_date'); + } + + /** + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getObjectType() + { + return $this->objectType; + } + + /** + * @param string $objectType + * + * @return self + */ + public function setObjectType($objectType) + { + $this->objectType = $objectType; + + return $this; + } + + /** + * @return \DateTime + */ + public function getDeletedDate() + { + $date = new DateTime(); + return $date->setTimestamp($this->deletedDate); + } + + /** + * @return self + */ + public function setDeletedDate(DateTime $deletedDate) + { + $this->deletedDate = $deletedDate->getTimestamp(); + + return $this; + } + + /** + * @return int + */ + public function getIntegrationEntityId() + { + return $this->integrationEntityId; + } + + /** + * @param int $integrationEntityId + * + * @return self + */ + public function setIntegrationEntityId($integrationEntityId) + { + $this->integrationEntityId = $integrationEntityId; + + return $this; + } +} diff --git a/plugins/MauticCrmBundle/EventListener/CompanySubscriber.php b/plugins/MauticCrmBundle/EventListener/CompanySubscriber.php index 38571d8cec6..50edf33152a 100644 --- a/plugins/MauticCrmBundle/EventListener/CompanySubscriber.php +++ b/plugins/MauticCrmBundle/EventListener/CompanySubscriber.php @@ -42,8 +42,9 @@ public function __construct(IntegrationHelper $integrationHelper, CompanyExport public static function getSubscribedEvents() { return [ - LeadEvents::COMPANY_POST_SAVE => ['onCompanyPostSave', 0], - LeadEvents::COMPANY_PRE_DELETE => ['onCompanyPreDelete', 10], + LeadEvents::COMPANY_POST_SAVE => ['onCompanyPostSave', 0], + LeadEvents::COMPANY_PRE_DELETE => ['onCompanyPreDelete', 10], + LeadEvents::COMPANY_POST_MERGE => ['onCompanyPostMerge', 255], ]; } @@ -60,11 +61,18 @@ public function onCompanyPostSave(Events\CompanyEvent $event) /** @var PipedriveIntegration $integrationObject */ $integrationObject = $this->integrationHelper->getIntegrationObject(PipedriveIntegration::INTEGRATION_NAME); - if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive()) { + + if (false === $integrationObject) { return; } $this->companyExport->setIntegration($integrationObject); + $operation = $this->companyExport->getOperation($company); + + if (!$integrationObject->shouldImportDataToPipedrive($operation)) { + return; + } + $this->companyExport->pushCompany($company); } @@ -81,11 +89,31 @@ public function onCompanyPreDelete(Events\CompanyEvent $event) /** @var PipedriveIntegration $integrationObject */ $integrationObject = $this->integrationHelper->getIntegrationObject(PipedriveIntegration::INTEGRATION_NAME); - if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive()) { + if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive('update')) { return; } $this->companyExport->setIntegration($integrationObject); $this->companyExport->delete($company); } + + public function OnCompanyPostMerge(Events\CompanyMergeEvent $event) + { + $company = $event->getVictor(); + + if ($company->getEventData('pipedrive.webhook')) { + return; + } + + $otherCompany = $event->getLoser(); + + /** @var PipedriveIntegration $integrationObject */ + $integrationObject = $this->integrationHelper->getIntegrationObject(PipedriveIntegration::INTEGRATION_NAME); + if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive()) { + return; + } + + $this->companyExport->setIntegration($integrationObject); + $this->companyExport->merge($company, $otherCompany); + } } diff --git a/plugins/MauticCrmBundle/EventListener/LeadSubscriber.php b/plugins/MauticCrmBundle/EventListener/LeadSubscriber.php index 4f02c29dbad..8721e1c652c 100644 --- a/plugins/MauticCrmBundle/EventListener/LeadSubscriber.php +++ b/plugins/MauticCrmBundle/EventListener/LeadSubscriber.php @@ -42,9 +42,10 @@ public function __construct(IntegrationHelper $integrationHelper, LeadExport $le public static function getSubscribedEvents() { return [ - LeadEvents::LEAD_POST_SAVE => ['onLeadPostSave', 0], - LeadEvents::LEAD_PRE_DELETE => ['onLeadPostDelete', 255], - LeadEvents::LEAD_COMPANY_CHANGE => ['onLeadCompanyChange', 0], + LeadEvents::LEAD_POST_SAVE => ['onLeadPostSave', 0], + LeadEvents::LEAD_PRE_DELETE => ['onLeadPostDelete', 255], + LeadEvents::LEAD_COMPANY_CHANGE => ['onLeadCompanyChange', 0], + LeadEvents::LEAD_POST_MERGE => ['onLeadPostMerge', 255], ]; } @@ -61,10 +62,11 @@ public function onLeadPostSave(Events\LeadEvent $event) } /** @var PipedriveIntegration $integrationObject */ $integrationObject = $this->integrationHelper->getIntegrationObject(PipedriveIntegration::INTEGRATION_NAME); - if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive()) { + $this->leadExport->setIntegration($integrationObject); + $operation = $this->leadExport->getOperation($lead); + if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive($operation)) { return; } - $this->leadExport->setIntegration($integrationObject); $changes = $lead->getChanges(true); if (!empty($changes['dateIdentified'])) { @@ -84,13 +86,33 @@ public function onLeadPostDelete(Events\LeadEvent $event) /** @var PipedriveIntegration $integrationObject */ $integrationObject = $this->integrationHelper->getIntegrationObject(PipedriveIntegration::INTEGRATION_NAME); - if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive()) { + if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive('update')) { return; } $this->leadExport->setIntegration($integrationObject); $this->leadExport->delete($lead); } + public function OnLeadPostMerge(Events\LeadMergeEvent $event) + { + $lead = $event->getVictor(); + + if ($lead->getEventData('pipedrive.webhook')) { + return; + } + + $otherLead = $event->getLoser(); + + /** @var PipedriveIntegration $integrationObject */ + $integrationObject = $this->integrationHelper->getIntegrationObject(PipedriveIntegration::INTEGRATION_NAME); + if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive()) { + return; + } + + $this->leadExport->setIntegration($integrationObject); + $this->leadExport->merge($lead, $otherLead); + } + public function onLeadCompanyChange(Events\LeadChangeCompanyEvent $event) { $lead = $event->getLead(); @@ -101,7 +123,7 @@ public function onLeadCompanyChange(Events\LeadChangeCompanyEvent $event) /** @var PipedriveIntegration $integrationObject */ $integrationObject = $this->integrationHelper->getIntegrationObject(PipedriveIntegration::INTEGRATION_NAME); - if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive()) { + if (false === $integrationObject || !$integrationObject->shouldImportDataToPipedrive('update')) { return; } $this->leadExport->setIntegration($integrationObject); diff --git a/plugins/MauticCrmBundle/Integration/Pipedrive/Export/CompanyExport.php b/plugins/MauticCrmBundle/Integration/Pipedrive/Export/CompanyExport.php index cdfa8773993..702695e7f22 100644 --- a/plugins/MauticCrmBundle/Integration/Pipedrive/Export/CompanyExport.php +++ b/plugins/MauticCrmBundle/Integration/Pipedrive/Export/CompanyExport.php @@ -120,6 +120,43 @@ public function delete(Company $company) return false; } + /** + * @return bool + */ + public function merge(Company $company, Company $otherCompany, array $mappedData = []) + { + $integrationEntity = $this->getCompanyIntegrationEntity(['internalEntityId' => $company->getId()]); + $created = false; + + if (!$integrationEntity) { + $created = $this->create($company, $mappedData); + $integrationEntity = $this->getCompanyIntegrationEntity(['internalEntityId' => $company->getId()]); + } + + if (!$integrationEntity) { + return false; + } + + $otherIntegrationEntity = $this->getCompanyIntegrationEntity(['internalEntityId' => $otherCompany->getId()]); + + if (!$otherIntegrationEntity) { + return $created ? true : $this->update($integrationEntity, $mappedData); + } + + try { + $this->getIntegration()->getApiHelper()->mergeCompany($integrationEntity->getIntegrationEntityId(), $otherIntegrationEntity->getIntegrationEntityId()); + + $this->em->remove($otherIntegrationEntity); + $this->em->flush(); + + return true; + } catch (\Exception $e) { + $this->getIntegration()->logIntegrationError($e); + } + + return false; + } + /** * @return array */ @@ -169,4 +206,15 @@ private function getCompanyIntegrationOwnerId(Company $company) return $pipedriveOwner->getOwnerId(); } + + public function getOperation(Company $company) + { + if (!$this->getIntegration()->isCompanySupportEnabled()) { + return false; //feature disabled + } + + $integrationEntity = $this->getCompanyIntegrationEntity(['internalEntityId' => $company->getId()]); + + return ($integrationEntity) ? 'update' : 'create'; + } } diff --git a/plugins/MauticCrmBundle/Integration/Pipedrive/Export/LeadExport.php b/plugins/MauticCrmBundle/Integration/Pipedrive/Export/LeadExport.php index b0cf0f9f95d..b361a224381 100644 --- a/plugins/MauticCrmBundle/Integration/Pipedrive/Export/LeadExport.php +++ b/plugins/MauticCrmBundle/Integration/Pipedrive/Export/LeadExport.php @@ -127,6 +127,44 @@ public function delete(Lead $lead) return false; } + /** + * @return bool + */ + public function merge(Lead $lead, Lead $otherLead) + { + $integrationEntity = $this->getLeadIntegrationEntity(['internalEntityId' => $lead->getId()]); + + $created = false; + + if (!$integrationEntity) { + $created = $this->create($lead); + $integrationEntity = $this->getLeadIntegrationEntity(['internalEntityId' => $lead->getId()]); + } + + if (!$integrationEntity) { + return false; + } + + $otherIntegrationEntity = $this->getLeadIntegrationEntity(['internalEntityId' => $otherLead->getId()]); + + if (!$otherIntegrationEntity) { + return $created ? true : $this->update($lead); + } + + try { + $this->getIntegration()->getApiHelper()->mergeLead($integrationEntity->getIntegrationEntityId(), $otherIntegrationEntity->getIntegrationEntityId()); + + $this->em->remove($otherIntegrationEntity); + $this->em->flush(); + + return true; + } catch (\Exception $e) { + $this->getIntegration()->logIntegrationError($e); + } + + return false; + } + /** * @return mixed */ @@ -213,4 +251,19 @@ private function convertMauticData($data) return $data; } + + public function getOperation(Lead $lead) + { + // stop for anynomouse + if ($lead->isAnonymous() || empty($lead->getEmail())) { + return false; + } + + $leadId = $lead->getId(); + /** @var IntegrationEntity $integrationEntity */ + $integrationEntity = $this->getLeadIntegrationEntity(['internalEntityId' => $leadId]); + $personData = $this->getIntegration()->getApiHelper()->findByEmail($lead->getEmail()); + + return ($integrationEntity) && !empty($personData) ? 'update' : 'create'; + } } diff --git a/plugins/MauticCrmBundle/Integration/Pipedrive/Import/CompanyImport.php b/plugins/MauticCrmBundle/Integration/Pipedrive/Import/CompanyImport.php index 0a3d296e528..6b2fab5c5ba 100644 --- a/plugins/MauticCrmBundle/Integration/Pipedrive/Import/CompanyImport.php +++ b/plugins/MauticCrmBundle/Integration/Pipedrive/Import/CompanyImport.php @@ -6,6 +6,7 @@ use Mautic\LeadBundle\Entity\Company; use Mautic\LeadBundle\Helper\IdentifyCompanyHelper; use Mautic\LeadBundle\Model\CompanyModel; +use MauticPlugin\MauticCrmBundle\Entity\PipedriveDeletion; use Symfony\Component\HttpFoundation\Response; class CompanyImport extends AbstractImport @@ -131,20 +132,79 @@ public function delete(array $data = []) throw new \Exception('Company doesn\'t have integration', Response::HTTP_NOT_FOUND); } - /** @var Company $company */ - $company = $this->em->getRepository(Company::class)->findOneById($integrationEntity->getInternalEntityId()); + $integrationSettings = $this->getIntegration()->getIntegrationSettings(); + $deleteViaCron = ($integrationSettings->getIsPublished() && !empty($integrationSettings->getFeatureSettings()['cronDelete'])); + + if ($deleteViaCron) { + $deletion = new PipedriveDeletion(); + $deletion + ->setObjectType('company') + ->setDeletedDate(new \DateTime()) + ->setIntegrationEntityId($integrationEntity->getId()); + + $this->em->persist($deletion); + $this->em->flush(); + } else { + /** @var Company $company */ + $company = $this->em->getRepository(Company::class)->findOneById($integrationEntity->getInternalEntityId()); + + if (!$company) { + throw new \Exception('Company doesn\'t exist', Response::HTTP_NOT_FOUND); + } + + // prevent listeners from exporting + $company->setEventData('pipedrive.webhook', 1); + $this->companyModel->deleteEntity($company); + + if (!empty($company->deletedId)) { + $this->em->remove($integrationEntity); + } + } + } - if (!$company) { - throw new \Exception('Company doesn\'t exists', Response::HTTP_NOT_FOUND); + /** + * @return bool + * + * @throws \Doctrine\ORM\OptimisticLockException + * @throws \Exception + */ + public function merge(array $data = [], $otherId = null) + { + if (!$this->getIntegration()->isCompanySupportEnabled()) { + return false; //feature disabled } + $otherIntegrationEntity = $this->getCompanyIntegrationEntity(['integrationEntityId' => $otherId]); + + if (!$otherIntegrationEntity) { + // Only destination entity exists, so handle it as an update. + return $this->update($data); + } + + $integrationEntity = $this->getCompanyIntegrationEntity(['integrationEntityId' => $data['id']]); + + if (!$integrationEntity) { + // Destination entity doesn't yet exist, so create it first. + $this->create($data); + $integrationEntity = $this->getCompanyIntegrationEntity(['integrationEntityId' => $data['id']]); + } + + /** @var Company $company */ + $company = $this->companyModel->getEntity($integrationEntity->getInternalEntityId()); + /** @var Company $otherCompany */ + $otherCompany = $this->companyModel->getEntity($otherIntegrationEntity->getInternalEntityId()); + // prevent listeners from exporting $company->setEventData('pipedrive.webhook', 1); - $this->companyModel->deleteEntity($company); - if (!empty($company->deletedId)) { - $this->em->remove($integrationEntity); - } + $this->companyModel->companyMerge($company, $otherCompany); + $this->em->remove($otherIntegrationEntity); + + $integrationEntity->setLastSyncDate(new \DateTime()); + $this->em->persist($integrationEntity); + $this->em->flush(); + + return true; } /** diff --git a/plugins/MauticCrmBundle/Integration/Pipedrive/Import/LeadImport.php b/plugins/MauticCrmBundle/Integration/Pipedrive/Import/LeadImport.php index fcec8a16791..c01df1d5529 100644 --- a/plugins/MauticCrmBundle/Integration/Pipedrive/Import/LeadImport.php +++ b/plugins/MauticCrmBundle/Integration/Pipedrive/Import/LeadImport.php @@ -3,10 +3,13 @@ namespace MauticPlugin\MauticCrmBundle\Integration\Pipedrive\Import; use Doctrine\ORM\EntityManager; +use Mautic\LeadBundle\Deduplicate\ContactMerger; +use Mautic\LeadBundle\Deduplicate\Exception\SameContactException; use Mautic\LeadBundle\Entity\Company; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\CompanyModel; use Mautic\LeadBundle\Model\LeadModel; +use MauticPlugin\MauticCrmBundle\Entity\PipedriveDeletion; use Symfony\Component\HttpFoundation\Response; class LeadImport extends AbstractImport @@ -21,15 +24,21 @@ class LeadImport extends AbstractImport */ private $companyModel; + /** + * @var ContactMerger + */ + private $contactMerger; + /** * LeadImport constructor. */ - public function __construct(EntityManager $em, LeadModel $leadModel, CompanyModel $companyModel) + public function __construct(EntityManager $em, LeadModel $leadModel, CompanyModel $companyModel, ContactMerger $contactMerger) { parent::__construct($em); - $this->leadModel = $leadModel; - $this->companyModel = $companyModel; + $this->leadModel = $leadModel; + $this->companyModel = $companyModel; + $this->contactMerger = $contactMerger; } /** @@ -148,21 +157,82 @@ public function delete(array $data = []) throw new \Exception('Lead doesn\'t have integration', Response::HTTP_NOT_FOUND); } - /** @var Lead $lead */ - $lead = $this->em->getRepository(Lead::class)->findOneById($integrationEntity->getInternalEntityId()); + $integrationSettings = $this->getIntegration()->getIntegrationSettings(); + $deleteViaCron = ($integrationSettings->getIsPublished() && !empty($integrationSettings->getFeatureSettings()['cronDelete'])); + + if ($deleteViaCron) { + $deletion = new PipedriveDeletion(); + $deletion + ->setObjectType('lead') + ->setDeletedDate(new \DateTime()) + ->setIntegrationEntityId($integrationEntity->getId()); + + $this->em->persist($deletion); + $this->em->flush(); + } else { + /** @var Lead $lead */ + $lead = $this->em->getRepository(Lead::class)->findOneById($integrationEntity->getInternalEntityId()); + + if (!$lead) { + throw new \Exception('Lead doesn\'t exists in Mautic', Response::HTTP_NOT_FOUND); + } + + // prevent listeners from exporting + $lead->setEventData('pipedrive.webhook', 1); + + $this->leadModel->deleteEntity($lead); - if (!$lead) { - throw new \Exception('Lead doesn\'t exists in Mautic', Response::HTTP_NOT_FOUND); + if (!empty($lead->deletedId)) { + $this->em->remove($integrationEntity); + } } + } + + /** + * @return bool + * + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\OptimisticLockException + * @throws \Exception + */ + public function merge(array $data = [], $otherId = null) + { + $otherIntegrationEntity = $this->getLeadIntegrationEntity(['integrationEntityId' => $otherId]); + + if (!$otherIntegrationEntity) { + // Only destination entity exists, so handle it as an update. + return $this->update($data); + } + + $integrationEntity = $this->getLeadIntegrationEntity(['integrationEntityId' => $data['id']]); + + if (!$integrationEntity) { + // Destination entity doesn't yet exist, so create it first. + $this->create($data); + $integrationEntity = $this->getLeadIntegrationEntity(['integrationEntityId' => $data['id']]); + } + + /** @var Lead $lead */ + $lead = $this->leadModel->getEntity($integrationEntity->getInternalEntityId()); + /** @var Lead $otherLead */ + $otherLead = $this->leadModel->getEntity($otherIntegrationEntity->getInternalEntityId()); // prevent listeners from exporting $lead->setEventData('pipedrive.webhook', 1); - $this->leadModel->deleteEntity($lead); - - if (!empty($lead->deletedId)) { - $this->em->remove($integrationEntity); + try { + $lead = $this->contactMerger->merge($lead, $otherLead); + $this->update($data); + $this->em->remove($otherIntegrationEntity); + } catch (SameContactException $exception) { + // Ignore } + + $integrationEntity->setLastSyncDate(new \DateTime()); + $this->em->persist($integrationEntity); + $this->em->flush(); + + return true; } /** diff --git a/plugins/MauticCrmBundle/Integration/PipedriveIntegration.php b/plugins/MauticCrmBundle/Integration/PipedriveIntegration.php index 1e6d5e2a71d..8b086408dc1 100644 --- a/plugins/MauticCrmBundle/Integration/PipedriveIntegration.php +++ b/plugins/MauticCrmBundle/Integration/PipedriveIntegration.php @@ -310,7 +310,8 @@ public function appendToForm(&$builder, $data, $formArea) ChoiceType::class, [ 'choices' => [ - 'mautic.pipedrive.add.edit.contact.import.enabled' => 'enabled', + 'mautic.pipedrive.add.edit.contact.import.create' => 'create', + 'mautic.pipedrive.add.edit.contact.import.update' => 'update', ], 'expanded' => true, 'multiple' => true, @@ -320,6 +321,22 @@ public function appendToForm(&$builder, $data, $formArea) 'required' => false, ] ); + + $builder->add( + 'cronDelete', + ChoiceType::class, + [ + 'choices' => [ + 'mautic.pipedrive.add.edit.contact.cron_delete.enabled' => 'enabled', + ], + 'expanded' => true, + 'multiple' => true, + 'label' => 'mautic.pipedrive.add.edit.contact.cron_delete', + 'label_attr' => ['class' => ''], + 'placeholder' => false, + 'required' => false, + ] + ); } } @@ -375,13 +392,12 @@ public function isCompanySupportEnabled() /** * @return bool */ - public function shouldImportDataToPipedrive() + public function shouldImportDataToPipedrive($operation = 'create') { - if (!$this->getIntegrationSettings()->getIsPublished() || empty($this->getIntegrationSettings()->getFeatureSettings()['import'])) { - return false; - } + $settings = $this->getIntegrationSettings(); + $features = $settings->getFeatureSettings(); - return true; + return $settings->getIsPublished() && !empty($features['import']) && in_array($operation, $features['import'], true); } /** diff --git a/plugins/MauticCrmBundle/MauticCrmBundle.php b/plugins/MauticCrmBundle/MauticCrmBundle.php index 20f43b1a4ce..2ea7c2f80f1 100644 --- a/plugins/MauticCrmBundle/MauticCrmBundle.php +++ b/plugins/MauticCrmBundle/MauticCrmBundle.php @@ -11,6 +11,7 @@ namespace MauticPlugin\MauticCrmBundle; +use Doctrine\DBAL\Schema\Schema; use Doctrine\ORM\EntityManager; use Mautic\CoreBundle\Factory\MauticFactory; use Mautic\PluginBundle\Bundle\PluginBundleBase; @@ -32,13 +33,18 @@ public static function onPluginInstall(Plugin $plugin, MauticFactory $factory, $ } } + public static function onPluginUpdate(Plugin $plugin, MauticFactory $factory, $metadata = null, Schema $installedSchema = null) + { + self::updatePluginSchema(self::getMetadata($factory->getEntityManager(), true), $installedSchema, $factory); + } + /** * Fix: plugin installer doesn't find metadata entities for the plugin * PluginBundle/Controller/PluginController:410. * * @return array|null */ - private static function getMetadata(EntityManager $em) + private static function getMetadata(EntityManager $em, $update = false) { $allMetadata = $em->getMetadataFactory()->getAllMetadata(); $currentSchema = $em->getConnection()->getSchemaManager()->createSchema(); @@ -53,7 +59,7 @@ private static function getMetadata(EntityManager $em) $table = $meta->getTableName(); - if ($currentSchema->hasTable($table)) { + if (!$update && $currentSchema->hasTable($table)) { continue; } diff --git a/plugins/MauticCrmBundle/Translations/en_US/messages.ini b/plugins/MauticCrmBundle/Translations/en_US/messages.ini index 17f4ec4433d..eedc37dd273 100644 --- a/plugins/MauticCrmBundle/Translations/en_US/messages.ini +++ b/plugins/MauticCrmBundle/Translations/en_US/messages.ini @@ -78,7 +78,10 @@ mautic.pipedrive.webhook_user="Webhook user" mautic.pipedrive.webhook_password="Webhook password" mautic.pipedrive.webhook_callback="Pipedrive webhook URL: " mautic.pipedrive.add.edit.contact.import="Import contacts to Pipedrive immediately on change in Mautic" -mautic.pipedrive.add.edit.contact.import.enabled="Enabled" +mautic.pipedrive.add.edit.contact.import.create="Create immediately on create in Mautic" +mautic.pipedrive.add.edit.contact.import.update="Update immediately on update in Mautic" +mautic.pipedrive.add.edit.contact.cron_delete="Handle deletions from Pipedrive via cron" +mautic.pipedrive.add.edit.contact.cron_delete.enabled="Enabled" mautic.crm.form.objects_to_pull_from="Choose what %crm% Objects to pull data from" mautic.zoho.object.lead="Leads" mautic.zoho.object.contact="Contacts" @@ -97,4 +100,4 @@ mautic.plugin.integration.campaigns.connectwise.activity.type="Activity Type" mautic.plugin.integration.campaigns.connectwise.members="Assign to member" mautic.plugin.config.push.activities="Push contact activities" mautic.plugin.config.integration.restart="Restart integration" -mautic.plugin.config.integration.restarted="%integration% restarted" \ No newline at end of file +mautic.plugin.config.integration.restarted="%integration% restarted"