Issues (3627)

Integration/CrmAbstractIntegration.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\LeadBundle\DataObject\LeadManipulator;
15
use Mautic\LeadBundle\Entity\Company;
16
use Mautic\LeadBundle\Entity\Lead;
17
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper;
18
use Mautic\PluginBundle\Entity\Integration;
19
use Mautic\PluginBundle\Integration\AbstractIntegration;
20
use MauticPlugin\MauticCrmBundle\Api\CrmApi;
21
22
abstract class CrmAbstractIntegration extends AbstractIntegration
23
{
24
    protected $auth;
25
    protected $helper;
26
27
    public function setIntegrationSettings(Integration $settings)
28
    {
29
        //make sure URL does not have ending /
30
        $keys = $this->getDecryptedApiKeys($settings);
31
        if (isset($keys['url']) && '/' == substr($keys['url'], -1)) {
32
            $keys['url'] = substr($keys['url'], 0, -1);
33
            $this->encryptAndSetApiKeys($keys, $settings);
34
        }
35
36
        parent::setIntegrationSettings($settings);
37
    }
38
39
    /**
40
     * {@inheritdoc}
41
     *
42
     * @return string
43
     */
44
    public function getAuthenticationType()
45
    {
46
        return 'rest';
47
    }
48
49
    /**
50
     * @return array
51
     */
52
    public function getSupportedFeatures()
53
    {
54
        return ['push_lead', 'get_leads'];
55
    }
56
57
    /**
58
     * @param Lead|array $lead
59
     * @param array      $config
60
     *
61
     * @return array|bool
62
     */
63
    public function pushLead($lead, $config = [])
64
    {
65
        $config = $this->mergeConfigToFeatureSettings($config);
66
67
        if (empty($config['leadFields'])) {
68
            return [];
69
        }
70
71
        $mappedData = $this->populateLeadData($lead, $config);
72
73
        $this->amendLeadDataBeforePush($mappedData);
74
75
        if (empty($mappedData)) {
76
            return false;
77
        }
78
79
        try {
80
            if ($this->isAuthorized()) {
81
                $this->getApiHelper()->createLead($mappedData, $lead);
82
83
                return true;
84
            }
85
        } catch (\Exception $e) {
86
            $this->logIntegrationError($e);
87
        }
88
89
        return false;
90
    }
91
92
    /**
93
     * @param array $params
94
     */
95
    public function getLeads($params, $query, &$executed, $result = [], $object = 'Lead')
96
    {
97
        $executed = null;
98
99
        $query = $this->getFetchQuery($params);
100
101
        try {
102
            if ($this->isAuthorized()) {
103
                $result = $this->getApiHelper()->getLeads($query);
104
105
                return $this->amendLeadDataBeforeMauticPopulate($result, $object);
106
            }
107
        } catch (\Exception $e) {
108
            $this->logIntegrationError($e);
109
        }
110
111
        return $executed;
112
    }
113
114
    /**
115
     * Amend mapped lead data before pushing to CRM.
116
     *
117
     * @param $mappedData
118
     */
119
    public function amendLeadDataBeforePush(&$mappedData)
120
    {
121
    }
122
123
    /**
124
     * get query to fetch lead data.
125
     *
126
     * @param $config
127
     */
128
    public function getFetchQuery($config)
129
    {
130
    }
131
132
    /**
133
     * Ammend mapped lead data before creating to Mautic.
134
     *
135
     * @param $data
136
     * @param $object
137
     */
138
    public function amendLeadDataBeforeMauticPopulate($data, $object)
139
    {
140
        return null;
141
    }
142
143
    /**
144
     * @return string
145
     */
146
    public function getClientIdKey()
147
    {
148
        return 'client_id';
149
    }
150
151
    /**
152
     * @return string
153
     */
154
    public function getClientSecretKey()
155
    {
156
        return 'client_secret';
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function sortFieldsAlphabetically()
163
    {
164
        return false;
165
    }
166
167
    /**
168
     * Get the API helper.
169
     *
170
     * @return CrmApi
171
     */
172
    public function getApiHelper()
173
    {
174
        if (empty($this->helper)) {
175
            $class        = '\\MauticPlugin\\MauticCrmBundle\\Api\\'.$this->getName().'Api';
176
            $this->helper = new $class($this);
177
        }
178
179
        return $this->helper;
180
    }
181
182
    /**
183
     * @param array $params
184
     */
185
    public function pushLeadActivity($params = [])
186
    {
187
    }
188
189
    /**
190
     * @param $leadId
191
     *
192
     * @return array
193
     */
194
    public function getLeadData(\DateTime $startDate = null, \DateTime $endDate = null, $leadId)
195
    {
196
        $leadIds      = (!is_array($leadId)) ? [$leadId] : $leadId;
197
        $leadActivity = [];
198
199
        $config = $this->mergeConfigToFeatureSettings();
200
        if (!isset($config['activityEvents'])) {
201
            // BC for pre 2.11.0
202
            $config['activityEvents'] = ['point.gained', 'form.submitted', 'email.read'];
203
        } elseif (empty($config['activityEvents'])) {
204
            // Inclusive filter meaning we only send events if something is selected
205
            return [];
206
        }
207
208
        $filters = [
209
            'search'        => '',
210
            'includeEvents' => $config['activityEvents'],
211
            'excludeEvents' => [],
212
        ];
213
214
        if ($startDate) {
215
            $filters['dateFrom'] = $startDate;
216
            $filters['dateTo']   = $endDate;
217
        }
218
219
        foreach ($leadIds as $leadId) {
220
            $i        = 0;
221
            $activity = [];
222
            $lead     = $this->em->getReference('MauticLeadBundle:Lead', $leadId);
223
            $page     = 1;
224
225
            while (true) {
226
                $engagements = $this->leadModel->getEngagements($lead, $filters, null, $page, 100, false);
227
                $events      = $engagements[0]['events'];
228
229
                if (empty($events)) {
230
                    break;
231
                }
232
233
                // inject lead into events
234
                foreach ($events as $event) {
235
                    $link  = '';
236
                    $label = (isset($event['eventLabel'])) ? $event['eventLabel'] : $event['eventType'];
237
                    if (is_array($label)) {
238
                        $link  = $label['href'];
239
                        $label = $label['label'];
240
                    }
241
242
                    $activity[$i]['eventType']   = $event['eventType'];
243
                    $activity[$i]['name']        = $event['eventType'].' - '.$label;
244
                    $activity[$i]['description'] = $link;
245
                    $activity[$i]['dateAdded']   = $event['timestamp'];
246
247
                    // We must keep BC with pre 2.11.0 formatting in order to prevent duplicates
248
                    switch ($event['eventType']) {
249
                        case 'point.gained':
250
                            $id = str_replace($event['eventType'], 'pointChange', $event['eventId']);
251
                            break;
252
                        case 'form.submitted':
253
                            $id = str_replace($event['eventType'], 'formSubmission', $event['eventId']);
254
                            break;
255
                        case 'email.read':
256
                            $id = str_replace($event['eventType'], 'emailStat', $event['eventId']);
257
                            break;
258
                        default:
259
                            // Just to keep congruent formatting with the three above
260
                            $id = str_replace(' ', '', ucwords(str_replace('.', ' ', $event['eventId'])));
261
                    }
262
263
                    $activity[$i]['id'] = $id;
264
                    ++$i;
265
                }
266
267
                ++$page;
268
269
                // Lots of entities will be loaded into memory while compiling these events so let's prevent memory overload by clearing the EM
270
                $entityToNotDetach = ['Mautic\PluginBundle\Entity\Integration', 'Mautic\PluginBundle\Entity\Plugin'];
271
                $loadedEntities    = $this->em->getUnitOfWork()->getIdentityMap();
272
                foreach ($loadedEntities as $name => $loadedEntity) {
273
                    if (!in_array($name, $entityToNotDetach)) {
274
                        $this->em->clear($name);
275
                    }
276
                }
277
            }
278
279
            $leadActivity[$leadId] = [
280
                'records' => $activity,
281
            ];
282
283
            unset($activity);
284
        }
285
286
        return $leadActivity;
287
    }
288
289
    /**
290
     * @param      $data
291
     * @param null $object
292
     *
293
     * @return Company|null
294
     */
295
    public function getMauticCompany($data, $object = null)
296
    {
297
        if (is_object($data)) {
298
            // Convert to array in all levels
299
            $data = json_encode(json_decode($data), true);
300
        } elseif (is_string($data)) {
301
            // Assume JSON
302
            $data = json_decode($data, true);
303
        }
304
        $config        = $this->mergeConfigToFeatureSettings([]);
305
        $matchedFields = $this->populateMauticLeadData($data, $config, 'company');
306
307
        $companyFieldTypes = $this->fieldModel->getFieldListWithProperties('company');
308
        foreach ($matchedFields as $companyField => $value) {
309
            if (isset($companyFieldTypes[$companyField]['type'])) {
310
                switch ($companyFieldTypes[$companyField]['type']) {
311
                    case 'text':
312
                        $matchedFields[$companyField] = substr($value, 0, 255);
313
                        break;
314
                    case 'date':
315
                        $date                         = new \DateTime($value);
316
                        $matchedFields[$companyField] = $date->format('Y-m-d');
317
                        break;
318
                    case 'datetime':
319
                        $date                         = new \DateTime($value);
320
                        $matchedFields[$companyField] = $date->format('Y-m-d H:i:s');
321
                        break;
322
                }
323
            }
324
        }
325
326
        // Default to new company
327
        $company         = new Company();
328
        $existingCompany = IdentifyCompanyHelper::identifyLeadsCompany($matchedFields, null, $this->companyModel);
329
        if (!empty($existingCompany[2])) {
330
            $company = $existingCompany[2];
331
        }
332
333
        if (!empty($existingCompany[2])) {
334
            $fieldsToUpdate = $this->getPriorityFieldsForMautic($config, $object, 'mautic_company');
335
            $fieldsToUpdate = array_intersect_key($config['companyFields'], $fieldsToUpdate);
336
            $matchedFields  = array_intersect_key($matchedFields, array_flip($fieldsToUpdate));
337
        } else {
338
            $matchedFields = $this->hydrateCompanyName($matchedFields);
339
340
            // If we don't have an company name, don't create the company because it'll result in what looks like an "empty" company
341
            if (empty($matchedFields['companyname'])) {
342
                return null;
343
            }
344
        }
345
346
        $this->companyModel->setFieldValues($company, $matchedFields, false);
347
        $this->companyModel->saveEntity($company, false);
348
349
        return $company;
350
    }
351
352
    /**
353
     * Create or update existing Mautic lead from the integration's profile data.
354
     *
355
     * @param mixed       $data        Profile data from integration
356
     * @param bool|true   $persist     Set to false to not persist lead to the database in this method
357
     * @param array|null  $socialCache
358
     * @param mixed||null $identifiers
0 ignored issues
show
Documentation Bug introduced by
The doc comment mixed||null at position 2 could not be parsed: Unknown type name '|' at position 2 in mixed||null.
Loading history...
359
     * @param string|null $object
360
     *
361
     * @return Lead
362
     */
363
    public function getMauticLead($data, $persist = true, $socialCache = null, $identifiers = null, $object = null)
364
    {
365
        if (is_object($data)) {
366
            // Convert to array in all levels
367
            $data = json_encode(json_decode($data), true);
368
        } elseif (is_string($data)) {
369
            // Assume JSON
370
            $data = json_decode($data, true);
371
        }
372
        $config = $this->mergeConfigToFeatureSettings([]);
373
        // Match that data with mapped lead fields
374
        $matchedFields = $this->populateMauticLeadData($data, $config);
375
376
        if (empty($matchedFields)) {
377
            return;
378
        }
379
380
        // Find unique identifier fields used by the integration
381
        /** @var \Mautic\LeadBundle\Model\LeadModel $leadModel */
382
        $leadModel           = $this->leadModel;
383
        $uniqueLeadFields    = $this->fieldModel->getUniqueIdentifierFields();
384
        $uniqueLeadFieldData = [];
385
        $leadFieldTypes      = $this->fieldModel->getFieldListWithProperties();
386
387
        foreach ($matchedFields as $leadField => $value) {
388
            if (array_key_exists($leadField, $uniqueLeadFields) && !empty($value)) {
389
                $uniqueLeadFieldData[$leadField] = $value;
390
            }
391
            if (isset($leadFieldTypes[$leadField]['type']) && 'text' == $leadFieldTypes[$leadField]['type']) {
392
                $matchedFields[$leadField] = substr($value, 0, 255);
393
            }
394
        }
395
396
        if (count(array_diff_key($uniqueLeadFields, $matchedFields)) == count($uniqueLeadFields)) {
397
            //return if uniqueIdentifiers have no data set to avoid duplicating leads.
398
            return;
399
        }
400
401
        // Default to new lead
402
        $lead = new Lead();
403
        $lead->setNewlyCreated(true);
404
405
        if (count($uniqueLeadFieldData)) {
406
            $existingLeads = $this->em->getRepository('MauticLeadBundle:Lead')
407
                ->getLeadsByUniqueFields($uniqueLeadFieldData);
408
            if (!empty($existingLeads)) {
409
                $lead = array_shift($existingLeads);
410
            }
411
        }
412
413
        $leadFields = $this->cleanPriorityFields($config, $object);
414
        if (!$lead->isNewlyCreated()) {
415
            $params = $this->commandParameters;
416
417
            $this->getLeadDoNotContactByDate('email', $matchedFields, $object, $lead, $data, $params);
418
419
            // Use only prioirty fields if updating
420
            $fieldsToUpdateInMautic = $this->getPriorityFieldsForMautic($config, $object, 'mautic');
421
            if (empty($fieldsToUpdateInMautic)) {
422
                return;
423
            }
424
425
            $fieldsToUpdateInMautic = array_intersect_key($leadFields, $fieldsToUpdateInMautic);
426
            $matchedFields          = array_intersect_key($matchedFields, array_flip($fieldsToUpdateInMautic));
427
            if ((isset($config['updateBlanks']) && isset($config['updateBlanks'][0]) && 'updateBlanks' == $config['updateBlanks'][0])) {
428
                $matchedFields = $this->getBlankFieldsToUpdateInMautic($matchedFields, $lead->getFields(true), $leadFields, $data, $object);
429
            }
430
        }
431
432
        $leadModel->setFieldValues($lead, $matchedFields, false, false);
433
        if (!empty($socialCache)) {
434
            // Update the social cache
435
            $leadSocialCache = $lead->getSocialCache();
436
            if (!isset($leadSocialCache[$this->getName()])) {
437
                $leadSocialCache[$this->getName()] = [];
438
            }
439
            $leadSocialCache[$this->getName()] = array_merge($leadSocialCache[$this->getName()], $socialCache);
440
441
            // Check for activity while here
442
            if (null !== $identifiers && in_array('public_activity', $this->getSupportedFeatures())) {
443
                $this->getPublicActivity($identifiers, $leadSocialCache[$this->getName()]);
444
            }
445
446
            $lead->setSocialCache($leadSocialCache);
447
        }
448
449
        // Update the internal info integration object that has updated the record
450
        if (isset($data['internal'])) {
451
            $internalInfo                   = $lead->getInternal();
452
            $internalInfo[$this->getName()] = $data['internal'];
453
            $lead->setInternal($internalInfo);
454
        }
455
456
        // Update the owner if it matches (needs to be set by the integration) when fetching the data
457
        if (isset($data['owner_email']) && isset($config['updateOwner']) && isset($config['updateOwner'][0])
458
            && 'updateOwner' == $config['updateOwner'][0]
459
        ) {
460
            if ($mauticUser = $this->em->getRepository('MauticUserBundle:User')->findOneBy(['email' => $data['owner_email']])) {
461
                $lead->setOwner($mauticUser);
462
            }
463
        }
464
465
        if ($persist && !empty($lead->getChanges(true))) {
466
            // Only persist if instructed to do so as it could be that calling code needs to manipulate the lead prior to executing event listeners
467
            $lead->setManipulator(new LeadManipulator(
468
                'plugin',
469
                $this->getName(),
470
                null,
471
                $this->getDisplayName()
472
            ));
473
            $leadModel->saveEntity($lead, false);
474
        }
475
476
        return $lead;
477
    }
478
479
    /**
480
     * @param $object
481
     *
482
     * @return array|mixed
483
     */
484
    protected function getFormFieldsByObject($object, $settings = [])
485
    {
486
        $settings['feature_settings']['objects'] = [$object => $object];
487
488
        $fields = ($this->isAuthorized()) ? $this->getAvailableLeadFields($settings) : [];
489
490
        return (isset($fields[$object])) ? $fields[$object] : [];
491
    }
492
493
    /**
494
     * @param        $config
495
     * @param null   $entityObject   Possibly used by the CRM
496
     * @param string $priorityObject
497
     *
498
     * @return array
499
     */
500
    protected function getPriorityFieldsForMautic($config, $entityObject = null, $priorityObject = 'mautic')
501
    {
502
        return $this->cleanPriorityFields(
503
            $this->getFieldsByPriority($config, $priorityObject, 1),
504
            $entityObject
505
        );
506
    }
507
508
    /**
509
     * @param        $config
510
     * @param null   $entityObject   Possibly used by the CRM
511
     * @param string $priorityObject
512
     *
513
     * @return array
514
     */
515
    protected function getPriorityFieldsForIntegration($config, $entityObject = null, $priorityObject = 'mautic')
516
    {
517
        return $this->cleanPriorityFields(
518
            $this->getFieldsByPriority($config, $priorityObject, 0),
519
            $entityObject
520
        );
521
    }
522
523
    /**
524
     * @param        $direction
525
     * @param string $priorityObject
526
     *
527
     * @return array
528
     */
529
    protected function getFieldsByPriority(array $config, $priorityObject, $direction)
530
    {
531
        return isset($config['update_'.$priorityObject]) ? array_keys($config['update_'.$priorityObject], $direction) : array_keys($config['leadFields']);
532
    }
533
534
    /**
535
     * @param       $fieldsToUpdate
536
     * @param array $objects
537
     *
538
     * @return array
539
     */
540
    protected function cleanPriorityFields($fieldsToUpdate, $objects = null)
541
    {
542
        if (!isset($fieldsToUpdate['leadFields'])) {
543
            return $fieldsToUpdate;
544
        }
545
546
        if (null === $objects || is_array($objects)) {
547
            return $fieldsToUpdate['leadFields'];
548
        }
549
550
        if (isset($fieldsToUpdate['leadFields'][$objects])) {
551
            return $fieldsToUpdate['leadFields'][$objects];
552
        }
553
554
        return $fieldsToUpdate;
555
    }
556
557
    /**
558
     * @return array
559
     */
560
    protected function getSyncTimeframeDates(array $params)
561
    {
562
        $fromDate = (isset($params['start'])) ? \DateTime::createFromFormat(\DateTime::ISO8601, $params['start'])->format('Y-m-d H:i:s')
563
            : null;
564
        $toDate = (isset($params['end'])) ? \DateTime::createFromFormat(\DateTime::ISO8601, $params['end'])->format('Y-m-d H:i:s')
565
            : null;
566
567
        return [$fromDate, $toDate];
568
    }
569
570
    /**
571
     * @param $objectFields
572
     */
573
    public function getBlankFieldsToUpdateInMautic($matchedFields, $leadFieldValues, $objectFields, $integrationData, $object = 'Lead')
574
    {
575
        foreach ($objectFields as $integrationField => $mauticField) {
576
            if (isset($leadFieldValues[$mauticField]) && empty($leadFieldValues[$mauticField]['value']) && !empty($integrationData[$integrationField.'__'.$object]) && $this->translator->trans('mautic.integration.form.lead.unknown') !== $integrationData[$integrationField.'__'.$object]) {
577
                $matchedFields[$mauticField] = $integrationData[$integrationField.'__'.$object];
578
            }
579
        }
580
581
        return $matchedFields;
582
    }
583
584
    /**
585
     * @param $fields
586
     * @param $sfRecord
587
     * @param $config
588
     * @param $objectFields
589
     */
590
    public function getBlankFieldsToUpdate($fields, $sfRecord, $objectFields, $config)
591
    {
592
        //check if update blank fields is selected
593
        if (isset($config['updateBlanks']) && isset($config['updateBlanks'][0])
594
            && 'updateBlanks' == $config['updateBlanks'][0]
595
            && !empty($sfRecord)
596
            && isset($objectFields['required']['fields'])
597
        ) {
598
            foreach ($sfRecord as $fieldName => $sfField) {
599
                if (array_key_exists($fieldName, $objectFields['required']['fields'])) {
600
                    continue; // this will be treated differently
601
                }
602
                if (empty($sfField) && array_key_exists($fieldName, $objectFields['create']) && !array_key_exists($fieldName, $fields)) {
603
                    //map to mautic field
604
                    $fields[$fieldName] = $objectFields['create'][$fieldName];
605
                }
606
            }
607
        }
608
609
        return $fields;
610
    }
611
612
    /**
613
     * @param $fields
614
     *
615
     * @return array
616
     */
617
    protected function prepareFieldsForPush($fields)
618
    {
619
        $fieldMappings = [];
620
        $required      = [];
621
        $config        = $this->mergeConfigToFeatureSettings();
622
623
        $leadFields = $config['leadFields'];
624
        foreach ($fields as $key => $field) {
625
            if ($field['required']) {
626
                $required[$key] = $field;
627
            }
628
        }
629
        $fieldMappings['required'] = [
630
            'fields' => $required,
631
        ];
632
        $fieldMappings['create'] = $leadFields;
633
634
        return $fieldMappings;
635
    }
636
637
    /**
638
     * @return array
639
     */
640
    private function hydrateCompanyName(array $matchedFields)
641
    {
642
        if (!empty($matchedFields['companyname'])) {
643
            return $matchedFields;
644
        }
645
646
        if (!empty($matchedFields['companywebsite'])) {
647
            $matchedFields['companyname'] = $matchedFields['companywebsite'];
648
649
            return $matchedFields;
650
        }
651
652
        // We need something as company name so save whatever we have
653
        if ($firstMatchedField = reset($matchedFields)) {
654
            $matchedFields['companyname'] = $firstMatchedField;
655
656
            return $matchedFields;
657
        }
658
659
        return $matchedFields;
660
    }
661
}
662