diff --git a/app/bundles/CampaignBundle/Model/EventModel.php b/app/bundles/CampaignBundle/Model/EventModel.php index 3ed9f5e3e50..82b2de2e401 100644 --- a/app/bundles/CampaignBundle/Model/EventModel.php +++ b/app/bundles/CampaignBundle/Model/EventModel.php @@ -390,6 +390,7 @@ public function triggerEvent($type, $eventDetails = null, $channel = null, $chan } elseif ($child['decisionPath'] == 'no') { // non-action paths should not be processed by this because the contact already took action in order to get here $childrenTriggered = true; + $this->logger->debug('CAMPAIGN: '.ucfirst($child['eventType']).' ID# '.$child['id'].' has a decision path of no'); } else { $this->logger->debug('CAMPAIGN: '.ucfirst($child['eventType']).' ID# '.$child['id'].' is being processed'); } @@ -626,8 +627,9 @@ public function triggerStartingEvents( ++$totalEventCount; $event['campaign'] = [ - 'id' => $campaign->getId(), - 'name' => $campaign->getName(), + 'id' => $campaign->getId(), + 'name' => $campaign->getName(), + 'createdBy' => $campaign->getCreatedBy(), ]; $decisionEvent = [ @@ -769,8 +771,7 @@ public function triggerScheduledEvents( ) { defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); - $campaignId = $campaign->getId(); - $campaignName = $campaign->getName(); + $campaignId = $campaign->getId(); $this->logger->debug('CAMPAIGN: Triggering scheduled events'); @@ -903,8 +904,9 @@ public function triggerScheduledEvents( // Set campaign ID $event['campaign'] = [ - 'id' => $campaignId, - 'name' => $campaignName, + 'id' => $campaign->getId(), + 'name' => $campaign->getName(), + 'createdBy' => $campaign->getCreatedBy(), ]; // Execute event @@ -1011,8 +1013,6 @@ public function triggerNegativeEvents( $this->logger->debug('CAMPAIGN: Triggering negative events'); $campaignId = $campaign->getId(); - $campaignName = $campaign->getName(); - $repo = $this->getRepository(); $campaignRepo = $this->getCampaignRepository(); $logRepo = $this->getLeadEventLogRepository(); @@ -1262,8 +1262,9 @@ public function triggerNegativeEvents( // Set event $event = $events[$id]; $event['campaign'] = [ - 'id' => $campaignId, - 'name' => $campaignName, + 'id' => $campaign->getId(), + 'name' => $campaign->getName(), + 'createdBy' => $campaign->getCreatedBy(), ]; // Set lead in case this is triggered by the system @@ -1542,9 +1543,11 @@ public function executeEvent( } // Set campaign ID + $event['campaign'] = [ - 'id' => $campaign->getId(), - 'name' => $campaign->getName(), + 'id' => $campaign->getId(), + 'name' => $campaign->getName(), + 'createdBy' => $campaign->getCreatedBy(), ]; // Ensure properties is an array @@ -1702,31 +1705,7 @@ public function executeEvent( $repo->deleteEntity($log); } - // Notify the lead owner if there is one otherwise campaign creator that there was a failure - if (!$owner = $lead->getOwner()) { - $ownerId = $campaign->getCreatedBy(); - $owner = $this->userModel->getEntity($ownerId); - } - - if ($owner && $owner->getId()) { - $this->notificationModel->addNotification( - $campaign->getName().' / '.$event['name'], - 'error', - false, - $this->translator->trans( - 'mautic.campaign.event.failed', - [ - '%contact%' => ''.$lead->getPrimaryIdentifier().'', - ] - ), - null, - null, - $owner - ); - } + $this->notifyOfFailure($lead, $campaign->getCreatedBy(), $campaign->getName().' / '.$event['name']); $this->logger->debug($debug); } else { @@ -2092,6 +2071,40 @@ public function getEventLineChartData($unit, \DateTime $dateFrom, \DateTime $dat return $chart->render(); } + /** + * @param Lead $lead + * @param $campaignCreatedBy + * @param $header + */ + public function notifyOfFailure(Lead $lead, $campaignCreatedBy, $header) + { + // Notify the lead owner if there is one otherwise campaign creator that there was a failure + if (!$owner = $lead->getOwner()) { + $ownerId = (int) $campaignCreatedBy; + $owner = $this->userModel->getEntity($ownerId); + } + + if ($owner && $owner->getId()) { + $this->notificationModel->addNotification( + $header, + 'error', + false, + $this->translator->trans( + 'mautic.campaign.event.failed', + [ + '%contact%' => ''.$lead->getPrimaryIdentifier().'', + ] + ), + null, + null, + $owner + ); + } + } + /** * Handles condition type events. * diff --git a/app/bundles/CampaignBundle/Translations/en_US/messages.ini b/app/bundles/CampaignBundle/Translations/en_US/messages.ini index 19d854159fb..51eb31af5de 100644 --- a/app/bundles/CampaignBundle/Translations/en_US/messages.ini +++ b/app/bundles/CampaignBundle/Translations/en_US/messages.ini @@ -25,6 +25,7 @@ mautic.campaign.event.inline.triggerimmediately="immediately" mautic.campaign.event.inline.triggerinterval="+ %interval% %unit%" mautic.campaign.event.last_error="Last execution error" mautic.campaign.event.failed="Failed to execute campaign event for %contact%." +mautic.campaign.event.failed.timeline="Generic error." mautic.campaign.event.intervalunit.choice.d="day(s)" mautic.campaign.event.intervalunit.choice.h="hour(s)" mautic.campaign.event.intervalunit.choice.i="minute(s)" diff --git a/app/bundles/PluginBundle/Exception/ApiErrorException.php b/app/bundles/PluginBundle/Exception/ApiErrorException.php index eda5252ee9f..12e4f0445be 100644 --- a/app/bundles/PluginBundle/Exception/ApiErrorException.php +++ b/app/bundles/PluginBundle/Exception/ApiErrorException.php @@ -11,10 +11,69 @@ namespace Mautic\PluginBundle\Exception; +use Mautic\LeadBundle\Entity\Lead; + class ApiErrorException extends \Exception { + /** + * @var + */ + private $contactId; + + /** + * @var Lead + */ + private $contact; + + /** + * ApiErrorException constructor. + * + * @param string $message + * @param int $code + * @param \Exception|null $previous + */ public function __construct($message = 'API error', $code = 0, \Exception $previous = null) { parent::__construct($message, $code, $previous); } + + /** + * @return mixed + */ + public function getContactId() + { + return $this->contactId; + } + + /** + * @param mixed $contactId + * + * @return ApiErrorException + */ + public function setContactId($contactId) + { + $this->contactId = $contactId; + + return $this; + } + + /** + * @return Lead + */ + public function getContact() + { + return $this->contact; + } + + /** + * @param Lead $contact + * + * @return ApiErrorException + */ + public function setContact(Lead $contact) + { + $this->contact = $contact; + + return $this; + } } diff --git a/app/bundles/PluginBundle/Integration/AbstractIntegration.php b/app/bundles/PluginBundle/Integration/AbstractIntegration.php index 25b2af8599f..a7f1b4824b5 100644 --- a/app/bundles/PluginBundle/Integration/AbstractIntegration.php +++ b/app/bundles/PluginBundle/Integration/AbstractIntegration.php @@ -21,6 +21,7 @@ use Mautic\PluginBundle\Event\PluginIntegrationFormDisplayEvent; use Mautic\PluginBundle\Event\PluginIntegrationKeyEvent; use Mautic\PluginBundle\Event\PluginIntegrationRequestEvent; +use Mautic\PluginBundle\Exception\ApiErrorException; use Mautic\PluginBundle\Helper\oAuthHelper; use Mautic\PluginBundle\PluginEvents; use Symfony\Component\Form\FormBuilder; @@ -66,6 +67,18 @@ abstract class AbstractIntegration */ protected $em; + /** + * Used for notifications. + * + * @var array|null + */ + protected $adminUsers; + + /** + * @var + */ + protected $notifications = []; + /** * @param MauticFactory $factory * @@ -1806,17 +1819,84 @@ public function checkImageExists($url) return $retcode == 200; } + /** + * @return \Mautic\CoreBundle\Model\NotificationModel + */ + public function getNotificationModel() + { + return $this->factory->getModel('core.notification'); + } + /** * @param \Exception $e + * @param null $contact */ - public function logIntegrationError(\Exception $e) + public function logIntegrationError(\Exception $e, Lead $contact = null) { $logger = $this->factory->getLogger(); - if ('dev' == MAUTIC_ENV) { - $logger->addError('INTEGRATION ERROR: '.$this->getName().' - '.$e); - } else { - $logger->addError('INTEGRATION ERROR: '.$this->getName().' - '.$e->getMessage()); + + if ($e instanceof ApiErrorException) { + if (null === $this->adminUsers) { + $this->adminUsers = $this->em->getRepository('MauticUserBundle:User')->getEntities( + [ + 'filter' => [ + 'force' => [ + [ + 'column' => 'r.isAdmin', + 'expr' => 'eq', + 'value' => true, + ], + ], + ], + ] + ); + } + + $errorMessage = ('dev' == MAUTIC_ENV) ? (string) $e : $e->getMessage(); + $errorHeader = $this->getTranslator()->trans( + 'mautic.integration.error', + [ + '%name%' => $this->getName(), + ] + ); + + if ($contact || $contact = $e->getContact()) { + // Append a link to the contact + $contactId = $contact->getId(); + $contactName = $contact->getPrimaryIdentifier(); + } elseif ($contactId = $e->getContactId()) { + $contactName = $this->getTranslator()->trans('mautic.integration.error.generic_contact_name', ['%id%' => $contactId]); + } + + if ($contactId) { + $contactLink = $this->factory->getRouter()->generate('mautic_contact_action', [ + 'objectAction' => 'view', 'objectId' => $contactId, + ], + UrlGeneratorInterface::ABSOLUTE_URL + ); + $errorMessage .= ' '.$contactName.''; + } + + // Prevent a flood of the same messages + $messageHash = md5($errorMessage); + if (!array_key_exists($messageHash, $this->notifications)) { + foreach ($this->adminUsers as $user) { + $this->getNotificationModel()->addNotification( + $errorHeader, + $this->getName(), + false, + $errorMessage, + 'text-danger fa-exclamation-circle', + null, + $user + ); + } + + $this->notifications[$messageHash] = true; + } } + + $logger->addError('INTEGRATION ERROR: '.$this->getName().' - '.$errorMessage); } /** diff --git a/app/bundles/PluginBundle/Translations/en_US/messages.ini b/app/bundles/PluginBundle/Translations/en_US/messages.ini index 61fd34ba53b..c7a8527ba91 100644 --- a/app/bundles/PluginBundle/Translations/en_US/messages.ini +++ b/app/bundles/PluginBundle/Translations/en_US/messages.ini @@ -1,6 +1,8 @@ mautic.campaign.plugin.leadpush="Push contact" mautic.integration.callbackuri="If applicable, use the following as the callback URL (may also be called the return URI) when configuring your application:" mautic.integration.closewindow="Close Window" +mautic.integration.error="%name% Error" +mautic.integration.error.generic_contact_name="Contact ID# %id%" mautic.integration.error.refreshtoken_expired="The refresh token has expired. Re-authorization is required." mautic.integration.filter.all="Show all plugins" mautic.integration.form.authorize="Authorize App" diff --git a/plugins/MauticCrmBundle/Api/SalesforceApi.php b/plugins/MauticCrmBundle/Api/SalesforceApi.php index 67258a6d7af..a2a747d6a27 100644 --- a/plugins/MauticCrmBundle/Api/SalesforceApi.php +++ b/plugins/MauticCrmBundle/Api/SalesforceApi.php @@ -42,7 +42,6 @@ public function request($operation, $elementData = [], $method = 'GET', $retry = if (!$object) { $object = $this->object; } - $notificactionModel = $this->integration->getNotificationModel(); if (!$queryUrl) { $queryUrl = $this->integration->getApiUrl(); $requestUrl = sprintf($queryUrl.'/%s/%s', $object, $operation); @@ -53,7 +52,6 @@ public function request($operation, $elementData = [], $method = 'GET', $retry = $response = $this->integration->makeRequest($requestUrl, $elementData, $method, $this->requestSettings); if (!empty($response['errors'])) { - $notificactionModel->addNotification(implode(', ', $response['errors']), 'Salesforce', false, $this->integration->getName().':'); throw new ApiErrorException(implode(', ', $response['errors'])); } elseif (is_array($response)) { $errors = []; @@ -68,7 +66,6 @@ public function request($operation, $elementData = [], $method = 'GET', $retry = } } $errors[] = $r['message']; - $notificactionModel->addNotification($r['message'], 'Salesforce', false, $this->integration->getName().':'); } } diff --git a/plugins/MauticCrmBundle/Integration/SalesforceIntegration.php b/plugins/MauticCrmBundle/Integration/SalesforceIntegration.php index d5cb3586466..175446ee9e5 100644 --- a/plugins/MauticCrmBundle/Integration/SalesforceIntegration.php +++ b/plugins/MauticCrmBundle/Integration/SalesforceIntegration.php @@ -17,6 +17,7 @@ use Mautic\LeadBundle\Model\LeadModel; use Mautic\PluginBundle\Entity\IntegrationEntity; use Mautic\PluginBundle\Entity\IntegrationEntityRepository; +use Mautic\PluginBundle\Exception\ApiErrorException; use MauticPlugin\MauticCrmBundle\Api\SalesforceApi; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -573,18 +574,22 @@ public function pushLead($lead, $config = []) $fields = array_keys($config['leadFields']); - $leadFields = $this->cleanSalesForceData($config, $fields, $object); - $fieldsToUpdateInSf = isset($config['update_mautic']) ? array_keys($config['update_mautic'], 1) : []; - $leadFields = array_diff_key($leadFields, array_flip($fieldsToUpdateInSf)); - - $mappedData[$object] = $this->populateLeadData($lead, ['leadFields' => $leadFields, 'object' => $object, 'feature_settings' => ['objects' => $config['objects']]]); + $leadFields = $this->cleanSalesForceData($config, $fields, $object); + $fieldsToUpdateInSf = isset($config['update_mautic']) ? array_keys($config['update_mautic'], 1) : []; + $leadFields = array_diff_key($leadFields, array_flip($fieldsToUpdateInSf)); + $mappedData[$object] = $this->populateLeadData( + $lead, + ['leadFields' => $leadFields, 'object' => $object, 'feature_settings' => ['objects' => $config['objects']]] + ); $this->amendLeadDataBeforePush($mappedData[$object]); if (isset($config['objects']) && array_search('Contact', $config['objects'])) { $contactFields = $this->cleanSalesForceData($config, $fields, 'Contact'); - $mappedData['Contact'] = $this->populateLeadData($lead, ['leadFields' => $contactFields, 'object' => 'Contact', 'feature_settings' => ['objects' => $config['objects']]]); + $mappedData['Contact'] = $this->populateLeadData( + $lead, + ['leadFields' => $contactFields, 'object' => 'Contact', 'feature_settings' => ['objects' => $config['objects']]] + ); $this->amendLeadDataBeforePush($mappedData['Contact']); - $object = 'Contact'; } if (empty($mappedData)) { return false; @@ -620,6 +625,10 @@ public function pushLead($lead, $config = []) return true; } } catch (\Exception $e) { + if ($e instanceof ApiErrorException) { + $e->setContact($lead); + } + $this->logIntegrationError($e); } @@ -1220,6 +1229,7 @@ protected function processCompositeResponse($response, array $salesforceIdMappin $created = 0; $updated = 0; $reference = ''; + $leadLink = ''; if (is_array($response)) { $persistEntities = []; foreach ($response as $item) { @@ -1240,14 +1250,17 @@ protected function processCompositeResponse($response, array $salesforceIdMappin $object = 'CampaignMember'; } if (isset($item['body'][0]['errorCode'])) { - $this->logIntegrationError(new \Exception($item['body'][0]['message'].'-'.$item['referenceId'])); + $exception = new ApiErrorException($item['body'][0]['message']); + if ($object == 'Contact' || $object = 'Lead') { + $exception->setContactId($contactId); + } + $this->logIntegrationError($exception); if ($integrationEntityId && $object !== 'CampaignMember') { $integrationEntity = $this->em->getReference('MauticPluginBundle:IntegrationEntity', $integrationEntityId); $integrationEntity->setLastSyncDate(new \DateTime()); - $persistEntities[] = $integrationEntity; - } elseif ($campaignId) { + } elseif (isset($campaignId) and $campaignId != null) { $integrationEntity = $this->em->getReference('MauticPluginBundle:IntegrationEntity', $campaignId); $integrationEntity->setLastSyncDate(new \DateTime()); @@ -1288,7 +1301,11 @@ protected function processCompositeResponse($response, array $salesforceIdMappin // Record was updated if ($integrationEntityId) { $integrationEntity = $this->em->getReference('MauticPluginBundle:IntegrationEntity', $integrationEntityId); - $integrationEntity->setLastSyncDate(new \DateTime()); + if (is_object($integrationEntity)) { + $integrationEntity->setLastSyncDate(new \DateTime()); + } else { + unset($integrationEntity); + } } else { // Found in Salesforce so create a new record for it $integrationEntity = new IntegrationEntity(); @@ -1300,8 +1317,9 @@ protected function processCompositeResponse($response, array $salesforceIdMappin $integrationEntity->setInternalEntity('lead'); $integrationEntity->setInternalEntityId($contactId); } - - $persistEntities[] = $integrationEntity; + if ($integrationEntity->getIntegrationEntityId()) { + $persistEntities[] = $integrationEntity; + } ++$updated; } else { $error = 'http status code '.$item['httpStatusCode']; @@ -1314,7 +1332,11 @@ protected function processCompositeResponse($response, array $salesforceIdMappin break; } - $this->logIntegrationError(new \Exception($error.' ('.$item['referenceId'].')')); + $exception = new ApiErrorException($error); + if (!empty($item['referenceId']) && ($object == 'Contact' || $object = 'Lead')) { + $exception->setContactId($item['referenceId']); + } + $this->logIntegrationError($exception); } } @@ -1328,6 +1350,11 @@ protected function processCompositeResponse($response, array $salesforceIdMappin return [$updated, $created]; } + /** + * @return array + * + * @throws \Exception + */ public function getCampaigns() { $silenceExceptions = (isset($settings['silence_exceptions'])) ? $settings['silence_exceptions'] : true; @@ -1343,6 +1370,13 @@ public function getCampaigns() return $campaigns; } + + /** + * @param $campaignId + * @param $settings + * + * @throws \Exception + */ public function getCampaignMembers($campaignId, $settings) { $silenceExceptions = (isset($settings['silence_exceptions'])) ? $settings['silence_exceptions'] : true; @@ -1464,6 +1498,12 @@ public function getCampaignMembers($campaignId, $settings) } } + /** + * @param $fields + * @param $object + * + * @return array + */ public function getMixedLeadFields($fields, $object) { $mixedFields = array_filter($fields['leadFields']); @@ -1480,26 +1520,13 @@ public function getMixedLeadFields($fields, $object) return $fields; } - public function getNotificationModel() - { - return $this->factory->getModel('core.notification'); - } - /** - * @param \Exception $e + * @param $campaignId + * + * @return array + * + * @throws \Exception */ - public function logIntegrationError(\Exception $e) - { - $logger = $this->factory->getLogger(); - if ('dev' == MAUTIC_ENV) { - $logger->addError('INTEGRATION ERROR: '.$this->getName().' - '.$e); - $this->getNotificationModel()->addNotification($e, $this->getName(), false, 'INTEGRATION ERROR: '.$this->getName().':', null, null, $this->factory->getUser()); - } else { - $logger->addError('INTEGRATION ERROR: '.$this->getName().' - '.$e->getMessage()); - $this->getNotificationModel()->addNotification($e->getMessage(), $this->getName(), false, 'INTEGRATION ERROR: '.$this->getName().':', null, null, $this->factory->getUser()); - } - } - public function getCampaignMemberStatus($campaignId) { $silenceExceptions = (isset($settings['silence_exceptions'])) ? $settings['silence_exceptions'] : true; @@ -1516,6 +1543,13 @@ public function getCampaignMemberStatus($campaignId) return $campaignMemberStatus; } + /** + * @param Lead $lead + * @param $integrationCampaignId + * @param $status + * + * @return array + */ public function pushLeadToCampaign(Lead $lead, $integrationCampaignId, $status) { $mauticData = $salesforceIdMapping = [];