Spaces:
No application file
No application file
| 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; | |
| } | |
| } | |