1 | <?php |
||
2 | |||
3 | /* |
||
4 | * @copyright 2014 Mautic Contributors. All rights reserved |
||
5 | * @author Mautic |
||
6 | * |
||
7 | * @link http://mautic.org |
||
8 | * |
||
9 | * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html |
||
10 | */ |
||
11 | |||
12 | namespace MauticPlugin\MauticCrmBundle\Integration; |
||
13 | |||
14 | use Mautic\CoreBundle\Helper\InputHelper; |
||
15 | use Mautic\LeadBundle\Entity\Company; |
||
16 | use Mautic\LeadBundle\Entity\DoNotContact; |
||
17 | use Mautic\LeadBundle\Entity\Lead; |
||
18 | use Mautic\LeadBundle\Helper\IdentifyCompanyHelper; |
||
19 | use Mautic\PluginBundle\Entity\IntegrationEntity; |
||
20 | use Mautic\PluginBundle\Entity\IntegrationEntityRepository; |
||
21 | use Mautic\PluginBundle\Exception\ApiErrorException; |
||
22 | use MauticPlugin\MauticCrmBundle\Api\SalesforceApi; |
||
23 | use MauticPlugin\MauticCrmBundle\Integration\Salesforce\CampaignMember\Fetcher; |
||
24 | use MauticPlugin\MauticCrmBundle\Integration\Salesforce\CampaignMember\Organizer; |
||
25 | use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Exception\NoObjectsToFetchException; |
||
26 | use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Helper\StateValidationHelper; |
||
27 | use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\CampaignMember; |
||
28 | use MauticPlugin\MauticCrmBundle\Integration\Salesforce\ResultsPaginator; |
||
29 | use Symfony\Component\Console\Helper\ProgressBar; |
||
30 | use Symfony\Component\Console\Output\ConsoleOutput; |
||
31 | use Symfony\Component\Form\Extension\Core\Type\ChoiceType; |
||
32 | use Symfony\Component\Form\Extension\Core\Type\TextType; |
||
33 | use Symfony\Component\Form\FormBuilder; |
||
34 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; |
||
35 | |||
36 | /** |
||
37 | * Class SalesforceIntegration. |
||
38 | * |
||
39 | * @method SalesforceApi getApiHelper |
||
40 | */ |
||
41 | class SalesforceIntegration extends CrmAbstractIntegration |
||
42 | { |
||
43 | private $objects = [ |
||
44 | 'Lead', |
||
45 | 'Contact', |
||
46 | 'Account', |
||
47 | ]; |
||
48 | |||
49 | /** |
||
50 | * {@inheritdoc} |
||
51 | * |
||
52 | * @return string |
||
53 | */ |
||
54 | public function getName() |
||
55 | { |
||
56 | return 'Salesforce'; |
||
57 | } |
||
58 | |||
59 | /** |
||
60 | * Get the array key for clientId. |
||
61 | * |
||
62 | * @return string |
||
63 | */ |
||
64 | public function getClientIdKey() |
||
65 | { |
||
66 | return 'client_id'; |
||
67 | } |
||
68 | |||
69 | /** |
||
70 | * Get the array key for client secret. |
||
71 | * |
||
72 | * @return string |
||
73 | */ |
||
74 | public function getClientSecretKey() |
||
75 | { |
||
76 | return 'client_secret'; |
||
77 | } |
||
78 | |||
79 | /** |
||
80 | * Get the array key for the auth token. |
||
81 | * |
||
82 | * @return string |
||
83 | */ |
||
84 | public function getAuthTokenKey() |
||
85 | { |
||
86 | return 'access_token'; |
||
87 | } |
||
88 | |||
89 | /** |
||
90 | * {@inheritdoc} |
||
91 | * |
||
92 | * @return array |
||
93 | */ |
||
94 | public function getRequiredKeyFields() |
||
95 | { |
||
96 | return [ |
||
97 | 'client_id' => 'mautic.integration.keyfield.consumerid', |
||
98 | 'client_secret' => 'mautic.integration.keyfield.consumersecret', |
||
99 | ]; |
||
100 | } |
||
101 | |||
102 | /** |
||
103 | * Get the keys for the refresh token and expiry. |
||
104 | * |
||
105 | * @return array |
||
106 | */ |
||
107 | public function getRefreshTokenKeys() |
||
108 | { |
||
109 | return ['refresh_token', '']; |
||
110 | } |
||
111 | |||
112 | /** |
||
113 | * @return array |
||
114 | */ |
||
115 | public function getSupportedFeatures() |
||
116 | { |
||
117 | return ['push_lead', 'get_leads', 'push_leads']; |
||
118 | } |
||
119 | |||
120 | /** |
||
121 | * {@inheritdoc} |
||
122 | * |
||
123 | * @return string |
||
124 | */ |
||
125 | public function getAccessTokenUrl() |
||
126 | { |
||
127 | $config = $this->mergeConfigToFeatureSettings([]); |
||
128 | |||
129 | if (isset($config['sandbox'][0]) and 'sandbox' === $config['sandbox'][0]) { |
||
130 | return 'https://test.salesforce.com/services/oauth2/token'; |
||
131 | } |
||
132 | |||
133 | return 'https://login.salesforce.com/services/oauth2/token'; |
||
134 | } |
||
135 | |||
136 | /** |
||
137 | * {@inheritdoc} |
||
138 | * |
||
139 | * @return string |
||
140 | */ |
||
141 | public function getAuthenticationUrl() |
||
142 | { |
||
143 | $config = $this->mergeConfigToFeatureSettings([]); |
||
144 | |||
145 | if (isset($config['sandbox'][0]) and 'sandbox' === $config['sandbox'][0]) { |
||
146 | return 'https://test.salesforce.com/services/oauth2/authorize'; |
||
147 | } |
||
148 | |||
149 | return 'https://login.salesforce.com/services/oauth2/authorize'; |
||
150 | } |
||
151 | |||
152 | /** |
||
153 | * @return string |
||
154 | */ |
||
155 | public function getAuthScope() |
||
156 | { |
||
157 | return 'api refresh_token'; |
||
158 | } |
||
159 | |||
160 | /** |
||
161 | * @return string |
||
162 | */ |
||
163 | public function getApiUrl() |
||
164 | { |
||
165 | return sprintf('%s/services/data/v34.0/sobjects', $this->keys['instance_url']); |
||
166 | } |
||
167 | |||
168 | /** |
||
169 | * @return string |
||
170 | */ |
||
171 | public function getQueryUrl() |
||
172 | { |
||
173 | return sprintf('%s/services/data/v34.0', $this->keys['instance_url']); |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * @return string |
||
178 | */ |
||
179 | public function getCompositeUrl() |
||
180 | { |
||
181 | return sprintf('%s/services/data/v38.0', $this->keys['instance_url']); |
||
182 | } |
||
183 | |||
184 | /** |
||
185 | * {@inheritdoc} |
||
186 | * |
||
187 | * @param bool $inAuthorization |
||
188 | */ |
||
189 | public function getBearerToken($inAuthorization = false) |
||
190 | { |
||
191 | if (!$inAuthorization && isset($this->keys[$this->getAuthTokenKey()])) { |
||
192 | return $this->keys[$this->getAuthTokenKey()]; |
||
193 | } |
||
194 | |||
195 | return false; |
||
196 | } |
||
197 | |||
198 | /** |
||
199 | * {@inheritdoc} |
||
200 | * |
||
201 | * @return string |
||
202 | */ |
||
203 | public function getAuthenticationType() |
||
204 | { |
||
205 | return 'oauth2'; |
||
206 | } |
||
207 | |||
208 | /** |
||
209 | * {@inheritdoc} |
||
210 | * |
||
211 | * @return bool |
||
212 | */ |
||
213 | public function getDataPriority() |
||
214 | { |
||
215 | return true; |
||
216 | } |
||
217 | |||
218 | /** |
||
219 | * {@inheritdoc} |
||
220 | * |
||
221 | * @return bool |
||
222 | */ |
||
223 | public function updateDncByDate() |
||
224 | { |
||
225 | $featureSettings = $this->settings->getFeatureSettings(); |
||
226 | if (isset($featureSettings['updateDncByDate'][0]) && 'updateDncByDate' === $featureSettings['updateDncByDate'][0]) { |
||
227 | return true; |
||
228 | } |
||
229 | |||
230 | return false; |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * Get available company fields for choices in the config UI. |
||
235 | * |
||
236 | * @param array $settings |
||
237 | * |
||
238 | * @return array |
||
239 | */ |
||
240 | public function getFormCompanyFields($settings = []) |
||
241 | { |
||
242 | return $this->getFormFieldsByObject('company', $settings); |
||
243 | } |
||
244 | |||
245 | /** |
||
246 | * @param array $settings |
||
247 | * |
||
248 | * @return array|mixed |
||
249 | * |
||
250 | * @throws \Exception |
||
251 | */ |
||
252 | public function getFormLeadFields($settings = []) |
||
253 | { |
||
254 | $leadFields = $this->getFormFieldsByObject('Lead', $settings); |
||
255 | $contactFields = $this->getFormFieldsByObject('Contact', $settings); |
||
256 | |||
257 | return array_merge($leadFields, $contactFields); |
||
258 | } |
||
259 | |||
260 | /** |
||
261 | * @param array $settings |
||
262 | * |
||
263 | * @return array|mixed |
||
264 | * |
||
265 | * @throws \Exception |
||
266 | */ |
||
267 | public function getAvailableLeadFields($settings = []) |
||
268 | { |
||
269 | $silenceExceptions = (isset($settings['silence_exceptions'])) ? $settings['silence_exceptions'] : true; |
||
270 | $salesForceObjects = []; |
||
271 | |||
272 | if (isset($settings['feature_settings']['objects'])) { |
||
273 | $salesForceObjects = $settings['feature_settings']['objects']; |
||
274 | } else { |
||
275 | $salesForceObjects[] = 'Lead'; |
||
276 | } |
||
277 | |||
278 | $isRequired = function (array $field, $object) { |
||
279 | return |
||
280 | ('boolean' !== $field['type'] && empty($field['nillable']) && !in_array($field['name'], ['Status', 'Id', 'CreatedDate'])) || |
||
281 | ('Lead' == $object && in_array($field['name'], ['Company'])) || |
||
282 | (in_array($object, ['Lead', 'Contact']) && 'Email' === $field['name']); |
||
283 | }; |
||
284 | |||
285 | $salesFields = []; |
||
286 | try { |
||
287 | if (!empty($salesForceObjects) and is_array($salesForceObjects)) { |
||
288 | foreach ($salesForceObjects as $sfObject) { |
||
289 | if ('Account' === $sfObject) { |
||
290 | // Match SF object to Mautic's |
||
291 | $sfObject = 'company'; |
||
292 | } |
||
293 | |||
294 | if (isset($sfObject) and 'Activity' == $sfObject) { |
||
295 | continue; |
||
296 | } |
||
297 | |||
298 | $sfObject = trim($sfObject); |
||
299 | // Check the cache first |
||
300 | $settings['cache_suffix'] = $cacheSuffix = '.'.$sfObject; |
||
301 | if ($fields = parent::getAvailableLeadFields($settings)) { |
||
302 | if (('company' === $sfObject && isset($fields['Id'])) || isset($fields['Id__'.$sfObject])) { |
||
303 | $salesFields[$sfObject] = $fields; |
||
304 | |||
305 | continue; |
||
306 | } |
||
307 | } |
||
308 | |||
309 | if ($this->isAuthorized()) { |
||
310 | if (!isset($salesFields[$sfObject])) { |
||
311 | $fields = $this->getApiHelper()->getLeadFields($sfObject); |
||
312 | if (!empty($fields['fields'])) { |
||
313 | foreach ($fields['fields'] as $fieldInfo) { |
||
314 | if ((!$fieldInfo['updateable'] && (!$fieldInfo['calculated'] && !in_array($fieldInfo['name'], ['Id', 'IsDeleted', 'CreatedDate']))) |
||
315 | || !isset($fieldInfo['name']) |
||
316 | || (in_array( |
||
317 | $fieldInfo['type'], |
||
318 | ['reference'] |
||
319 | ) && 'AccountId' != $fieldInfo['name']) |
||
320 | ) { |
||
321 | continue; |
||
322 | } |
||
323 | switch ($fieldInfo['type']) { |
||
324 | case 'boolean': $type = 'boolean'; |
||
325 | break; |
||
326 | case 'datetime': $type = 'datetime'; |
||
327 | break; |
||
328 | case 'date': $type = 'date'; |
||
329 | break; |
||
330 | default: $type = 'string'; |
||
331 | } |
||
332 | if ('company' !== $sfObject) { |
||
333 | if ('AccountId' == $fieldInfo['name']) { |
||
334 | $fieldInfo['label'] = 'Company'; |
||
335 | } |
||
336 | $salesFields[$sfObject][$fieldInfo['name'].'__'.$sfObject] = [ |
||
337 | 'type' => $type, |
||
338 | 'label' => $sfObject.'-'.$fieldInfo['label'], |
||
339 | 'required' => $isRequired($fieldInfo, $sfObject), |
||
340 | 'group' => $sfObject, |
||
341 | 'optionLabel' => $fieldInfo['label'], |
||
342 | ]; |
||
343 | |||
344 | // CreateDate can be updatable just in Mautic |
||
345 | if (in_array($fieldInfo['name'], ['CreatedDate'])) { |
||
346 | $salesFields[$sfObject][$fieldInfo['name'].'__'.$sfObject]['update_mautic'] = 1; |
||
347 | } |
||
348 | } else { |
||
349 | $salesFields[$sfObject][$fieldInfo['name']] = [ |
||
350 | 'type' => $type, |
||
351 | 'label' => $fieldInfo['label'], |
||
352 | 'required' => $isRequired($fieldInfo, $sfObject), |
||
353 | ]; |
||
354 | } |
||
355 | } |
||
356 | |||
357 | $this->cache->set('leadFields'.$cacheSuffix, $salesFields[$sfObject]); |
||
358 | } |
||
359 | } |
||
360 | |||
361 | asort($salesFields[$sfObject]); |
||
362 | } |
||
363 | } |
||
364 | } |
||
365 | } catch (\Exception $e) { |
||
366 | $this->logIntegrationError($e); |
||
367 | |||
368 | if (!$silenceExceptions) { |
||
369 | throw $e; |
||
370 | } |
||
371 | } |
||
372 | |||
373 | return $salesFields; |
||
374 | } |
||
375 | |||
376 | /** |
||
377 | * {@inheritdoc} |
||
378 | * |
||
379 | * @param $section |
||
380 | * |
||
381 | * @return array |
||
382 | */ |
||
383 | public function getFormNotes($section) |
||
384 | { |
||
385 | if ('authorization' == $section) { |
||
386 | return ['mautic.salesforce.form.oauth_requirements', 'warning']; |
||
387 | } |
||
388 | |||
389 | return parent::getFormNotes($section); |
||
390 | } |
||
391 | |||
392 | /** |
||
393 | * @param $params |
||
394 | * |
||
395 | * @return mixed |
||
396 | */ |
||
397 | public function getFetchQuery($params) |
||
398 | { |
||
399 | return $params; |
||
400 | } |
||
401 | |||
402 | /** |
||
403 | * @param $data |
||
404 | * @param $object |
||
405 | * @param array $params |
||
406 | * |
||
407 | * @return array |
||
408 | */ |
||
409 | public function amendLeadDataBeforeMauticPopulate($data, $object, $params = []) |
||
410 | { |
||
411 | $updated = 0; |
||
412 | $created = 0; |
||
413 | $counter = 0; |
||
414 | $entity = null; |
||
415 | $detachClass = null; |
||
416 | $mauticObjectReference = null; |
||
417 | $integrationMapping = []; |
||
418 | |||
419 | if (isset($data['records']) and 'Activity' !== $object) { |
||
420 | foreach ($data['records'] as $record) { |
||
421 | $this->logger->debug('SALESFORCE: amendLeadDataBeforeMauticPopulate record '.var_export($record, true)); |
||
422 | if (isset($params['progress'])) { |
||
423 | $params['progress']->advance(); |
||
424 | } |
||
425 | |||
426 | $dataObject = []; |
||
427 | if (isset($record['attributes']['type']) && 'Account' == $record['attributes']['type']) { |
||
428 | $newName = ''; |
||
429 | } else { |
||
430 | $newName = '__'.$object; |
||
431 | } |
||
432 | |||
433 | foreach ($record as $key => $item) { |
||
434 | if (is_bool($item)) { |
||
435 | $dataObject[$key.$newName] = (int) $item; |
||
436 | } else { |
||
437 | $dataObject[$key.$newName] = $item; |
||
438 | } |
||
439 | } |
||
440 | |||
441 | if ($dataObject) { |
||
442 | $entity = false; |
||
443 | switch ($object) { |
||
444 | case 'Contact': |
||
445 | if (isset($dataObject['Email__Contact'])) { |
||
446 | // Sanitize email to make sure we match it |
||
447 | // correctly against mautic emails |
||
448 | $dataObject['Email__Contact'] = InputHelper::email($dataObject['Email__Contact']); |
||
449 | } |
||
450 | |||
451 | //get company from account id and assign company name |
||
452 | if (isset($dataObject['AccountId__'.$object])) { |
||
453 | $companyName = $this->getCompanyName($dataObject['AccountId__'.$object], 'Name'); |
||
454 | |||
455 | if ($companyName) { |
||
456 | $dataObject['AccountId__'.$object] = $companyName; |
||
457 | } else { |
||
458 | unset($dataObject['AccountId__'.$object]); //no company was found in Salesforce |
||
459 | } |
||
460 | } |
||
461 | // no break |
||
462 | case 'Lead': |
||
463 | // Set owner so that it maps if configured to do so |
||
464 | if (!empty($dataObject['Owner__Lead']['Email'])) { |
||
465 | $dataObject['owner_email'] = $dataObject['Owner__Lead']['Email']; |
||
466 | } elseif (!empty($dataObject['Owner__Contact']['Email'])) { |
||
467 | $dataObject['owner_email'] = $dataObject['Owner__Contact']['Email']; |
||
468 | } |
||
469 | |||
470 | if (isset($dataObject['Email__Lead'])) { |
||
471 | // Sanitize email to make sure we match it |
||
472 | // correctly against mautic_leads emails |
||
473 | $dataObject['Email__Lead'] = InputHelper::email($dataObject['Email__Lead']); |
||
474 | } |
||
475 | |||
476 | // normalize multiselect field |
||
477 | foreach ($dataObject as &$dataO) { |
||
478 | if (is_string($dataO)) { |
||
479 | $dataO = str_replace(';', '|', $dataO); |
||
480 | } |
||
481 | } |
||
482 | $entity = $this->getMauticLead($dataObject, true, null, null, $object); |
||
483 | $mauticObjectReference = 'lead'; |
||
484 | $detachClass = Lead::class; |
||
485 | |||
486 | break; |
||
487 | case 'Account': |
||
488 | $entity = $this->getMauticCompany($dataObject, 'Account'); |
||
489 | $mauticObjectReference = 'company'; |
||
490 | $detachClass = Company::class; |
||
491 | |||
492 | break; |
||
493 | default: |
||
494 | $this->logIntegrationError( |
||
495 | new \Exception( |
||
496 | sprintf('Received an unexpected object without an internalObjectReference "%s"', $object) |
||
497 | ) |
||
498 | ); |
||
499 | break; |
||
500 | } |
||
501 | |||
502 | if (!$entity) { |
||
503 | continue; |
||
504 | } |
||
505 | |||
506 | $integrationMapping[$entity->getId()] = [ |
||
507 | 'entity' => $entity, |
||
508 | 'integration_entity_id' => $record['Id'], |
||
509 | ]; |
||
510 | |||
511 | if (method_exists($entity, 'isNewlyCreated') && $entity->isNewlyCreated()) { |
||
512 | ++$created; |
||
513 | } else { |
||
514 | ++$updated; |
||
515 | } |
||
516 | |||
517 | ++$counter; |
||
518 | |||
519 | if ($counter >= 100) { |
||
520 | // Persist integration entities |
||
521 | $this->buildIntegrationEntities($integrationMapping, $object, $mauticObjectReference, $params); |
||
522 | $counter = 0; |
||
523 | $this->em->clear($detachClass); |
||
524 | $integrationMapping = []; |
||
525 | } |
||
526 | } |
||
527 | } |
||
528 | |||
529 | if (count($integrationMapping)) { |
||
530 | // Persist integration entities |
||
531 | $this->buildIntegrationEntities($integrationMapping, $object, $mauticObjectReference, $params); |
||
532 | $this->em->clear($detachClass); |
||
533 | } |
||
534 | |||
535 | unset($data['records']); |
||
536 | $this->logger->debug('SALESFORCE: amendLeadDataBeforeMauticPopulate response '.var_export($data, true)); |
||
537 | |||
538 | unset($data); |
||
539 | $this->persistIntegrationEntities = []; |
||
540 | unset($dataObject); |
||
541 | } |
||
542 | |||
543 | return [$updated, $created]; |
||
544 | } |
||
545 | |||
546 | /** |
||
547 | * @param FormBuilder $builder |
||
548 | * @param array $data |
||
549 | * @param string $formArea |
||
550 | */ |
||
551 | public function appendToForm(&$builder, $data, $formArea) |
||
552 | { |
||
553 | if ('features' == $formArea) { |
||
554 | $builder->add( |
||
555 | 'sandbox', |
||
556 | ChoiceType::class, |
||
557 | [ |
||
558 | 'choices' => [ |
||
559 | 'mautic.salesforce.sandbox' => 'sandbox', |
||
560 | ], |
||
561 | 'expanded' => true, |
||
562 | 'multiple' => true, |
||
563 | 'label' => 'mautic.salesforce.form.sandbox', |
||
564 | 'label_attr' => ['class' => 'control-label'], |
||
565 | 'placeholder' => false, |
||
566 | 'required' => false, |
||
567 | 'attr' => [ |
||
568 | 'onclick' => 'Mautic.postForm(mQuery(\'form[name="integration_details"]\'),\'\');', |
||
569 | ], |
||
570 | ] |
||
571 | ); |
||
572 | |||
573 | $builder->add( |
||
574 | 'updateOwner', |
||
575 | ChoiceType::class, |
||
576 | [ |
||
577 | 'choices' => [ |
||
578 | 'mautic.salesforce.updateOwner' => 'updateOwner', |
||
579 | ], |
||
580 | 'expanded' => true, |
||
581 | 'multiple' => true, |
||
582 | 'label' => 'mautic.salesforce.form.updateOwner', |
||
583 | 'label_attr' => ['class' => 'control-label'], |
||
584 | 'placeholder' => false, |
||
585 | 'required' => false, |
||
586 | 'attr' => [ |
||
587 | 'onclick' => 'Mautic.postForm(mQuery(\'form[name="integration_details"]\'),\'\');', |
||
588 | ], |
||
589 | ] |
||
590 | ); |
||
591 | $builder->add( |
||
592 | 'updateBlanks', |
||
593 | ChoiceType::class, |
||
594 | [ |
||
595 | 'choices' => [ |
||
596 | 'mautic.integrations.blanks' => 'updateBlanks', |
||
597 | ], |
||
598 | 'expanded' => true, |
||
599 | 'multiple' => true, |
||
600 | 'label' => 'mautic.integrations.form.blanks', |
||
601 | 'label_attr' => ['class' => 'control-label'], |
||
602 | 'placeholder' => false, |
||
603 | 'required' => false, |
||
604 | ] |
||
605 | ); |
||
606 | $builder->add( |
||
607 | 'updateDncByDate', |
||
608 | ChoiceType::class, |
||
609 | [ |
||
610 | 'choices' => [ |
||
611 | 'mautic.integrations.update.dnc.by.date' => 'updateDncByDate', |
||
612 | ], |
||
613 | 'expanded' => true, |
||
614 | 'multiple' => true, |
||
615 | 'label' => 'mautic.integrations.form.update.dnc.by.date.label', |
||
616 | 'label_attr' => ['class' => 'control-label'], |
||
617 | 'placeholder' => false, |
||
618 | 'required' => false, |
||
619 | ] |
||
620 | ); |
||
621 | |||
622 | $builder->add( |
||
623 | 'objects', |
||
624 | ChoiceType::class, |
||
625 | [ |
||
626 | 'choices' => [ |
||
627 | 'mautic.salesforce.object.lead' => 'Lead', |
||
628 | 'mautic.salesforce.object.contact' => 'Contact', |
||
629 | 'mautic.salesforce.object.company' => 'company', |
||
630 | 'mautic.salesforce.object.activity' => 'Activity', |
||
631 | ], |
||
632 | 'expanded' => true, |
||
633 | 'multiple' => true, |
||
634 | 'label' => 'mautic.salesforce.form.objects_to_pull_from', |
||
635 | 'label_attr' => ['class' => ''], |
||
636 | 'placeholder' => false, |
||
637 | 'required' => false, |
||
638 | ] |
||
639 | ); |
||
640 | |||
641 | $builder->add( |
||
642 | 'activityEvents', |
||
643 | ChoiceType::class, |
||
644 | [ |
||
645 | 'choices' => array_flip($this->leadModel->getEngagementTypes()), // Choice type expects labels as keys |
||
646 | 'label' => 'mautic.salesforce.form.activity_included_events', |
||
647 | 'label_attr' => [ |
||
648 | 'class' => 'control-label', |
||
649 | 'data-toggle' => 'tooltip', |
||
650 | 'title' => $this->translator->trans('mautic.salesforce.form.activity.events.tooltip'), |
||
651 | ], |
||
652 | 'multiple' => true, |
||
653 | 'empty_data' => ['point.gained', 'form.submitted', 'email.read'], // BC with pre 2.11.0 |
||
654 | 'required' => false, |
||
655 | ] |
||
656 | ); |
||
657 | |||
658 | $builder->add( |
||
659 | 'namespace', |
||
660 | TextType::class, |
||
661 | [ |
||
662 | 'label' => 'mautic.salesforce.form.namespace_prefix', |
||
663 | 'label_attr' => ['class' => 'control-label'], |
||
664 | 'attr' => ['class' => 'form-control'], |
||
665 | 'required' => false, |
||
666 | ] |
||
667 | ); |
||
668 | } |
||
669 | } |
||
670 | |||
671 | /** |
||
672 | * @param array $fields |
||
673 | * @param array $keys |
||
674 | * @param mixed $object |
||
675 | * |
||
676 | * @return array |
||
677 | */ |
||
678 | public function prepareFieldsForSync($fields, $keys, $object = null) |
||
679 | { |
||
680 | $leadFields = []; |
||
681 | if (null === $object) { |
||
682 | $object = 'Lead'; |
||
683 | } |
||
684 | |||
685 | $objects = (!is_array($object)) ? [$object] : $object; |
||
686 | if (is_string($object) && 'Account' === $object) { |
||
687 | return isset($fields['companyFields']) ? $fields['companyFields'] : $fields; |
||
688 | } |
||
689 | |||
690 | if (isset($fields['leadFields'])) { |
||
691 | $fields = $fields['leadFields']; |
||
692 | $keys = array_keys($fields); |
||
693 | } |
||
694 | |||
695 | foreach ($objects as $obj) { |
||
696 | if (!isset($leadFields[$obj])) { |
||
697 | $leadFields[$obj] = []; |
||
698 | } |
||
699 | |||
700 | foreach ($keys as $key) { |
||
701 | if (strpos($key, '__'.$obj)) { |
||
702 | $newKey = str_replace('__'.$obj, '', $key); |
||
703 | if ('Id' === $newKey) { |
||
704 | // Don't map Id for push |
||
705 | continue; |
||
706 | } |
||
707 | |||
708 | $leadFields[$obj][$newKey] = $fields[$key]; |
||
709 | } |
||
710 | } |
||
711 | } |
||
712 | |||
713 | return (is_array($object)) ? $leadFields : $leadFields[$object]; |
||
714 | } |
||
715 | |||
716 | /** |
||
717 | * @param \Mautic\LeadBundle\Entity\Lead $lead |
||
718 | * @param array $config |
||
719 | * |
||
720 | * @return array|bool |
||
721 | */ |
||
722 | public function pushLead($lead, $config = []) |
||
723 | { |
||
724 | $config = $this->mergeConfigToFeatureSettings($config); |
||
725 | |||
726 | if (empty($config['leadFields'])) { |
||
727 | return []; |
||
728 | } |
||
729 | |||
730 | $mappedData = $this->mapContactDataForPush($lead, $config); |
||
731 | |||
732 | // No fields are mapped so bail |
||
733 | if (empty($mappedData)) { |
||
734 | return false; |
||
735 | } |
||
736 | |||
737 | try { |
||
738 | if ($this->isAuthorized()) { |
||
739 | $existingPersons = $this->getApiHelper()->getPerson( |
||
740 | [ |
||
741 | 'Lead' => isset($mappedData['Lead']['create']) ? $mappedData['Lead']['create'] : null, |
||
742 | 'Contact' => isset($mappedData['Contact']['create']) ? $mappedData['Contact']['create'] : null, |
||
743 | ] |
||
744 | ); |
||
745 | |||
746 | $personFound = false; |
||
747 | $people = [ |
||
748 | 'Contact' => [], |
||
749 | 'Lead' => [], |
||
750 | ]; |
||
751 | |||
752 | foreach (['Contact', 'Lead'] as $object) { |
||
753 | if (!empty($existingPersons[$object])) { |
||
754 | $fieldsToUpdate = $mappedData[$object]['update']; |
||
755 | $fieldsToUpdate = $this->getBlankFieldsToUpdate($fieldsToUpdate, $existingPersons[$object], $mappedData, $config); |
||
756 | $personFound = true; |
||
757 | foreach ($existingPersons[$object] as $person) { |
||
758 | if (!empty($fieldsToUpdate)) { |
||
759 | if (isset($fieldsToUpdate['AccountId'])) { |
||
760 | $accountId = $this->getCompanyName($fieldsToUpdate['AccountId'], 'Id', 'Name'); |
||
761 | if (!$accountId) { |
||
762 | //company was not found so create a new company in Salesforce |
||
763 | $company = $lead->getPrimaryCompany(); |
||
764 | if (!empty($company)) { |
||
765 | $company = $this->companyModel->getEntity($company['id']); |
||
766 | $sfCompany = $this->pushCompany($company); |
||
767 | if ($sfCompany) { |
||
768 | $fieldsToUpdate['AccountId'] = key($sfCompany); |
||
769 | } |
||
770 | } |
||
771 | } else { |
||
772 | $fieldsToUpdate['AccountId'] = $accountId; |
||
773 | } |
||
774 | } |
||
775 | |||
776 | $personData = $this->getApiHelper()->updateObject($fieldsToUpdate, $object, $person['Id']); |
||
777 | } |
||
778 | |||
779 | $people[$object][$person['Id']] = $person['Id']; |
||
780 | } |
||
781 | } |
||
782 | |||
783 | if ('Lead' === $object && !$personFound && isset($mappedData[$object]['create'])) { |
||
784 | $personData = $this->getApiHelper()->createLead($mappedData[$object]['create']); |
||
785 | $people[$object][$personData['Id']] = $personData['Id']; |
||
786 | $personFound = true; |
||
787 | } |
||
788 | |||
789 | if (isset($personData['Id'])) { |
||
790 | /** @var IntegrationEntityRepository $integrationEntityRepo */ |
||
791 | $integrationEntityRepo = $this->em->getRepository('MauticPluginBundle:IntegrationEntity'); |
||
792 | $integrationId = $integrationEntityRepo->getIntegrationsEntityId('Salesforce', $object, 'lead', $lead->getId()); |
||
793 | |||
794 | $integrationEntity = (empty($integrationId)) |
||
795 | ? $this->createIntegrationEntity($object, $personData['Id'], 'lead', $lead->getId(), [], false) |
||
796 | : |
||
797 | $this->em->getReference('MauticPluginBundle:IntegrationEntity', $integrationId[0]['id']); |
||
798 | |||
799 | $integrationEntity->setLastSyncDate($this->getLastSyncDate()); |
||
800 | $integrationEntityRepo->saveEntity($integrationEntity); |
||
801 | } |
||
802 | } |
||
803 | |||
804 | // Return success if any Contact or Lead was updated or created |
||
805 | return ($personFound) ? $people : false; |
||
806 | } |
||
807 | } catch (\Exception $e) { |
||
808 | if ($e instanceof ApiErrorException) { |
||
809 | $e->setContact($lead); |
||
810 | } |
||
811 | |||
812 | $this->logIntegrationError($e); |
||
813 | } |
||
814 | |||
815 | return false; |
||
816 | } |
||
817 | |||
818 | /** |
||
819 | * @param \Mautic\LeadBundle\Entity\Company $company |
||
820 | * @param array $config |
||
821 | * |
||
822 | * @return array|bool |
||
823 | */ |
||
824 | public function pushCompany($company, $config = []) |
||
825 | { |
||
826 | $config = $this->mergeConfigToFeatureSettings($config); |
||
827 | |||
828 | if (empty($config['companyFields']) || !$company) { |
||
829 | return []; |
||
830 | } |
||
831 | $object = 'company'; |
||
832 | $mappedData = $this->mapCompanyDataForPush($company, $config); |
||
833 | |||
834 | // No fields are mapped so bail |
||
835 | if (empty($mappedData)) { |
||
836 | return false; |
||
837 | } |
||
838 | |||
839 | try { |
||
840 | if ($this->isAuthorized()) { |
||
841 | $existingCompanies = $this->getApiHelper()->getCompany( |
||
842 | [ |
||
843 | $object => $mappedData[$object]['create'], |
||
844 | ] |
||
845 | ); |
||
846 | $companyFound = false; |
||
847 | $companies = []; |
||
848 | |||
849 | if (!empty($existingCompanies[$object])) { |
||
850 | $fieldsToUpdate = $mappedData[$object]['update']; |
||
851 | |||
852 | $fieldsToUpdate = $this->getBlankFieldsToUpdate($fieldsToUpdate, $existingCompanies[$object], $mappedData, $config); |
||
853 | $companyFound = true; |
||
854 | |||
855 | foreach ($existingCompanies[$object] as $sfCompany) { |
||
856 | if (!empty($fieldsToUpdate)) { |
||
857 | $companyData = $this->getApiHelper()->updateObject($fieldsToUpdate, $object, $sfCompany['Id']); |
||
858 | } |
||
859 | $companies[$sfCompany['Id']] = $sfCompany['Id']; |
||
860 | } |
||
861 | } |
||
862 | |||
863 | if (!$companyFound) { |
||
864 | $companyData = $this->getApiHelper()->createObject($mappedData[$object]['create'], 'Account'); |
||
865 | $companies[$companyData['Id']] = $companyData['Id']; |
||
866 | $companyFound = true; |
||
867 | } |
||
868 | |||
869 | if (isset($companyData['Id'])) { |
||
870 | /** @var IntegrationEntityRepository $integrationEntityRepo */ |
||
871 | $integrationEntityRepo = $this->em->getRepository('MauticPluginBundle:IntegrationEntity'); |
||
872 | $integrationId = $integrationEntityRepo->getIntegrationsEntityId('Salesforce', $object, 'company', $company->getId()); |
||
873 | |||
874 | $integrationEntity = (empty($integrationId)) |
||
875 | ? $this->createIntegrationEntity($object, $companyData['Id'], 'lead', $company->getId(), [], false) |
||
876 | : |
||
877 | $this->em->getReference('MauticPluginBundle:IntegrationEntity', $integrationId[0]['id']); |
||
878 | |||
879 | $integrationEntity->setLastSyncDate($this->getLastSyncDate()); |
||
880 | $integrationEntityRepo->saveEntity($integrationEntity); |
||
881 | } |
||
882 | |||
883 | // Return success if any company was updated or created |
||
884 | return ($companyFound) ? $companies : false; |
||
885 | } |
||
886 | } catch (\Exception $e) { |
||
887 | $this->logIntegrationError($e); |
||
888 | } |
||
889 | |||
890 | return false; |
||
891 | } |
||
892 | |||
893 | /** |
||
894 | * @param array $params |
||
895 | * @param null $query |
||
896 | * @param null $executed |
||
897 | * @param array $result |
||
898 | * @param string $object |
||
899 | * |
||
900 | * @return array|null |
||
901 | */ |
||
902 | public function getLeads($params = [], $query = null, &$executed = null, $result = [], $object = 'Lead') |
||
903 | { |
||
904 | if (!$query) { |
||
905 | $query = $this->getFetchQuery($params); |
||
906 | } |
||
907 | |||
908 | if (!is_array($executed)) { |
||
909 | $executed = [ |
||
910 | 0 => 0, |
||
911 | 1 => 0, |
||
912 | ]; |
||
913 | } |
||
914 | |||
915 | try { |
||
916 | if ($this->isAuthorized()) { |
||
917 | $progress = null; |
||
918 | $paginator = new ResultsPaginator($this->logger, $this->keys['instance_url']); |
||
919 | |||
920 | while (true) { |
||
921 | $result = $this->getApiHelper()->getLeads($query, $object); |
||
922 | $paginator->setResults($result); |
||
923 | |||
924 | if (isset($params['output']) && !isset($params['progress'])) { |
||
925 | $progress = new ProgressBar($params['output'], $paginator->getTotal()); |
||
926 | $progress->setFormat(' %current%/%max% [%bar%] %percent:3s%% ('.$object.')'); |
||
927 | |||
928 | $params['progress'] = $progress; |
||
929 | } |
||
930 | |||
931 | list($justUpdated, $justCreated) = $this->amendLeadDataBeforeMauticPopulate($result, $object, $params); |
||
932 | |||
933 | $executed[0] += $justUpdated; |
||
934 | $executed[1] += $justCreated; |
||
935 | |||
936 | if (!$nextUrl = $paginator->getNextResultsUrl()) { |
||
937 | // No more records to fetch |
||
938 | break; |
||
939 | } |
||
940 | |||
941 | $query['nextUrl'] = $nextUrl; |
||
942 | } |
||
943 | |||
944 | if ($progress) { |
||
945 | $progress->finish(); |
||
946 | } |
||
947 | } |
||
948 | } catch (\Exception $e) { |
||
949 | $this->logIntegrationError($e); |
||
950 | } |
||
951 | |||
952 | $this->logger->debug('SALESFORCE: '.$this->getApiHelper()->getRequestCounter().' API requests made for getLeads: '.$object); |
||
953 | |||
954 | return $executed; |
||
955 | } |
||
956 | |||
957 | /** |
||
958 | * @param array $params |
||
959 | * @param null $query |
||
960 | * @param null $executed |
||
961 | * |
||
962 | * @return array|null |
||
963 | */ |
||
964 | public function getCompanies($params = [], $query = null, $executed = null) |
||
965 | { |
||
966 | return $this->getLeads($params, $query, $executed, [], 'Account'); |
||
967 | } |
||
968 | |||
969 | /** |
||
970 | * @param array $params |
||
971 | * |
||
972 | * @return int|null |
||
973 | * |
||
974 | * @throws \Exception |
||
975 | */ |
||
976 | public function pushLeadActivity($params = []) |
||
977 | { |
||
978 | $executed = null; |
||
979 | |||
980 | $query = $this->getFetchQuery($params); |
||
981 | $config = $this->mergeConfigToFeatureSettings([]); |
||
982 | |||
983 | /** @var SalesforceApi $apiHelper */ |
||
984 | $apiHelper = $this->getApiHelper(); |
||
985 | |||
986 | $salesForceObjects[] = 'Lead'; |
||
987 | if (isset($config['objects']) && !empty($config['objects'])) { |
||
988 | $salesForceObjects = $config['objects']; |
||
989 | } |
||
990 | |||
991 | // Ensure that Contact is attempted before Lead |
||
992 | sort($salesForceObjects); |
||
993 | |||
994 | /** @var IntegrationEntityRepository $integrationEntityRepo */ |
||
995 | $integrationEntityRepo = $this->em->getRepository('MauticPluginBundle:IntegrationEntity'); |
||
996 | $startDate = new \DateTime($query['start']); |
||
997 | $endDate = new \DateTime($query['end']); |
||
998 | $limit = 100; |
||
999 | |||
1000 | foreach ($salesForceObjects as $object) { |
||
1001 | if (!in_array($object, ['Contact', 'Lead'])) { |
||
1002 | continue; |
||
1003 | } |
||
1004 | |||
1005 | try { |
||
1006 | if ($this->isAuthorized()) { |
||
1007 | // Get first batch |
||
1008 | $start = 0; |
||
1009 | $salesForceIds = $integrationEntityRepo->getIntegrationsEntityId( |
||
1010 | 'Salesforce', |
||
1011 | $object, |
||
1012 | 'lead', |
||
1013 | null, |
||
1014 | $startDate->format('Y-m-d H:m:s'), |
||
1015 | $endDate->format('Y-m-d H:m:s'), |
||
1016 | true, |
||
1017 | $start, |
||
1018 | $limit |
||
1019 | ); |
||
1020 | while (!empty($salesForceIds)) { |
||
1021 | $executed += count($salesForceIds); |
||
1022 | |||
1023 | // Extract a list of lead Ids |
||
1024 | $leadIds = []; |
||
1025 | $sfIds = []; |
||
1026 | foreach ($salesForceIds as $ids) { |
||
1027 | $leadIds[] = $ids['internal_entity_id']; |
||
1028 | $sfIds[] = $ids['integration_entity_id']; |
||
1029 | } |
||
1030 | |||
1031 | // Collect lead activity for this batch |
||
1032 | $leadActivity = $this->getLeadData( |
||
1033 | $startDate, |
||
1034 | $endDate, |
||
1035 | $leadIds |
||
1036 | ); |
||
1037 | |||
1038 | $this->logger->debug('SALESFORCE: Syncing activity for '.count($leadActivity).' contacts ('.implode(', ', array_keys($leadActivity)).')'); |
||
1039 | $this->logger->debug('SALESFORCE: Syncing activity for '.var_export($sfIds, true)); |
||
1040 | |||
1041 | $salesForceLeadData = []; |
||
1042 | foreach ($salesForceIds as $ids) { |
||
1043 | $leadId = $ids['internal_entity_id']; |
||
1044 | if (isset($leadActivity[$leadId])) { |
||
1045 | $sfId = $ids['integration_entity_id']; |
||
1046 | $salesForceLeadData[$sfId] = $leadActivity[$leadId]; |
||
1047 | $salesForceLeadData[$sfId]['id'] = $ids['integration_entity_id']; |
||
1048 | $salesForceLeadData[$sfId]['leadId'] = $ids['internal_entity_id']; |
||
1049 | $salesForceLeadData[$sfId]['leadUrl'] = $this->router->generate( |
||
1050 | 'mautic_plugin_timeline_view', |
||
1051 | ['integration' => 'Salesforce', 'leadId' => $leadId], |
||
1052 | UrlGeneratorInterface::ABSOLUTE_URL |
||
1053 | ); |
||
1054 | } else { |
||
1055 | $this->logger->debug('SALESFORCE: No activity found for contact ID '.$leadId); |
||
1056 | } |
||
1057 | } |
||
1058 | |||
1059 | if (!empty($salesForceLeadData)) { |
||
1060 | $apiHelper->createLeadActivity($salesForceLeadData, $object); |
||
1061 | } else { |
||
1062 | $this->logger->debug('SALESFORCE: No contact activity to sync'); |
||
1063 | } |
||
1064 | |||
1065 | // Get the next batch |
||
1066 | $start += $limit; |
||
1067 | $salesForceIds = $integrationEntityRepo->getIntegrationsEntityId( |
||
1068 | 'Salesforce', |
||
1069 | $object, |
||
1070 | 'lead', |
||
1071 | null, |
||
1072 | $startDate->format('Y-m-d H:m:s'), |
||
1073 | $endDate->format('Y-m-d H:m:s'), |
||
1074 | true, |
||
1075 | $start, |
||
1076 | $limit |
||
1077 | ); |
||
1078 | } |
||
1079 | } |
||
1080 | } catch (\Exception $e) { |
||
1081 | $this->logIntegrationError($e); |
||
1082 | } |
||
1083 | } |
||
1084 | |||
1085 | return $executed; |
||
1086 | } |
||
1087 | |||
1088 | /** |
||
1089 | * Return key recognized by integration. |
||
1090 | * |
||
1091 | * @param $key |
||
1092 | * @param $field |
||
1093 | * |
||
1094 | * @return mixed |
||
1095 | */ |
||
1096 | public function convertLeadFieldKey($key, $field) |
||
1097 | { |
||
1098 | $search = []; |
||
1099 | foreach ($this->objects as $object) { |
||
1100 | $search[] = '__'.$object; |
||
1101 | } |
||
1102 | |||
1103 | return str_replace($search, '', $key); |
||
1104 | } |
||
1105 | |||
1106 | /** |
||
1107 | * @param array $params |
||
1108 | * |
||
1109 | * @return mixed |
||
1110 | */ |
||
1111 | public function pushLeads($params = []) |
||
1112 | { |
||
1113 | $limit = (isset($params['limit'])) ? $params['limit'] : 100; |
||
1114 | list($fromDate, $toDate) = $this->getSyncTimeframeDates($params); |
||
1115 | $config = $this->mergeConfigToFeatureSettings($params); |
||
1116 | $integrationEntityRepo = $this->getIntegrationEntityRepository(); |
||
1117 | |||
1118 | $totalUpdated = 0; |
||
1119 | $totalCreated = 0; |
||
1120 | $totalErrors = 0; |
||
1121 | |||
1122 | list($fieldMapping, $mauticLeadFieldString, $supportedObjects) = $this->prepareFieldsForPush($config); |
||
1123 | |||
1124 | if (empty($fieldMapping)) { |
||
1125 | return [0, 0, 0, 0]; |
||
1126 | } |
||
1127 | |||
1128 | $originalLimit = $limit; |
||
1129 | $progress = false; |
||
1130 | |||
1131 | // Get a total number of contacts to be updated and/or created for the progress counter |
||
1132 | $totalToUpdate = array_sum( |
||
1133 | $integrationEntityRepo->findLeadsToUpdate( |
||
1134 | 'Salesforce', |
||
1135 | 'lead', |
||
1136 | $mauticLeadFieldString, |
||
1137 | false, |
||
1138 | $fromDate, |
||
1139 | $toDate, |
||
1140 | $supportedObjects, |
||
1141 | [] |
||
1142 | ) |
||
1143 | ); |
||
1144 | $totalToCreate = (in_array('Lead', $supportedObjects)) ? $integrationEntityRepo->findLeadsToCreate( |
||
1145 | 'Salesforce', |
||
1146 | $mauticLeadFieldString, |
||
1147 | false, |
||
1148 | $fromDate, |
||
1149 | $toDate |
||
1150 | ) : 0; |
||
1151 | $totalCount = $totalToProcess = $totalToCreate + $totalToUpdate; |
||
1152 | |||
1153 | if (defined('IN_MAUTIC_CONSOLE')) { |
||
1154 | // start with update |
||
1155 | if ($totalToUpdate + $totalToCreate) { |
||
1156 | $output = new ConsoleOutput(); |
||
1157 | $output->writeln("About $totalToUpdate to update and about $totalToCreate to create/update"); |
||
1158 | $progress = new ProgressBar($output, $totalCount); |
||
1159 | } |
||
1160 | } |
||
1161 | |||
1162 | // Start with contacts so we know who is a contact when we go to process converted leads |
||
1163 | if (count($supportedObjects) > 1) { |
||
1164 | $sfObject = 'Contact'; |
||
1165 | } else { |
||
1166 | // Only Lead or Contact is enabled so start with which ever that is |
||
1167 | reset($supportedObjects); |
||
1168 | $sfObject = key($supportedObjects); |
||
1169 | } |
||
1170 | $noMoreUpdates = false; |
||
1171 | $trackedContacts = [ |
||
1172 | 'Contact' => [], |
||
1173 | 'Lead' => [], |
||
1174 | ]; |
||
1175 | |||
1176 | // Loop to maximize composite that may include updating contacts, updating leads, and creating leads |
||
1177 | while ($totalCount > 0) { |
||
1178 | $limit = $originalLimit; |
||
1179 | $mauticData = []; |
||
1180 | $checkEmailsInSF = []; |
||
1181 | $leadsToSync = []; |
||
1182 | $processedLeads = []; |
||
1183 | |||
1184 | // Process the updates |
||
1185 | if (!$noMoreUpdates) { |
||
1186 | $noMoreUpdates = $this->getMauticContactsToUpdate( |
||
1187 | $checkEmailsInSF, |
||
1188 | $mauticLeadFieldString, |
||
1189 | $sfObject, |
||
1190 | $trackedContacts, |
||
1191 | $limit, |
||
1192 | $fromDate, |
||
1193 | $toDate, |
||
1194 | $totalCount |
||
1195 | ); |
||
1196 | |||
1197 | if ($noMoreUpdates && 'Contact' === $sfObject && isset($supportedObjects['Lead'])) { |
||
1198 | // Try Leads |
||
1199 | $sfObject = 'Lead'; |
||
1200 | $noMoreUpdates = $this->getMauticContactsToUpdate( |
||
1201 | $checkEmailsInSF, |
||
1202 | $mauticLeadFieldString, |
||
1203 | $sfObject, |
||
1204 | $trackedContacts, |
||
1205 | $limit, |
||
1206 | $fromDate, |
||
1207 | $toDate, |
||
1208 | $totalCount |
||
1209 | ); |
||
1210 | } |
||
1211 | |||
1212 | if ($limit) { |
||
1213 | // Mainly done for test mocking purposes |
||
1214 | $limit = $this->getSalesforceSyncLimit($checkEmailsInSF, $limit); |
||
1215 | } |
||
1216 | } |
||
1217 | |||
1218 | // If there is still room - grab Mautic leads to create if the Lead object is enabled |
||
1219 | $sfEntityRecords = []; |
||
1220 | if ('Lead' === $sfObject && (null === $limit || $limit > 0) && !empty($mauticLeadFieldString)) { |
||
1221 | try { |
||
1222 | $sfEntityRecords = $this->getMauticContactsToCreate( |
||
1223 | $checkEmailsInSF, |
||
1224 | $fieldMapping, |
||
1225 | $mauticLeadFieldString, |
||
1226 | $limit, |
||
1227 | $fromDate, |
||
1228 | $toDate, |
||
1229 | $totalCount, |
||
1230 | $progress |
||
1231 | ); |
||
1232 | } catch (ApiErrorException $exception) { |
||
1233 | $this->cleanupFromSync($leadsToSync, $exception); |
||
1234 | } |
||
1235 | } elseif ($checkEmailsInSF) { |
||
1236 | $sfEntityRecords = $this->getSalesforceObjectsByEmails($sfObject, $checkEmailsInSF, implode(',', array_keys($fieldMapping[$sfObject]['create']))); |
||
1237 | |||
1238 | if (!isset($sfEntityRecords['records'])) { |
||
1239 | // Something is wrong so throw an exception to prevent creating a bunch of new leads |
||
1240 | $this->cleanupFromSync( |
||
1241 | $leadsToSync, |
||
1242 | json_encode($sfEntityRecords) |
||
1243 | ); |
||
1244 | } |
||
1245 | } |
||
1246 | |||
1247 | $this->pushLeadDoNotContactByDate('email', $checkEmailsInSF, $sfObject, $params); |
||
1248 | |||
1249 | // We're done |
||
1250 | if (!$checkEmailsInSF) { |
||
1251 | break; |
||
1252 | } |
||
1253 | |||
1254 | $this->prepareMauticContactsToUpdate( |
||
1255 | $mauticData, |
||
1256 | $checkEmailsInSF, |
||
1257 | $processedLeads, |
||
1258 | $trackedContacts, |
||
1259 | $leadsToSync, |
||
1260 | $fieldMapping, |
||
1261 | $mauticLeadFieldString, |
||
1262 | $sfEntityRecords, |
||
1263 | $progress |
||
1264 | ); |
||
1265 | |||
1266 | // Only create left over if Lead object is enabled in integration settings |
||
1267 | if ($checkEmailsInSF && isset($fieldMapping['Lead'])) { |
||
1268 | $this->prepareMauticContactsToCreate( |
||
1269 | $mauticData, |
||
1270 | $checkEmailsInSF, |
||
1271 | $processedLeads, |
||
1272 | $fieldMapping |
||
1273 | ); |
||
1274 | } |
||
1275 | // Persist pending changes |
||
1276 | $this->cleanupFromSync($leadsToSync); |
||
1277 | // Make the request |
||
1278 | $this->makeCompositeRequest($mauticData, $totalUpdated, $totalCreated, $totalErrors); |
||
1279 | |||
1280 | // Stop gap - if 100% let's kill the script |
||
1281 | if ($progress && $progress->getProgressPercent() >= 1) { |
||
1282 | break; |
||
1283 | } |
||
1284 | } |
||
1285 | |||
1286 | if ($progress) { |
||
1287 | $progress->finish(); |
||
1288 | $output->writeln(''); |
||
1289 | } |
||
1290 | |||
1291 | $this->logger->debug('SALESFORCE: '.$this->getApiHelper()->getRequestCounter().' API requests made for pushLeads'); |
||
1292 | |||
1293 | // Assume that those not touched are ignored due to not having matching fields, duplicates, etc |
||
1294 | $totalIgnored = $totalToProcess - ($totalUpdated + $totalCreated + $totalErrors); |
||
1295 | |||
1296 | return [$totalUpdated, $totalCreated, $totalErrors, $totalIgnored]; |
||
1297 | } |
||
1298 | |||
1299 | /** |
||
1300 | * @param $lead |
||
1301 | * |
||
1302 | * @return array |
||
1303 | */ |
||
1304 | public function getSalesforceLeadId($lead) |
||
1305 | { |
||
1306 | $config = $this->mergeConfigToFeatureSettings([]); |
||
1307 | $integrationEntityRepo = $this->getIntegrationEntityRepository(); |
||
1308 | |||
1309 | if (isset($config['objects'])) { |
||
1310 | //try searching for lead as this has been changed before in updated done to the plugin |
||
1311 | if (false !== array_search('Contact', $config['objects'])) { |
||
1312 | $resultContact = $integrationEntityRepo->getIntegrationsEntityId('Salesforce', 'Contact', 'lead', $lead->getId()); |
||
1313 | |||
1314 | if ($resultContact) { |
||
1315 | return $resultContact; |
||
1316 | } |
||
1317 | } |
||
1318 | } |
||
1319 | |||
1320 | return $integrationEntityRepo->getIntegrationsEntityId('Salesforce', 'Lead', 'lead', $lead->getId()); |
||
1321 | } |
||
1322 | |||
1323 | /** |
||
1324 | * @return array |
||
1325 | * |
||
1326 | * @throws \Exception |
||
1327 | */ |
||
1328 | public function getCampaigns() |
||
1329 | { |
||
1330 | $campaigns = []; |
||
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
1331 | try { |
||
1332 | $campaigns = $this->getApiHelper()->getCampaigns(); |
||
1333 | } catch (\Exception $e) { |
||
1334 | $this->logIntegrationError($e); |
||
1335 | } |
||
1336 | |||
1337 | return $campaigns; |
||
1338 | } |
||
1339 | |||
1340 | /** |
||
1341 | * @return array |
||
1342 | * |
||
1343 | * @throws \Exception |
||
1344 | */ |
||
1345 | public function getCampaignChoices() |
||
1346 | { |
||
1347 | $choices = []; |
||
1348 | $campaigns = $this->getCampaigns(); |
||
1349 | |||
1350 | if (!empty($campaigns['records'])) { |
||
1351 | foreach ($campaigns['records'] as $campaign) { |
||
1352 | $choices[] = [ |
||
1353 | 'value' => $campaign['Id'], |
||
1354 | 'label' => $campaign['Name'], |
||
1355 | ]; |
||
1356 | } |
||
1357 | } |
||
1358 | |||
1359 | return $choices; |
||
1360 | } |
||
1361 | |||
1362 | /** |
||
1363 | * @param $campaignId |
||
1364 | * |
||
1365 | * @throws \Exception |
||
1366 | */ |
||
1367 | public function getCampaignMembers($campaignId) |
||
1368 | { |
||
1369 | /** @var IntegrationEntityRepository $integrationEntityRepo */ |
||
1370 | $integrationEntityRepo = $this->em->getRepository('MauticPluginBundle:IntegrationEntity'); |
||
1371 | $mixedFields = $this->getIntegrationSettings()->getFeatureSettings(); |
||
1372 | |||
1373 | // Get the last time the campaign was synced to prevent resyncing the entire SF campaign |
||
1374 | $cacheKey = $this->getName().'.CampaignSync.'.$campaignId; |
||
1375 | $lastSyncDate = $this->getCache()->get($cacheKey); |
||
1376 | $syncStarted = (new \DateTime())->format('c'); |
||
1377 | |||
1378 | if (false === $lastSyncDate) { |
||
1379 | // Sync all records |
||
1380 | $lastSyncDate = null; |
||
1381 | } |
||
1382 | |||
1383 | // Consume in batches |
||
1384 | $paginator = new ResultsPaginator($this->logger, $this->keys['instance_url']); |
||
1385 | $nextRecordsUrl = null; |
||
1386 | |||
1387 | while (true) { |
||
1388 | try { |
||
1389 | $results = $this->getApiHelper()->getCampaignMembers($campaignId, $lastSyncDate, $nextRecordsUrl); |
||
1390 | $paginator->setResults($results); |
||
1391 | |||
1392 | $organizer = new Organizer($results['records']); |
||
1393 | $fetcher = new Fetcher($integrationEntityRepo, $organizer, $campaignId); |
||
1394 | |||
1395 | // Create Mautic contacts from Campaign Members if they don't already exist |
||
1396 | foreach (['Contact', 'Lead'] as $object) { |
||
1397 | $fields = $this->getMixedLeadFields($mixedFields, $object); |
||
1398 | |||
1399 | try { |
||
1400 | $query = $fetcher->getQueryForUnknownObjects($fields, $object); |
||
1401 | $this->getLeads([], $query, $executed, [], $object); |
||
1402 | } catch (NoObjectsToFetchException $exception) { |
||
1403 | // No more IDs to fetch so break and continue on |
||
1404 | continue; |
||
1405 | } |
||
1406 | } |
||
1407 | |||
1408 | // Create integration entities for members we aren't already tracking |
||
1409 | $unknownMembers = $fetcher->getUnknownCampaignMembers(); |
||
1410 | $persistEntities = []; |
||
1411 | $counter = 0; |
||
1412 | |||
1413 | foreach ($unknownMembers as $mauticContactId) { |
||
1414 | $persistEntities[] = $this->createIntegrationEntity( |
||
1415 | CampaignMember::OBJECT, |
||
1416 | $campaignId, |
||
1417 | 'lead', |
||
1418 | $mauticContactId, |
||
1419 | [], |
||
1420 | false |
||
1421 | ); |
||
1422 | |||
1423 | ++$counter; |
||
1424 | |||
1425 | if (20 === $counter) { |
||
1426 | // Batch to control RAM use |
||
1427 | $this->em->getRepository('MauticPluginBundle:IntegrationEntity')->saveEntities($persistEntities); |
||
1428 | $this->em->clear(IntegrationEntity::class); |
||
1429 | $persistEntities = []; |
||
1430 | $counter = 0; |
||
1431 | } |
||
1432 | } |
||
1433 | |||
1434 | // Catch left overs |
||
1435 | if ($persistEntities) { |
||
1436 | $this->em->getRepository('MauticPluginBundle:IntegrationEntity')->saveEntities($persistEntities); |
||
1437 | $this->em->clear(IntegrationEntity::class); |
||
1438 | } |
||
1439 | |||
1440 | unset($unknownMembers, $fetcher, $organizer, $persistEntities); |
||
1441 | |||
1442 | // Do we continue? |
||
1443 | if (!$nextRecordsUrl = $paginator->getNextResultsUrl()) { |
||
1444 | // No more results to fetch |
||
1445 | |||
1446 | // Store the latest sync date at the end in case something happens during the actual sync process and it needs to be re-ran |
||
1447 | $this->cache->set($cacheKey, $syncStarted); |
||
1448 | |||
1449 | break; |
||
1450 | } |
||
1451 | } catch (\Exception $e) { |
||
1452 | $this->logIntegrationError($e); |
||
1453 | |||
1454 | break; |
||
1455 | } |
||
1456 | } |
||
1457 | } |
||
1458 | |||
1459 | /** |
||
1460 | * @param $fields |
||
1461 | * @param $object |
||
1462 | * |
||
1463 | * @return array |
||
1464 | */ |
||
1465 | public function getMixedLeadFields($fields, $object) |
||
1466 | { |
||
1467 | $mixedFields = array_filter($fields['leadFields']); |
||
1468 | $fields = []; |
||
1469 | foreach ($mixedFields as $sfField => $mField) { |
||
1470 | if (false !== strpos($sfField, '__'.$object)) { |
||
1471 | $fields[] = str_replace('__'.$object, '', $sfField); |
||
1472 | } |
||
1473 | if (false !== strpos($sfField, '-'.$object)) { |
||
1474 | $fields[] = str_replace('-'.$object, '', $sfField); |
||
1475 | } |
||
1476 | } |
||
1477 | |||
1478 | return $fields; |
||
1479 | } |
||
1480 | |||
1481 | /** |
||
1482 | * @param $campaignId |
||
1483 | * |
||
1484 | * @return array |
||
1485 | * |
||
1486 | * @throws \Exception |
||
1487 | */ |
||
1488 | public function getCampaignMemberStatus($campaignId) |
||
1489 | { |
||
1490 | $campaignMemberStatus = []; |
||
1491 | try { |
||
1492 | $campaignMemberStatus = $this->getApiHelper()->getCampaignMemberStatus($campaignId); |
||
1493 | } catch (\Exception $e) { |
||
1494 | $this->logIntegrationError($e); |
||
1495 | } |
||
1496 | |||
1497 | return $campaignMemberStatus; |
||
1498 | } |
||
1499 | |||
1500 | /** |
||
1501 | * @param $campaignId |
||
1502 | * @param $status |
||
1503 | * |
||
1504 | * @return array |
||
1505 | */ |
||
1506 | public function pushLeadToCampaign(Lead $lead, $campaignId, $status = '', $personIds = null) |
||
1507 | { |
||
1508 | if (empty($personIds)) { |
||
1509 | // personIds should have been generated by pushLead() |
||
1510 | |||
1511 | return false; |
||
1512 | } |
||
1513 | |||
1514 | $mauticData = []; |
||
1515 | $objectId = null; |
||
1516 | |||
1517 | /** @var IntegrationEntityRepository $integrationEntityRepo */ |
||
1518 | $integrationEntityRepo = $this->em->getRepository('MauticPluginBundle:IntegrationEntity'); |
||
1519 | |||
1520 | $body = [ |
||
1521 | 'Status' => $status, |
||
1522 | ]; |
||
1523 | $object = 'CampaignMember'; |
||
1524 | $url = '/services/data/v38.0/sobjects/'.$object; |
||
1525 | |||
1526 | if (!empty($lead->getEmail())) { |
||
1527 | $pushPeople = []; |
||
1528 | $pushObject = null; |
||
1529 | if (!empty($personIds)) { |
||
1530 | // Give precendence to Contact CampaignMembers |
||
1531 | if (!empty($personIds['Contact'])) { |
||
1532 | $pushObject = 'Contact'; |
||
1533 | $campaignMembers = $this->getApiHelper()->checkCampaignMembership($campaignId, $pushObject, $personIds[$pushObject]); |
||
1534 | $pushPeople = $personIds[$pushObject]; |
||
1535 | } |
||
1536 | |||
1537 | if (empty($campaignMembers) && !empty($personIds['Lead'])) { |
||
1538 | $pushObject = 'Lead'; |
||
1539 | $campaignMembers = $this->getApiHelper()->checkCampaignMembership($campaignId, $pushObject, $personIds[$pushObject]); |
||
1540 | $pushPeople = $personIds[$pushObject]; |
||
1541 | } |
||
1542 | } // pushLead should have handled this |
||
1543 | |||
1544 | foreach ($pushPeople as $memberId) { |
||
1545 | $campaignMappingId = '-'.$campaignId; |
||
1546 | |||
1547 | if (isset($campaignMembers[$memberId])) { |
||
1548 | $existingCampaignMember = $integrationEntityRepo->getIntegrationsEntityId( |
||
1549 | 'Salesforce', |
||
1550 | 'CampaignMember', |
||
1551 | 'lead', |
||
1552 | null, |
||
1553 | null, |
||
1554 | null, |
||
1555 | false, |
||
1556 | 0, |
||
1557 | 0, |
||
1558 | [$campaignMembers[$memberId]] |
||
1559 | ); |
||
1560 | if ($existingCampaignMember) { |
||
1561 | foreach ($existingCampaignMember as $member) { |
||
1562 | $integrationEntity = $integrationEntityRepo->getEntity($member['id']); |
||
1563 | $referenceId = $integrationEntity->getId(); |
||
1564 | $internalLeadId = $integrationEntity->getInternalEntityId(); |
||
1565 | } |
||
1566 | } |
||
1567 | $id = !empty($lead->getId()) ? $lead->getId() : ''; |
||
1568 | $id .= '-CampaignMember'.$campaignMembers[$memberId]; |
||
1569 | $id .= !empty($referenceId) ? '-'.$referenceId : ''; |
||
1570 | $id .= $campaignMappingId; |
||
1571 | $patchurl = $url.'/'.$campaignMembers[$memberId]; |
||
1572 | $mauticData[$id] = [ |
||
1573 | 'method' => 'PATCH', |
||
1574 | 'url' => $patchurl, |
||
1575 | 'referenceId' => $id, |
||
1576 | 'body' => $body, |
||
1577 | 'httpHeaders' => [ |
||
1578 | 'Sforce-Auto-Assign' => 'FALSE', |
||
1579 | ], |
||
1580 | ]; |
||
1581 | } else { |
||
1582 | $id = (!empty($lead->getId()) ? $lead->getId() : '').'-CampaignMemberNew-null'.$campaignMappingId; |
||
1583 | $mauticData[$id] = [ |
||
1584 | 'method' => 'POST', |
||
1585 | 'url' => $url, |
||
1586 | 'referenceId' => $id, |
||
1587 | 'body' => array_merge( |
||
1588 | $body, |
||
1589 | [ |
||
1590 | 'CampaignId' => $campaignId, |
||
1591 | "{$pushObject}Id" => $memberId, |
||
1592 | ] |
||
1593 | ), |
||
1594 | ]; |
||
1595 | } |
||
1596 | } |
||
1597 | |||
1598 | $request['allOrNone'] = 'false'; |
||
1599 | $request['compositeRequest'] = array_values($mauticData); |
||
1600 | |||
1601 | $this->logger->debug('SALESFORCE: pushLeadToCampaign '.var_export($request, true)); |
||
1602 | |||
1603 | if (!empty($request)) { |
||
1604 | $result = $this->getApiHelper()->syncMauticToSalesforce($request); |
||
1605 | |||
1606 | return (bool) array_sum($this->processCompositeResponse($result['compositeResponse'])); |
||
1607 | } |
||
1608 | } |
||
1609 | |||
1610 | return false; |
||
1611 | } |
||
1612 | |||
1613 | /** |
||
1614 | * @param $email |
||
1615 | * |
||
1616 | * @return mixed|string |
||
1617 | */ |
||
1618 | protected function getSyncKey($email) |
||
1619 | { |
||
1620 | return mb_strtolower($this->cleanPushData($email)); |
||
1621 | } |
||
1622 | |||
1623 | /** |
||
1624 | * @param $checkEmailsInSF |
||
1625 | * @param $mauticLeadFieldString |
||
1626 | * @param $sfObject |
||
1627 | * @param $trackedContacts |
||
1628 | * @param $limit |
||
1629 | * @param $fromDate |
||
1630 | * @param $toDate |
||
1631 | * @param $totalCount |
||
1632 | * |
||
1633 | * @return bool |
||
1634 | */ |
||
1635 | protected function getMauticContactsToUpdate( |
||
1636 | &$checkEmailsInSF, |
||
1637 | $mauticLeadFieldString, |
||
1638 | &$sfObject, |
||
1639 | &$trackedContacts, |
||
1640 | $limit, |
||
1641 | $fromDate, |
||
1642 | $toDate, |
||
1643 | &$totalCount |
||
1644 | ) { |
||
1645 | // Fetch them separately so we can determine if Leads are already Contacts |
||
1646 | $toUpdate = $this->getIntegrationEntityRepository()->findLeadsToUpdate( |
||
1647 | 'Salesforce', |
||
1648 | 'lead', |
||
1649 | $mauticLeadFieldString, |
||
1650 | $limit, |
||
1651 | $fromDate, |
||
1652 | $toDate, |
||
1653 | $sfObject |
||
1654 | )[$sfObject]; |
||
1655 | |||
1656 | $toUpdateCount = count($toUpdate); |
||
1657 | $totalCount -= $toUpdateCount; |
||
1658 | |||
1659 | foreach ($toUpdate as $lead) { |
||
1660 | if (!empty($lead['email'])) { |
||
1661 | $lead = $this->getCompoundMauticFields($lead); |
||
1662 | $key = $this->getSyncKey($lead['email']); |
||
1663 | $trackedContacts[$lead['integration_entity']][$key] = $lead['id']; |
||
1664 | |||
1665 | if ('Contact' == $sfObject) { |
||
1666 | $this->setContactToSync($checkEmailsInSF, $lead); |
||
1667 | } elseif (isset($trackedContacts['Contact'][$key])) { |
||
1668 | // We already know this is a converted contact so just ignore it |
||
1669 | $integrationEntity = $this->em->getReference( |
||
1670 | 'MauticPluginBundle:IntegrationEntity', |
||
1671 | $lead['id'] |
||
1672 | ); |
||
1673 | $this->deleteIntegrationEntities[] = $integrationEntity; |
||
1674 | $this->logger->debug('SALESFORCE: Converted lead '.$lead['email']); |
||
1675 | } else { |
||
1676 | $this->setContactToSync($checkEmailsInSF, $lead); |
||
1677 | } |
||
1678 | } |
||
1679 | } |
||
1680 | |||
1681 | return 0 === $toUpdateCount; |
||
1682 | } |
||
1683 | |||
1684 | /** |
||
1685 | * @param $checkEmailsInSF |
||
1686 | * @param $fieldMapping |
||
1687 | * @param $mauticLeadFieldString |
||
1688 | * @param $limit |
||
1689 | * @param $fromDate |
||
1690 | * @param $toDate |
||
1691 | * @param $totalCount |
||
1692 | * @param null $progress |
||
1693 | * |
||
1694 | * @return array |
||
1695 | * |
||
1696 | * @throws ApiErrorException |
||
1697 | */ |
||
1698 | protected function getMauticContactsToCreate( |
||
1699 | &$checkEmailsInSF, |
||
1700 | $fieldMapping, |
||
1701 | $mauticLeadFieldString, |
||
1702 | $limit, |
||
1703 | $fromDate, |
||
1704 | $toDate, |
||
1705 | &$totalCount, |
||
1706 | $progress = null |
||
1707 | ) { |
||
1708 | $integrationEntityRepo = $this->getIntegrationEntityRepository(); |
||
1709 | $leadsToCreate = $integrationEntityRepo->findLeadsToCreate( |
||
1710 | 'Salesforce', |
||
1711 | $mauticLeadFieldString, |
||
1712 | $limit, |
||
1713 | $fromDate, |
||
1714 | $toDate |
||
1715 | ); |
||
1716 | $totalCount -= count($leadsToCreate); |
||
1717 | $foundContacts = []; |
||
1718 | $sfEntityRecords = [ |
||
1719 | 'totalSize' => 0, |
||
1720 | 'records' => [], |
||
1721 | ]; |
||
1722 | $error = false; |
||
1723 | |||
1724 | foreach ($leadsToCreate as $lead) { |
||
1725 | $lead = $this->getCompoundMauticFields($lead); |
||
1726 | |||
1727 | if (isset($lead['email'])) { |
||
1728 | $this->setContactToSync($checkEmailsInSF, $lead); |
||
1729 | } elseif ($progress) { |
||
1730 | $progress->advance(); |
||
1731 | } |
||
1732 | } |
||
1733 | |||
1734 | // When creating, we have to check for Contacts first then Lead |
||
1735 | if (isset($fieldMapping['Contact'])) { |
||
1736 | $sfEntityRecords = $this->getSalesforceObjectsByEmails('Contact', $checkEmailsInSF, implode(',', array_keys($fieldMapping['Contact']['create']))); |
||
1737 | if (isset($sfEntityRecords['records'])) { |
||
1738 | foreach ($sfEntityRecords['records'] as $sfContactRecord) { |
||
1739 | if (!isset($sfContactRecord['Email'])) { |
||
1740 | continue; |
||
1741 | } |
||
1742 | $key = $this->getSyncKey($sfContactRecord['Email']); |
||
1743 | $foundContacts[$key] = $key; |
||
1744 | } |
||
1745 | } else { |
||
1746 | $error = json_encode($sfEntityRecords); |
||
1747 | } |
||
1748 | } |
||
1749 | |||
1750 | // For any Mautic contacts left over, check to see if existing Leads exist |
||
1751 | if (isset($fieldMapping['Lead']) && $checkSfLeads = array_diff_key($checkEmailsInSF, $foundContacts)) { |
||
1752 | $sfLeadRecords = $this->getSalesforceObjectsByEmails('Lead', $checkSfLeads, implode(',', array_keys($fieldMapping['Lead']['create']))); |
||
1753 | |||
1754 | if (isset($sfLeadRecords['records'])) { |
||
1755 | // Merge contact records with these |
||
1756 | $sfEntityRecords['records'] = array_merge($sfEntityRecords['records'], $sfLeadRecords['records']); |
||
1757 | $sfEntityRecords['totalSize'] = (int) $sfEntityRecords['totalSize'] + (int) $sfLeadRecords['totalSize']; |
||
1758 | } else { |
||
1759 | $error = json_encode($sfLeadRecords); |
||
1760 | } |
||
1761 | } |
||
1762 | |||
1763 | if ($error) { |
||
1764 | throw new ApiErrorException($error); |
||
1765 | } |
||
1766 | |||
1767 | unset($leadsToCreate, $checkSfLeads); |
||
1768 | |||
1769 | return $sfEntityRecords; |
||
1770 | } |
||
1771 | |||
1772 | /** |
||
1773 | * @param $mauticData |
||
1774 | * @param $objectFields |
||
1775 | * @param $object |
||
1776 | * @param null $objectId |
||
1777 | * @param null $sfRecord |
||
1778 | * |
||
1779 | * @return array |
||
1780 | */ |
||
1781 | protected function buildCompositeBody( |
||
1782 | &$mauticData, |
||
1783 | $objectFields, |
||
1784 | $object, |
||
1785 | &$entity, |
||
1786 | $objectId = null, |
||
1787 | $sfRecord = null |
||
1788 | ) { |
||
1789 | $body = []; |
||
1790 | $updateEntity = []; |
||
1791 | $company = null; |
||
1792 | $config = $this->mergeConfigToFeatureSettings([]); |
||
1793 | |||
1794 | if ((isset($entity['email']) && !empty($entity['email'])) || (isset($entity['companyname']) && !empty($entity['companyname']))) { |
||
1795 | //use a composite patch here that can update and create (one query) every 200 records |
||
1796 | if (isset($objectFields['update'])) { |
||
1797 | $fields = ($objectId) ? $objectFields['update'] : $objectFields['create']; |
||
1798 | if (isset($entity['company']) && isset($entity['integration_entity']) && 'Contact' == $object) { |
||
1799 | $accountId = $this->getCompanyName($entity['company'], 'Id', 'Name'); |
||
1800 | |||
1801 | if (!$accountId) { |
||
1802 | //company was not found so create a new company in Salesforce |
||
1803 | $lead = $this->leadModel->getEntity($entity['internal_entity_id']); |
||
1804 | if ($lead) { |
||
1805 | $companies = $this->leadModel->getCompanies($lead); |
||
1806 | if (!empty($companies)) { |
||
1807 | foreach ($companies as $companyData) { |
||
1808 | if ($companyData['is_primary']) { |
||
1809 | $company = $this->companyModel->getEntity($companyData['company_id']); |
||
1810 | } |
||
1811 | } |
||
1812 | if ($company) { |
||
1813 | $sfCompany = $this->pushCompany($company); |
||
1814 | if (!empty($sfCompany)) { |
||
1815 | $entity['company'] = key($sfCompany); |
||
1816 | } |
||
1817 | } |
||
1818 | } else { |
||
1819 | unset($entity['company']); |
||
1820 | } |
||
1821 | } |
||
1822 | } else { |
||
1823 | $entity['company'] = $accountId; |
||
1824 | } |
||
1825 | } |
||
1826 | $fields = $this->getBlankFieldsToUpdate($fields, $sfRecord, $objectFields, $config); |
||
1827 | } else { |
||
1828 | $fields = $objectFields; |
||
1829 | } |
||
1830 | |||
1831 | foreach ($fields as $sfField => $mauticField) { |
||
1832 | if (isset($entity[$mauticField])) { |
||
1833 | $fieldType = (isset($objectFields['types']) && isset($objectFields['types'][$sfField])) ? $objectFields['types'][$sfField] |
||
1834 | : 'string'; |
||
1835 | if (!empty($entity[$mauticField]) and 'boolean' != $fieldType) { |
||
1836 | $body[$sfField] = $this->cleanPushData($entity[$mauticField], $fieldType); |
||
1837 | } elseif ('boolean' == $fieldType) { |
||
1838 | $body[$sfField] = $this->cleanPushData($entity[$mauticField], $fieldType); |
||
1839 | } |
||
1840 | } |
||
1841 | if (array_key_exists($sfField, $objectFields['required']['fields']) && empty($body[$sfField])) { |
||
1842 | if (isset($sfRecord[$sfField])) { |
||
1843 | $body[$sfField] = $sfRecord[$sfField]; |
||
1844 | if (empty($entity[$mauticField]) && !empty($sfRecord[$sfField]) |
||
1845 | && $sfRecord[$sfField] !== $this->translator->trans( |
||
1846 | 'mautic.integration.form.lead.unknown' |
||
1847 | ) |
||
1848 | ) { |
||
1849 | $updateEntity[$mauticField] = $sfRecord[$sfField]; |
||
1850 | } |
||
1851 | } else { |
||
1852 | $body[$sfField] = $this->translator->trans('mautic.integration.form.lead.unknown'); |
||
1853 | } |
||
1854 | } |
||
1855 | } |
||
1856 | |||
1857 | $this->amendLeadDataBeforePush($body); |
||
1858 | |||
1859 | if (!empty($body)) { |
||
1860 | $url = '/services/data/v38.0/sobjects/'.$object; |
||
1861 | if ($objectId) { |
||
1862 | $url .= '/'.$objectId; |
||
1863 | } |
||
1864 | $id = $entity['internal_entity_id'].'-'.$object.(!empty($entity['id']) ? '-'.$entity['id'] : ''); |
||
1865 | $method = ($objectId) ? 'PATCH' : 'POST'; |
||
1866 | $mauticData[$id] = [ |
||
1867 | 'method' => $method, |
||
1868 | 'url' => $url, |
||
1869 | 'referenceId' => $id, |
||
1870 | 'body' => $body, |
||
1871 | 'httpHeaders' => [ |
||
1872 | 'Sforce-Auto-Assign' => ($objectId) ? 'FALSE' : 'TRUE', |
||
1873 | ], |
||
1874 | ]; |
||
1875 | } |
||
1876 | } |
||
1877 | |||
1878 | return $updateEntity; |
||
1879 | } |
||
1880 | |||
1881 | /** |
||
1882 | * @param $object |
||
1883 | * |
||
1884 | * @return array |
||
1885 | */ |
||
1886 | protected function getRequiredFieldString(array $config, array $availableFields, $object) |
||
1887 | { |
||
1888 | $requiredFields = $this->getRequiredFields($availableFields[$object]); |
||
1889 | |||
1890 | if ('company' != $object) { |
||
1891 | $requiredFields = $this->prepareFieldsForSync($config['leadFields'], array_keys($requiredFields), $object); |
||
1892 | } |
||
1893 | |||
1894 | $requiredString = implode(',', array_keys($requiredFields)); |
||
1895 | |||
1896 | return [$requiredFields, $requiredString]; |
||
1897 | } |
||
1898 | |||
1899 | /** |
||
1900 | * @param $config |
||
1901 | * |
||
1902 | * @return array |
||
1903 | */ |
||
1904 | protected function prepareFieldsForPush($config) |
||
1905 | { |
||
1906 | $leadFields = array_unique(array_values($config['leadFields'])); |
||
1907 | $leadFields = array_combine($leadFields, $leadFields); |
||
1908 | unset($leadFields['mauticContactTimelineLink']); |
||
1909 | unset($leadFields['mauticContactIsContactableByEmail']); |
||
1910 | |||
1911 | $fieldsToUpdateInSf = $this->getPriorityFieldsForIntegration($config); |
||
1912 | $fieldKeys = array_keys($config['leadFields']); |
||
1913 | $supportedObjects = []; |
||
1914 | $objectFields = []; |
||
1915 | |||
1916 | // Important to have contacts first!! |
||
1917 | if (false !== array_search('Contact', $config['objects'])) { |
||
1918 | $supportedObjects['Contact'] = 'Contact'; |
||
1919 | $fieldsToCreate = $this->prepareFieldsForSync($config['leadFields'], $fieldKeys, 'Contact'); |
||
1920 | $objectFields['Contact'] = [ |
||
1921 | 'update' => isset($fieldsToUpdateInSf['Contact']) ? array_intersect_key($fieldsToCreate, $fieldsToUpdateInSf['Contact']) : [], |
||
1922 | 'create' => $fieldsToCreate, |
||
1923 | ]; |
||
1924 | } |
||
1925 | if (false !== array_search('Lead', $config['objects'])) { |
||
1926 | $supportedObjects['Lead'] = 'Lead'; |
||
1927 | $fieldsToCreate = $this->prepareFieldsForSync($config['leadFields'], $fieldKeys, 'Lead'); |
||
1928 | $objectFields['Lead'] = [ |
||
1929 | 'update' => isset($fieldsToUpdateInSf['Lead']) ? array_intersect_key($fieldsToCreate, $fieldsToUpdateInSf['Lead']) : [], |
||
1930 | 'create' => $fieldsToCreate, |
||
1931 | ]; |
||
1932 | } |
||
1933 | |||
1934 | $mauticLeadFieldString = implode(', l.', $leadFields); |
||
1935 | $mauticLeadFieldString = 'l.'.$mauticLeadFieldString; |
||
1936 | $availableFields = $this->getAvailableLeadFields(['feature_settings' => ['objects' => $supportedObjects]]); |
||
1937 | |||
1938 | // Setup required fields and field types |
||
1939 | foreach ($supportedObjects as $object) { |
||
1940 | $objectFields[$object]['types'] = []; |
||
1941 | if (isset($availableFields[$object])) { |
||
1942 | $fieldData = $this->prepareFieldsForSync($availableFields[$object], array_keys($availableFields[$object]), $object); |
||
1943 | foreach ($fieldData as $fieldName => $field) { |
||
1944 | $objectFields[$object]['types'][$fieldName] = (isset($field['type'])) ? $field['type'] : 'string'; |
||
1945 | } |
||
1946 | } |
||
1947 | |||
1948 | list($fields, $string) = $this->getRequiredFieldString( |
||
1949 | $config, |
||
1950 | $availableFields, |
||
1951 | $object |
||
1952 | ); |
||
1953 | |||
1954 | $objectFields[$object]['required'] = [ |
||
1955 | 'fields' => $fields, |
||
1956 | 'string' => $string, |
||
1957 | ]; |
||
1958 | } |
||
1959 | |||
1960 | return [$objectFields, $mauticLeadFieldString, $supportedObjects]; |
||
1961 | } |
||
1962 | |||
1963 | /** |
||
1964 | * @param $config |
||
1965 | * @param null $object |
||
1966 | * @param string $priorityObject |
||
1967 | * |
||
1968 | * @return mixed |
||
1969 | */ |
||
1970 | protected function getPriorityFieldsForMautic($config, $object = null, $priorityObject = 'mautic') |
||
1971 | { |
||
1972 | $fields = parent::getPriorityFieldsForMautic($config, $object, $priorityObject); |
||
1973 | |||
1974 | return ($object && isset($fields[$object])) ? $fields[$object] : $fields; |
||
1975 | } |
||
1976 | |||
1977 | /** |
||
1978 | * @param $config |
||
1979 | * @param null $object |
||
1980 | * @param string $priorityObject |
||
1981 | * |
||
1982 | * @return mixed |
||
1983 | */ |
||
1984 | protected function getPriorityFieldsForIntegration($config, $object = null, $priorityObject = 'mautic') |
||
1985 | { |
||
1986 | $fields = parent::getPriorityFieldsForIntegration($config, $object, $priorityObject); |
||
1987 | unset($fields['Contact']['Id'], $fields['Lead']['Id']); |
||
1988 | |||
1989 | return ($object && isset($fields[$object])) ? $fields[$object] : $fields; |
||
1990 | } |
||
1991 | |||
1992 | /** |
||
1993 | * @param $response |
||
1994 | * @param int $totalUpdated |
||
1995 | * @param int $totalCreated |
||
1996 | * @param int $totalErrored |
||
1997 | * |
||
1998 | * @return array |
||
1999 | */ |
||
2000 | protected function processCompositeResponse($response, &$totalUpdated = 0, &$totalCreated = 0, &$totalErrored = 0) |
||
2001 | { |
||
2002 | if (is_array($response)) { |
||
2003 | foreach ($response as $item) { |
||
2004 | $contactId = $integrationEntityId = $campaignId = null; |
||
2005 | $object = 'Lead'; |
||
2006 | $internalObject = 'lead'; |
||
2007 | if (!empty($item['referenceId'])) { |
||
2008 | $reference = explode('-', $item['referenceId']); |
||
2009 | if (3 === count($reference)) { |
||
2010 | list($contactId, $object, $integrationEntityId) = $reference; |
||
2011 | } elseif (4 === count($reference)) { |
||
2012 | list($contactId, $object, $integrationEntityId, $campaignId) = $reference; |
||
2013 | } else { |
||
2014 | list($contactId, $object) = $reference; |
||
2015 | } |
||
2016 | } |
||
2017 | if (strstr($object, 'CampaignMember')) { |
||
2018 | $object = 'CampaignMember'; |
||
2019 | } |
||
2020 | if ('Account' == $object) { |
||
2021 | $internalObject = 'company'; |
||
2022 | } |
||
2023 | if (isset($item['body'][0]['errorCode'])) { |
||
2024 | $exception = new ApiErrorException($item['body'][0]['message']); |
||
2025 | if ('Contact' == $object || $object = 'Lead') { |
||
2026 | $exception->setContactId($contactId); |
||
2027 | } |
||
2028 | $this->logIntegrationError($exception); |
||
2029 | $integrationEntity = null; |
||
2030 | if ($integrationEntityId && 'CampaignMember' !== $object) { |
||
2031 | $integrationEntity = $this->integrationEntityModel->getEntityByIdAndSetSyncDate($integrationEntityId, new \DateTime()); |
||
2032 | } elseif (isset($campaignId)) { |
||
2033 | $integrationEntity = $this->integrationEntityModel->getEntityByIdAndSetSyncDate($campaignId, $this->getLastSyncDate()); |
||
2034 | } elseif ($contactId) { |
||
2035 | $integrationEntity = $this->createIntegrationEntity( |
||
2036 | $object, |
||
2037 | null, |
||
2038 | $internalObject.'-error', |
||
2039 | $contactId, |
||
2040 | null, |
||
2041 | false |
||
2042 | ); |
||
2043 | } |
||
2044 | |||
2045 | if ($integrationEntity) { |
||
2046 | $integrationEntity->setInternalEntity('ENTITY_IS_DELETED' === $item['body'][0]['errorCode'] ? $internalObject.'-deleted' : $internalObject.'-error') |
||
2047 | ->setInternal(['error' => $item['body'][0]['message']]); |
||
2048 | $this->persistIntegrationEntities[] = $integrationEntity; |
||
2049 | } |
||
2050 | ++$totalErrored; |
||
2051 | } elseif (!empty($item['body']['success'])) { |
||
2052 | if (201 === $item['httpStatusCode']) { |
||
2053 | // New object created |
||
2054 | if ('CampaignMember' === $object) { |
||
2055 | $internal = ['Id' => $item['body']['id']]; |
||
2056 | } else { |
||
2057 | $internal = []; |
||
2058 | } |
||
2059 | $this->salesforceIdMapping[$contactId] = $item['body']['id']; |
||
2060 | $this->persistIntegrationEntities[] = $this->createIntegrationEntity( |
||
2061 | $object, |
||
2062 | $this->salesforceIdMapping[$contactId], |
||
2063 | $internalObject, |
||
2064 | $contactId, |
||
2065 | $internal, |
||
2066 | false |
||
2067 | ); |
||
2068 | } |
||
2069 | ++$totalCreated; |
||
2070 | } elseif (204 === $item['httpStatusCode']) { |
||
2071 | // Record was updated |
||
2072 | if ($integrationEntityId) { |
||
2073 | $integrationEntity = $this->integrationEntityModel->getEntityByIdAndSetSyncDate($integrationEntityId, $this->getLastSyncDate()); |
||
2074 | if ($integrationEntity) { |
||
2075 | if (isset($this->salesforceIdMapping[$contactId])) { |
||
2076 | $integrationEntity->setIntegrationEntityId($this->salesforceIdMapping[$contactId]); |
||
2077 | } |
||
2078 | |||
2079 | $this->persistIntegrationEntities[] = $integrationEntity; |
||
2080 | } |
||
2081 | } elseif (!empty($this->salesforceIdMapping[$contactId])) { |
||
2082 | // Found in Salesforce so create a new record for it |
||
2083 | $this->persistIntegrationEntities[] = $this->createIntegrationEntity( |
||
2084 | $object, |
||
2085 | $this->salesforceIdMapping[$contactId], |
||
2086 | $internalObject, |
||
2087 | $contactId, |
||
2088 | [], |
||
2089 | false |
||
2090 | ); |
||
2091 | } |
||
2092 | |||
2093 | ++$totalUpdated; |
||
2094 | } else { |
||
2095 | $error = 'http status code '.$item['httpStatusCode']; |
||
2096 | switch (true) { |
||
2097 | case !empty($item['body'][0]['message']['message']): |
||
2098 | $error = $item['body'][0]['message']['message']; |
||
2099 | break; |
||
2100 | case !empty($item['body']['message']): |
||
2101 | $error = $item['body']['message']; |
||
2102 | break; |
||
2103 | } |
||
2104 | |||
2105 | $exception = new ApiErrorException($error); |
||
2106 | if (!empty($item['referenceId']) && ('Contact' == $object || $object = 'Lead')) { |
||
2107 | $exception->setContactId($item['referenceId']); |
||
2108 | } |
||
2109 | $this->logIntegrationError($exception); |
||
2110 | ++$totalErrored; |
||
2111 | |||
2112 | if ($integrationEntityId) { |
||
2113 | $integrationEntity = $this->integrationEntityModel->getEntityByIdAndSetSyncDate($integrationEntityId, $this->getLastSyncDate()); |
||
2114 | if ($integrationEntity) { |
||
2115 | if (isset($this->salesforceIdMapping[$contactId])) { |
||
2116 | $integrationEntity->setIntegrationEntityId($this->salesforceIdMapping[$contactId]); |
||
2117 | } |
||
2118 | |||
2119 | $this->persistIntegrationEntities[] = $integrationEntity; |
||
2120 | } |
||
2121 | } elseif (!empty($this->salesforceIdMapping[$contactId])) { |
||
2122 | // Found in Salesforce so create a new record for it |
||
2123 | $this->persistIntegrationEntities[] = $this->createIntegrationEntity( |
||
2124 | $object, |
||
2125 | $this->salesforceIdMapping[$contactId], |
||
2126 | $internalObject, |
||
2127 | $contactId, |
||
2128 | [], |
||
2129 | false |
||
2130 | ); |
||
2131 | } |
||
2132 | } |
||
2133 | } |
||
2134 | } |
||
2135 | |||
2136 | $this->cleanupFromSync(); |
||
2137 | |||
2138 | return [$totalUpdated, $totalCreated]; |
||
2139 | } |
||
2140 | |||
2141 | /** |
||
2142 | * @param $sfObject |
||
2143 | * @param $checkEmailsInSF |
||
2144 | * @param $requiredFieldString |
||
2145 | * |
||
2146 | * @return array |
||
2147 | */ |
||
2148 | protected function getSalesforceObjectsByEmails($sfObject, $checkEmailsInSF, $requiredFieldString) |
||
2149 | { |
||
2150 | // Salesforce craps out with double quotes and unescaped single quotes |
||
2151 | $findEmailsInSF = array_map( |
||
2152 | function ($lead) { |
||
2153 | return str_replace("'", "\'", $this->cleanPushData($lead['email'])); |
||
2154 | }, |
||
2155 | $checkEmailsInSF |
||
2156 | ); |
||
2157 | |||
2158 | $fieldString = "'".implode("','", $findEmailsInSF)."'"; |
||
2159 | $queryUrl = $this->getQueryUrl(); |
||
2160 | $findQuery = ('Lead' === $sfObject) |
||
2161 | ? |
||
2162 | 'select Id, '.$requiredFieldString.', ConvertedContactId from Lead where isDeleted = false and Email in ('.$fieldString.')' |
||
2163 | : |
||
2164 | 'select Id, '.$requiredFieldString.' from Contact where isDeleted = false and Email in ('.$fieldString.')'; |
||
2165 | |||
2166 | return $this->getApiHelper()->request('query', ['q' => $findQuery], 'GET', false, null, $queryUrl); |
||
2167 | } |
||
2168 | |||
2169 | /** |
||
2170 | * @param $mauticData |
||
2171 | * @param $checkEmailsInSF |
||
2172 | * @param $processedLeads |
||
2173 | * @param $trackedContacts |
||
2174 | * @param $leadsToSync |
||
2175 | * @param $objectFields |
||
2176 | * @param $mauticLeadFieldString |
||
2177 | * @param $sfEntityRecords |
||
2178 | * @param null $progress |
||
2179 | */ |
||
2180 | protected function prepareMauticContactsToUpdate( |
||
2181 | &$mauticData, |
||
2182 | &$checkEmailsInSF, |
||
2183 | &$processedLeads, |
||
2184 | &$trackedContacts, |
||
2185 | &$leadsToSync, |
||
2186 | $objectFields, |
||
2187 | $mauticLeadFieldString, |
||
2188 | $sfEntityRecords, |
||
2189 | $progress = null |
||
2190 | ) { |
||
2191 | foreach ($sfEntityRecords['records'] as $sfKey => $sfEntityRecord) { |
||
2192 | $skipObject = false; |
||
2193 | $syncLead = false; |
||
2194 | $sfObject = $sfEntityRecord['attributes']['type']; |
||
2195 | if (!isset($sfEntityRecord['Email'])) { |
||
2196 | // This is a record we don't recognize so continue |
||
2197 | return; |
||
2198 | } |
||
2199 | $key = $this->getSyncKey($sfEntityRecord['Email']); |
||
2200 | if (!isset($sfEntityRecord['Id']) || (!isset($checkEmailsInSF[$key]) && !isset($processedLeads[$key]))) { |
||
2201 | // This is a record we don't recognize so continue |
||
2202 | return; |
||
2203 | } |
||
2204 | |||
2205 | $leadData = (isset($processedLeads[$key])) ? $processedLeads[$key] : $checkEmailsInSF[$key]; |
||
2206 | $contactId = $leadData['internal_entity_id']; |
||
2207 | |||
2208 | if ( |
||
2209 | isset($checkEmailsInSF[$key]) |
||
2210 | && ( |
||
2211 | ( |
||
2212 | 'Lead' === $sfObject && !empty($sfEntityRecord['ConvertedContactId']) |
||
2213 | ) |
||
2214 | || ( |
||
2215 | isset($checkEmailsInSF[$key]['integration_entity']) && 'Contact' === $sfObject |
||
2216 | && 'Lead' === $checkEmailsInSF[$key]['integration_entity'] |
||
2217 | ) |
||
2218 | ) |
||
2219 | ) { |
||
2220 | $deleted = false; |
||
2221 | // This is a converted lead so remove the Lead entity leaving the Contact entity |
||
2222 | if (!empty($trackedContacts['Lead'][$key])) { |
||
2223 | $this->deleteIntegrationEntities[] = $this->em->getReference( |
||
2224 | 'MauticPluginBundle:IntegrationEntity', |
||
2225 | $trackedContacts['Lead'][$key] |
||
2226 | ); |
||
2227 | $deleted = true; |
||
2228 | unset($trackedContacts['Lead'][$key]); |
||
2229 | } |
||
2230 | |||
2231 | if ($contactEntity = $this->checkLeadIsContact($trackedContacts['Contact'], $key, $contactId, $mauticLeadFieldString)) { |
||
2232 | // This Lead is already a Contact but was not updated for whatever reason |
||
2233 | if (!$deleted) { |
||
2234 | $this->deleteIntegrationEntities[] = $this->em->getReference( |
||
2235 | 'MauticPluginBundle:IntegrationEntity', |
||
2236 | $checkEmailsInSF[$key]['id'] |
||
2237 | ); |
||
2238 | } |
||
2239 | |||
2240 | // Update the Contact record instead |
||
2241 | $checkEmailsInSF[$key] = $contactEntity; |
||
2242 | $trackedContacts['Contact'][$key] = $contactEntity['id']; |
||
2243 | } else { |
||
2244 | $id = (!empty($sfEntityRecord['ConvertedContactId'])) ? $sfEntityRecord['ConvertedContactId'] : $sfEntityRecord['Id']; |
||
2245 | // This contact does not have a Contact record |
||
2246 | $integrationEntity = $this->createIntegrationEntity( |
||
2247 | 'Contact', |
||
2248 | $id, |
||
2249 | 'lead', |
||
2250 | $contactId |
||
2251 | ); |
||
2252 | |||
2253 | $checkEmailsInSF[$key]['integration_entity'] = 'Contact'; |
||
2254 | $checkEmailsInSF[$key]['integration_entity_id'] = $id; |
||
2255 | $checkEmailsInSF[$key]['id'] = $integrationEntity; |
||
2256 | } |
||
2257 | |||
2258 | $this->logger->debug('SALESFORCE: Converted lead '.$sfEntityRecord['Email']); |
||
2259 | |||
2260 | // skip if this is a Lead object since it'll be handled with the Contact entry |
||
2261 | if ('Lead' === $sfObject) { |
||
2262 | unset($checkEmailsInSF[$key]); |
||
2263 | unset($sfEntityRecords['records'][$sfKey]); |
||
2264 | $skipObject = true; |
||
2265 | } |
||
2266 | } |
||
2267 | |||
2268 | if (!$skipObject) { |
||
2269 | // Only progress if we have a unique Lead and not updating a Salesforce entry duplicate |
||
2270 | if (!isset($processedLeads[$key])) { |
||
2271 | if ($progress) { |
||
2272 | $progress->advance(); |
||
2273 | } |
||
2274 | |||
2275 | // Mark that this lead has been processed |
||
2276 | $leadData = $processedLeads[$key] = $checkEmailsInSF[$key]; |
||
2277 | } |
||
2278 | |||
2279 | // Keep track of Mautic ID to Salesforce ID for the integration table |
||
2280 | $this->salesforceIdMapping[$contactId] = (!empty($sfEntityRecord['ConvertedContactId'])) ? $sfEntityRecord['ConvertedContactId'] |
||
2281 | : $sfEntityRecord['Id']; |
||
2282 | |||
2283 | $leadEntity = $this->em->getReference('MauticLeadBundle:Lead', $leadData['internal_entity_id']); |
||
2284 | if ($updateLead = $this->buildCompositeBody( |
||
2285 | $mauticData, |
||
2286 | $objectFields[$sfObject], |
||
2287 | $sfObject, |
||
2288 | $leadData, |
||
2289 | $sfEntityRecord['Id'], |
||
2290 | $sfEntityRecord |
||
2291 | ) |
||
2292 | ) { |
||
2293 | // Get the lead entity |
||
2294 | /* @var Lead $leadEntity */ |
||
2295 | foreach ($updateLead as $mauticField => $sfValue) { |
||
2296 | $leadEntity->addUpdatedField($mauticField, $sfValue); |
||
2297 | } |
||
2298 | |||
2299 | $syncLead = !empty($leadEntity->getChanges(true)); |
||
2300 | } |
||
2301 | |||
2302 | // Validate if we have a company for this Mautic contact |
||
2303 | if (!empty($sfEntityRecord['Company']) |
||
2304 | && $sfEntityRecord['Company'] !== $this->translator->trans( |
||
2305 | 'mautic.integration.form.lead.unknown' |
||
2306 | ) |
||
2307 | ) { |
||
2308 | $company = IdentifyCompanyHelper::identifyLeadsCompany( |
||
2309 | ['company' => $sfEntityRecord['Company']], |
||
2310 | null, |
||
2311 | $this->companyModel |
||
2312 | ); |
||
2313 | |||
2314 | if (!empty($company[2])) { |
||
2315 | $syncLead = $this->companyModel->addLeadToCompany($company[2], $leadEntity); |
||
2316 | $this->em->detach($company[2]); |
||
2317 | } |
||
2318 | } |
||
2319 | |||
2320 | if ($syncLead) { |
||
2321 | $leadsToSync[] = $leadEntity; |
||
2322 | } else { |
||
2323 | $this->em->detach($leadEntity); |
||
2324 | } |
||
2325 | } |
||
2326 | |||
2327 | unset($checkEmailsInSF[$key]); |
||
2328 | } |
||
2329 | } |
||
2330 | |||
2331 | /** |
||
2332 | * @param $mauticData |
||
2333 | * @param $checkEmailsInSF |
||
2334 | * @param $processedLeads |
||
2335 | * @param $objectFields |
||
2336 | */ |
||
2337 | protected function prepareMauticContactsToCreate( |
||
2338 | &$mauticData, |
||
2339 | &$checkEmailsInSF, |
||
2340 | &$processedLeads, |
||
2341 | $objectFields |
||
2342 | ) { |
||
2343 | foreach ($checkEmailsInSF as $key => $lead) { |
||
2344 | if (!empty($lead['integration_entity_id'])) { |
||
2345 | if ($this->buildCompositeBody( |
||
2346 | $mauticData, |
||
2347 | $objectFields[$lead['integration_entity']], |
||
2348 | $lead['integration_entity'], |
||
2349 | $lead, |
||
2350 | $lead['integration_entity_id'] |
||
2351 | ) |
||
2352 | ) { |
||
2353 | $this->logger->debug('SALESFORCE: Contact has existing ID so updating '.$lead['email']); |
||
2354 | } |
||
2355 | } else { |
||
2356 | $this->buildCompositeBody( |
||
2357 | $mauticData, |
||
2358 | $objectFields['Lead'], |
||
2359 | 'Lead', |
||
2360 | $lead |
||
2361 | ); |
||
2362 | } |
||
2363 | |||
2364 | $processedLeads[$key] = $checkEmailsInSF[$key]; |
||
2365 | unset($checkEmailsInSF[$key]); |
||
2366 | } |
||
2367 | } |
||
2368 | |||
2369 | /** |
||
2370 | * @param $mauticData |
||
2371 | * @param int $totalUpdated |
||
2372 | * @param int $totalCreated |
||
2373 | * @param int $totalErrored |
||
2374 | */ |
||
2375 | protected function makeCompositeRequest($mauticData, &$totalUpdated = 0, &$totalCreated = 0, &$totalErrored = 0) |
||
2376 | { |
||
2377 | if (empty($mauticData)) { |
||
2378 | return; |
||
2379 | } |
||
2380 | |||
2381 | /** @var SalesforceApi $apiHelper */ |
||
2382 | $apiHelper = $this->getApiHelper(); |
||
2383 | |||
2384 | // We can only send 25 at a time |
||
2385 | $request = []; |
||
2386 | $request['allOrNone'] = 'false'; |
||
2387 | $chunked = array_chunk($mauticData, 25); |
||
2388 | |||
2389 | foreach ($chunked as $chunk) { |
||
2390 | // We can only submit 25 at a time |
||
2391 | if ($chunk) { |
||
2392 | $request['compositeRequest'] = $chunk; |
||
2393 | $result = $apiHelper->syncMauticToSalesforce($request); |
||
2394 | $this->logger->debug('SALESFORCE: Sync Composite '.var_export($request, true)); |
||
2395 | $this->processCompositeResponse($result['compositeResponse'], $totalUpdated, $totalCreated, $totalErrored); |
||
2396 | } |
||
2397 | } |
||
2398 | } |
||
2399 | |||
2400 | /** |
||
2401 | * @param $checkEmailsInSF |
||
2402 | * @param $lead |
||
2403 | * |
||
2404 | * @return bool|mixed|string |
||
2405 | */ |
||
2406 | protected function setContactToSync(&$checkEmailsInSF, $lead) |
||
2407 | { |
||
2408 | $key = $this->getSyncKey($lead['email']); |
||
2409 | if (isset($checkEmailsInSF[$key])) { |
||
2410 | // this is a duplicate in Mautic |
||
2411 | $this->mauticDuplicates[$lead['internal_entity_id']] = 'lead-duplicate'; |
||
2412 | |||
2413 | return false; |
||
2414 | } |
||
2415 | |||
2416 | $checkEmailsInSF[$key] = $lead; |
||
2417 | |||
2418 | return $key; |
||
2419 | } |
||
2420 | |||
2421 | /** |
||
2422 | * @param $currentContactList |
||
2423 | * @param $limit |
||
2424 | * |
||
2425 | * @return int |
||
2426 | */ |
||
2427 | protected function getSalesforceSyncLimit($currentContactList, $limit) |
||
2428 | { |
||
2429 | return $limit - count($currentContactList); |
||
2430 | } |
||
2431 | |||
2432 | /** |
||
2433 | * @param $trackedContacts |
||
2434 | * @param $email |
||
2435 | * @param $contactId |
||
2436 | * @param $leadFields |
||
2437 | * |
||
2438 | * @return array|bool |
||
2439 | */ |
||
2440 | protected function checkLeadIsContact(&$trackedContacts, $email, $contactId, $leadFields) |
||
2441 | { |
||
2442 | if (empty($trackedContacts[$email])) { |
||
2443 | // Check if there's an existing entry |
||
2444 | return $this->getIntegrationEntityRepository()->getIntegrationEntity( |
||
2445 | $this->getName(), |
||
2446 | 'Contact', |
||
2447 | 'lead', |
||
2448 | $contactId, |
||
2449 | $leadFields |
||
2450 | ); |
||
2451 | } |
||
2452 | |||
2453 | return false; |
||
2454 | } |
||
2455 | |||
2456 | /** |
||
2457 | * @param $fieldsToUpdate |
||
2458 | * @param array $objects |
||
2459 | * |
||
2460 | * @return array |
||
2461 | */ |
||
2462 | protected function cleanPriorityFields($fieldsToUpdate, $objects = null) |
||
2463 | { |
||
2464 | if (null === $objects) { |
||
2465 | $objects = ['Lead', 'Contact']; |
||
2466 | } |
||
2467 | |||
2468 | if (isset($fieldsToUpdate['leadFields'])) { |
||
2469 | // Pass in the whole config |
||
2470 | $fields = $fieldsToUpdate; |
||
2471 | } else { |
||
2472 | $fields = array_flip($fieldsToUpdate); |
||
2473 | } |
||
2474 | |||
2475 | return $this->prepareFieldsForSync($fields, $fieldsToUpdate, $objects); |
||
2476 | } |
||
2477 | |||
2478 | /** |
||
2479 | * @param $config |
||
2480 | * |
||
2481 | * @return array |
||
2482 | */ |
||
2483 | protected function mapContactDataForPush(Lead $lead, $config) |
||
2484 | { |
||
2485 | $fields = array_keys($config['leadFields']); |
||
2486 | $fieldsToUpdateInSf = $this->getPriorityFieldsForIntegration($config); |
||
2487 | $fieldMapping = [ |
||
2488 | 'Lead' => [], |
||
2489 | 'Contact' => [], |
||
2490 | ]; |
||
2491 | $mappedData = [ |
||
2492 | 'Lead' => [], |
||
2493 | 'Contact' => [], |
||
2494 | ]; |
||
2495 | |||
2496 | foreach (['Lead', 'Contact'] as $object) { |
||
2497 | if (isset($config['objects']) && false !== array_search($object, $config['objects'])) { |
||
2498 | $fieldMapping[$object]['create'] = $this->prepareFieldsForSync($config['leadFields'], $fields, $object); |
||
2499 | $fieldMapping[$object]['update'] = isset($fieldsToUpdateInSf[$object]) ? array_intersect_key( |
||
2500 | $fieldMapping[$object]['create'], |
||
2501 | $fieldsToUpdateInSf[$object] |
||
2502 | ) : []; |
||
2503 | |||
2504 | // Create an update and |
||
2505 | $mappedData[$object]['create'] = $this->populateLeadData( |
||
2506 | $lead, |
||
2507 | [ |
||
2508 | 'leadFields' => $fieldMapping[$object]['create'], // map with all fields available |
||
2509 | 'object' => $object, |
||
2510 | 'feature_settings' => [ |
||
2511 | 'objects' => $config['objects'], |
||
2512 | ], |
||
2513 | ] |
||
2514 | ); |
||
2515 | |||
2516 | if (isset($mappedData[$object]['create']['Id'])) { |
||
2517 | unset($mappedData[$object]['create']['Id']); |
||
2518 | } |
||
2519 | |||
2520 | $this->amendLeadDataBeforePush($mappedData[$object]['create']); |
||
2521 | |||
2522 | // Set the update fields |
||
2523 | $mappedData[$object]['update'] = array_intersect_key($mappedData[$object]['create'], $fieldMapping[$object]['update']); |
||
2524 | } |
||
2525 | } |
||
2526 | |||
2527 | return $mappedData; |
||
2528 | } |
||
2529 | |||
2530 | /** |
||
2531 | * @param $config |
||
2532 | * |
||
2533 | * @return array |
||
2534 | */ |
||
2535 | protected function mapCompanyDataForPush(Company $company, $config) |
||
2536 | { |
||
2537 | $object = 'company'; |
||
2538 | $entity = []; |
||
2539 | $mappedData = [ |
||
2540 | $object => [], |
||
2541 | ]; |
||
2542 | |||
2543 | if (isset($config['objects']) && false !== array_search($object, $config['objects'])) { |
||
2544 | $fieldKeys = array_keys($config['companyFields']); |
||
2545 | $fieldsToCreate = $this->prepareFieldsForSync($config['companyFields'], $fieldKeys, 'Account'); |
||
2546 | $fieldsToUpdateInSf = $this->getPriorityFieldsForIntegration($config, 'Account', 'mautic_company'); |
||
2547 | |||
2548 | $fieldMapping[$object] = [ |
||
2549 | 'update' => !empty($fieldsToUpdateInSf) ? array_intersect_key($fieldsToCreate, $fieldsToUpdateInSf) : [], |
||
2550 | 'create' => $fieldsToCreate, |
||
2551 | ]; |
||
2552 | $entity['primaryCompany'] = $company->getProfileFields(); |
||
2553 | |||
2554 | // Create an update and |
||
2555 | $mappedData[$object]['create'] = $this->populateCompanyData( |
||
2556 | $entity, |
||
2557 | [ |
||
2558 | 'companyFields' => $fieldMapping[$object]['create'], // map with all fields available |
||
2559 | 'object' => $object, |
||
2560 | 'feature_settings' => [ |
||
2561 | 'objects' => $config['objects'], |
||
2562 | ], |
||
2563 | ] |
||
2564 | ); |
||
2565 | |||
2566 | if (isset($mappedData[$object]['create']['Id'])) { |
||
2567 | unset($mappedData[$object]['create']['Id']); |
||
2568 | } |
||
2569 | |||
2570 | $this->amendLeadDataBeforePush($mappedData[$object]['create']); |
||
2571 | |||
2572 | // Set the update fields |
||
2573 | $mappedData[$object]['update'] = array_intersect_key($mappedData[$object]['create'], $fieldMapping[$object]['update']); |
||
2574 | } |
||
2575 | |||
2576 | return $mappedData; |
||
2577 | } |
||
2578 | |||
2579 | /** |
||
2580 | * @param $mappedData |
||
2581 | */ |
||
2582 | public function amendLeadDataBeforePush(&$mappedData) |
||
2583 | { |
||
2584 | // normalize for multiselect field |
||
2585 | foreach ($mappedData as &$data) { |
||
2586 | $data = str_replace('|', ';', $data); |
||
2587 | } |
||
2588 | |||
2589 | $mappedData = StateValidationHelper::validate($mappedData); |
||
2590 | } |
||
2591 | |||
2592 | /** |
||
2593 | * @param string $object |
||
2594 | * |
||
2595 | * @return array |
||
2596 | */ |
||
2597 | public function getFieldsForQuery($object) |
||
2598 | { |
||
2599 | $fields = $this->getIntegrationSettings()->getFeatureSettings(); |
||
2600 | switch ($object) { |
||
2601 | case 'company': |
||
2602 | case 'Account': |
||
2603 | $fields = array_keys(array_filter($fields['companyFields'])); |
||
2604 | break; |
||
2605 | default: |
||
2606 | $mixedFields = array_filter($fields['leadFields']); |
||
2607 | $fields = []; |
||
2608 | foreach ($mixedFields as $sfField => $mField) { |
||
2609 | if (false !== strpos($sfField, '__'.$object)) { |
||
2610 | $fields[] = str_replace('__'.$object, '', $sfField); |
||
2611 | } |
||
2612 | if (false !== strpos($sfField, '-'.$object)) { |
||
2613 | $fields[] = str_replace('-'.$object, '', $sfField); |
||
2614 | } |
||
2615 | } |
||
2616 | } |
||
2617 | |||
2618 | return $fields; |
||
2619 | } |
||
2620 | |||
2621 | /** |
||
2622 | * @param $sfObject |
||
2623 | * @param $sfFieldString |
||
2624 | * |
||
2625 | * @return mixed|string |
||
2626 | * |
||
2627 | * @throws ApiErrorException |
||
2628 | */ |
||
2629 | public function getDncHistory($sfObject, $sfFieldString) |
||
2630 | { |
||
2631 | //get last modified date for donot contact in Salesforce |
||
2632 | $historySelect = 'Select Field, '.$sfObject.'Id, CreatedDate, isDeleted, NewValue from '.$sfObject.'History where Field = \'HasOptedOutOfEmail\' and '.$sfObject.'Id IN ('.$sfFieldString.') ORDER BY CreatedDate DESC'; |
||
2633 | $queryUrl = $this->getQueryUrl(); |
||
2634 | |||
2635 | return $this->getApiHelper()->request('query', ['q' => $historySelect], 'GET', false, null, $queryUrl); |
||
2636 | } |
||
2637 | |||
2638 | /** |
||
2639 | * Update the record in each system taking the last modified record. |
||
2640 | * |
||
2641 | * @param string $channel |
||
2642 | * @param string $sfObject |
||
2643 | * |
||
2644 | * @return int |
||
2645 | * |
||
2646 | * @throws ApiErrorException |
||
2647 | */ |
||
2648 | public function pushLeadDoNotContactByDate($channel, &$sfRecords, $sfObject, $params = []) |
||
2649 | { |
||
2650 | $filters = []; |
||
2651 | $leadIds = []; |
||
2652 | |||
2653 | if (empty($sfRecords) || !isset($sfRecords['mauticContactIsContactableByEmail']) && !$this->updateDncByDate()) { |
||
2654 | return; |
||
2655 | } |
||
2656 | |||
2657 | foreach ($sfRecords as $record) { |
||
2658 | if (empty($record['integration_entity_id'])) { |
||
2659 | continue; |
||
2660 | } |
||
2661 | |||
2662 | $leadIds[$record['internal_entity_id']] = $record['integration_entity_id']; |
||
2663 | $leadEmails[$record['internal_entity_id']] = $record['email']; |
||
2664 | } |
||
2665 | |||
2666 | $sfFieldString = "'".implode("','", $leadIds)."'"; |
||
2667 | |||
2668 | $historySF = $this->getDncHistory($sfObject, $sfFieldString); |
||
2669 | //if there is no records of when it was modified in SF then just exit |
||
2670 | if (empty($historySF['records'])) { |
||
2671 | return; |
||
2672 | } |
||
2673 | |||
2674 | //get last modified date for donot contact in Mautic |
||
2675 | $auditLogRepo = $this->em->getRepository('MauticCoreBundle:AuditLog'); |
||
2676 | $filters['search'] = 'dnc_channel_status%'.$channel; |
||
2677 | $lastModifiedDNCDate = $auditLogRepo->getAuditLogsForLeads(array_flip($leadIds), $filters, ['dateAdded', 'DESC'], $params['start']); |
||
2678 | $trackedIds = []; |
||
2679 | foreach ($historySF['records'] as $sfModifiedDNC) { |
||
2680 | // if we have no history in Mautic, then update the Mautic record |
||
2681 | if (empty($lastModifiedDNCDate)) { |
||
2682 | $leads = array_flip($leadIds); |
||
2683 | $leadId = $leads[$sfModifiedDNC[$sfObject.'Id']]; |
||
2684 | $this->updateMauticDNC($leadId, $sfModifiedDNC['NewValue']); |
||
2685 | $key = $this->getSyncKey($leadEmails[$leadId]); |
||
2686 | unset($sfRecords[$key]['mauticContactIsContactableByEmail']); |
||
2687 | continue; |
||
2688 | } |
||
2689 | |||
2690 | foreach ($lastModifiedDNCDate as $logs) { |
||
2691 | $leadId = $logs['objectId']; |
||
2692 | if (strtotime($logs['dateAdded']->format('c')) > strtotime($sfModifiedDNC['CreatedDate'])) { |
||
2693 | $trackedIds[] = $leadId; |
||
2694 | } |
||
2695 | if (((isset($leadIds[$leadId]) && $leadIds[$leadId] == $sfModifiedDNC[$sfObject.'Id'])) |
||
2696 | && ((strtotime($sfModifiedDNC['CreatedDate']) > strtotime($logs['dateAdded']->format('c')))) && !in_array($leadId, $trackedIds)) { |
||
2697 | //SF was updated last so update Mautic record |
||
2698 | $key = $this->getSyncKey($leadEmails[$leadId]); |
||
2699 | unset($sfRecords[$key]['mauticContactIsContactableByEmail']); |
||
2700 | $this->updateMauticDNC($leadId, $sfModifiedDNC['NewValue']); |
||
2701 | $trackedIds[] = $leadId; |
||
2702 | break; |
||
2703 | } |
||
2704 | } |
||
2705 | } |
||
2706 | } |
||
2707 | |||
2708 | /** |
||
2709 | * @param $leadId |
||
2710 | * @param $newDncValue |
||
2711 | */ |
||
2712 | private function updateMauticDNC($leadId, $newDncValue) |
||
2713 | { |
||
2714 | $lead = $this->leadModel->getEntity($leadId); |
||
2715 | |||
2716 | if (true == $newDncValue) { |
||
2717 | $this->doNotContact->addDncForContact($lead->getId(), 'email', DoNotContact::MANUAL, 'Set by Salesforce', true, false, true); |
||
2718 | } elseif (false == $newDncValue) { |
||
2719 | $this->doNotContact->removeDncForContact($lead->getId(), 'email', true); |
||
2720 | } |
||
2721 | } |
||
2722 | |||
2723 | /** |
||
2724 | * @param array $params |
||
2725 | * |
||
2726 | * @return mixed |
||
2727 | */ |
||
2728 | public function pushCompanies($params = []) |
||
2729 | { |
||
2730 | $limit = (isset($params['limit'])) ? $params['limit'] : 100; |
||
2731 | list($fromDate, $toDate) = $this->getSyncTimeframeDates($params); |
||
2732 | $config = $this->mergeConfigToFeatureSettings($params); |
||
2733 | $integrationEntityRepo = $this->getIntegrationEntityRepository(); |
||
2734 | |||
2735 | if (!isset($config['companyFields'])) { |
||
2736 | return [0, 0, 0, 0]; |
||
2737 | } |
||
2738 | |||
2739 | $totalUpdated = 0; |
||
2740 | $totalCreated = 0; |
||
2741 | $totalErrors = 0; |
||
2742 | $sfObject = 'Account'; |
||
2743 | |||
2744 | //all available fields in Salesforce for Account |
||
2745 | $availableFields = $this->getAvailableLeadFields(['feature_settings' => ['objects' => [$sfObject]]]); |
||
2746 | |||
2747 | //get company fields from Mautic that have been mapped |
||
2748 | $mauticCompanyFieldString = implode(', l.', $config['companyFields']); |
||
2749 | $mauticCompanyFieldString = 'l.'.$mauticCompanyFieldString; |
||
2750 | |||
2751 | $fieldKeys = array_keys($config['companyFields']); |
||
2752 | $fieldsToCreate = $this->prepareFieldsForSync($config['companyFields'], $fieldKeys, $sfObject); |
||
2753 | $fieldsToUpdateInSf = $this->getPriorityFieldsForIntegration($config, $sfObject, 'mautic_company'); |
||
2754 | |||
2755 | $objectFields['company'] = [ |
||
2756 | 'update' => !empty($fieldsToUpdateInSf) ? array_intersect_key($fieldsToCreate, $fieldsToUpdateInSf) : [], |
||
2757 | 'create' => $fieldsToCreate, |
||
2758 | ]; |
||
2759 | |||
2760 | list($fields, $string) = $this->getRequiredFieldString( |
||
2761 | $config, |
||
2762 | $availableFields, |
||
2763 | 'company' |
||
2764 | ); |
||
2765 | |||
2766 | $objectFields['company']['required'] = [ |
||
2767 | 'fields' => $fields, |
||
2768 | 'string' => $string, |
||
2769 | ]; |
||
2770 | |||
2771 | if (empty($objectFields)) { |
||
2772 | return [0, 0, 0, 0]; |
||
2773 | } |
||
2774 | |||
2775 | $originalLimit = $limit; |
||
2776 | $progress = false; |
||
2777 | |||
2778 | // Get a total number of companies to be updated and/or created for the progress counter |
||
2779 | $totalToUpdate = array_sum( |
||
2780 | $integrationEntityRepo->findLeadsToUpdate( |
||
2781 | 'Salesforce', |
||
2782 | 'company', |
||
2783 | $mauticCompanyFieldString, |
||
2784 | false, |
||
2785 | $fromDate, |
||
2786 | $toDate, |
||
2787 | $sfObject, |
||
2788 | [] |
||
2789 | ) |
||
2790 | ); |
||
2791 | $totalToCreate = $integrationEntityRepo->findLeadsToCreate( |
||
2792 | 'Salesforce', |
||
2793 | $mauticCompanyFieldString, |
||
2794 | false, |
||
2795 | $fromDate, |
||
2796 | $toDate, |
||
2797 | 'company' |
||
2798 | ); |
||
2799 | |||
2800 | $totalCount = $totalToProcess = $totalToCreate + $totalToUpdate; |
||
2801 | |||
2802 | if (defined('IN_MAUTIC_CONSOLE')) { |
||
2803 | // start with update |
||
2804 | if ($totalToUpdate + $totalToCreate) { |
||
2805 | $output = new ConsoleOutput(); |
||
2806 | $output->writeln("About $totalToUpdate to update and about $totalToCreate to create/update"); |
||
2807 | $progress = new ProgressBar($output, $totalCount); |
||
2808 | } |
||
2809 | } |
||
2810 | |||
2811 | $noMoreUpdates = false; |
||
2812 | |||
2813 | while ($totalCount > 0) { |
||
2814 | $limit = $originalLimit; |
||
2815 | $mauticData = []; |
||
2816 | $checkCompaniesInSF = []; |
||
2817 | $companiesToSync = []; |
||
2818 | $processedCompanies = []; |
||
2819 | |||
2820 | // Process the updates |
||
2821 | if (!$noMoreUpdates) { |
||
2822 | $noMoreUpdates = $this->getMauticRecordsToUpdate( |
||
2823 | $checkCompaniesInSF, |
||
2824 | $mauticCompanyFieldString, |
||
2825 | $sfObject, |
||
2826 | $limit, |
||
2827 | $fromDate, |
||
2828 | $toDate, |
||
2829 | $totalCount, |
||
2830 | 'company' |
||
2831 | ); |
||
2832 | |||
2833 | if ($limit) { |
||
2834 | // Mainly done for test mocking purposes |
||
2835 | $limit = $this->getSalesforceSyncLimit($checkCompaniesInSF, $limit); |
||
2836 | } |
||
2837 | } |
||
2838 | |||
2839 | // If there is still room - grab Mautic companies to create if the Lead object is enabled |
||
2840 | $sfEntityRecords = []; |
||
2841 | if ((null === $limit || $limit > 0) && !empty($mauticCompanyFieldString)) { |
||
2842 | $this->getMauticEntitesToCreate( |
||
2843 | $checkCompaniesInSF, |
||
2844 | $mauticCompanyFieldString, |
||
2845 | $limit, |
||
2846 | $fromDate, |
||
2847 | $toDate, |
||
2848 | $totalCount, |
||
2849 | $progress |
||
2850 | ); |
||
2851 | } |
||
2852 | |||
2853 | if ($checkCompaniesInSF) { |
||
2854 | $sfEntityRecords = $this->getSalesforceAccountsByName($checkCompaniesInSF, implode(',', array_keys($config['companyFields']))); |
||
2855 | |||
2856 | if (!isset($sfEntityRecords['records'])) { |
||
2857 | // Something is wrong so throw an exception to prevent creating a bunch of new companies |
||
2858 | $this->cleanupFromSync( |
||
2859 | $companiesToSync, |
||
2860 | json_encode($sfEntityRecords) |
||
2861 | ); |
||
2862 | } |
||
2863 | } |
||
2864 | |||
2865 | // We're done |
||
2866 | if (!$checkCompaniesInSF) { |
||
2867 | break; |
||
2868 | } |
||
2869 | |||
2870 | if (!empty($sfEntityRecords) and isset($sfEntityRecords['records'])) { |
||
2871 | $this->prepareMauticCompaniesToUpdate( |
||
2872 | $mauticData, |
||
2873 | $checkCompaniesInSF, |
||
2874 | $processedCompanies, |
||
2875 | $companiesToSync, |
||
2876 | $objectFields, |
||
2877 | $sfEntityRecords, |
||
2878 | $progress |
||
2879 | ); |
||
2880 | } |
||
2881 | |||
2882 | // Only create left over if Lead object is enabled in integration settings |
||
2883 | if ($checkCompaniesInSF) { |
||
2884 | $this->prepareMauticCompaniesToCreate( |
||
2885 | $mauticData, |
||
2886 | $checkCompaniesInSF, |
||
2887 | $processedCompanies, |
||
2888 | $objectFields |
||
2889 | ); |
||
2890 | } |
||
2891 | |||
2892 | // Persist pending changes |
||
2893 | $this->cleanupFromSync($companiesToSync); |
||
2894 | |||
2895 | $this->makeCompositeRequest($mauticData, $totalUpdated, $totalCreated, $totalErrors); |
||
2896 | |||
2897 | // Stop gap - if 100% let's kill the script |
||
2898 | if ($progress && $progress->getProgressPercent() >= 1) { |
||
2899 | break; |
||
2900 | } |
||
2901 | } |
||
2902 | |||
2903 | if ($progress) { |
||
2904 | $progress->finish(); |
||
2905 | $output->writeln(''); |
||
2906 | } |
||
2907 | |||
2908 | $this->logger->debug('SALESFORCE: '.$this->getApiHelper()->getRequestCounter().' API requests made for pushCompanies'); |
||
2909 | |||
2910 | // Assume that those not touched are ignored due to not having matching fields, duplicates, etc |
||
2911 | $totalIgnored = $totalToProcess - ($totalUpdated + $totalCreated + $totalErrors); |
||
2912 | |||
2913 | if ($totalIgnored < 0) { //this could have been marked as deleted so it was not pushed |
||
2914 | $totalIgnored = $totalIgnored * -1; |
||
2915 | } |
||
2916 | |||
2917 | return [$totalUpdated, $totalCreated, $totalErrors, $totalIgnored]; |
||
2918 | } |
||
2919 | |||
2920 | /** |
||
2921 | * @param $mauticData |
||
2922 | * @param $objectFields |
||
2923 | * @param $sfEntityRecords |
||
2924 | * @param null $progress |
||
2925 | */ |
||
2926 | protected function prepareMauticCompaniesToUpdate( |
||
2927 | &$mauticData, |
||
2928 | &$checkCompaniesInSF, |
||
2929 | &$processedCompanies, |
||
2930 | &$companiesToSync, |
||
2931 | $objectFields, |
||
2932 | $sfEntityRecords, |
||
2933 | $progress = null |
||
2934 | ) { |
||
2935 | foreach ($sfEntityRecords['records'] as $sfEntityRecord) { |
||
2936 | $syncCompany = false; |
||
2937 | $update = false; |
||
2938 | $sfObject = $sfEntityRecord['attributes']['type']; |
||
2939 | if (!isset($sfEntityRecord['Name'])) { |
||
2940 | // This is a record we don't recognize so continue |
||
2941 | return; |
||
2942 | } |
||
2943 | $key = $sfEntityRecord['Id']; |
||
2944 | |||
2945 | if (!isset($sfEntityRecord['Id'])) { |
||
2946 | // This is a record we don't recognize so continue |
||
2947 | return; |
||
2948 | } |
||
2949 | |||
2950 | $id = $sfEntityRecord['Id']; |
||
2951 | if (isset($checkCompaniesInSF[$key])) { |
||
2952 | $companyData = (isset($processedCompanies[$key])) ? $processedCompanies[$key] : $checkCompaniesInSF[$key]; |
||
2953 | $update = true; |
||
2954 | } else { |
||
2955 | foreach ($checkCompaniesInSF as $mauticKey => $mauticCompanies) { |
||
2956 | $key = $mauticKey; |
||
2957 | |||
2958 | if (isset($mauticCompanies['companyname']) && $mauticCompanies['companyname'] == $sfEntityRecord['Name']) { |
||
2959 | $companyData = (isset($processedCompanies[$key])) ? $processedCompanies[$key] : $checkCompaniesInSF[$key]; |
||
2960 | $companyId = $companyData['internal_entity_id']; |
||
2961 | |||
2962 | $integrationEntity = $this->createIntegrationEntity( |
||
2963 | $sfObject, |
||
2964 | $id, |
||
2965 | 'company', |
||
2966 | $companyId |
||
2967 | ); |
||
2968 | |||
2969 | $checkCompaniesInSF[$key]['integration_entity'] = $sfObject; |
||
2970 | $checkCompaniesInSF[$key]['integration_entity_id'] = $id; |
||
2971 | $checkCompaniesInSF[$key]['id'] = $integrationEntity->getId(); |
||
2972 | $update = true; |
||
2973 | } |
||
2974 | } |
||
2975 | } |
||
2976 | |||
2977 | if (!$update) { |
||
2978 | return; |
||
2979 | } |
||
2980 | |||
2981 | if (!isset($processedCompanies[$key])) { |
||
2982 | if ($progress) { |
||
2983 | $progress->advance(); |
||
2984 | } |
||
2985 | // Mark that this lead has been processed |
||
2986 | $companyData = $processedCompanies[$key] = $checkCompaniesInSF[$key]; |
||
2987 | } |
||
2988 | |||
2989 | $companyEntity = $this->em->getReference('MauticLeadBundle:Company', $companyData['internal_entity_id']); |
||
2990 | |||
2991 | if ($updateCompany = $this->buildCompositeBody( |
||
2992 | $mauticData, |
||
2993 | $objectFields['company'], |
||
2994 | $sfObject, |
||
2995 | $companyData, |
||
2996 | $sfEntityRecord['Id'], |
||
2997 | $sfEntityRecord |
||
2998 | ) |
||
2999 | ) { |
||
3000 | // Get the company entity |
||
3001 | /* @var Lead $leadEntity */ |
||
3002 | foreach ($updateCompany as $mauticField => $sfValue) { |
||
3003 | $companyEntity->addUpdatedField($mauticField, $sfValue); |
||
3004 | } |
||
3005 | |||
3006 | $syncCompany = !empty($companyEntity->getChanges(true)); |
||
3007 | } |
||
3008 | if ($syncCompany) { |
||
3009 | $companiesToSync[] = $companyEntity; |
||
3010 | } else { |
||
3011 | $this->em->detach($companyEntity); |
||
3012 | } |
||
3013 | |||
3014 | unset($checkCompaniesInSF[$key]); |
||
3015 | } |
||
3016 | } |
||
3017 | |||
3018 | /** |
||
3019 | * @param $mauticData |
||
3020 | * @param $processedCompanies |
||
3021 | * @param $objectFields |
||
3022 | */ |
||
3023 | protected function prepareMauticCompaniesToCreate( |
||
3024 | &$mauticData, |
||
3025 | &$checkCompaniesInSF, |
||
3026 | &$processedCompanies, |
||
3027 | $objectFields |
||
3028 | ) { |
||
3029 | foreach ($checkCompaniesInSF as $key => $company) { |
||
3030 | if (!empty($company['integration_entity_id']) and array_key_exists($key, $processedCompanies)) { |
||
3031 | if ($this->buildCompositeBody( |
||
3032 | $mauticData, |
||
3033 | $objectFields['company'], |
||
3034 | $company['integration_entity'], |
||
3035 | $company, |
||
3036 | $company['integration_entity_id'] |
||
3037 | ) |
||
3038 | ) { |
||
3039 | $this->logger->debug('SALESFORCE: Company has existing ID so updating '.$company['integration_entity_id']); |
||
3040 | } |
||
3041 | } else { |
||
3042 | $this->buildCompositeBody( |
||
3043 | $mauticData, |
||
3044 | $objectFields['company'], |
||
3045 | 'Account', |
||
3046 | $company |
||
3047 | ); |
||
3048 | } |
||
3049 | |||
3050 | $processedCompanies[$key] = $checkCompaniesInSF[$key]; |
||
3051 | unset($checkCompaniesInSF[$key]); |
||
3052 | } |
||
3053 | } |
||
3054 | |||
3055 | /** |
||
3056 | * @param $sfObject |
||
3057 | * @param $limit |
||
3058 | * @param $fromDate |
||
3059 | * @param $toDate |
||
3060 | * @param $totalCount |
||
3061 | * |
||
3062 | * @return bool |
||
3063 | */ |
||
3064 | protected function getMauticRecordsToUpdate( |
||
3065 | &$checkIdsInSF, |
||
3066 | $mauticEntityFieldString, |
||
3067 | &$sfObject, |
||
3068 | $limit, |
||
3069 | $fromDate, |
||
3070 | $toDate, |
||
3071 | &$totalCount, |
||
3072 | $internalEntity |
||
3073 | ) { |
||
3074 | // Fetch them separately so we can determine if Leads are already Contacts |
||
3075 | $toUpdate = $this->getIntegrationEntityRepository()->findLeadsToUpdate( |
||
3076 | 'Salesforce', |
||
3077 | $internalEntity, |
||
3078 | $mauticEntityFieldString, |
||
3079 | $limit, |
||
3080 | $fromDate, |
||
3081 | $toDate, |
||
3082 | $sfObject |
||
3083 | )[$sfObject]; |
||
3084 | |||
3085 | $toUpdateCount = count($toUpdate); |
||
3086 | $totalCount -= $toUpdateCount; |
||
3087 | |||
3088 | foreach ($toUpdate as $entity) { |
||
3089 | if (!empty($entity['integration_entity_id'])) { |
||
3090 | $checkIdsInSF[$entity['integration_entity_id']] = $entity; |
||
3091 | } |
||
3092 | } |
||
3093 | |||
3094 | return 0 === $toUpdateCount; |
||
3095 | } |
||
3096 | |||
3097 | /** |
||
3098 | * @param $checkIdsInSF |
||
3099 | * @param $mauticCompanyFieldString |
||
3100 | * @param $limit |
||
3101 | * @param $fromDate |
||
3102 | * @param $toDate |
||
3103 | * @param $totalCount |
||
3104 | * @param null $progress |
||
3105 | */ |
||
3106 | protected function getMauticEntitesToCreate( |
||
3107 | &$checkIdsInSF, |
||
3108 | $mauticCompanyFieldString, |
||
3109 | $limit, |
||
3110 | $fromDate, |
||
3111 | $toDate, |
||
3112 | &$totalCount, |
||
3113 | $progress = null |
||
3114 | ) { |
||
3115 | $integrationEntityRepo = $this->getIntegrationEntityRepository(); |
||
3116 | $entitiesToCreate = $integrationEntityRepo->findLeadsToCreate( |
||
3117 | 'Salesforce', |
||
3118 | $mauticCompanyFieldString, |
||
3119 | $limit, |
||
3120 | $fromDate, |
||
3121 | $toDate, |
||
3122 | 'company' |
||
3123 | ); |
||
3124 | $totalCount -= count($entitiesToCreate); |
||
3125 | |||
3126 | foreach ($entitiesToCreate as $entity) { |
||
3127 | if (isset($entity['companyname'])) { |
||
3128 | $checkIdsInSF[$entity['internal_entity_id']] = $entity; |
||
3129 | } elseif ($progress) { |
||
3130 | $progress->advance(); |
||
3131 | } |
||
3132 | } |
||
3133 | } |
||
3134 | |||
3135 | /** |
||
3136 | * @param $checkIdsInSF |
||
3137 | * @param $requiredFieldString |
||
3138 | * |
||
3139 | * @return array |
||
3140 | * |
||
3141 | * @throws ApiErrorException |
||
3142 | * @throws \Doctrine\ORM\ORMException |
||
3143 | * @throws \Exception |
||
3144 | */ |
||
3145 | protected function getSalesforceAccountsByName(&$checkIdsInSF, $requiredFieldString) |
||
3146 | { |
||
3147 | $searchForIds = []; |
||
3148 | $searchForNames = []; |
||
3149 | |||
3150 | foreach ($checkIdsInSF as $key => $company) { |
||
3151 | if (!empty($company['integration_entity_id'])) { |
||
3152 | $searchForIds[$key] = $company['integration_entity_id']; |
||
3153 | |||
3154 | continue; |
||
3155 | } |
||
3156 | |||
3157 | if (!empty($company['companyname'])) { |
||
3158 | $searchForNames[$key] = $company['companyname']; |
||
3159 | } |
||
3160 | } |
||
3161 | |||
3162 | $resultsByName = $this->getApiHelper()->getCompaniesByName($searchForNames, $requiredFieldString); |
||
3163 | $resultsById = []; |
||
3164 | if (!empty($searchForIds)) { |
||
3165 | $resultsById = $this->getApiHelper()->getCompaniesById($searchForIds, $requiredFieldString); |
||
3166 | |||
3167 | //mark as deleleted |
||
3168 | foreach ($resultsById['records'] as $sfId => $record) { |
||
3169 | if (isset($record['IsDeleted']) && 1 == $record['IsDeleted']) { |
||
3170 | if ($foundKey = array_search($record['Id'], $searchForIds)) { |
||
3171 | $integrationEntity = $this->em->getReference('MauticPluginBundle:IntegrationEntity', $checkIdsInSF[$foundKey]['id']); |
||
3172 | $integrationEntity->setInternalEntity('company-deleted'); |
||
3173 | $this->persistIntegrationEntities[] = $integrationEntity; |
||
3174 | unset($checkIdsInSF[$foundKey]); |
||
3175 | } |
||
3176 | |||
3177 | unset($resultsById['records'][$sfId]); |
||
3178 | } |
||
3179 | } |
||
3180 | } |
||
3181 | |||
3182 | $this->cleanupFromSync(); |
||
3183 | |||
3184 | return array_merge($resultsByName, $resultsById); |
||
3185 | } |
||
3186 | |||
3187 | public function getCompanyName($accountId, $field, $searchBy = 'Id') |
||
3188 | { |
||
3189 | $companyField = null; |
||
3190 | $accountId = str_replace("'", "\'", $this->cleanPushData($accountId)); |
||
3191 | $companyQuery = 'Select Id, Name from Account where '.$searchBy.' = \''.$accountId.'\' and IsDeleted = false'; |
||
3192 | $contactCompany = $this->getApiHelper()->getLeads($companyQuery, 'Account'); |
||
3193 | |||
3194 | if (!empty($contactCompany['records'])) { |
||
3195 | foreach ($contactCompany['records'] as $company) { |
||
3196 | if (!empty($company[$field])) { |
||
3197 | $companyField = $company[$field]; |
||
3198 | break; |
||
3199 | } |
||
3200 | } |
||
3201 | } |
||
3202 | |||
3203 | return $companyField; |
||
3204 | } |
||
3205 | |||
3206 | public function getLeadDoNotContactByDate($channel, $matchedFields, $object, $lead, $sfData, $params = []) |
||
3207 | { |
||
3208 | if (isset($matchedFields['mauticContactIsContactableByEmail']) and true === $this->updateDncByDate()) { |
||
3209 | $matchedFields['internal_entity_id'] = $lead->getId(); |
||
3210 | $matchedFields['integration_entity_id'] = $sfData['Id__'.$object]; |
||
3211 | $record[$lead->getEmail()] = $matchedFields; |
||
3212 | $this->pushLeadDoNotContactByDate($channel, $record, $object, $params); |
||
3213 | |||
3214 | return $record[$lead->getEmail()]; |
||
3215 | } |
||
3216 | |||
3217 | return $matchedFields; |
||
3218 | } |
||
3219 | } |
||
3220 |