mautic / plugins /MauticCrmBundle /Integration /SalesforceIntegration.php
chrisbryan17's picture
Upload folder using huggingface_hub
d2897cd verified
raw
history blame contribute delete
120 kB
<?php
namespace MauticPlugin\MauticCrmBundle\Integration;
use Doctrine\ORM\ORMException;
use Exception;
use Mautic\CoreBundle\Entity\Notification;
use Mautic\CoreBundle\Entity\Transformer\NotificationArrayTransformer;
use Mautic\CoreBundle\Helper\EmojiHelper;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper;
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
use Mautic\PluginBundle\Exception\ApiErrorException;
use Mautic\UserBundle\Entity\Role;
use Mautic\UserBundle\Entity\User;
use MauticPlugin\MauticCrmBundle\Api\SalesforceApi;
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\CampaignMember\Fetcher;
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\CampaignMember\Organizer;
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Exception\NoObjectsToFetchException;
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Helper\StateValidationHelper;
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\CampaignMember;
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\ResultsPaginator;
use Psr\Cache\InvalidArgumentException;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* @method SalesforceApi getApiHelper()
*/
class SalesforceIntegration extends CrmAbstractIntegration
{
/**
* @var string []
*/
private array $objects = [
'Lead',
'Contact',
'Account',
];
private string|bool $failureFetchingLeads = false;
public function getName(): string
{
return 'Salesforce';
}
/**
* Get the array key for clientId.
*/
public function getClientIdKey(): string
{
return 'client_id';
}
/**
* Get the array key for client secret.
*/
public function getClientSecretKey(): string
{
return 'client_secret';
}
/**
* Get the array key for the auth token.
*/
public function getAuthTokenKey(): string
{
return 'access_token';
}
/**
* @return array<string, string>
*/
public function getRequiredKeyFields(): array
{
return [
'client_id' => 'mautic.integration.keyfield.consumerid',
'client_secret' => 'mautic.integration.keyfield.consumersecret',
];
}
/**
* Get the keys for the refresh token and expiry.
*/
public function getRefreshTokenKeys(): array
{
return ['refresh_token', ''];
}
public function getSupportedFeatures(): array
{
return ['push_lead', 'get_leads', 'push_leads'];
}
public function getAccessTokenUrl(): string
{
$config = $this->mergeConfigToFeatureSettings([]);
if (isset($config['sandbox'][0]) and 'sandbox' === $config['sandbox'][0]) {
return 'https://test.salesforce.com/services/oauth2/token';
}
return 'https://login.salesforce.com/services/oauth2/token';
}
public function getAuthenticationUrl(): string
{
$config = $this->mergeConfigToFeatureSettings([]);
if (isset($config['sandbox'][0]) and 'sandbox' === $config['sandbox'][0]) {
return 'https://test.salesforce.com/services/oauth2/authorize';
}
return 'https://login.salesforce.com/services/oauth2/authorize';
}
public function getAuthScope(): string
{
return 'api refresh_token';
}
public function getApiUrl(): string
{
return sprintf('%s/services/data/v34.0/sobjects', $this->keys['instance_url']);
}
public function getQueryUrl(): string
{
return sprintf('%s/services/data/v34.0', $this->keys['instance_url']);
}
public function getCompositeUrl(): string
{
return sprintf('%s/services/data/v38.0', $this->keys['instance_url']);
}
/**
* @param bool $inAuthorization
*/
public function getBearerToken($inAuthorization = false)
{
if (!$inAuthorization && isset($this->keys[$this->getAuthTokenKey()])) {
return $this->keys[$this->getAuthTokenKey()];
}
return false;
}
public function getAuthenticationType(): string
{
return 'oauth2';
}
public function getDataPriority(): bool
{
return true;
}
public function updateDncByDate(): bool
{
$featureSettings = $this->settings->getFeatureSettings();
if (isset($featureSettings['updateDncByDate'][0]) && 'updateDncByDate' === $featureSettings['updateDncByDate'][0]) {
return true;
}
return false;
}
/**
* Get available company fields for choices in the config UI.
*
* @param array $settings
*
* @return array
*/
public function getFormCompanyFields($settings = [])
{
return $this->getFormFieldsByObject('company', $settings);
}
/**
* @param mixed[] $settings
*
* @return mixed[]
*
* @throws \Exception
*/
public function getFormLeadFields(array $settings = []): array
{
$leadFields = $this->getFormFieldsByObject('Lead', $settings);
$contactFields = $this->getFormFieldsByObject('Contact', $settings);
return array_merge($leadFields, $contactFields);
}
/**
* @param array $settings
*
* @return mixed[]
*
* @throws InvalidArgumentException
*/
public function getAvailableLeadFields($settings = []): array
{
$silenceExceptions = $settings['silence_exceptions'] ?? true;
$salesForceObjects = [];
if (isset($settings['feature_settings']['objects'])) {
$salesForceObjects = $settings['feature_settings']['objects'];
} else {
$salesForceObjects[] = 'Lead';
}
$isRequired = fn (array $field, $object): bool => ('boolean' !== $field['type'] && empty($field['nillable']) && !in_array($field['name'], ['Status', 'Id', 'CreatedDate']))
|| ('Lead' == $object && in_array($field['name'], ['Company']))
|| (in_array($object, ['Lead', 'Contact']) && 'Email' === $field['name']);
$salesFields = [];
try {
if (!empty($salesForceObjects) and is_array($salesForceObjects)) {
foreach ($salesForceObjects as $sfObject) {
if ('Account' === $sfObject) {
// Match SF object to Mautic's
$sfObject = 'company';
}
if (isset($sfObject) and 'Activity' == $sfObject) {
continue;
}
$sfObject = trim($sfObject);
// Check the cache first
$settings['cache_suffix'] = $cacheSuffix = '.'.$sfObject;
if ($fields = parent::getAvailableLeadFields($settings)) {
if (('company' === $sfObject && isset($fields['Id'])) || isset($fields['Id__'.$sfObject])) {
$salesFields[$sfObject] = $fields;
continue;
}
}
if ($this->isAuthorized()) {
if (!isset($salesFields[$sfObject])) {
$fields = $this->getApiHelper()->getLeadFields($sfObject);
if (!empty($fields['fields'])) {
foreach ($fields['fields'] as $fieldInfo) {
if ((!$fieldInfo['updateable'] && (!$fieldInfo['calculated'] && !in_array($fieldInfo['name'], ['Id', 'IsDeleted', 'CreatedDate'])))
|| !isset($fieldInfo['name'])
|| (in_array(
$fieldInfo['type'],
['reference']
) && 'AccountId' != $fieldInfo['name'])
) {
continue;
}
$type = match ($fieldInfo['type']) {
'boolean' => 'boolean',
'datetime' => 'datetime',
'date' => 'date',
default => 'string',
};
if ('company' !== $sfObject) {
if ('AccountId' == $fieldInfo['name']) {
$fieldInfo['label'] = 'Company';
}
$salesFields[$sfObject][$fieldInfo['name'].'__'.$sfObject] = [
'type' => $type,
'label' => $sfObject.'-'.$fieldInfo['label'],
'required' => $isRequired($fieldInfo, $sfObject),
'group' => $sfObject,
'optionLabel' => $fieldInfo['label'],
];
// CreateDate can be updatable just in Mautic
if (in_array($fieldInfo['name'], ['CreatedDate'])) {
$salesFields[$sfObject][$fieldInfo['name'].'__'.$sfObject]['update_mautic'] = 1;
}
} else {
$salesFields[$sfObject][$fieldInfo['name']] = [
'type' => $type,
'label' => $fieldInfo['label'],
'required' => $isRequired($fieldInfo, $sfObject),
];
}
}
$this->cache->set('leadFields'.$cacheSuffix, $salesFields[$sfObject]);
}
}
asort($salesFields[$sfObject]);
}
}
}
} catch (\Exception $e) {
$this->logIntegrationError($e);
if (!$silenceExceptions) {
throw $e;
}
}
return $salesFields;
}
/**
* @return array
*/
public function getFormNotes($section)
{
if ('authorization' == $section) {
return ['mautic.salesforce.form.oauth_requirements', 'warning'];
}
return parent::getFormNotes($section);
}
/**
* @return mixed
*/
public function getFetchQuery($params)
{
return $params;
}
/**
* @param array<mixed> $params
*
* @return array<mixed>
*
* @throws ApiErrorException
*/
public function amendLeadDataBeforeMauticPopulate($data, $object, $params = []): array
{
$updated = 0;
$created = 0;
$counter = 0;
$entity = null;
$detachClass = null;
$mauticObjectReference = null;
$integrationMapping = [];
$DNCUpdates = [];
if (isset($data['records']) and 'Activity' !== $object) {
foreach ($data['records'] as $record) {
$this->logger->debug('SALESFORCE: amendLeadDataBeforeMauticPopulate record '.var_export($record, true));
if (isset($params['progress'])) {
$params['progress']->advance();
}
$dataObject = [];
if (isset($record['attributes']['type']) && 'Account' == $record['attributes']['type']) {
$newName = '';
} else {
$newName = '__'.$object;
}
foreach ($record as $key => $item) {
if (is_bool($item)) {
$dataObject[$key.$newName] = (int) $item;
} else {
$dataObject[$key.$newName] = $item;
}
}
if ($dataObject) {
$entity = null;
switch ($object) {
case 'Contact':
if (isset($dataObject['Email__Contact'])) {
// Sanitize email to make sure we match it
// correctly against mautic emails
$dataObject['Email__Contact'] = InputHelper::email($dataObject['Email__Contact']);
}
// get company from account id and assign company name
if (isset($dataObject['AccountId__'.$object])) {
$companyName = $this->getCompanyName($dataObject['AccountId__'.$object], 'Name');
if ($companyName) {
$dataObject['AccountId__'.$object] = $companyName;
} else {
unset($dataObject['AccountId__'.$object]); // no company was found in Salesforce
}
}
// no break
case 'Lead':
// Set owner so that it maps if configured to do so
if (!empty($dataObject['Owner__Lead']['Email'])) {
$dataObject['owner_email'] = $dataObject['Owner__Lead']['Email'];
} elseif (!empty($dataObject['Owner__Contact']['Email'])) {
$dataObject['owner_email'] = $dataObject['Owner__Contact']['Email'];
}
if (isset($dataObject['Email__Lead'])) {
// Sanitize email to make sure we match it
// correctly against mautic_leads emails
$dataObject['Email__Lead'] = InputHelper::email($dataObject['Email__Lead']);
}
// normalize multiselect field
foreach ($dataObject as &$dataO) {
if (is_string($dataO)) {
$dataO = str_replace(';', '|', $dataO);
}
}
$entity = $this->getMauticLead($dataObject, true, null, null, $object);
$mauticObjectReference = 'lead';
$detachClass = Lead::class;
break;
case 'Account':
$entity = $this->getMauticCompany($dataObject, 'Account');
$mauticObjectReference = 'company';
$detachClass = Company::class;
break;
default:
$this->logIntegrationError(
new \Exception(
sprintf('Received an unexpected object without an internalObjectReference "%s"', $object)
)
);
break;
}
if (!$entity) {
continue;
}
$integrationMapping[$entity->getId()] = [
'entity' => $entity,
'integration_entity_id' => $record['Id'],
];
if (method_exists($entity, 'isNewlyCreated') && $entity->isNewlyCreated()) {
++$created;
if (isset($record['HasOptedOutOfEmail'])) {
$DNCUpdates[$object][$entity->getEmail()] = [
'integration_entity_id' => $record['Id'],
'internal_entity_id' => $entity->getId(),
'email' => $entity->getEmail(),
'is_new' => true,
'opted_out' => $record['HasOptedOutOfEmail'],
];
}
} else {
++$updated;
}
++$counter;
if ($counter >= 100) {
// Persist integration entities
$this->buildIntegrationEntities($integrationMapping, $object, $mauticObjectReference, $params);
$counter = 0;
$this->em->detach($entity);
$integrationMapping = [];
}
}
}
if (count($integrationMapping)) {
// Persist integration entities
$this->buildIntegrationEntities($integrationMapping, $object, $mauticObjectReference, $params);
$this->em->detach($entity);
}
foreach ($DNCUpdates as $objectName => $sfEntity) {
$this->pushLeadDoNotContactByDate('email', $sfEntity, $objectName, $params);
}
unset($data['records']);
$this->logger->debug('SALESFORCE: amendLeadDataBeforeMauticPopulate response '.var_export($data, true));
unset($data);
$this->persistIntegrationEntities = [];
unset($dataObject);
}
return [$updated, $created];
}
/**
* @param FormBuilder $builder
* @param array $data
* @param string $formArea
*/
public function appendToForm(&$builder, $data, $formArea): void
{
if ('features' == $formArea) {
$builder->add(
'sandbox',
ChoiceType::class,
[
'choices' => [
'mautic.salesforce.sandbox' => 'sandbox',
],
'expanded' => true,
'multiple' => true,
'label' => 'mautic.salesforce.form.sandbox',
'label_attr' => ['class' => 'control-label'],
'placeholder' => false,
'required' => false,
'attr' => [
'onclick' => 'Mautic.postForm(mQuery(\'form[name="integration_details"]\'),\'\');',
],
]
);
$builder->add(
'updateOwner',
ChoiceType::class,
[
'choices' => [
'mautic.salesforce.updateOwner' => 'updateOwner',
],
'expanded' => true,
'multiple' => true,
'label' => 'mautic.salesforce.form.updateOwner',
'label_attr' => ['class' => 'control-label'],
'placeholder' => false,
'required' => false,
'attr' => [
'onclick' => 'Mautic.postForm(mQuery(\'form[name="integration_details"]\'),\'\');',
],
]
);
$builder->add(
'updateBlanks',
ChoiceType::class,
[
'choices' => [
'mautic.integrations.blanks' => 'updateBlanks',
],
'expanded' => true,
'multiple' => true,
'label' => 'mautic.integrations.form.blanks',
'label_attr' => ['class' => 'control-label'],
'placeholder' => false,
'required' => false,
]
);
$builder->add(
'updateDncByDate',
ChoiceType::class,
[
'choices' => [
'mautic.integrations.update.dnc.by.date' => 'updateDncByDate',
],
'expanded' => true,
'multiple' => true,
'label' => 'mautic.integrations.form.update.dnc.by.date.label',
'label_attr' => ['class' => 'control-label'],
'placeholder' => false,
'required' => false,
]
);
$builder->add(
'objects',
ChoiceType::class,
[
'choices' => [
'mautic.salesforce.object.lead' => 'Lead',
'mautic.salesforce.object.contact' => 'Contact',
'mautic.salesforce.object.company' => 'company',
'mautic.salesforce.object.activity' => 'Activity',
],
'expanded' => true,
'multiple' => true,
'label' => 'mautic.salesforce.form.objects_to_pull_from',
'label_attr' => ['class' => ''],
'placeholder' => false,
'required' => false,
]
);
$builder->add(
'activityEvents',
ChoiceType::class,
[
'choices' => array_flip($this->leadModel->getEngagementTypes()), // Choice type expects labels as keys
'label' => 'mautic.salesforce.form.activity_included_events',
'label_attr' => [
'class' => 'control-label',
'data-toggle' => 'tooltip',
'title' => $this->translator->trans('mautic.salesforce.form.activity.events.tooltip'),
],
'multiple' => true,
'empty_data' => ['point.gained', 'form.submitted', 'email.read'], // BC with pre 2.11.0
'required' => false,
]
);
$builder->add(
'namespace',
TextType::class,
[
'label' => 'mautic.salesforce.form.namespace_prefix',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
]
);
}
}
/**
* @param array $fields
* @param array $keys
* @param mixed $object
*
* @return array
*/
public function prepareFieldsForSync($fields, $keys, $object = null)
{
$leadFields = [];
if (null === $object) {
$object = 'Lead';
}
$objects = (!is_array($object)) ? [$object] : $object;
if (is_string($object) && 'Account' === $object) {
return $fields['companyFields'] ?? $fields;
}
if (isset($fields['leadFields'])) {
$fields = $fields['leadFields'];
$keys = array_keys($fields);
}
foreach ($objects as $obj) {
if (!isset($leadFields[$obj])) {
$leadFields[$obj] = [];
}
foreach ($keys as $key) {
if (strpos($key, '__'.$obj)) {
$newKey = str_replace('__'.$obj, '', $key);
if ('Id' === $newKey) {
// Don't map Id for push
continue;
}
$leadFields[$obj][$newKey] = $fields[$key];
}
}
}
return (is_array($object)) ? $leadFields : $leadFields[$object];
}
/**
* @param Lead $lead
* @param array $config
*
* @return array|bool
*/
public function pushLead($lead, $config = [])
{
$config = $this->mergeConfigToFeatureSettings($config);
if (empty($config['leadFields'])) {
return [];
}
$mappedData = $this->mapContactDataForPush($lead, $config);
// No fields are mapped so bail
if (empty($mappedData)) {
return false;
}
try {
if ($this->isAuthorized()) {
$existingPersons = $this->getApiHelper()->getPerson(
[
'Lead' => $mappedData['Lead']['create'] ?? null,
'Contact' => $mappedData['Contact']['create'] ?? null,
]
);
$personFound = false;
$people = [
'Contact' => [],
'Lead' => [],
];
foreach (['Contact', 'Lead'] as $object) {
if (!empty($existingPersons[$object])) {
$fieldsToUpdate = $mappedData[$object]['update'];
$fieldsToUpdate = $this->getBlankFieldsToUpdate($fieldsToUpdate, $existingPersons[$object], $mappedData, $config);
$personFound = true;
foreach ($existingPersons[$object] as $person) {
if (!empty($fieldsToUpdate)) {
if (isset($fieldsToUpdate['AccountId'])) {
$accountId = $this->getCompanyName($fieldsToUpdate['AccountId'], 'Id', 'Name');
if (!$accountId) {
// company was not found so create a new company in Salesforce
$company = $lead->getPrimaryCompany();
if (!empty($company)) {
$company = $this->companyModel->getEntity($company['id']);
$sfCompany = $this->pushCompany($company);
if ($sfCompany) {
$fieldsToUpdate['AccountId'] = key($sfCompany);
}
}
} else {
$fieldsToUpdate['AccountId'] = $accountId;
}
}
$personData = $this->getApiHelper()->updateObject($fieldsToUpdate, $object, $person['Id']);
}
$people[$object][$person['Id']] = $person['Id'];
}
}
if ('Lead' === $object && !$personFound && isset($mappedData[$object]['create'])) {
$personData = $this->getApiHelper()->createLead($mappedData[$object]['create']);
$people[$object][$personData['Id']] = $personData['Id'];
$personFound = true;
}
if (isset($personData['Id'])) {
/** @var IntegrationEntityRepository $integrationEntityRepo */
$integrationEntityRepo = $this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class);
$integrationId = $integrationEntityRepo->getIntegrationsEntityId('Salesforce', $object, 'lead', $lead->getId());
$integrationEntity = (empty($integrationId))
? $this->createIntegrationEntity($object, $personData['Id'], 'lead', $lead->getId(), [], false)
:
$this->em->getReference(\Mautic\PluginBundle\Entity\IntegrationEntity::class, $integrationId[0]['id']);
$integrationEntity->setLastSyncDate($this->getLastSyncDate());
$integrationEntityRepo->saveEntity($integrationEntity);
}
}
// Return success if any Contact or Lead was updated or created
return ($personFound) ? $people : false;
}
} catch (\Exception $e) {
if ($e instanceof ApiErrorException) {
$e->setContact($lead);
}
$this->logIntegrationError($e);
}
return false;
}
/**
* @param Company $company
* @param array $config
*
* @return array|bool
*/
public function pushCompany($company, $config = [])
{
$config = $this->mergeConfigToFeatureSettings($config);
if (empty($config['companyFields']) || !$company) {
return [];
}
$object = 'company';
$mappedData = $this->mapCompanyDataForPush($company, $config);
// No fields are mapped so bail
if (empty($mappedData)) {
return false;
}
try {
if ($this->isAuthorized()) {
$existingCompanies = $this->getApiHelper()->getCompany(
[
$object => $mappedData[$object]['create'],
]
);
$companyFound = false;
$companies = [];
if (!empty($existingCompanies[$object])) {
$fieldsToUpdate = $mappedData[$object]['update'];
$fieldsToUpdate = $this->getBlankFieldsToUpdate($fieldsToUpdate, $existingCompanies[$object], $mappedData, $config);
$companyFound = true;
foreach ($existingCompanies[$object] as $sfCompany) {
if (!empty($fieldsToUpdate)) {
$companyData = $this->getApiHelper()->updateObject($fieldsToUpdate, $object, $sfCompany['Id']);
}
$companies[$sfCompany['Id']] = $sfCompany['Id'];
}
}
if (!$companyFound) {
$companyData = $this->getApiHelper()->createObject($mappedData[$object]['create'], 'Account');
$companies[$companyData['Id']] = $companyData['Id'];
$companyFound = true;
}
if (isset($companyData['Id'])) {
/** @var IntegrationEntityRepository $integrationEntityRepo */
$integrationEntityRepo = $this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class);
$integrationId = $integrationEntityRepo->getIntegrationsEntityId('Salesforce', $object, 'company', $company->getId());
$integrationEntity = (empty($integrationId))
? $this->createIntegrationEntity($object, $companyData['Id'], 'lead', $company->getId(), [], false)
:
$this->em->getReference(\Mautic\PluginBundle\Entity\IntegrationEntity::class, $integrationId[0]['id']);
$integrationEntity->setLastSyncDate($this->getLastSyncDate());
$integrationEntityRepo->saveEntity($integrationEntity);
}
// Return success if any company was updated or created
return ($companyFound) ? $companies : false;
}
} catch (\Exception $e) {
$this->logIntegrationError($e);
}
return false;
}
/**
* @param array $params
* @param array $result
* @param string $object
*
* @return array|null
*/
public function getLeads($params = [], $query = null, &$executed = null, $result = [], $object = 'Lead')
{
if (!$query) {
$query = $this->getFetchQuery($params);
}
if (!is_array($executed)) {
$executed = [
0 => 0,
1 => 0,
];
}
try {
if ($this->isAuthorized()) {
$progress = null;
$paginator = new ResultsPaginator($this->logger, $this->keys['instance_url']);
while (true) {
$result = $this->getApiHelper()->getLeads($query, $object);
$paginator->setResults($result);
if (isset($params['output']) && !isset($params['progress'])) {
$progress = new ProgressBar($params['output'], $paginator->getTotal());
$progress->setFormat(' %current%/%max% [%bar%] %percent:3s%% ('.$object.')');
$params['progress'] = $progress;
}
[$justUpdated, $justCreated] = $this->amendLeadDataBeforeMauticPopulate($result, $object, $params);
$executed[0] += $justUpdated;
$executed[1] += $justCreated;
if (!$nextUrl = $paginator->getNextResultsUrl()) {
// No more records to fetch
break;
}
$query['nextUrl'] = $nextUrl;
}
if ($progress) {
$progress->finish();
}
}
} catch (\Exception $e) {
$this->logIntegrationError($e);
$this->failureFetchingLeads = $e->getMessage();
}
$this->logger->debug('SALESFORCE: '.$this->getApiHelper()->getRequestCounter().' API requests made for getLeads: '.$object);
return $executed;
}
public function upsertUnreadAdminsNotification(string $header, string $message, string $type = 'error', bool $preventUnreadDuplicates = true): void
{
$notificationTemplate = new Notification();
$notificationTemplate->setType($type);
$notificationTemplate->setIsRead(false);
$notificationTemplate->setHeader(EmojiHelper::toHtml(InputHelper::strict_html($header)));
$notificationTemplate->setMessage(EmojiHelper::toHtml(InputHelper::strict_html($message)));
$notificationTemplate->setIconClass(null);
$persistEntities = [];
$transformer = new NotificationArrayTransformer();
foreach ($this->getAdminUsers() as $adminUser) {
if ($preventUnreadDuplicates) {
/* @var Notification|null $exists */
$notificationTemplate->setUser($adminUser);
$searchArray = $transformer->transform($notificationTemplate);
$search = array_intersect_key(
$searchArray,
array_flip(['type', 'isRead', 'header', 'message', 'user'])
);
$exists = $this->getNotificationModel()->getRepository()->findOneBy($search);
if ($exists) {
continue;
}
$notification = clone $notificationTemplate;
$notification->setDateAdded(new \DateTime()); // not sure what date to use
}
$persistEntities[] = $notification;
}
$this->getNotificationModel()->getRepository()->saveEntities($persistEntities);
$this->getNotificationModel()->getRepository()->detachEntities($persistEntities);
}
/**
* Get all enabled admin users.
*
* @return array|User[]
*/
private function getAdminUsers(): array
{
$userRepository = $this->em->getRepository(User::class);
$adminRole = $this->em->getRepository(Role::class)->findOneBy(['isAdmin' => true]);
return $userRepository->findBy(
[
'role' => $adminRole,
'isPublished' => true,
]
);
}
/**
* @param array $params
*
* @return array|null
*/
public function getCompanies($params = [], $query = null, $executed = null)
{
return $this->getLeads($params, $query, $executed, [], 'Account');
}
/**
* @param array $params
*
* @return int|null
*
* @throws \Exception
*/
public function pushLeadActivity($params = [])
{
$executed = null;
$query = $this->getFetchQuery($params);
$config = $this->mergeConfigToFeatureSettings([]);
/** @var SalesforceApi $apiHelper */
$apiHelper = $this->getApiHelper();
$salesForceObjects[] = 'Lead';
if (isset($config['objects']) && !empty($config['objects'])) {
$salesForceObjects = $config['objects'];
}
// Ensure that Contact is attempted before Lead
sort($salesForceObjects);
/** @var IntegrationEntityRepository $integrationEntityRepo */
$integrationEntityRepo = $this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class);
$startDate = new \DateTime($query['start']);
$endDate = new \DateTime($query['end']);
$limit = 100;
foreach ($salesForceObjects as $object) {
if (!in_array($object, ['Contact', 'Lead'])) {
continue;
}
try {
if ($this->isAuthorized()) {
// Get first batch
$start = 0;
$salesForceIds = $integrationEntityRepo->getIntegrationsEntityId(
'Salesforce',
$object,
'lead',
null,
$startDate->format('Y-m-d H:m:s'),
$endDate->format('Y-m-d H:m:s'),
true,
$start,
$limit
);
while (!empty($salesForceIds)) {
$executed += count($salesForceIds);
// Extract a list of lead Ids
$leadIds = [];
$sfIds = [];
foreach ($salesForceIds as $ids) {
$leadIds[] = $ids['internal_entity_id'];
$sfIds[] = $ids['integration_entity_id'];
}
// Collect lead activity for this batch
$leadActivity = $this->getLeadData(
$startDate,
$endDate,
$leadIds
);
$this->logger->debug('SALESFORCE: Syncing activity for '.count($leadActivity).' contacts ('.implode(', ', array_keys($leadActivity)).')');
$this->logger->debug('SALESFORCE: Syncing activity for '.var_export($sfIds, true));
$salesForceLeadData = [];
foreach ($salesForceIds as $ids) {
$leadId = $ids['internal_entity_id'];
if (isset($leadActivity[$leadId])) {
$sfId = $ids['integration_entity_id'];
$salesForceLeadData[$sfId] = $leadActivity[$leadId];
$salesForceLeadData[$sfId]['id'] = $ids['integration_entity_id'];
$salesForceLeadData[$sfId]['leadId'] = $ids['internal_entity_id'];
$salesForceLeadData[$sfId]['leadUrl'] = $this->router->generate(
'mautic_plugin_timeline_view',
['integration' => 'Salesforce', 'leadId' => $leadId],
UrlGeneratorInterface::ABSOLUTE_URL
);
} else {
$this->logger->debug('SALESFORCE: No activity found for contact ID '.$leadId);
}
}
if (!empty($salesForceLeadData)) {
$apiHelper->createLeadActivity($salesForceLeadData, $object);
} else {
$this->logger->debug('SALESFORCE: No contact activity to sync');
}
// Get the next batch
$start += $limit;
$salesForceIds = $integrationEntityRepo->getIntegrationsEntityId(
'Salesforce',
$object,
'lead',
null,
$startDate->format('Y-m-d H:m:s'),
$endDate->format('Y-m-d H:m:s'),
true,
$start,
$limit
);
}
}
} catch (\Exception $e) {
$this->logIntegrationError($e);
}
}
return $executed;
}
/**
* Return key recognized by integration.
*/
public function convertLeadFieldKey(string $key, $field): string
{
$search = [];
foreach ($this->objects as $object) {
$search[] = '__'.$object;
}
return str_replace($search, '', $key);
}
/**
* @param array $params
*
* @return mixed[]
*/
public function pushLeads($params = []): array
{
$limit = $params['limit'] ?? 100;
[$fromDate, $toDate] = $this->getSyncTimeframeDates($params);
$config = $this->mergeConfigToFeatureSettings($params);
$integrationEntityRepo = $this->getIntegrationEntityRepository();
$totalUpdated = 0;
$totalCreated = 0;
$totalErrors = 0;
[$fieldMapping, $mauticLeadFieldString, $supportedObjects] = $this->prepareFieldsForPush($config);
if (empty($fieldMapping)) {
return [0, 0, 0, 0];
}
$originalLimit = $limit;
$progress = false;
// Get a total number of contacts to be updated and/or created for the progress counter
$totalToUpdate = array_sum(
$integrationEntityRepo->findLeadsToUpdate(
'Salesforce',
'lead',
$mauticLeadFieldString,
false,
$fromDate,
$toDate,
$supportedObjects,
[]
)
);
$totalToCreate = (in_array('Lead', $supportedObjects)) ? $integrationEntityRepo->findLeadsToCreate(
'Salesforce',
$mauticLeadFieldString,
false,
$fromDate,
$toDate
) : 0;
$totalCount = $totalToProcess = $totalToCreate + $totalToUpdate;
if (defined('IN_MAUTIC_CONSOLE')) {
// start with update
if ($totalToUpdate + $totalToCreate) {
$output = new ConsoleOutput();
$output->writeln("About $totalToUpdate to update and about $totalToCreate to create/update");
$progress = new ProgressBar($output, $totalCount);
}
}
// Start with contacts so we know who is a contact when we go to process converted leads
if (count($supportedObjects) > 1) {
$sfObject = 'Contact';
} else {
$sfObject = array_key_first($supportedObjects);
}
$noMoreUpdates = false;
$trackedContacts = [
'Contact' => [],
'Lead' => [],
];
// Loop to maximize composite that may include updating contacts, updating leads, and creating leads
while ($totalCount > 0) {
$limit = $originalLimit;
$mauticData = [];
$checkEmailsInSF = [];
$leadsToSync = [];
$processedLeads = [];
// Process the updates
if (!$noMoreUpdates) {
$noMoreUpdates = $this->getMauticContactsToUpdate(
$checkEmailsInSF,
$mauticLeadFieldString,
$sfObject,
$trackedContacts,
$limit,
$fromDate,
$toDate,
$totalCount
);
if ($noMoreUpdates && 'Contact' === $sfObject && isset($supportedObjects['Lead'])) {
// Try Leads
$sfObject = 'Lead';
$noMoreUpdates = $this->getMauticContactsToUpdate(
$checkEmailsInSF,
$mauticLeadFieldString,
$sfObject,
$trackedContacts,
$limit,
$fromDate,
$toDate,
$totalCount
);
}
if ($limit) {
// Mainly done for test mocking purposes
$limit = $this->getSalesforceSyncLimit($checkEmailsInSF, $limit);
}
}
// If there is still room - grab Mautic leads to create if the Lead object is enabled
$sfEntityRecords = [];
if ('Lead' === $sfObject && (null === $limit || $limit > 0) && !empty($mauticLeadFieldString)) {
try {
$sfEntityRecords = $this->getMauticContactsToCreate(
$checkEmailsInSF,
$fieldMapping,
$mauticLeadFieldString,
$limit,
$fromDate,
$toDate,
$totalCount,
$progress
);
} catch (ApiErrorException $exception) {
$this->cleanupFromSync($leadsToSync, $exception);
}
} elseif ($checkEmailsInSF) {
$sfEntityRecords = $this->getSalesforceObjectsByEmails($sfObject, $checkEmailsInSF, implode(',', array_keys($fieldMapping[$sfObject]['create'])));
if (!isset($sfEntityRecords['records'])) {
// Something is wrong so throw an exception to prevent creating a bunch of new leads
$this->cleanupFromSync(
$leadsToSync,
json_encode($sfEntityRecords)
);
}
}
$this->pushLeadDoNotContactByDate('email', $checkEmailsInSF, $sfObject, $params);
// We're done
if (!$checkEmailsInSF) {
break;
}
$this->prepareMauticContactsToUpdate(
$mauticData,
$checkEmailsInSF,
$processedLeads,
$trackedContacts,
$leadsToSync,
$fieldMapping,
$mauticLeadFieldString,
$sfEntityRecords,
$progress
);
// Only create left over if Lead object is enabled in integration settings
if ($checkEmailsInSF && isset($fieldMapping['Lead'])) {
$this->prepareMauticContactsToCreate(
$mauticData,
$checkEmailsInSF,
$processedLeads,
$fieldMapping
);
}
// Persist pending changes
$this->cleanupFromSync($leadsToSync);
// Make the request
$this->makeCompositeRequest($mauticData, $totalUpdated, $totalCreated, $totalErrors);
// Stop gap - if 100% let's kill the script
if ($progress && $progress->getProgressPercent() >= 1) {
break;
}
}
if ($progress) {
$progress->finish();
$output->writeln('');
}
$this->logger->debug('SALESFORCE: '.$this->getApiHelper()->getRequestCounter().' API requests made for pushLeads');
// Assume that those not touched are ignored due to not having matching fields, duplicates, etc
$totalIgnored = $totalToProcess - ($totalUpdated + $totalCreated + $totalErrors);
return [$totalUpdated, $totalCreated, $totalErrors, $totalIgnored];
}
/**
* @return array
*/
public function getSalesforceLeadId($lead)
{
$config = $this->mergeConfigToFeatureSettings([]);
$integrationEntityRepo = $this->getIntegrationEntityRepository();
if (isset($config['objects'])) {
// try searching for lead as this has been changed before in updated done to the plugin
if (false !== array_search('Contact', $config['objects'])) {
$resultContact = $integrationEntityRepo->getIntegrationsEntityId('Salesforce', 'Contact', 'lead', $lead->getId());
if ($resultContact) {
return $resultContact;
}
}
}
return $integrationEntityRepo->getIntegrationsEntityId('Salesforce', 'Lead', 'lead', $lead->getId());
}
/**
* @return array
*
* @throws \Exception
*/
public function getCampaigns()
{
$campaigns = [];
try {
$campaigns = $this->getApiHelper()->getCampaigns();
} catch (\Exception $e) {
$this->logIntegrationError($e);
}
return $campaigns;
}
/**
* @return array<mixed>
*
* @throws \Exception
*/
public function getCampaignChoices(): array
{
$choices = [];
$campaigns = $this->getCampaigns();
if (!empty($campaigns['records'])) {
foreach ($campaigns['records'] as $campaign) {
$choices[] = [
'value' => $campaign['Id'],
'label' => $campaign['Name'],
];
}
}
return $choices;
}
/**
* @param int $campaignId
*
* @throws InvalidArgumentException
*/
public function getCampaignMembers($campaignId): void
{
$this->failureFetchingLeads = false;
/** @var IntegrationEntityRepository $integrationEntityRepo */
$integrationEntityRepo = $this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class);
$mixedFields = $this->getIntegrationSettings()->getFeatureSettings();
// Get the last time the campaign was synced to prevent resyncing the entire SF campaign
$cacheKey = $this->getName().'.CampaignSync.'.$campaignId;
$lastSyncDate = $this->getCache()->get($cacheKey);
$syncStarted = (new \DateTime())->format('c');
if (false === $lastSyncDate) {
// Sync all records
$lastSyncDate = null;
}
// Consume in batches
$paginator = new ResultsPaginator($this->logger, $this->keys['instance_url']);
$nextRecordsUrl = null;
while (true) {
try {
$results = $this->getApiHelper()->getCampaignMembers($campaignId, $lastSyncDate, $nextRecordsUrl);
$paginator->setResults($results);
$organizer = new Organizer($results['records']);
$fetcher = new Fetcher($integrationEntityRepo, $organizer, $campaignId);
// Create Mautic contacts from Campaign Members if they don't already exist
foreach (['Contact', 'Lead'] as $object) {
$fields = $this->getMixedLeadFields($mixedFields, $object);
try {
$query = $fetcher->getQueryForUnknownObjects($fields, $object);
$this->getLeads([], $query, $executed, [], $object);
if ($this->failureFetchingLeads) {
// Something failed while fetching the leads (i.e API error limit) so we have to fail here to prevent the campaign
// from caching the timestamp that will cause contacts to not be pulled/added to the segment
throw new ApiErrorException($this->failureFetchingLeads);
}
} catch (NoObjectsToFetchException) {
// No more IDs to fetch so break and continue on
continue;
}
}
// Create integration entities for members we aren't already tracking
$unknownMembers = $fetcher->getUnknownCampaignMembers();
$persistEntities = [];
$counter = 0;
foreach ($unknownMembers as $mauticContactId) {
$persistEntities[] = $this->createIntegrationEntity(
CampaignMember::OBJECT,
$campaignId,
'lead',
$mauticContactId,
[],
false
);
++$counter;
if (20 === $counter) {
// Batch to control RAM use
$this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class)->saveEntities($persistEntities);
$this->integrationEntityModel->getRepository()->detachEntities($persistEntities);
$persistEntities = [];
$counter = 0;
}
}
// Catch left overs
if ($persistEntities) {
$this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class)->saveEntities($persistEntities);
$this->integrationEntityModel->getRepository()->detachEntities($persistEntities);
}
unset($unknownMembers, $fetcher, $organizer, $persistEntities);
// Do we continue?
if (!$nextRecordsUrl = $paginator->getNextResultsUrl()) {
// No more results to fetch
// Store the latest sync date at the end in case something happens during the actual sync process and it needs to be re-ran
$this->cache->set($cacheKey, $syncStarted);
break;
}
} catch (\Exception $e) {
$this->logIntegrationError($e);
break;
}
}
}
public function getMixedLeadFields($fields, $object): array
{
$mixedFields = array_filter($fields['leadFields'] ?? []);
$fields = [];
foreach ($mixedFields as $sfField => $mField) {
if (str_contains($sfField, '__'.$object)) {
$fields[] = str_replace('__'.$object, '', $sfField);
}
if (str_contains($sfField, '-'.$object)) {
$fields[] = str_replace('-'.$object, '', $sfField);
}
}
return $fields;
}
/**
* @return array
*
* @throws \Exception
*/
public function getCampaignMemberStatus($campaignId)
{
$campaignMemberStatus = [];
try {
$campaignMemberStatus = $this->getApiHelper()->getCampaignMemberStatus($campaignId);
} catch (\Exception $e) {
$this->logIntegrationError($e);
}
return $campaignMemberStatus;
}
public function pushLeadToCampaign(Lead $lead, $campaignId, $status = '', $personIds = null): bool
{
if (empty($personIds)) {
// personIds should have been generated by pushLead()
return false;
}
$mauticData = [];
/** @var IntegrationEntityRepository $integrationEntityRepo */
$integrationEntityRepo = $this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class);
$body = [
'Status' => $status,
];
$object = 'CampaignMember';
$url = '/services/data/v38.0/sobjects/'.$object;
if (!empty($lead->getEmail())) {
$pushPeople = [];
$pushObject = null;
if (!empty($personIds)) {
// Give precendence to Contact CampaignMembers
if (!empty($personIds['Contact'])) {
$pushObject = 'Contact';
$campaignMembers = $this->getApiHelper()->checkCampaignMembership($campaignId, $pushObject, $personIds[$pushObject]);
$pushPeople = $personIds[$pushObject];
}
if (empty($campaignMembers) && !empty($personIds['Lead'])) {
$pushObject = 'Lead';
$campaignMembers = $this->getApiHelper()->checkCampaignMembership($campaignId, $pushObject, $personIds[$pushObject]);
$pushPeople = $personIds[$pushObject];
}
} // pushLead should have handled this
foreach ($pushPeople as $memberId) {
$campaignMappingId = '-'.$campaignId;
if (isset($campaignMembers[$memberId])) {
$existingCampaignMember = $integrationEntityRepo->getIntegrationsEntityId(
'Salesforce',
'CampaignMember',
'lead',
null,
null,
null,
false,
0,
0,
[$campaignMembers[$memberId]]
);
foreach ($existingCampaignMember as $member) {
$integrationEntity = $integrationEntityRepo->getEntity($member['id']);
$referenceId = $integrationEntity->getId();
$internalLeadId = $integrationEntity->getInternalEntityId();
}
$id = !empty($lead->getId()) ? $lead->getId() : '';
$id .= '-CampaignMember'.$campaignMembers[$memberId];
$id .= !empty($referenceId) ? '-'.$referenceId : '';
$id .= $campaignMappingId;
$patchurl = $url.'/'.$campaignMembers[$memberId];
$mauticData[$id] = [
'method' => 'PATCH',
'url' => $patchurl,
'referenceId' => $id,
'body' => $body,
'httpHeaders' => [
'Sforce-Auto-Assign' => 'FALSE',
],
];
} else {
$id = (!empty($lead->getId()) ? $lead->getId() : '').'-CampaignMemberNew-null'.$campaignMappingId;
$mauticData[$id] = [
'method' => 'POST',
'url' => $url,
'referenceId' => $id,
'body' => array_merge(
$body,
[
'CampaignId' => $campaignId,
"{$pushObject}Id" => $memberId,
]
),
];
}
}
$request['allOrNone'] = 'false';
$request['compositeRequest'] = array_values($mauticData);
$this->logger->debug('SALESFORCE: pushLeadToCampaign '.var_export($request, true));
$result = $this->getApiHelper()->syncMauticToSalesforce($request);
return (bool) array_sum($this->processCompositeResponse($result['compositeResponse']));
}
return false;
}
protected function getSyncKey($email): string
{
return mb_strtolower($this->cleanPushData($email));
}
protected function getMauticContactsToUpdate(
&$checkEmailsInSF,
$mauticLeadFieldString,
&$sfObject,
&$trackedContacts,
$limit,
$fromDate,
$toDate,
&$totalCount
): bool {
// Fetch them separately so we can determine if Leads are already Contacts
$toUpdate = $this->getIntegrationEntityRepository()->findLeadsToUpdate(
'Salesforce',
'lead',
$mauticLeadFieldString,
$limit,
$fromDate,
$toDate,
$sfObject
)[$sfObject];
$toUpdateCount = count($toUpdate);
$totalCount -= $toUpdateCount;
foreach ($toUpdate as $lead) {
if (!empty($lead['email'])) {
$lead = $this->getCompoundMauticFields($lead);
$key = $this->getSyncKey($lead['email']);
$trackedContacts[$lead['integration_entity']][$key] = $lead['id'];
if ('Contact' == $sfObject) {
$this->setContactToSync($checkEmailsInSF, $lead);
} elseif (isset($trackedContacts['Contact'][$key])) {
// We already know this is a converted contact so just ignore it
$integrationEntity = $this->em->getReference(
\Mautic\PluginBundle\Entity\IntegrationEntity::class,
$lead['id']
);
$this->deleteIntegrationEntities[] = $integrationEntity;
$this->logger->debug('SALESFORCE: Converted lead '.$lead['email']);
} else {
$this->setContactToSync($checkEmailsInSF, $lead);
}
}
}
return 0 === $toUpdateCount;
}
/**
* @return array
*
* @throws ApiErrorException
*/
protected function getMauticContactsToCreate(
&$checkEmailsInSF,
$fieldMapping,
$mauticLeadFieldString,
$limit,
$fromDate,
$toDate,
&$totalCount,
$progress = null
) {
$integrationEntityRepo = $this->getIntegrationEntityRepository();
$leadsToCreate = $integrationEntityRepo->findLeadsToCreate(
'Salesforce',
$mauticLeadFieldString,
$limit,
$fromDate,
$toDate
);
$totalCount -= count($leadsToCreate);
$foundContacts = [];
$sfEntityRecords = [
'totalSize' => 0,
'records' => [],
];
$error = false;
foreach ($leadsToCreate as $lead) {
$lead = $this->getCompoundMauticFields($lead);
if (isset($lead['email'])) {
$this->setContactToSync($checkEmailsInSF, $lead);
} elseif ($progress) {
$progress->advance();
}
}
// When creating, we have to check for Contacts first then Lead
if (isset($fieldMapping['Contact'])) {
$sfEntityRecords = $this->getSalesforceObjectsByEmails('Contact', $checkEmailsInSF, implode(',', array_keys($fieldMapping['Contact']['create'])));
if (isset($sfEntityRecords['records'])) {
foreach ($sfEntityRecords['records'] as $sfContactRecord) {
if (!isset($sfContactRecord['Email'])) {
continue;
}
$key = $this->getSyncKey($sfContactRecord['Email']);
$foundContacts[$key] = $key;
}
} else {
$error = json_encode($sfEntityRecords);
}
}
// For any Mautic contacts left over, check to see if existing Leads exist
if (isset($fieldMapping['Lead']) && $checkSfLeads = array_diff_key($checkEmailsInSF, $foundContacts)) {
$sfLeadRecords = $this->getSalesforceObjectsByEmails('Lead', $checkSfLeads, implode(',', array_keys($fieldMapping['Lead']['create'])));
if (isset($sfLeadRecords['records'])) {
// Merge contact records with these
$sfEntityRecords['records'] = array_merge($sfEntityRecords['records'], $sfLeadRecords['records']);
$sfEntityRecords['totalSize'] = (int) $sfEntityRecords['totalSize'] + (int) $sfLeadRecords['totalSize'];
} else {
$error = json_encode($sfLeadRecords);
}
}
if ($error) {
throw new ApiErrorException($error);
}
unset($leadsToCreate, $checkSfLeads);
return $sfEntityRecords;
}
protected function buildCompositeBody(
&$mauticData,
$objectFields,
$object,
&$entity,
$objectId = null,
$sfRecord = null
): array {
$body = [];
$updateEntity = [];
$company = null;
$config = $this->mergeConfigToFeatureSettings([]);
if ((isset($entity['email']) && !empty($entity['email'])) || (isset($entity['companyname']) && !empty($entity['companyname']))) {
// use a composite patch here that can update and create (one query) every 200 records
if (isset($objectFields['update'])) {
$fields = ($objectId) ? $objectFields['update'] : $objectFields['create'];
if (isset($entity['company']) && isset($entity['integration_entity']) && 'Contact' == $object) {
$accountId = $this->getCompanyName($entity['company'], 'Id', 'Name');
if (!$accountId) {
// company was not found so create a new company in Salesforce
$lead = $this->leadModel->getEntity($entity['internal_entity_id']);
if ($lead) {
$companies = $this->leadModel->getCompanies($lead);
if (!empty($companies)) {
foreach ($companies as $companyData) {
if ($companyData['is_primary']) {
$company = $this->companyModel->getEntity($companyData['company_id']);
}
}
if ($company) {
$sfCompany = $this->pushCompany($company);
if (!empty($sfCompany)) {
$entity['company'] = key($sfCompany);
}
}
} else {
unset($entity['company']);
}
}
} else {
$entity['company'] = $accountId;
}
}
$fields = $this->getBlankFieldsToUpdate($fields, $sfRecord, $objectFields, $config);
} else {
$fields = $objectFields;
}
foreach ($fields as $sfField => $mauticField) {
if (isset($entity[$mauticField])) {
$fieldType = (isset($objectFields['types']) && isset($objectFields['types'][$sfField])) ? $objectFields['types'][$sfField]
: 'string';
if (!empty($entity[$mauticField]) and 'boolean' != $fieldType) {
$body[$sfField] = $this->cleanPushData($entity[$mauticField], $fieldType);
} elseif ('boolean' == $fieldType) {
$body[$sfField] = $this->cleanPushData($entity[$mauticField], $fieldType);
}
}
if (array_key_exists($sfField, $objectFields['required']['fields']) && empty($body[$sfField])) {
if (isset($sfRecord[$sfField])) {
$body[$sfField] = $sfRecord[$sfField];
if (empty($entity[$mauticField]) && !empty($sfRecord[$sfField])
&& $sfRecord[$sfField] !== $this->translator->trans(
'mautic.integration.form.lead.unknown'
)
) {
$updateEntity[$mauticField] = $sfRecord[$sfField];
}
} else {
$body[$sfField] = $this->translator->trans('mautic.integration.form.lead.unknown');
}
}
}
$this->amendLeadDataBeforePush($body);
if (!empty($body)) {
$url = '/services/data/v38.0/sobjects/'.$object;
if ($objectId) {
$url .= '/'.$objectId;
}
$id = $entity['internal_entity_id'].'-'.$object.(!empty($entity['id']) ? '-'.$entity['id'] : '');
$method = ($objectId) ? 'PATCH' : 'POST';
$mauticData[$id] = [
'method' => $method,
'url' => $url,
'referenceId' => $id,
'body' => $body,
'httpHeaders' => [
'Sforce-Auto-Assign' => ($objectId) ? 'FALSE' : 'TRUE',
],
];
}
}
return $updateEntity;
}
protected function getRequiredFieldString(array $config, array $availableFields, $object): array
{
$requiredFields = $this->getRequiredFields($availableFields[$object]);
if ('company' != $object) {
$requiredFields = $this->prepareFieldsForSync($config['leadFields'] ?? [], array_keys($requiredFields), $object);
}
$requiredString = implode(',', array_keys($requiredFields));
return [$requiredFields, $requiredString];
}
protected function prepareFieldsForPush($config): array
{
$leadFields = array_unique(array_values($config['leadFields']));
$leadFields = array_combine($leadFields, $leadFields);
unset($leadFields['mauticContactTimelineLink']);
unset($leadFields['mauticContactIsContactableByEmail']);
$fieldsToUpdateInSf = $this->getPriorityFieldsForIntegration($config);
$fieldKeys = array_keys($config['leadFields']);
$supportedObjects = [];
$objectFields = [];
// Important to have contacts first!!
if (false !== array_search('Contact', $config['objects'])) {
$supportedObjects['Contact'] = 'Contact';
$fieldsToCreate = $this->prepareFieldsForSync($config['leadFields'] ?? [], $fieldKeys, 'Contact');
$objectFields['Contact'] = [
'update' => isset($fieldsToUpdateInSf['Contact']) ? array_intersect_key($fieldsToCreate, $fieldsToUpdateInSf['Contact']) : [],
'create' => $fieldsToCreate,
];
}
if (false !== array_search('Lead', $config['objects'])) {
$supportedObjects['Lead'] = 'Lead';
$fieldsToCreate = $this->prepareFieldsForSync($config['leadFields'] ?? [], $fieldKeys, 'Lead');
$objectFields['Lead'] = [
'update' => isset($fieldsToUpdateInSf['Lead']) ? array_intersect_key($fieldsToCreate, $fieldsToUpdateInSf['Lead']) : [],
'create' => $fieldsToCreate,
];
}
$mauticLeadFieldString = implode(', l.', $leadFields);
$mauticLeadFieldString = 'l.'.$mauticLeadFieldString;
$availableFields = $this->getAvailableLeadFields(['feature_settings' => ['objects' => $supportedObjects]]);
// Setup required fields and field types
foreach ($supportedObjects as $object) {
$objectFields[$object]['types'] = [];
if (isset($availableFields[$object])) {
$fieldData = $this->prepareFieldsForSync($availableFields[$object], array_keys($availableFields[$object]), $object);
foreach ($fieldData as $fieldName => $field) {
$objectFields[$object]['types'][$fieldName] = $field['type'] ?? 'string';
}
}
[$fields, $string] = $this->getRequiredFieldString(
$config,
$availableFields,
$object
);
$objectFields[$object]['required'] = [
'fields' => $fields,
'string' => $string,
];
}
return [$objectFields, $mauticLeadFieldString, $supportedObjects];
}
/**
* @param string $priorityObject
*
* @return mixed
*/
protected function getPriorityFieldsForMautic($config, $object = null, $priorityObject = 'mautic')
{
$fields = parent::getPriorityFieldsForMautic($config, $object, $priorityObject);
return ($object && isset($fields[$object])) ? $fields[$object] : $fields;
}
/**
* @param string $priorityObject
*
* @return mixed
*/
protected function getPriorityFieldsForIntegration($config, $object = null, $priorityObject = 'mautic')
{
$fields = parent::getPriorityFieldsForIntegration($config, $object, $priorityObject);
unset($fields['Contact']['Id'], $fields['Lead']['Id']);
return ($object && isset($fields[$object])) ? $fields[$object] : $fields;
}
/**
* @param int $totalUpdated
* @param int $totalCreated
* @param int $totalErrored
*/
protected function processCompositeResponse($response, &$totalUpdated = 0, &$totalCreated = 0, &$totalErrored = 0): array
{
if (is_array($response)) {
foreach ($response as $item) {
$contactId = $integrationEntityId = $campaignId = null;
$object = 'Lead';
$internalObject = 'lead';
if (!empty($item['referenceId'])) {
$reference = explode('-', $item['referenceId']);
if (3 === count($reference)) {
[$contactId, $object, $integrationEntityId] = $reference;
} elseif (4 === count($reference)) {
[$contactId, $object, $integrationEntityId, $campaignId] = $reference;
} else {
[$contactId, $object] = $reference;
}
}
if (strstr($object, 'CampaignMember')) {
$object = 'CampaignMember';
}
if ('Account' == $object) {
$internalObject = 'company';
}
if (isset($item['body'][0]['errorCode'])) {
$exception = new ApiErrorException($item['body'][0]['message']);
if ('Contact' == $object || $object = 'Lead') {
$exception->setContactId($contactId);
}
$this->logIntegrationError($exception);
$integrationEntity = null;
if ($integrationEntityId && 'CampaignMember' !== $object) {
$integrationEntity = $this->integrationEntityModel->getEntityByIdAndSetSyncDate($integrationEntityId, new \DateTime());
} elseif (isset($campaignId)) {
$integrationEntity = $this->integrationEntityModel->getEntityByIdAndSetSyncDate($campaignId, $this->getLastSyncDate());
} elseif ($contactId) {
$integrationEntity = $this->createIntegrationEntity(
$object,
null,
$internalObject.'-error',
$contactId,
null,
false
);
}
if ($integrationEntity) {
$integrationEntity->setInternalEntity('ENTITY_IS_DELETED' === $item['body'][0]['errorCode'] ? $internalObject.'-deleted' : $internalObject.'-error')
->setInternal(['error' => $item['body'][0]['message']]);
$this->persistIntegrationEntities[] = $integrationEntity;
}
++$totalErrored;
} elseif (!empty($item['body']['success'])) {
if (201 === $item['httpStatusCode']) {
// New object created
if ('CampaignMember' === $object) {
$internal = ['Id' => $item['body']['id']];
} else {
$internal = [];
}
$this->salesforceIdMapping[$contactId] = $item['body']['id'];
$this->persistIntegrationEntities[] = $this->createIntegrationEntity(
$object,
$this->salesforceIdMapping[$contactId],
$internalObject,
$contactId,
$internal,
false
);
}
++$totalCreated;
} elseif (204 === $item['httpStatusCode']) {
// Record was updated
if ($integrationEntityId) {
$integrationEntity = $this->integrationEntityModel->getEntityByIdAndSetSyncDate($integrationEntityId, $this->getLastSyncDate());
if ($integrationEntity) {
if (isset($this->salesforceIdMapping[$contactId])) {
$integrationEntity->setIntegrationEntityId($this->salesforceIdMapping[$contactId]);
}
$this->persistIntegrationEntities[] = $integrationEntity;
}
} elseif (!empty($this->salesforceIdMapping[$contactId])) {
// Found in Salesforce so create a new record for it
$this->persistIntegrationEntities[] = $this->createIntegrationEntity(
$object,
$this->salesforceIdMapping[$contactId],
$internalObject,
$contactId,
[],
false
);
}
++$totalUpdated;
} else {
$error = 'http status code '.$item['httpStatusCode'];
switch (true) {
case !empty($item['body'][0]['message']['message']):
$error = $item['body'][0]['message']['message'];
break;
case !empty($item['body']['message']):
$error = $item['body']['message'];
break;
}
$exception = new ApiErrorException($error);
if (!empty($item['referenceId']) && ('Contact' == $object || $object = 'Lead')) {
$exception->setContactId($item['referenceId']);
}
$this->logIntegrationError($exception);
++$totalErrored;
if ($integrationEntityId) {
$integrationEntity = $this->integrationEntityModel->getEntityByIdAndSetSyncDate($integrationEntityId, $this->getLastSyncDate());
if ($integrationEntity) {
if (isset($this->salesforceIdMapping[$contactId])) {
$integrationEntity->setIntegrationEntityId($this->salesforceIdMapping[$contactId]);
}
$this->persistIntegrationEntities[] = $integrationEntity;
}
} elseif (!empty($this->salesforceIdMapping[$contactId])) {
// Found in Salesforce so create a new record for it
$this->persistIntegrationEntities[] = $this->createIntegrationEntity(
$object,
$this->salesforceIdMapping[$contactId],
$internalObject,
$contactId,
[],
false
);
}
}
}
}
$this->cleanupFromSync();
return [$totalUpdated, $totalCreated];
}
/**
* @return array
*/
protected function getSalesforceObjectsByEmails($sfObject, $checkEmailsInSF, $requiredFieldString)
{
// Salesforce craps out with double quotes and unescaped single quotes
$findEmailsInSF = array_map(
fn ($lead): string => str_replace("'", "\'", $this->cleanPushData($lead['email'])),
$checkEmailsInSF
);
$fieldString = "'".implode("','", $findEmailsInSF)."'";
$queryUrl = $this->getQueryUrl();
$findQuery = ('Lead' === $sfObject)
?
'select Id, '.$requiredFieldString.', ConvertedContactId from Lead where isDeleted = false and Email in ('.$fieldString.')'
:
'select Id, '.$requiredFieldString.' from Contact where isDeleted = false and Email in ('.$fieldString.')';
return $this->getApiHelper()->request('query', ['q' => $findQuery], 'GET', false, null, $queryUrl);
}
protected function prepareMauticContactsToUpdate(
&$mauticData,
&$checkEmailsInSF,
&$processedLeads,
&$trackedContacts,
&$leadsToSync,
$objectFields,
$mauticLeadFieldString,
$sfEntityRecords,
$progress = null
) {
foreach ($sfEntityRecords['records'] as $sfKey => $sfEntityRecord) {
$skipObject = false;
$syncLead = false;
$sfObject = $sfEntityRecord['attributes']['type'];
if (!isset($sfEntityRecord['Email'])) {
// This is a record we don't recognize so continue
return;
}
$key = $this->getSyncKey($sfEntityRecord['Email']);
if (!isset($sfEntityRecord['Id']) || (!isset($checkEmailsInSF[$key]) && !isset($processedLeads[$key]))) {
// This is a record we don't recognize so continue
return;
}
$leadData = $processedLeads[$key] ?? $checkEmailsInSF[$key];
$contactId = $leadData['internal_entity_id'];
if (
isset($checkEmailsInSF[$key])
&& (
(
'Lead' === $sfObject && !empty($sfEntityRecord['ConvertedContactId'])
)
|| (
isset($checkEmailsInSF[$key]['integration_entity']) && 'Contact' === $sfObject
&& 'Lead' === $checkEmailsInSF[$key]['integration_entity']
)
)
) {
$deleted = false;
// This is a converted lead so remove the Lead entity leaving the Contact entity
if (!empty($trackedContacts['Lead'][$key])) {
$this->deleteIntegrationEntities[] = $this->em->getReference(
\Mautic\PluginBundle\Entity\IntegrationEntity::class,
$trackedContacts['Lead'][$key]
);
$deleted = true;
unset($trackedContacts['Lead'][$key]);
}
if ($contactEntity = $this->checkLeadIsContact($trackedContacts['Contact'], $key, $contactId, $mauticLeadFieldString)) {
// This Lead is already a Contact but was not updated for whatever reason
if (!$deleted) {
$this->deleteIntegrationEntities[] = $this->em->getReference(
\Mautic\PluginBundle\Entity\IntegrationEntity::class,
$checkEmailsInSF[$key]['id']
);
}
// Update the Contact record instead
$checkEmailsInSF[$key] = $contactEntity;
$trackedContacts['Contact'][$key] = $contactEntity['id'];
} else {
$id = (!empty($sfEntityRecord['ConvertedContactId'])) ? $sfEntityRecord['ConvertedContactId'] : $sfEntityRecord['Id'];
// This contact does not have a Contact record
$integrationEntity = $this->createIntegrationEntity(
'Contact',
$id,
'lead',
$contactId
);
$checkEmailsInSF[$key]['integration_entity'] = 'Contact';
$checkEmailsInSF[$key]['integration_entity_id'] = $id;
$checkEmailsInSF[$key]['id'] = $integrationEntity;
}
$this->logger->debug('SALESFORCE: Converted lead '.$sfEntityRecord['Email']);
// skip if this is a Lead object since it'll be handled with the Contact entry
if ('Lead' === $sfObject) {
unset($checkEmailsInSF[$key]);
unset($sfEntityRecords['records'][$sfKey]);
$skipObject = true;
}
}
if (!$skipObject) {
// Only progress if we have a unique Lead and not updating a Salesforce entry duplicate
if (!isset($processedLeads[$key])) {
if ($progress) {
$progress->advance();
}
// Mark that this lead has been processed
$leadData = $processedLeads[$key] = $checkEmailsInSF[$key];
}
// Keep track of Mautic ID to Salesforce ID for the integration table
$this->salesforceIdMapping[$contactId] = (!empty($sfEntityRecord['ConvertedContactId'])) ? $sfEntityRecord['ConvertedContactId']
: $sfEntityRecord['Id'];
$leadEntity = $this->em->getReference(Lead::class, $leadData['internal_entity_id']);
if ($updateLead = $this->buildCompositeBody(
$mauticData,
$objectFields[$sfObject],
$sfObject,
$leadData,
$sfEntityRecord['Id'],
$sfEntityRecord
)
) {
// Get the lead entity
/* @var Lead $leadEntity */
foreach ($updateLead as $mauticField => $sfValue) {
$leadEntity->addUpdatedField($mauticField, $sfValue);
}
$syncLead = !empty($leadEntity->getChanges(true));
}
// Validate if we have a company for this Mautic contact
if (!empty($sfEntityRecord['Company'])
&& $sfEntityRecord['Company'] !== $this->translator->trans(
'mautic.integration.form.lead.unknown'
)
) {
$company = IdentifyCompanyHelper::identifyLeadsCompany(
['company' => $sfEntityRecord['Company']],
null,
$this->companyModel
);
if (!empty($company[2])) {
$syncLead = $this->companyModel->addLeadToCompany($company[2], $leadEntity);
$this->em->detach($company[2]);
}
}
if ($syncLead) {
$leadsToSync[] = $leadEntity;
} else {
$this->em->detach($leadEntity);
}
}
unset($checkEmailsInSF[$key]);
}
}
protected function prepareMauticContactsToCreate(
&$mauticData,
&$checkEmailsInSF,
&$processedLeads,
$objectFields
) {
foreach ($checkEmailsInSF as $key => $lead) {
if (!empty($lead['integration_entity_id'])) {
if ($this->buildCompositeBody(
$mauticData,
$objectFields[$lead['integration_entity']],
$lead['integration_entity'],
$lead,
$lead['integration_entity_id']
)
) {
$this->logger->debug('SALESFORCE: Contact has existing ID so updating '.$lead['email']);
}
} else {
$this->buildCompositeBody(
$mauticData,
$objectFields['Lead'],
'Lead',
$lead
);
}
$processedLeads[$key] = $checkEmailsInSF[$key];
unset($checkEmailsInSF[$key]);
}
}
/**
* @param int $totalUpdated
* @param int $totalCreated
* @param int $totalErrored
*/
protected function makeCompositeRequest($mauticData, &$totalUpdated = 0, &$totalCreated = 0, &$totalErrored = 0)
{
if (empty($mauticData)) {
return;
}
/** @var SalesforceApi $apiHelper */
$apiHelper = $this->getApiHelper();
// We can only send 25 at a time
$request = [];
$request['allOrNone'] = 'false';
$chunked = array_chunk($mauticData, 25);
foreach ($chunked as $chunk) {
// We can only submit 25 at a time
if ($chunk) {
$request['compositeRequest'] = $chunk;
$result = $apiHelper->syncMauticToSalesforce($request);
$this->logger->debug('SALESFORCE: Sync Composite '.var_export($request, true));
$this->processCompositeResponse($result['compositeResponse'], $totalUpdated, $totalCreated, $totalErrored);
}
}
}
/**
* @return bool|mixed|string
*/
protected function setContactToSync(&$checkEmailsInSF, $lead)
{
$key = $this->getSyncKey($lead['email']);
if (isset($checkEmailsInSF[$key])) {
// this is a duplicate in Mautic
$this->mauticDuplicates[$lead['internal_entity_id']] = 'lead-duplicate';
return false;
}
$checkEmailsInSF[$key] = $lead;
return $key;
}
/**
* @return int
*/
protected function getSalesforceSyncLimit($currentContactList, $limit)
{
return $limit - count($currentContactList);
}
/**
* @return array|bool
*/
protected function checkLeadIsContact(&$trackedContacts, $email, $contactId, $leadFields)
{
if (empty($trackedContacts[$email])) {
// Check if there's an existing entry
return $this->getIntegrationEntityRepository()->getIntegrationEntity(
$this->getName(),
'Contact',
'lead',
$contactId,
$leadFields
);
}
return false;
}
/**
* @param array $objects
*
* @return array
*/
protected function cleanPriorityFields($fieldsToUpdate, $objects = null)
{
if (null === $objects) {
$objects = ['Lead', 'Contact'];
}
if (isset($fieldsToUpdate['leadFields'])) {
// Pass in the whole config
$fields = $fieldsToUpdate;
} else {
$fields = array_flip($fieldsToUpdate);
}
return $this->prepareFieldsForSync($fields, $fieldsToUpdate, $objects);
}
protected function mapContactDataForPush(Lead $lead, $config): array
{
$fields = array_keys($config['leadFields'] ?? []);
$fieldsToUpdateInSf = $this->getPriorityFieldsForIntegration($config);
$fieldMapping = [
'Lead' => [],
'Contact' => [],
];
$mappedData = [
'Lead' => [],
'Contact' => [],
];
foreach (['Lead', 'Contact'] as $object) {
if (isset($config['objects']) && false !== array_search($object, $config['objects'])) {
$fieldMapping[$object]['create'] = $this->prepareFieldsForSync($config['leadFields'] ?? [], $fields, $object);
$fieldMapping[$object]['update'] = isset($fieldsToUpdateInSf[$object]) ? array_intersect_key(
$fieldMapping[$object]['create'],
$fieldsToUpdateInSf[$object]
) : [];
// Create an update and
$mappedData[$object]['create'] = $this->populateLeadData(
$lead,
[
'leadFields' => $fieldMapping[$object]['create'], // map with all fields available
'object' => $object,
'feature_settings' => [
'objects' => $config['objects'],
],
]
);
if (isset($mappedData[$object]['create']['Id'])) {
unset($mappedData[$object]['create']['Id']);
}
$this->amendLeadDataBeforePush($mappedData[$object]['create']);
// Set the update fields
$mappedData[$object]['update'] = array_intersect_key($mappedData[$object]['create'], $fieldMapping[$object]['update']);
}
}
return $mappedData;
}
protected function mapCompanyDataForPush(Company $company, $config): array
{
$object = 'company';
$entity = [];
$mappedData = [
$object => [],
];
if (isset($config['objects']) && false !== array_search($object, $config['objects'])) {
$fieldKeys = array_keys($config['companyFields']);
$fieldsToCreate = $this->prepareFieldsForSync($config['companyFields'], $fieldKeys, 'Account');
$fieldsToUpdateInSf = $this->getPriorityFieldsForIntegration($config, 'Account', 'mautic_company');
$fieldMapping[$object] = [
'update' => !empty($fieldsToUpdateInSf) ? array_intersect_key($fieldsToCreate, $fieldsToUpdateInSf) : [],
'create' => $fieldsToCreate,
];
$entity['primaryCompany'] = $company->getProfileFields();
// Create an update and
$mappedData[$object]['create'] = $this->populateCompanyData(
$entity,
[
'companyFields' => $fieldMapping[$object]['create'], // map with all fields available
'object' => $object,
'feature_settings' => [
'objects' => $config['objects'],
],
]
);
if (isset($mappedData[$object]['create']['Id'])) {
unset($mappedData[$object]['create']['Id']);
}
$this->amendLeadDataBeforePush($mappedData[$object]['create']);
// Set the update fields
$mappedData[$object]['update'] = array_intersect_key($mappedData[$object]['create'], $fieldMapping[$object]['update']);
}
return $mappedData;
}
public function amendLeadDataBeforePush(&$mappedData): void
{
// normalize for multiselect field
foreach ($mappedData as &$data) {
if (is_string($data)) {
$data = str_replace('|', ';', $data);
}
}
$mappedData = StateValidationHelper::validate($mappedData);
}
/**
* @param string $object
*
* @return array
*/
public function getFieldsForQuery($object)
{
$fields = $this->getIntegrationSettings()->getFeatureSettings();
switch ($object) {
case 'company':
case 'Account':
$fields = array_keys(array_filter($fields['companyFields']));
break;
default:
$mixedFields = array_filter($fields['leadFields'] ?? []);
$fields = [];
foreach ($mixedFields as $sfField => $mField) {
if (str_contains($sfField, '__'.$object)) {
$fields[] = str_replace('__'.$object, '', $sfField);
}
if (str_contains($sfField, '-'.$object)) {
$fields[] = str_replace('-'.$object, '', $sfField);
}
}
if (!in_array('HasOptedOutOfEmail', $fields)) {
$fields[] = 'HasOptedOutOfEmail';
}
}
return $fields;
}
/**
* @param string $sfObject
* @param string $sfFieldString
*
* @return mixed
*
* @throws ApiErrorException
*/
public function getDncHistory($sfObject, $sfFieldString)
{
return $this->getDoNotContactHistory($sfObject, $sfFieldString, 'DESC');
}
public function getDoNotContactHistory(string $object, string $ids, string $order = 'DESC'): mixed
{
// get last modified date for do not contact in Salesforce
$query = sprintf('Select
Field,
%sId,
CreatedDate,
isDeleted,
NewValue
from
%sHistory
where
Field = \'HasOptedOutOfEmail\'
and %sId IN (%s)
ORDER BY CreatedDate %s', $object, $object, $object, $ids, $order);
$url = $this->getQueryUrl();
return $this->getApiHelper()->request('query', ['q' => $query], 'GET', false, null, $url);
}
/**
* Update the record in each system taking the last modified record.
*
* @param string $channel
* @param string $sfObject
*
* @throws ApiErrorException
*/
public function pushLeadDoNotContactByDate($channel, &$sfRecords, $sfObject, $params = []): void
{
$filters = [];
$leadIds = [];
$DNCCreatedContacts = [];
if (empty($sfRecords) || !isset($sfRecords['mauticContactIsContactableByEmail']) && !$this->updateDncByDate()) {
return;
}
foreach ($sfRecords as $record) {
if (empty($record['integration_entity_id'])) {
continue;
}
$leadIds[$record['internal_entity_id']] = $record['integration_entity_id'];
$leadEmails[$record['internal_entity_id']] = $record['email'];
if (isset($record['opted_out']) && $record['opted_out'] && isset($record['is_new']) && $record['is_new']) {
$DNCCreatedContacts[] = $record['internal_entity_id'];
}
}
$sfFieldString = "'".implode("','", $leadIds)."'";
$historySF = $this->getDoNotContactHistory($sfObject, $sfFieldString, 'ASC');
if (count($DNCCreatedContacts)) {
$this->updateMauticDNC($DNCCreatedContacts, true);
}
// if there is no records of when it was modified in SF then just exit
if (empty($historySF['records'])) {
return;
}
// get last modified date for donot contact in Mautic
$auditLogRepo = $this->em->getRepository(\Mautic\CoreBundle\Entity\AuditLog::class);
$filters['search'] = 'dnc_channel_status%'.$channel;
$lastModifiedDNCDate = $auditLogRepo->getAuditLogsForLeads(array_flip($leadIds), $filters, ['dateAdded', 'DESC'], $params['start']);
$trackedIds = [];
foreach ($historySF['records'] as $sfModifiedDNC) {
// if we have no history in Mautic, then update the Mautic record
if (empty($lastModifiedDNCDate)) {
$leads = array_flip($leadIds);
$leadId = $leads[$sfModifiedDNC[$sfObject.'Id']];
$this->updateMauticDNC($leadId, $sfModifiedDNC['NewValue']);
$key = $this->getSyncKey($leadEmails[$leadId]);
unset($sfRecords[$key]['mauticContactIsContactableByEmail']);
continue;
}
foreach ($lastModifiedDNCDate as $logs) {
$leadId = $logs['objectId'];
if (strtotime($logs['dateAdded']->format('c')) > strtotime($sfModifiedDNC['CreatedDate'])) {
$trackedIds[] = $leadId;
}
if ((isset($leadIds[$leadId]) && $leadIds[$leadId] == $sfModifiedDNC[$sfObject.'Id'])
&& (strtotime($sfModifiedDNC['CreatedDate']) > strtotime($logs['dateAdded']->format('c'))) && !in_array($leadId, $trackedIds)) {
// SF was updated last so update Mautic record
$key = $this->getSyncKey($leadEmails[$leadId]);
unset($sfRecords[$key]['mauticContactIsContactableByEmail']);
$this->updateMauticDNC($leadId, $sfModifiedDNC['NewValue']);
$trackedIds[] = $leadId;
break;
}
}
}
}
/**
* @param int|int[] $leadId
* @param bool $newDncValue
*/
private function updateMauticDNC($leadId, $newDncValue): void
{
$leadIds = is_array($leadId) ? $leadId : [$leadId];
foreach ($leadIds as $leadId) {
$lead = $this->leadModel->getEntity($leadId);
if (true == $newDncValue) {
$this->doNotContact->addDncForContact($lead->getId(), 'email', DoNotContact::MANUAL, 'Set by Salesforce', true, true, true);
} elseif (false == $newDncValue) {
$this->doNotContact->removeDncForContact($lead->getId(), 'email', true);
}
}
}
/**
* @param array $params
*
* @return mixed[]
*/
public function pushCompanies($params = []): array
{
$limit = $params['limit'] ?? 100;
[$fromDate, $toDate] = $this->getSyncTimeframeDates($params);
$config = $this->mergeConfigToFeatureSettings($params);
$integrationEntityRepo = $this->getIntegrationEntityRepository();
if (!isset($config['companyFields'])) {
return [0, 0, 0, 0];
}
$totalUpdated = 0;
$totalCreated = 0;
$totalErrors = 0;
$sfObject = 'Account';
// all available fields in Salesforce for Account
$availableFields = $this->getAvailableLeadFields(['feature_settings' => ['objects' => [$sfObject]]]);
// get company fields from Mautic that have been mapped
$mauticCompanyFieldString = implode(', l.', $config['companyFields']);
$mauticCompanyFieldString = 'l.'.$mauticCompanyFieldString;
$fieldKeys = array_keys($config['companyFields']);
$fieldsToCreate = $this->prepareFieldsForSync($config['companyFields'], $fieldKeys, $sfObject);
$fieldsToUpdateInSf = $this->getPriorityFieldsForIntegration($config, $sfObject, 'mautic_company');
$objectFields['company'] = [
'update' => !empty($fieldsToUpdateInSf) ? array_intersect_key($fieldsToCreate, $fieldsToUpdateInSf) : [],
'create' => $fieldsToCreate,
];
[$fields, $string] = $this->getRequiredFieldString(
$config,
$availableFields,
'company'
);
$objectFields['company']['required'] = [
'fields' => $fields,
'string' => $string,
];
if (empty($objectFields)) {
return [0, 0, 0, 0];
}
$originalLimit = $limit;
$progress = false;
// Get a total number of companies to be updated and/or created for the progress counter
$totalToUpdate = array_sum(
$integrationEntityRepo->findLeadsToUpdate(
'Salesforce',
'company',
$mauticCompanyFieldString,
false,
$fromDate,
$toDate,
$sfObject,
[]
)
);
$totalToCreate = $integrationEntityRepo->findLeadsToCreate(
'Salesforce',
$mauticCompanyFieldString,
false,
$fromDate,
$toDate,
'company'
);
$totalCount = $totalToProcess = $totalToCreate + $totalToUpdate;
if (defined('IN_MAUTIC_CONSOLE')) {
// start with update
if ($totalToUpdate + $totalToCreate) {
$output = new ConsoleOutput();
$output->writeln("About $totalToUpdate to update and about $totalToCreate to create/update");
$progress = new ProgressBar($output, $totalCount);
}
}
$noMoreUpdates = false;
while ($totalCount > 0) {
$limit = $originalLimit;
$mauticData = [];
$checkCompaniesInSF = [];
$companiesToSync = [];
$processedCompanies = [];
// Process the updates
if (!$noMoreUpdates) {
$noMoreUpdates = $this->getMauticRecordsToUpdate(
$checkCompaniesInSF,
$mauticCompanyFieldString,
$sfObject,
$limit,
$fromDate,
$toDate,
$totalCount,
'company'
);
if ($limit) {
// Mainly done for test mocking purposes
$limit = $this->getSalesforceSyncLimit($checkCompaniesInSF, $limit);
}
}
// If there is still room - grab Mautic companies to create if the Lead object is enabled
$sfEntityRecords = [];
if ((null === $limit || $limit > 0) && !empty($mauticCompanyFieldString)) {
$this->getMauticEntitesToCreate(
$checkCompaniesInSF,
$mauticCompanyFieldString,
$limit,
$fromDate,
$toDate,
$totalCount,
$progress
);
}
if ($checkCompaniesInSF) {
$sfEntityRecords = $this->getSalesforceAccountsByName($checkCompaniesInSF, implode(',', array_keys($config['companyFields'])));
if (!isset($sfEntityRecords['records'])) {
// Something is wrong so throw an exception to prevent creating a bunch of new companies
$this->cleanupFromSync(
$companiesToSync,
json_encode($sfEntityRecords)
);
}
}
// We're done
if (!$checkCompaniesInSF) {
break;
}
if (!empty($sfEntityRecords) and isset($sfEntityRecords['records'])) {
$this->prepareMauticCompaniesToUpdate(
$mauticData,
$checkCompaniesInSF,
$processedCompanies,
$companiesToSync,
$objectFields,
$sfEntityRecords,
$progress
);
}
// Only create left over if Lead object is enabled in integration settings
if ($checkCompaniesInSF) {
$this->prepareMauticCompaniesToCreate(
$mauticData,
$checkCompaniesInSF,
$processedCompanies,
$objectFields
);
}
// Persist pending changes
$this->cleanupFromSync($companiesToSync);
$this->makeCompositeRequest($mauticData, $totalUpdated, $totalCreated, $totalErrors);
// Stop gap - if 100% let's kill the script
if ($progress && $progress->getProgressPercent() >= 1) {
break;
}
}
if ($progress) {
$progress->finish();
$output->writeln('');
}
$this->logger->debug('SALESFORCE: '.$this->getApiHelper()->getRequestCounter().' API requests made for pushCompanies');
// Assume that those not touched are ignored due to not having matching fields, duplicates, etc
$totalIgnored = $totalToProcess - ($totalUpdated + $totalCreated + $totalErrors);
if ($totalIgnored < 0) { // this could have been marked as deleted so it was not pushed
$totalIgnored = $totalIgnored * -1;
}
return [$totalUpdated, $totalCreated, $totalErrors, $totalIgnored];
}
protected function prepareMauticCompaniesToUpdate(
&$mauticData,
&$checkCompaniesInSF,
&$processedCompanies,
&$companiesToSync,
$objectFields,
$sfEntityRecords,
$progress = null
) {
foreach ($sfEntityRecords['records'] as $sfEntityRecord) {
$syncCompany = false;
$update = false;
$sfObject = $sfEntityRecord['attributes']['type'];
if (!isset($sfEntityRecord['Name'])) {
// This is a record we don't recognize so continue
return;
}
$key = $sfEntityRecord['Id'];
if (!isset($sfEntityRecord['Id'])) {
// This is a record we don't recognize so continue
return;
}
$id = $sfEntityRecord['Id'];
if (isset($checkCompaniesInSF[$key])) {
$companyData = $processedCompanies[$key] ?? $checkCompaniesInSF[$key];
$update = true;
} else {
foreach ($checkCompaniesInSF as $mauticKey => $mauticCompanies) {
$key = $mauticKey;
if (isset($mauticCompanies['companyname']) && $mauticCompanies['companyname'] == $sfEntityRecord['Name']) {
$companyData = $processedCompanies[$key] ?? $checkCompaniesInSF[$key];
$companyId = $companyData['internal_entity_id'];
$integrationEntity = $this->createIntegrationEntity(
$sfObject,
$id,
'company',
$companyId
);
$checkCompaniesInSF[$key]['integration_entity'] = $sfObject;
$checkCompaniesInSF[$key]['integration_entity_id'] = $id;
$checkCompaniesInSF[$key]['id'] = $integrationEntity->getId();
$update = true;
}
}
}
if (!$update) {
return;
}
if (!isset($processedCompanies[$key])) {
if ($progress) {
$progress->advance();
}
// Mark that this lead has been processed
$companyData = $processedCompanies[$key] = $checkCompaniesInSF[$key];
}
$companyEntity = $this->em->getReference(Company::class, $companyData['internal_entity_id']);
if ($updateCompany = $this->buildCompositeBody(
$mauticData,
$objectFields['company'],
$sfObject,
$companyData,
$sfEntityRecord['Id'],
$sfEntityRecord
)
) {
// Get the company entity
/* @var Lead $leadEntity */
foreach ($updateCompany as $mauticField => $sfValue) {
$companyEntity->addUpdatedField($mauticField, $sfValue);
}
$syncCompany = !empty($companyEntity->getChanges(true));
}
if ($syncCompany) {
$companiesToSync[] = $companyEntity;
} else {
$this->em->detach($companyEntity);
}
unset($checkCompaniesInSF[$key]);
}
}
protected function prepareMauticCompaniesToCreate(
&$mauticData,
&$checkCompaniesInSF,
&$processedCompanies,
$objectFields
) {
foreach ($checkCompaniesInSF as $key => $company) {
if (!empty($company['integration_entity_id']) and array_key_exists($key, $processedCompanies)) {
if ($this->buildCompositeBody(
$mauticData,
$objectFields['company'],
$company['integration_entity'],
$company,
$company['integration_entity_id']
)
) {
$this->logger->debug('SALESFORCE: Company has existing ID so updating '.$company['integration_entity_id']);
}
} else {
$this->buildCompositeBody(
$mauticData,
$objectFields['company'],
'Account',
$company
);
}
$processedCompanies[$key] = $checkCompaniesInSF[$key];
unset($checkCompaniesInSF[$key]);
}
}
protected function getMauticRecordsToUpdate(
&$checkIdsInSF,
$mauticEntityFieldString,
&$sfObject,
$limit,
$fromDate,
$toDate,
&$totalCount,
$internalEntity
): bool {
// Fetch them separately so we can determine if Leads are already Contacts
$toUpdate = $this->getIntegrationEntityRepository()->findLeadsToUpdate(
'Salesforce',
$internalEntity,
$mauticEntityFieldString,
$limit,
$fromDate,
$toDate,
$sfObject
)[$sfObject];
$toUpdateCount = count($toUpdate);
$totalCount -= $toUpdateCount;
foreach ($toUpdate as $entity) {
if (!empty($entity['integration_entity_id'])) {
$checkIdsInSF[$entity['integration_entity_id']] = $entity;
}
}
return 0 === $toUpdateCount;
}
protected function getMauticEntitesToCreate(
&$checkIdsInSF,
$mauticCompanyFieldString,
$limit,
$fromDate,
$toDate,
&$totalCount,
$progress = null
) {
$integrationEntityRepo = $this->getIntegrationEntityRepository();
$entitiesToCreate = $integrationEntityRepo->findLeadsToCreate(
'Salesforce',
$mauticCompanyFieldString,
$limit,
$fromDate,
$toDate,
'company'
);
$totalCount -= count($entitiesToCreate);
foreach ($entitiesToCreate as $entity) {
if (isset($entity['companyname'])) {
$checkIdsInSF[$entity['internal_entity_id']] = $entity;
} elseif ($progress) {
$progress->advance();
}
}
}
/**
* @throws ApiErrorException
* @throws ORMException
* @throws \Exception
*/
protected function getSalesforceAccountsByName(&$checkIdsInSF, $requiredFieldString): array
{
$searchForIds = [];
$searchForNames = [];
foreach ($checkIdsInSF as $key => $company) {
if (!empty($company['integration_entity_id'])) {
$searchForIds[$key] = $company['integration_entity_id'];
continue;
}
if (!empty($company['companyname'])) {
$searchForNames[$key] = $company['companyname'];
}
}
$resultsByName = $this->getApiHelper()->getCompaniesByName($searchForNames, $requiredFieldString);
$resultsById = [];
if (!empty($searchForIds)) {
$resultsById = $this->getApiHelper()->getCompaniesById($searchForIds, $requiredFieldString);
// mark as deleleted
foreach ($resultsById['records'] as $sfId => $record) {
if (isset($record['IsDeleted']) && 1 == $record['IsDeleted']) {
if ($foundKey = array_search($record['Id'], $searchForIds)) {
$integrationEntity = $this->em->getReference(\Mautic\PluginBundle\Entity\IntegrationEntity::class, $checkIdsInSF[$foundKey]['id']);
$integrationEntity->setInternalEntity('company-deleted');
$this->persistIntegrationEntities[] = $integrationEntity;
unset($checkIdsInSF[$foundKey]);
}
unset($resultsById['records'][$sfId]);
}
}
}
$this->cleanupFromSync();
return array_merge($resultsByName, $resultsById);
}
public function getCompanyName($accountId, $field, $searchBy = 'Id')
{
$companyField = null;
$accountId = str_replace("'", "\'", $this->cleanPushData($accountId));
$companyQuery = 'Select Id, Name from Account where '.$searchBy.' = \''.$accountId.'\' and IsDeleted = false';
$contactCompany = $this->getApiHelper()->getLeads($companyQuery, 'Account');
if (!empty($contactCompany['records'])) {
foreach ($contactCompany['records'] as $company) {
if (!empty($company[$field])) {
$companyField = $company[$field];
break;
}
}
}
return $companyField;
}
public function getLeadDoNotContactByDate($channel, $matchedFields, $object, $lead, $sfData, $params = [])
{
if (isset($matchedFields['mauticContactIsContactableByEmail']) and true === $this->updateDncByDate()) {
$matchedFields['internal_entity_id'] = $lead->getId();
$matchedFields['integration_entity_id'] = $sfData['Id__'.$object];
$record[$lead->getEmail()] = $matchedFields;
$this->pushLeadDoNotContactByDate($channel, $record, $object, $params);
return $record[$lead->getEmail()];
}
return $matchedFields;
}
}