Issues (3627)

Integration/SalesforceIntegration.php (1 issue)

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
The assignment to $campaigns is dead and can be removed.
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