diff --git a/Config/config.php b/Config/config.php index d0645cf..4dc0702 100644 --- a/Config/config.php +++ b/Config/config.php @@ -115,6 +115,15 @@ 'mautic.contactsource.dashboard.subscriber' => [ 'class' => 'MauticPlugin\MauticContactSourceBundle\EventListener\DashboardSubscriber', ], + 'mautic.contactsource.subscriber.batch' => [ + 'class' => 'MauticPlugin\MauticContactSourceBundle\EventListener\BatchSubscriber', + 'arguments' => [ + 'mautic.contactsource.model.contactsource', + 'mautic.campaign.model.campaign', + 'mautic.contactsource.model.api', + 'mautic.lead.model.import', + ], + ], ], 'forms' => [ 'mautic.contactsource.form.type.contactsourceshow_list' => [ @@ -197,6 +206,18 @@ 'class' => 'MauticPlugin\MauticContactSourceBundle\Helper\JSONHelper', ], ], + 'extension' => [ + 'mautic.contactsource.extension.lead_import' => [ + 'class' => 'MauticPlugin\MauticContactSourceBundle\Form\Extension\LeadImportExtension', + 'arguments' => [ + 'doctrine.orm.entity_manager', + ], + 'tag' => 'form.type_extension', + 'tagArguments' => [ + 'extended_type' => 'Mautic\LeadBundle\Form\Type\LeadImportType', + ], + ], + ], ], 'menu' => [ diff --git a/EventListener/BatchSubscriber.php b/EventListener/BatchSubscriber.php new file mode 100644 index 0000000..d418b3a --- /dev/null +++ b/EventListener/BatchSubscriber.php @@ -0,0 +1,127 @@ +sourceModel = $sourceModel; + $this->campaignModel = $campaignModel; + $this->apiModel = $apiModel; + $this->importModel = $importModel; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [LeadEvents::LEAD_PRE_SAVE => 'onLeadPreSave', + LeadEvents::LEAD_POST_SAVE => 'onLeadPostSave', ]; + } + + public function onLeadPreSave(LeadEvent $leadEvent) + { + if (true == $leadEvent->getLead()->imported + && $this->em->getUnitOfWork()->getIdentityMap()['Mautic\LeadBundle\Entity\Import']) { + $this->apiModel->setImported(true); + + $import = array_shift($this->em->getUnitOfWork()->getIdentityMap()['Mautic\LeadBundle\Entity\Import']); + $importProperties = $import->getProperties(); + + $campaignId = $importProperties['parser']['campaign']; + $sourceId = $importProperties['parser']['source']; + + $this->apiModel->contact = $leadEvent->getLead(); + $this->apiModel->setCampaignId($campaignId); + $this->apiModel->setCampaign($this->campaignModel->getEntity($campaignId)); + $this->apiModel->setSourceId($sourceId); + $this->apiModel->setContactSource($this->apiModel->getContactSourceModel()->getEntity($sourceId)); + + $this->apiModel->parseSourceCampaignSettings(); + $this->apiModel->processOffline(); + $this->apiModel->applyAttribution(); + + $leadEvent->setLead($this->apiModel->contact); + } + } + + public function onLeadPostSave(LeadEvent $leadEvent) + { + if (true == $leadEvent->getLead()->imported + && $this->em->getUnitOfWork()->getIdentityMap()['Mautic\LeadBundle\Entity\Import']) { + $this->apiModel->setImported(true); + + $import = array_shift($this->em->getUnitOfWork()->getIdentityMap()['Mautic\LeadBundle\Entity\Import']); + $importProperties = $import->getProperties(); + + $campaignId = $importProperties['parser']['campaign']; + $sourceId = $importProperties['parser']['source']; + + $this->apiModel->setContact($leadEvent->getLead()); + $this->apiModel->setCampaignId($campaignId); + $this->apiModel->setCampaign($this->campaignModel->getEntity($campaignId)); + $this->apiModel->setSourceId($sourceId); + $this->apiModel->setContactSource($this->apiModel->getContactSourceModel()->getEntity($sourceId)); + $this->apiModel->parseSourceCampaignSettings(); + + $this->apiModel->addContactsToCampaign($this->apiModel->getCampaign(), [$leadEvent->getLead()], true); + $this->apiModel->logResults(); + + $leadEvent->setLead($this->apiModel->getContact()); + } + } +} diff --git a/Form/Extension/LeadImportExtension.php b/Form/Extension/LeadImportExtension.php new file mode 100644 index 0000000..6103456 --- /dev/null +++ b/Form/Extension/LeadImportExtension.php @@ -0,0 +1,100 @@ +em = $em; + } + + /** + * Returns the name of the type being extended. + * + * @return string The name of the type being extended + */ + public function getExtendedType() + { + return LeadImportType::class; + } + + /** + * Add a custom 'object' type to write to a corresponding table for that new custom value. + * + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add( + 'source', + EntityType::class, + [ + 'class' => 'MauticContactSourceBundle:ContactSource', + 'empty_value' => 'Select A Source', + 'choice_label' => function ($source) { + return $source->getName(); + }, + ] + ); + + $builder->get('source') + ->addModelTransformer(new CallbackTransformer( + function ($source) { + // transform the ID back to an Object + return $source ? $this->em->getRepository('MauticCampaignBundle:Event')->find($source) : null; + }, + function ($source) { + // transform the object to a ID + return $source ? $source->getID() : null; + } + )); + + $builder->add( + 'campaign', + EntityType::class, + [ + 'class' => 'MauticCampaignBundle:Campaign', + 'empty_value' => 'Select A Campaign', + 'choice_label' => function ($campaign) { + return $campaign->getName(); + }, + ] + ); + + $builder->get('campaign') + ->addModelTransformer(new CallbackTransformer( + function ($campaign) { + // transform the ID back to an Object + return $campaign ? $this->em->getRepository('MauticCampaignBundle:Campaign')->find($campaign) : null; + }, + function ($campaign) { + // transform the object to a ID + return $campaign ? $campaign->getID() : null; + } + )); + } +} diff --git a/Model/Api.php b/Model/Api.php index 760ce48..66d3e8a 100644 --- a/Model/Api.php +++ b/Model/Api.php @@ -173,6 +173,9 @@ class Api /** @var bool */ protected $authenticated = false; + /** @var bool */ + protected $imported = false; + /** * Api constructor. * @@ -247,6 +250,30 @@ public function setSourceId($sourceId = null) return $this; } + /** + * @param ContactSource $contactSource + * + * @return $this + */ + public function setContactSource($contactSource = null) + { + $this->contactSource = $contactSource; + + return $this; + } + + /** + * @param Contact $contact + * + * @return $this + */ + public function setContact($contact = null) + { + $this->contact = $contact; + + return $this; + } + /** * @param int $campaignId * @@ -259,6 +286,18 @@ public function setCampaignId($campaignId = null) return $this; } + /** + * @param Campaign $campaign + * + * @return $this + */ + public function setCampaign($campaign = null) + { + $this->campaign = $campaign; + + return $this; + } + /** * Parse and validate for public access, without field and token validation. * @@ -289,12 +328,12 @@ private function parseSourceId() $this->sourceId = intval($this->request->get('sourceId')); if (!$this->sourceId) { throw new ContactSourceException( - 'The sourceId was not supplied. Please provide your sourceId.', - Codes::HTTP_BAD_REQUEST, - null, - Stat::TYPE_INVALID, - 'sourceId' - ); + 'The sourceId was not supplied. Please provide your sourceId.', + Codes::HTTP_BAD_REQUEST, + null, + Stat::TYPE_INVALID, + 'sourceId' + ); } } @@ -313,20 +352,20 @@ private function parseSource() $this->contactSource = $this->contactSourceModel->getEntity($this->sourceId); if (!$this->contactSource) { throw new ContactSourceException( - 'The sourceId specified does not exist.', - Codes::HTTP_NOT_FOUND, - null, - Stat::TYPE_INVALID, - 'sourceId' - ); + 'The sourceId specified does not exist.', + Codes::HTTP_NOT_FOUND, + null, + Stat::TYPE_INVALID, + 'sourceId' + ); } elseif (false === $this->contactSource->getIsPublished()) { throw new ContactSourceException( - 'The sourceId specified has been unpublished (deactivated).', - Codes::HTTP_GONE, - null, - Stat::TYPE_INVALID, - 'sourceId' - ); + 'The sourceId specified has been unpublished (deactivated).', + Codes::HTTP_GONE, + null, + Stat::TYPE_INVALID, + 'sourceId' + ); } $this->addTrace('contactSourceId', (int) $this->sourceId); } @@ -353,27 +392,30 @@ private function addTrace($parameter, $value) * @throws ContactSourceException * @throws \Exception */ - private function parseSourceCampaignSettings() + public function parseSourceCampaignSettings() { // Check that the campaign is in the whitelist for this source. $campaignSettings = $this->campaignSettingsModel->setContactSource($this->contactSource) - ->getCampaignSettingsById($this->campaignId); + ->getCampaignSettingsById($this->campaignId); // @todo - Support or thwart multiple copies of the same campaign, should it occur. In the meantime... $campaignSettings = reset($campaignSettings); - if (!$campaignSettings) { + if (!$campaignSettings && !$this->imported) { throw new ContactSourceException( - 'The campaignId supplied is not currently in the permitted list of campaigns for this source.', - Codes::HTTP_GONE, - null, - Stat::TYPE_INVALID, - 'campaignId' - ); + 'The campaignId supplied is not currently in the permitted list of campaigns for this source.', + Codes::HTTP_GONE, + null, + Stat::TYPE_INVALID, + 'campaignId' + ); + } + // Establish parameters from campaign settings; skip some settings on contact import from file. + if (!$this->imported) { + $this->realTime = (bool) isset($campaignSettings->realTime) && $campaignSettings->realTime; + $this->limits = isset($campaignSettings->limits) ? $campaignSettings->limits : []; + $this->scrubRate = isset($campaignSettings->scrubRate) ? intval($campaignSettings->scrubRate) : 0; } - // Establish parameters from campaign settings. - $this->realTime = (bool) isset($campaignSettings->realTime) && $campaignSettings->realTime; - $this->limits = isset($campaignSettings->limits) ? $campaignSettings->limits : []; - $this->scrubRate = isset($campaignSettings->scrubRate) ? intval($campaignSettings->scrubRate) : 0; + $this->cost = isset($campaignSettings->cost) ? (abs(floatval($campaignSettings->cost))) : 0; $this->utmSource = !empty($this->contactSource->getUtmSource()) ? $this->contactSource->getUtmSource() : null; // Apply field overrides @@ -400,20 +442,20 @@ private function parseCampaign() $this->campaign = $this->campaignModel->getEntity($this->campaignId); if (!$this->campaign) { throw new ContactSourceException( - 'The campaignId specified does not exist.', - Codes::HTTP_GONE, - null, - Stat::TYPE_INVALID, - 'campaignId' - ); + 'The campaignId specified does not exist.', + Codes::HTTP_GONE, + null, + Stat::TYPE_INVALID, + 'campaignId' + ); } elseif (false === $this->campaign->getIsPublished()) { throw new ContactSourceException( - 'The campaignId specified has been unpublished (deactivated).', - Codes::HTTP_GONE, - null, - Stat::TYPE_INVALID, - 'campaignId' - ); + 'The campaignId specified has been unpublished (deactivated).', + Codes::HTTP_GONE, + null, + Stat::TYPE_INVALID, + 'campaignId' + ); } $this->addTrace('contactSourceCampaignId', (int) $this->campaignId); } @@ -614,12 +656,12 @@ private function parseVerbosity() $this->setVerbose(true); } else { throw new ContactSourceException( - 'The verbose token passed was not correct. This field should only be used for debugging.', - Codes::HTTP_UNAUTHORIZED, - null, - Stat::TYPE_INVALID, - 'verbose' - ); + 'The verbose token passed was not correct. This field should only be used for debugging.', + Codes::HTTP_UNAUTHORIZED, + null, + Stat::TYPE_INVALID, + 'verbose' + ); } } } @@ -681,12 +723,12 @@ private function parseCampaignId() $this->campaignId = intval($this->request->get('campaignId')); if (!$this->campaignId) { throw new ContactSourceException( - 'The campaignId was not supplied. Please provide your campaignId.', - Codes::HTTP_BAD_REQUEST, - null, - Stat::TYPE_INVALID, - 'campaignId' - ); + 'The campaignId was not supplied. Please provide your campaignId.', + Codes::HTTP_BAD_REQUEST, + null, + Stat::TYPE_INVALID, + 'campaignId' + ); } } @@ -1176,11 +1218,18 @@ public function addContactsToCampaign( ) { foreach ($contacts as $contact) { $campaignContact = new CampaignContact(); - $campaignContact->setCampaign($campaign); - $campaignContact->setDateAdded(new \DateTime()); - $campaignContact->setLead($contact); - $campaignContact->setManuallyAdded($manuallyAdded); - $saved = $this->campaignModel->saveCampaignLead($campaignContact); + $alreadyExists = false; + if (!null == $contact->getDateModified()) { // see if New Contact b/c isNew() is unreliable on PostSave Event + $leadRepository = $this->em->getRepository('MauticCampaignBundle:Lead'); + $alreadyExists = $leadRepository->checkLeadInCampaigns($contact, ['campaigns' => [$campaign->getId()]]); + } + if (!$alreadyExists) { + $campaignContact->setCampaign($campaign); + $campaignContact->setDateAdded(new \DateTime()); + $campaignContact->setLead($contact); + $campaignContact->setManuallyAdded($manuallyAdded); + $saved = $this->campaignModel->saveCampaignLead($campaignContact); + } // @todo - Support non realtime event firing. // if (!$realTime) { @@ -1202,7 +1251,7 @@ public function addContactsToCampaign( /** * Assign the status for an offline (not real-time) contact acceptance/rejection. */ - private function processOffline() + public function processOffline() { if (!$this->realTime) { // Establish scrub now. @@ -1291,10 +1340,13 @@ private function processRealTime() * * @throws \Exception */ - private function applyAttribution() + public function applyAttribution() { if ($this->valid && $this->cost && Stat::TYPE_ACCEPTED === $this->status) { - $this->contact = $this->contactModel->getEntity($this->contact->getId()); + // check if an id exists and attribution field exists b/c sometimes its not a well-formed entity + if ($this->contact->getId() && !property_exists($this->contact, 'attribution')) { + $this->contact = $this->contactModel->getEntity($this->contact->getId()); + } $originalAttribution = $this->contact->getAttribution(); // Attribution is always a negative number to represent cost. $this->attribution = ($this->cost * -1); @@ -1304,7 +1356,11 @@ private function applyAttribution() $originalAttribution ); $this->dispatchContextCreate(); - $this->contactModel->saveEntity($this->contact); + + if (!$this->imported) { + // contacts imported via file upload hit this method via a pre-save event so dont save here. + $this->contactModel->saveEntity($this->contact); + } } } @@ -1317,15 +1373,15 @@ private function createCache() { if ($this->valid && $this->contact->getId()) { $this->getCacheModel()->setContact($this->contact) - ->setContactSource($this->contactSource) - ->create($this->campaignId); + ->setContactSource($this->contactSource) + ->create($this->campaignId); } } /** * Use LeadTimelineEvent. */ - private function logResults() + public function logResults() { if ($this->valid) { $statLevel = 'INFO'; @@ -1452,6 +1508,14 @@ private function saveSyncedData( return $newIntegrationEntity; } + /** + * @return Contact + */ + public function getContact() + { + return $this->contact; + } + /** * @return ContactSource */ @@ -1460,6 +1524,14 @@ public function getContactSource() return $this->contactSource; } + /** + * @return ContactSourceModel + */ + public function getContactSourceModel() + { + return $this->contactSourceModel; + } + /** * Get the result array of the import process. * @@ -1567,4 +1639,32 @@ public function getCampaignFields($asEntities = false) { return null; } + + /** + * @return Campaign + */ + public function getCampaign() + { + return $this->campaign; + } + + /** + * @return bool + */ + public function getImported() + { + return $this->imported; + } + + /** + * @param $imported + * + * @return $this + */ + public function setImported($imported) + { + $this->imported = $imported; + + return $this; + } } diff --git a/Model/Cache.php b/Model/Cache.php index ae48ab0..a7eac96 100644 --- a/Model/Cache.php +++ b/Model/Cache.php @@ -50,12 +50,12 @@ class Cache extends AbstractCommonModel /** * Cache constructor. * - * @param Translator $translator + * @param $translator * @param UtmSourceHelper $utmSourceHelper * @param CoreParametersHelper $coreParametersHelper */ public function __construct( - Translator $translator, + $translator, UtmSourceHelper $utmSourceHelper, CoreParametersHelper $coreParametersHelper ) {