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 = [];