Spaces:
No application file
No application file
| namespace MauticPlugin\MauticCrmBundle\Integration; | |
| use Mautic\FormBundle\Entity\Form; | |
| use Mautic\LeadBundle\Entity\Company; | |
| use Mautic\LeadBundle\Entity\Lead; | |
| use Mautic\LeadBundle\Helper\IdentifyCompanyHelper; | |
| use Mautic\PluginBundle\Entity\IntegrationEntity; | |
| use Mautic\PluginBundle\Entity\IntegrationEntityRepository; | |
| use Mautic\PluginBundle\Exception\ApiErrorException; | |
| use MauticPlugin\MauticCrmBundle\Api\Zoho\Mapper; | |
| use MauticPlugin\MauticCrmBundle\Api\ZohoApi; | |
| use Symfony\Component\Console\Helper\ProgressBar; | |
| use Symfony\Component\Console\Output\ConsoleOutput; | |
| use Symfony\Component\Console\Output\OutputInterface; | |
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | |
| use Symfony\Component\Form\FormBuilder; | |
| /** | |
| * @method ZohoApi getApiHelper() | |
| */ | |
| class ZohoIntegration extends CrmAbstractIntegration | |
| { | |
| /** | |
| * Returns the name of the social integration that must match the name of the file. | |
| */ | |
| public function getName(): string | |
| { | |
| return 'Zoho'; | |
| } | |
| public function getAuthenticationType(): string | |
| { | |
| return 'oauth2'; | |
| } | |
| /** | |
| * @return array<string, string> | |
| */ | |
| public function getRequiredKeyFields(): array | |
| { | |
| return [ | |
| $this->getClientIdKey() => 'mautic.zoho.form.client_id', | |
| $this->getClientSecretKey() => 'mautic.zoho.form.client_secret', | |
| ]; | |
| } | |
| public function getClientIdKey(): string | |
| { | |
| return 'client_id'; | |
| } | |
| public function getClientSecretKey(): string | |
| { | |
| return 'client_secret'; | |
| } | |
| public function getAuthTokenKey(): string | |
| { | |
| return 'access_token'; | |
| } | |
| public function getAuthScope(): string | |
| { | |
| return 'ZohoCRM.modules.ALL,ZohoCRM.settings.ALL,ZohoCRM.bulk.all,ZohoCRM.users.all,ZohoCRM.org.all'; | |
| } | |
| /** | |
| * @return string | |
| */ | |
| public function getDatacenter() | |
| { | |
| $featureSettings = $this->getKeys(); | |
| return !empty($featureSettings['datacenter']) ? $featureSettings['datacenter'] : 'zoho.com'; | |
| } | |
| public function getApiUrl(): string | |
| { | |
| return sprintf('https://accounts.%s', $this->getDatacenter()); | |
| } | |
| public function getAccessTokenUrl(): string | |
| { | |
| return $this->getApiUrl().'/oauth/v2/token'; | |
| } | |
| public function getAuthenticationUrl(): string | |
| { | |
| return $this->getApiUrl().'/oauth/v2/auth'; | |
| } | |
| public function getSupportedFeatures(): array | |
| { | |
| return ['push_lead', 'get_leads', 'push_leads']; | |
| } | |
| /** | |
| * Refresh tokens. | |
| */ | |
| public function getRefreshTokenKeys(): array | |
| { | |
| return [ | |
| 'refresh_token', | |
| 'expires', | |
| ]; | |
| } | |
| public function prepareResponseForExtraction($data) | |
| { | |
| // Extract expiry and set expires for zoho | |
| if (is_array($data) && isset($data['expires_in'])) { | |
| $data['expires'] = $data['expires_in'] + time(); | |
| } | |
| return $data; | |
| } | |
| /** | |
| * Amend mapped lead data before creating to Mautic. | |
| * | |
| * @param array $data | |
| * @param string $object | |
| */ | |
| public function amendLeadDataBeforeMauticPopulate($data, $object = null): array | |
| { | |
| if ('company' === $object) { | |
| $object = 'Accounts'; | |
| } elseif ('Lead' === $object || 'Contact' === $object) { | |
| $object .= 's'; // pluralize object name for Zoho | |
| } | |
| $config = $this->mergeConfigToFeatureSettings([]); | |
| $result = []; | |
| if (isset($data['data'])) { | |
| $entity = null; | |
| /** @var IntegrationEntityRepository $integrationEntityRepo */ | |
| $integrationEntityRepo = $this->em->getRepository(IntegrationEntity::class); | |
| $objects = $data['data']; | |
| $integrationEntities = []; | |
| /** @var array $objects */ | |
| foreach ($objects as $recordId => $entityData) { | |
| $isModified = false; | |
| if ('Accounts' === $object) { | |
| $recordId = $entityData['id']; | |
| // first try to find integration entity | |
| $integrationId = $integrationEntityRepo->getIntegrationsEntityId( | |
| 'Zoho', | |
| $object, | |
| 'company', | |
| null, | |
| null, | |
| null, | |
| false, | |
| 0, | |
| 0, | |
| [$recordId] | |
| ); | |
| if (count($integrationId)) { // company exists, then update local fields | |
| /** @var Company $entity */ | |
| $entity = $this->companyModel->getEntity($integrationId[0]['internal_entity_id']); | |
| $matchedFields = $this->populateMauticLeadData($entityData, $config, 'company'); | |
| // Match that data with mapped lead fields | |
| $fieldsToUpdateInMautic = $this->getPriorityFieldsForMautic($config, $object, 'mautic_company'); | |
| if (!empty($fieldsToUpdateInMautic)) { | |
| $fieldsToUpdateInMautic = array_intersect_key($config['companyFields'], $fieldsToUpdateInMautic); | |
| $newMatchedFields = array_intersect_key($matchedFields, array_flip($fieldsToUpdateInMautic)); | |
| } else { | |
| $newMatchedFields = $matchedFields; | |
| } | |
| if (!isset($newMatchedFields['companyname'])) { | |
| if (isset($newMatchedFields['companywebsite'])) { | |
| $newMatchedFields['companyname'] = $newMatchedFields['companywebsite']; | |
| } | |
| } | |
| // update values if already empty | |
| foreach ($matchedFields as $field => $value) { | |
| if (empty($entity->getFieldValue($field))) { | |
| $newMatchedFields[$field] = $value; | |
| } | |
| } | |
| // remove unchanged fields | |
| foreach ($newMatchedFields as $k => $v) { | |
| if ($entity->getFieldValue($k) === $v) { | |
| unset($newMatchedFields[$k]); | |
| } | |
| } | |
| if (count($newMatchedFields)) { | |
| $this->companyModel->setFieldValues($entity, $newMatchedFields, false); | |
| $this->companyModel->saveEntity($entity, false); | |
| $isModified = true; | |
| } | |
| } else { | |
| $entity = $this->getMauticCompany($entityData, 'Accounts'); | |
| } | |
| if ($entity) { | |
| $result[] = $entity->getName(); | |
| } | |
| $mauticObjectReference = 'company'; | |
| } elseif ('Leads' === $object) { | |
| $recordId = $entityData['id']; | |
| // first try to find integration entity | |
| $integrationId = $integrationEntityRepo->getIntegrationsEntityId( | |
| 'Zoho', | |
| $object, | |
| 'lead', | |
| null, | |
| null, | |
| null, | |
| false, | |
| 0, | |
| 0, | |
| [$recordId] | |
| ); | |
| if (count($integrationId)) { // lead exists, then update | |
| /** @var Lead $entity */ | |
| $entity = $this->leadModel->getEntity($integrationId[0]['internal_entity_id']); | |
| $matchedFields = $this->populateMauticLeadData($entityData, $config, $object); | |
| // Match that data with mapped lead fields | |
| $fieldsToUpdateInMautic = $this->getPriorityFieldsForMautic($config, $object, 'mautic'); | |
| if (!empty($fieldsToUpdateInMautic)) { | |
| $fieldsToUpdateInMautic = array_intersect_key($config['leadFields'], $fieldsToUpdateInMautic); | |
| $newMatchedFields = array_intersect_key($matchedFields, array_flip($fieldsToUpdateInMautic)); | |
| } else { | |
| $newMatchedFields = $matchedFields; | |
| } | |
| // update values if already empty | |
| foreach ($matchedFields as $field => $value) { | |
| if (empty($entity->getFieldValue($field))) { | |
| $newMatchedFields[$field] = $value; | |
| } | |
| } | |
| // remove unchanged fields | |
| foreach ($newMatchedFields as $k => $v) { | |
| if ($entity->getFieldValue($k) === $v) { | |
| unset($newMatchedFields[$k]); | |
| } | |
| } | |
| if (count($newMatchedFields)) { | |
| $this->leadModel->setFieldValues($entity, $newMatchedFields, false, false); | |
| $this->leadModel->saveEntity($entity, false); | |
| $isModified = true; | |
| } | |
| } else { | |
| /** @var Lead $entity */ | |
| $entity = $this->getMauticLead($entityData, true, null, null, $object); | |
| } | |
| if ($entity) { | |
| $result[] = $entity->getEmail(); | |
| } | |
| // Associate lead company | |
| if (!empty($entityData['Company']) | |
| && $entityData['Company'] !== $this->translator->trans( | |
| 'mautic.integration.form.lead.unknown' | |
| ) | |
| ) { | |
| $company = IdentifyCompanyHelper::identifyLeadsCompany( | |
| ['company' => $entityData['Company']], | |
| null, | |
| $this->companyModel | |
| ); | |
| if (!empty($company[2])) { | |
| $syncLead = $this->companyModel->addLeadToCompany($company[2], $entity); | |
| $this->em->detach($company[2]); | |
| } | |
| } | |
| $mauticObjectReference = 'lead'; | |
| } elseif ('Contacts' === $object) { | |
| $recordId = $entityData['id']; | |
| $integrationId = $integrationEntityRepo->getIntegrationsEntityId( | |
| 'Zoho', | |
| $object, | |
| 'lead', | |
| null, | |
| null, | |
| null, | |
| false, | |
| 0, | |
| 0, | |
| [$recordId] | |
| ); | |
| if (count($integrationId)) { // contact exists, then update | |
| /** @var Lead $entity */ | |
| $entity = $this->leadModel->getEntity($integrationId[0]['internal_entity_id']); | |
| $matchedFields = $this->populateMauticLeadData($entityData, $config, $object); | |
| // Match that data with mapped lead fields | |
| $fieldsToUpdateInMautic = $this->getPriorityFieldsForMautic($config, $object, 'mautic'); | |
| if (!empty($fieldsToUpdateInMautic)) { | |
| $fieldsToUpdateInMautic = array_intersect_key($config['leadFields'], $fieldsToUpdateInMautic); | |
| $newMatchedFields = array_intersect_key($matchedFields, array_flip($fieldsToUpdateInMautic)); | |
| } else { | |
| $newMatchedFields = $matchedFields; | |
| } | |
| // update values if already empty | |
| foreach ($matchedFields as $field => $value) { | |
| if (empty($entity->getFieldValue($field))) { | |
| $newMatchedFields[$field] = $value; | |
| } | |
| } | |
| // remove unchanged fields | |
| foreach ($newMatchedFields as $k => $v) { | |
| if ($entity->getFieldValue($k) === $v) { | |
| unset($newMatchedFields[$k]); | |
| } | |
| } | |
| if (count($newMatchedFields)) { | |
| $this->leadModel->setFieldValues($entity, $newMatchedFields, false, false); | |
| $this->leadModel->saveEntity($entity, false); | |
| $isModified = true; | |
| } | |
| } else { | |
| /** @var Lead $entity */ | |
| $entity = $this->getMauticLead($entityData, true, null, null, $object); | |
| } | |
| if ($entity) { | |
| $result[] = $entity->getEmail(); | |
| // Associate lead company | |
| if (!empty($entityData['AccountName']) | |
| && $entityData['AccountName'] !== $this->translator->trans( | |
| 'mautic.integration.form.lead.unknown' | |
| ) | |
| ) { | |
| $company = IdentifyCompanyHelper::identifyLeadsCompany( | |
| ['company' => $entityData['AccountName']], | |
| null, | |
| $this->companyModel | |
| ); | |
| if (!empty($company[2])) { | |
| $syncLead = $this->companyModel->addLeadToCompany($company[2], $entity); | |
| $this->em->detach($company[2]); | |
| } | |
| } | |
| } | |
| $mauticObjectReference = 'lead'; | |
| } else { | |
| $this->logIntegrationError( | |
| new \Exception( | |
| sprintf('Received an unexpected object "%s"', $object) | |
| ) | |
| ); | |
| continue; | |
| } | |
| if ($entity) { | |
| $integrationId = $integrationEntityRepo->getIntegrationsEntityId( | |
| 'Zoho', | |
| $object, | |
| $mauticObjectReference, | |
| $entity->getId() | |
| ); | |
| if (0 === count($integrationId)) { | |
| $integrationEntity = new IntegrationEntity(); | |
| $integrationEntity->setDateAdded(new \DateTime()); | |
| $integrationEntity->setIntegration('Zoho'); | |
| $integrationEntity->setIntegrationEntity($object); | |
| $integrationEntity->setIntegrationEntityId($recordId); | |
| $integrationEntity->setInternalEntity($mauticObjectReference); | |
| $integrationEntity->setInternalEntityId($entity->getId()); | |
| $integrationEntities[] = $integrationEntity; | |
| } else { | |
| $integrationEntity = $integrationEntityRepo->getEntity($integrationId[0]['id']); | |
| if ($isModified) { | |
| $integrationEntity->setLastSyncDate(new \DateTime()); | |
| $integrationEntities[] = $integrationEntity; | |
| } | |
| } | |
| $this->em->detach($entity); | |
| unset($entity); | |
| } else { | |
| continue; | |
| } | |
| } | |
| $this->em->getRepository(IntegrationEntity::class)->saveEntities($integrationEntities); | |
| $this->integrationEntityModel->getRepository()->detachEntities($integrationEntities); | |
| unset($integrationEntities); | |
| } | |
| return $result; | |
| } | |
| /** | |
| * @param array $params | |
| * @param string $query | |
| * @param array $result | |
| * @param string $object | |
| */ | |
| public function getLeads($params, $query, &$executed, $result = [], $object = 'Lead'): int | |
| { | |
| if ('Lead' === $object || 'Contact' === $object) { | |
| $object .= 's'; // pluralize object name for Zoho | |
| } | |
| $executed = 0; | |
| try { | |
| if ($this->isAuthorized()) { | |
| $config = $this->mergeConfigToFeatureSettings(); | |
| $fields = $config['leadFields']; | |
| $config['object'] = $object; | |
| $aFields = $this->getAvailableLeadFields($config); | |
| $mappedData = []; | |
| foreach (array_keys($fields) as $k) { | |
| if (isset($aFields[$object][$k])) { | |
| $mappedData[] = $aFields[$object][$k]['api_name']; | |
| } | |
| } | |
| $maxRecords = 200; | |
| $fields = implode(',', $mappedData); | |
| $oparams['fields'] = $fields; | |
| $oparams['per_page'] = $maxRecords; // maximum number of records | |
| if (isset($params['fetchAll'], $params['start']) && !$params['fetchAll']) { | |
| $oparams['lastModifiedTime'] = date('c', strtotime($params['start'])); | |
| } | |
| $oparams['page'] = 1; | |
| if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) { | |
| $progress = new ProgressBar($params['output']); | |
| $progress->start(); | |
| } | |
| while (true) { | |
| $data = $this->getApiHelper()->getLeads($oparams, $object); | |
| if (!isset($data['data'])) { | |
| break; // no more data, exit loop | |
| } | |
| $result = $this->amendLeadDataBeforeMauticPopulate($data, $object); | |
| $executed += count($result); | |
| if (isset($params['output'])) { | |
| if ($params['output']->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { | |
| $params['output']->writeln($result); | |
| } else { | |
| $progress->advance(); | |
| } | |
| } | |
| // prepare next loop | |
| ++$oparams['page']; | |
| } | |
| if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) { | |
| $progress->finish(); | |
| } | |
| } | |
| } catch (\Exception $e) { | |
| $this->logIntegrationError($e); | |
| } | |
| return $executed; | |
| } | |
| /** | |
| * @param array $params | |
| * @param array $result | |
| */ | |
| public function getCompanies($params = [], $query = null, &$executed = null, &$result = []): int | |
| { | |
| $executed = 0; | |
| $object = 'company'; | |
| try { | |
| if ($this->isAuthorized()) { | |
| $config = $this->mergeConfigToFeatureSettings(); | |
| $fields = $config['companyFields']; | |
| $config['object'] = $object; | |
| $aFields = $this->getAvailableLeadFields($config); | |
| $mappedData = []; | |
| foreach (array_keys($fields) as $k) { | |
| if (isset($aFields[$object][$k])) { | |
| $mappedData[] = $aFields[$object][$k]['api_name']; | |
| } | |
| } | |
| $maxRecords = 200; | |
| $fields = implode(',', $mappedData); | |
| $oparams['fields'] = $fields; | |
| $oparams['per_page'] = $maxRecords; // maximum number of records | |
| if (isset($params['fetchAll'], $params['start']) && !$params['fetchAll']) { | |
| $oparams['lastModifiedTime'] = date('c', strtotime($params['start'])); | |
| } | |
| $oparams['page'] = 1; | |
| if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) { | |
| $progress = new ProgressBar($params['output']); | |
| $progress->start(); | |
| } | |
| while (true) { | |
| $data = $this->getApiHelper()->getCompanies($oparams); | |
| if (!isset($data['data'])) { | |
| break; // no more data, exit loop | |
| } | |
| $result = $this->amendLeadDataBeforeMauticPopulate($data, $object); | |
| $executed += count($result); | |
| if (isset($params['output'])) { | |
| if ($params['output']->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { | |
| $params['output']->writeln($result); | |
| } else { | |
| $progress->advance(); | |
| } | |
| } | |
| // prepare next loop | |
| ++$oparams['page']; | |
| } | |
| if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) { | |
| $progress->finish(); | |
| } | |
| } | |
| } catch (\Exception $e) { | |
| $this->logIntegrationError($e); | |
| } | |
| return $executed; | |
| } | |
| /** | |
| * @param array $data | |
| * @param array $config | |
| */ | |
| public function populateMauticLeadData($data, $config = [], $object = 'Leads'): array | |
| { | |
| // Match that data with mapped lead fields | |
| $aFields = $this->getAvailableLeadFields($config); | |
| $matchedFields = []; | |
| $fieldsName = ('company' === $object) ? 'companyFields' : 'leadFields'; | |
| if (isset($aFields[$object])) { | |
| $aFields = $aFields[$object]; | |
| } | |
| foreach ($aFields as $k => $v) { | |
| foreach ($data as $dk => $dv) { | |
| if ($dk === $v['api_name']) { | |
| $matchedFields[$config[$fieldsName][$k]] = $dv; | |
| } | |
| } | |
| } | |
| return $matchedFields; | |
| } | |
| /** | |
| * Generate the auth login URL. Note that if oauth2, response_type=code is assumed. If this is not the case, | |
| * override this function. | |
| * | |
| * @return string | |
| */ | |
| public function getAuthLoginUrl() | |
| { | |
| $authType = $this->getAuthenticationType(); | |
| if ('oauth2' == $authType) { | |
| $callback = $this->getAuthCallbackUrl(); | |
| $clientIdKey = $this->getClientIdKey(); | |
| $state = $this->getAuthLoginState(); | |
| $url = $this->getAuthenticationUrl() | |
| .'?client_id='.$this->keys[$clientIdKey] | |
| .'&response_type=code' | |
| .'&redirect_uri='.urlencode($callback) | |
| .'&state='.$state.'&prompt=consent&access_type=offline'; | |
| if ($scope = $this->getAuthScope()) { | |
| $url .= '&scope='.urlencode($scope); | |
| } | |
| if ($this->session) { | |
| $this->session->set($this->getName().'_csrf_token', $state); | |
| } | |
| return $url; | |
| } else { | |
| return $this->router->generate( | |
| 'mautic_integration_auth_callback', | |
| ['integration' => $this->getName()] | |
| ); | |
| } | |
| } | |
| /** | |
| * @param Form|FormBuilder $builder | |
| * @param array $data | |
| * @param string $formArea | |
| * | |
| * @throws \InvalidArgumentException | |
| */ | |
| public function appendToForm(&$builder, $data, $formArea): void | |
| { | |
| if ('features' == $formArea) { | |
| $builder->add( | |
| 'updateBlanks', | |
| ChoiceType::class, | |
| [ | |
| 'choices' => [ | |
| 'mautic.integrations.blanks' => 'updateBlanks', | |
| ], | |
| 'expanded' => true, | |
| 'multiple' => true, | |
| 'label' => 'mautic.integrations.form.blanks', | |
| 'label_attr' => ['class' => 'control-label'], | |
| 'placeholder' => false, | |
| 'required' => false, | |
| ] | |
| ); | |
| } | |
| if ('keys' === $formArea) { | |
| $builder->add( | |
| 'datacenter', | |
| ChoiceType::class, | |
| [ | |
| 'choices' => [ | |
| 'mautic.plugin.zoho.zone_us' => 'zoho.com', | |
| 'mautic.plugin.zoho.zone_europe' => 'zoho.eu', | |
| 'mautic.plugin.zoho.zone_japan' => 'zoho.co.jp', | |
| 'mautic.plugin.zoho.zone_china' => 'zoho.com.cn', | |
| ], | |
| 'label' => 'mautic.plugin.zoho.zone_select', | |
| 'placeholder' => false, | |
| 'required' => true, | |
| 'attr' => [ | |
| 'tooltip' => 'mautic.plugin.zoho.zone.tooltip', | |
| ], | |
| ] | |
| ); | |
| } elseif ('features' === $formArea) { | |
| $builder->add( | |
| 'objects', | |
| ChoiceType::class, | |
| [ | |
| 'choices' => [ | |
| 'mautic.zoho.object.lead' => 'Leads', | |
| 'mautic.zoho.object.contact' => 'Contacts', | |
| 'mautic.zoho.object.account' => 'company', | |
| ], | |
| 'expanded' => true, | |
| 'multiple' => true, | |
| 'label' => $this->getTranslator()->trans('mautic.crm.form.objects_to_pull_from', ['%crm%' => 'Zoho']), | |
| 'label_attr' => ['class' => ''], | |
| 'placeholder' => false, | |
| 'required' => false, | |
| ] | |
| ); | |
| } | |
| } | |
| /** | |
| * Get available company fields for choices in the config UI. | |
| * | |
| * @param array $settings | |
| * | |
| * @return array | |
| */ | |
| public function getFormCompanyFields($settings = []) | |
| { | |
| return $this->getFormFieldsByObject('Accounts', $settings); | |
| } | |
| /** | |
| * @param mixed[] $settings | |
| * | |
| * @return mixed[] | |
| */ | |
| public function getFormLeadFields($settings = []): array | |
| { | |
| $leadFields = $this->getFormFieldsByObject('Leads', $settings); | |
| $contactFields = $this->getFormFieldsByObject('Contacts', $settings); | |
| return array_merge($leadFields, $contactFields); | |
| } | |
| /** | |
| * @param mixed[] $settings | |
| * | |
| * @throws ApiErrorException | |
| */ | |
| public function getAvailableLeadFields($settings = []): array | |
| { | |
| $zohoFields = []; | |
| $silenceExceptions = $settings['silence_exceptions'] ?? true; | |
| if (isset($settings['feature_settings']['objects'])) { | |
| $zohoObjects = $settings['feature_settings']['objects']; | |
| } else { | |
| $settings = $this->settings->getFeatureSettings(); | |
| $zohoObjects = $settings['objects'] ?? ['Leads']; | |
| } | |
| try { | |
| if ($this->isAuthorized()) { | |
| if (!empty($zohoObjects) && is_array($zohoObjects)) { | |
| foreach ($zohoObjects as $zohoObject) { | |
| // Check the cache first | |
| $settings['cache_suffix'] = $cacheSuffix = '.'.$zohoObject; | |
| if ($fields = parent::getAvailableLeadFields($settings)) { | |
| $zohoFields[$zohoObject] = $fields; | |
| continue; | |
| } | |
| $leadObject = $this->getApiHelper()->getLeadFields($zohoObject); | |
| if (null === $leadObject || (isset($leadObject['status']) && 'error' === $leadObject['status'])) { | |
| return []; | |
| } | |
| /** @var array $opts */ | |
| $opts = $leadObject['fields']; | |
| foreach ($opts as $field) { | |
| if (true == $field['read_only']) { | |
| continue; | |
| } | |
| $is_required = false; | |
| if (true == $field['system_mandatory']) { | |
| $is_required = true; | |
| } | |
| $zohoFields[$zohoObject][$field['api_name']] = [ | |
| 'type' => 'string', | |
| 'label' => $field['display_label'], | |
| 'api_name' => $field['api_name'], | |
| 'required' => $is_required, | |
| ]; | |
| } | |
| if (empty($settings['ignore_field_cache'])) { | |
| $this->cache->set('leadFields'.$cacheSuffix, $zohoFields[$zohoObject]); | |
| } | |
| } | |
| } | |
| } | |
| } catch (ApiErrorException $exception) { | |
| $this->logIntegrationError($exception); | |
| if (!$silenceExceptions) { | |
| if (str_contains($exception->getMessage(), 'Invalid Ticket Id')) { | |
| // Use a bit more friendly message | |
| $exception = new ApiErrorException('There was an issue with communicating with Zoho. Please try to reauthorize.'); | |
| } | |
| throw $exception; | |
| } | |
| return []; | |
| } | |
| return $zohoFields; | |
| } | |
| /** | |
| * @param array $params | |
| * | |
| * @return mixed[] | |
| */ | |
| public function pushLeads($params = []): array | |
| { | |
| $maxRecords = (isset($params['limit']) && $params['limit'] < 100) ? $params['limit'] : 100; | |
| if (isset($params['fetchAll']) && $params['fetchAll']) { | |
| $params['start'] = null; | |
| $params['end'] = null; | |
| } | |
| $config = $this->mergeConfigToFeatureSettings(); | |
| $integrationEntityRepo = $this->em->getRepository(IntegrationEntity::class); | |
| $fieldsToUpdateInZoho = isset($config['update_mautic']) ? array_keys($config['update_mautic'], 0) : []; | |
| $leadFields = array_unique(array_values($config['leadFields'])); | |
| $totalUpdated = $totalCreated = $totalErrors = 0; | |
| if ($key = array_search('mauticContactTimelineLink', $leadFields)) { | |
| unset($leadFields[$key]); | |
| } | |
| if ($key = array_search('mauticContactIsContactableByEmail', $leadFields)) { | |
| unset($leadFields[$key]); | |
| } | |
| if (empty($leadFields)) { | |
| return [0, 0, 0]; | |
| } | |
| $fields = implode(', l.', $leadFields); | |
| $fields = 'l.'.$fields; | |
| $availableFields = $this->getAvailableLeadFields(['feature_settings' => ['objects' => ['Leads', 'Contacts']]]); | |
| $fieldsToUpdate['Leads'] = array_values(array_intersect(array_keys($availableFields['Leads']), $fieldsToUpdateInZoho)); | |
| $fieldsToUpdate['Contacts'] = array_values(array_intersect(array_keys($availableFields['Contacts']), $fieldsToUpdateInZoho)); | |
| $fieldsToUpdate['Leads'] = array_intersect_key($config['leadFields'], array_flip($fieldsToUpdate['Leads'])); | |
| $fieldsToUpdate['Contacts'] = array_intersect_key($config['leadFields'], array_flip($fieldsToUpdate['Contacts'])); | |
| $progress = false; | |
| $totalToUpdate = array_sum( | |
| $integrationEntityRepo->findLeadsToUpdate('Zoho', 'lead', $fields, 0, $params['start'], $params['end'], ['Contacts', 'Leads']) | |
| ); | |
| $totalToCreate = $integrationEntityRepo->findLeadsToCreate('Zoho', $fields, 0, $params['start'], $params['end']); | |
| $totalCount = $totalToCreate + $totalToUpdate; | |
| if (defined('IN_MAUTIC_CONSOLE')) { | |
| // start with update | |
| if ($totalToUpdate + $totalToCreate) { | |
| $output = new ConsoleOutput(); | |
| $output->writeln("About $totalToUpdate to update and about $totalToCreate to create/update"); | |
| $progress = new ProgressBar($output, $totalCount); | |
| } | |
| } | |
| // Start with contacts so we know who is a contact when we go to process converted leads | |
| $leadsToCreateInZ = []; | |
| $leadsToUpdateInZ = []; | |
| $isContact = []; | |
| $integrationEntities = []; | |
| // Fetch them separately so we can determine which oneas are already there | |
| $toUpdate = $integrationEntityRepo->findLeadsToUpdate( | |
| 'Zoho', | |
| 'lead', | |
| $fields, | |
| $totalToUpdate, | |
| $params['start'], | |
| $params['end'], | |
| 'Contacts', | |
| [] | |
| )['Contacts']; | |
| if (is_array($toUpdate)) { | |
| foreach ($toUpdate as $lead) { | |
| if (isset($lead['email']) && !empty($lead['email'])) { | |
| $key = mb_strtolower($this->cleanPushData($lead['email'])); | |
| $lead['integration_entity'] = 'Contacts'; | |
| $leadsToUpdateInZ[$key] = $lead; | |
| $isContact[$key] = $lead; | |
| } | |
| } | |
| } | |
| // Switch to Lead | |
| $toUpdate = $integrationEntityRepo->findLeadsToUpdate( | |
| 'Zoho', | |
| 'lead', | |
| $fields, | |
| $totalToUpdate, | |
| $params['start'], | |
| $params['end'], | |
| 'Leads', | |
| [] | |
| )['Leads']; | |
| if (is_array($toUpdate)) { | |
| foreach ($toUpdate as $lead) { | |
| if (isset($lead['email']) && !empty($lead['email'])) { | |
| $key = mb_strtolower($this->cleanPushData($lead['email'])); | |
| $lead = $this->getCompoundMauticFields($lead); | |
| if (isset($isContact[$key])) { | |
| $isContact[$key] = $lead; // lead-converted | |
| } else { | |
| $integrationId = $integrationEntityRepo->getIntegrationsEntityId( | |
| 'Zoho', | |
| 'Leads', | |
| 'lead', | |
| $lead['internal_entity_id'] | |
| ); | |
| $lead['integration_entity'] = 'Leads'; | |
| $leadsToUpdateInZ[$key] = $lead; | |
| $integrationEntity = $this->em->getReference(IntegrationEntity::class, $integrationId[0]['id']); | |
| $integrationEntities[] = $integrationEntity->setLastSyncDate(new \DateTime()); | |
| } | |
| } | |
| } | |
| } | |
| unset($toUpdate); | |
| // convert ignored contacts | |
| foreach ($isContact as $email => $lead) { | |
| $integrationId = $integrationEntityRepo->getIntegrationsEntityId( | |
| 'Zoho', | |
| 'Leads', | |
| 'lead', | |
| $lead['internal_entity_id'] | |
| ); | |
| if (count($integrationId)) { // lead exists, then update | |
| $integrationEntity = $this->em->getReference(IntegrationEntity::class, $integrationId[0]['id']); | |
| $integrationEntity->setLastSyncDate(new \DateTime()); | |
| $integrationEntity->setInternalEntity('lead-converted'); | |
| $integrationEntities[] = $integrationEntity; | |
| unset($leadsToUpdateInZ[$email]); | |
| } | |
| } | |
| // create lead records, including deleted on Zoho side (last_sync = null) | |
| /** @var array $leadsToCreate */ | |
| $leadsToCreate = $integrationEntityRepo->findLeadsToCreate('Zoho', $fields, $totalToCreate, $params['start'], $params['end']); | |
| if (is_array($leadsToCreate)) { | |
| foreach ($leadsToCreate as $lead) { | |
| if (isset($lead['email']) && !empty($lead['email'])) { | |
| $key = mb_strtolower($this->cleanPushData($lead['email'])); | |
| $lead = $this->getCompoundMauticFields($lead); | |
| $lead['integration_entity'] = 'Leads'; | |
| $leadsToCreateInZ[$key] = $lead; | |
| } | |
| } | |
| } | |
| unset($leadsToCreate); | |
| if (count($integrationEntities)) { | |
| // Persist updated entities if applicable | |
| $integrationEntityRepo->saveEntities($integrationEntities); | |
| $this->integrationEntityModel->getRepository()->detachEntities($integrationEntities); | |
| } | |
| // update leads and contacts | |
| $mapper = new Mapper($availableFields); | |
| foreach (['Leads', 'Contacts'] as $zObject) { | |
| $counter = 1; | |
| $mapper->setObject($zObject); | |
| foreach ($leadsToUpdateInZ as $lead) { | |
| if ($zObject !== $lead['integration_entity']) { | |
| continue; | |
| } | |
| if ($progress) { | |
| $progress->advance(); | |
| } | |
| $existingPerson = $this->getExistingRecord('Email', $lead['email'], $zObject); | |
| $objectFields = $this->prepareFieldsForPush($availableFields[$zObject]); | |
| $fieldsToUpdate[$zObject] = $this->getBlankFieldsToUpdate($fieldsToUpdate[$zObject], $existingPerson, $objectFields, $config); | |
| $totalUpdated += $mapper | |
| ->setMappedFields($fieldsToUpdate[$zObject]) | |
| ->setContact($lead) | |
| ->map($lead['internal_entity_id'], $lead['integration_entity_id']); | |
| ++$counter; | |
| // ONLY 100 RECORDS CAN BE SENT AT A TIME | |
| if ($maxRecords === $counter) { | |
| $this->updateContactInZoho($mapper, $zObject, $totalUpdated, $totalErrors); | |
| $counter = 1; | |
| } | |
| } | |
| if ($counter > 1) { | |
| $this->updateContactInZoho($mapper, $zObject, $totalUpdated, $totalErrors); | |
| } | |
| } | |
| // create leads and contacts | |
| foreach (['Leads', 'Contacts'] as $zObject) { | |
| $counter = 1; | |
| $mapper->setObject($zObject); | |
| foreach ($leadsToCreateInZ as $lead) { | |
| if ($zObject !== $lead['integration_entity']) { | |
| continue; | |
| } | |
| if ($progress) { | |
| $progress->advance(); | |
| } | |
| $totalCreated += $mapper | |
| ->setMappedFields($config['leadFields']) | |
| ->setContact($lead) | |
| ->map($lead['internal_entity_id']); | |
| ++$counter; | |
| // ONLY 100 RECORDS CAN BE SENT AT A TIME | |
| if ($maxRecords === $counter) { | |
| $this->createContactInZoho($mapper, $zObject, $totalCreated, $totalErrors); | |
| $counter = 1; | |
| } | |
| } | |
| if ($counter > 1) { | |
| $this->createContactInZoho($mapper, $zObject, $totalCreated, $totalErrors); | |
| } | |
| } | |
| if ($progress) { | |
| $progress->finish(); | |
| $output->writeln(''); | |
| } | |
| return [$totalUpdated, $totalCreated, $totalErrors, $totalCount - ($totalCreated + $totalUpdated + $totalErrors)]; | |
| } | |
| /** | |
| * @param Lead|array $lead | |
| * @param array $config | |
| * | |
| * @return array|bool | |
| */ | |
| public function pushLead($lead, $config = []) | |
| { | |
| $config = $this->mergeConfigToFeatureSettings($config); | |
| $zObject = 'Leads'; | |
| $fieldsToUpdateInZoho = isset($config['update_mautic']) ? array_keys($config['update_mautic'], 0) : []; | |
| $availableFields = $this->getAvailableLeadFields(['feature_settings' => ['objects' => ['Leads', 'Contacts']]]); | |
| $fieldsToUpdate['Leads'] = array_values(array_intersect(array_keys($availableFields['Leads']), $fieldsToUpdateInZoho)); | |
| $fieldsToUpdate['Contacts'] = array_values(array_intersect(array_keys($availableFields['Contacts']), $fieldsToUpdateInZoho)); | |
| $fieldsToUpdate['Leads'] = array_intersect_key($config['leadFields'], array_flip($fieldsToUpdate['Leads'])); | |
| $fieldsToUpdate['Contacts'] = array_intersect_key($config['leadFields'], array_flip($fieldsToUpdate['Contacts'])); | |
| $objectFields = $this->prepareFieldsForPush($availableFields[$zObject]); | |
| $existingPerson = $this->getExistingRecord('Email', $lead->getEmail(), $zObject); | |
| $fieldsToUpdate[$zObject] = $this->getBlankFieldsToUpdate($fieldsToUpdate[$zObject], $existingPerson, $objectFields, $config); | |
| if (empty($config['leadFields'])) { | |
| return []; | |
| } | |
| $mapper = new Mapper($availableFields); | |
| $mapper->setObject($zObject); | |
| $integrationEntityRepo = $this->em->getRepository(IntegrationEntity::class); | |
| $integrationId = $integrationEntityRepo->getIntegrationsEntityId('Zoho', $zObject, 'lead', $lead->getId()); | |
| $counter = 0; | |
| $errorCounter = 0; | |
| try { | |
| if ($this->isAuthorized()) { | |
| if (!empty($existingPerson) && empty($integrationId)) { | |
| $this->createIntegrationEntity($zObject, $existingPerson['id'], 'lead', $lead->getId()); | |
| $mapper | |
| ->setMappedFields($fieldsToUpdate[$zObject]) | |
| ->setContact($lead->getProfileFields()) | |
| ->map($lead->getId(), $existingPerson['id']); | |
| $this->updateContactInZoho($mapper, $zObject, $counter, $errorCounter); | |
| } elseif (!empty($existingPerson) && !empty($integrationId)) { // contact exists, then update | |
| $mapper | |
| ->setMappedFields($fieldsToUpdate[$zObject]) | |
| ->setContact($lead->getProfileFields()) | |
| ->map($lead->getId(), $existingPerson['id']); | |
| $this->updateContactInZoho($mapper, $zObject, $counter, $errorCounter); | |
| } else { | |
| $mapper | |
| ->setMappedFields($config['leadFields']) | |
| ->setContact($lead->getProfileFields()) | |
| ->map($lead->getId()); | |
| $this->createContactInZoho($mapper, $zObject, $counter, $errorCounter); | |
| } | |
| return true; | |
| } | |
| } catch (\Exception $e) { | |
| $this->logIntegrationError($e); | |
| } | |
| return false; | |
| } | |
| public function getBlankFieldsToUpdate($fields, $sfRecord, $objectFields, $config) | |
| { | |
| // check if update blank fields is selected | |
| if (isset($config['updateBlanks']) && isset($config['updateBlanks'][0]) && 'updateBlanks' == $config['updateBlanks'][0]) { | |
| foreach ($sfRecord as $fieldName => $sfField) { | |
| if (array_key_exists($fieldName, $objectFields['required']['fields'])) { | |
| continue; // this will be treated differently | |
| } | |
| if ('null' === $sfField && array_key_exists($fieldName, $objectFields['create']) && !array_key_exists($fieldName, $fields)) { | |
| // map to mautic field | |
| $fields[$fieldName] = $objectFields['create'][$fieldName]; | |
| } | |
| } | |
| } | |
| return $fields; | |
| } | |
| /** | |
| * Get if data priority is enabled in the integration or not default is false. | |
| */ | |
| public function getDataPriority(): bool | |
| { | |
| return true; | |
| } | |
| /** | |
| * @param array $response | |
| * @param string $zObject | |
| * @param bool $createIntegrationEntity | |
| * | |
| * @throws \MauticPlugin\MauticCrmBundle\Api\Zoho\Exception\MatchingKeyNotFoundException | |
| */ | |
| private function consumeResponse($response, $zObject, $createIntegrationEntity = false, Mapper $mapper = null): int | |
| { | |
| $rows = $response; | |
| if (isset($rows['data'][0])) { | |
| $rows = $rows['data']; | |
| } | |
| $failed = 0; | |
| foreach ($rows as $key => $row) { | |
| $mauticId = $mapper->getContactIdByKey($key); | |
| if ('SUCCESS' === $row['code'] && $createIntegrationEntity) { | |
| $zohoId = $row['details']['id']; | |
| $this->logger->debug('CREATE INTEGRATION ENTITY: '.$zohoId); | |
| $integrationId = $this->getIntegrationEntityRepository()->getIntegrationsEntityId( | |
| 'Zoho', | |
| $zObject, | |
| 'lead', | |
| null, | |
| null, | |
| null, | |
| false, | |
| 0, | |
| 0, | |
| $zohoId | |
| ); | |
| if (0 === count($integrationId)) { | |
| $this->createIntegrationEntity($zObject, $zohoId, 'lead', $mauticId); | |
| } | |
| } elseif (isset($row['status']) && 'error' === $row['status']) { | |
| ++$failed; | |
| $exception = new ApiErrorException($row['message']); | |
| $exception->setContactId($mauticId); | |
| $this->logIntegrationError($exception); | |
| } | |
| } | |
| return $failed; | |
| } | |
| /** | |
| * @param string $seachColumn | |
| * @param string $searchValue | |
| * @param string $object | |
| */ | |
| private function getExistingRecord($seachColumn, $searchValue, $object = 'Leads'): array | |
| { | |
| $availableFields = $this->getAvailableLeadFields(['feature_settings' => ['objects' => ['Leads', 'Contacts']]]); | |
| $records = $this->getApiHelper()->getSearchRecords($seachColumn, $searchValue, $object); | |
| $idField = [ | |
| 'id' => [ | |
| 'type' => 'string', | |
| 'label' => 'ID', | |
| 'api_name' => 'id', | |
| 'required' => true, | |
| ], | |
| ]; | |
| return $this->parseZohoRecord($records, array_merge($availableFields[$object], $idField)); | |
| } | |
| private function parseZohoRecord($data, $fields): array | |
| { | |
| $parsedData = []; | |
| if (empty($data['data'])) { | |
| return $parsedData; | |
| } | |
| $records = $data['data'][0]; | |
| foreach ($fields as $field) { | |
| foreach ($records as $recordKey => $recordValue) { | |
| if ($recordKey === $field['api_name']) { | |
| $parsedData[$recordKey] = $recordValue; | |
| continue; | |
| } | |
| } | |
| } | |
| return $parsedData; | |
| } | |
| /** | |
| * @param string $object | |
| * @param int $counter | |
| * @param int $errorCounter | |
| */ | |
| private function updateContactInZoho(Mapper $mapper, $object, &$counter, &$errorCounter): void | |
| { | |
| $response = $this->getApiHelper()->updateLead($mapper->getArray(), $object); | |
| $failed = $this->consumeResponse($response, $object, false, $mapper); | |
| $counter -= $failed; | |
| $errorCounter += $failed; | |
| } | |
| /** | |
| * @param string $object | |
| * @param int $counter | |
| * @param int $errorCounter | |
| */ | |
| private function createContactInZoho(Mapper $mapper, $object, &$counter, &$errorCounter): void | |
| { | |
| $response = $this->getApiHelper()->createLead($mapper->getArray(), $object); | |
| $failed = $this->consumeResponse($response, $object, true, $mapper); | |
| $counter -= $failed; | |
| $errorCounter += $failed; | |
| } | |
| /** | |
| * @param array $objects | |
| * | |
| * @return array | |
| */ | |
| protected function cleanPriorityFields($fieldsToUpdate, $objects = null) | |
| { | |
| if (null === $objects) { | |
| $objects = ['Leads', 'Contacts']; | |
| } | |
| if (isset($fieldsToUpdate['leadFields'])) { | |
| // Pass in the whole config | |
| $fields = $fieldsToUpdate; | |
| } else { | |
| $fields = array_flip($fieldsToUpdate); | |
| } | |
| return $this->prepareFieldsForSync($fields, $fieldsToUpdate, $objects); | |
| } | |
| /** | |
| * @param array $fields | |
| * @param array $keys | |
| * @param mixed $object | |
| * | |
| * @return array | |
| */ | |
| public function prepareFieldsForSync($fields, $keys, $object = null) | |
| { | |
| $leadFields = []; | |
| if (null === $object) { | |
| $object = 'Leads'; | |
| } | |
| $objects = (!is_array($object)) ? [$object] : $object; | |
| if (is_string($object) && 'Accounts' === $object) { | |
| return $fields['companyFields'] ?? $fields; | |
| } | |
| if (isset($fields['leadFields'])) { | |
| $fields = $fields['leadFields']; | |
| $keys = array_keys($fields); | |
| } | |
| foreach ($objects as $obj) { | |
| if (!isset($leadFields[$obj])) { | |
| $leadFields[$obj] = []; | |
| } | |
| foreach ($keys as $key) { | |
| $leadFields[$obj][$key] = $fields[$key]; | |
| } | |
| } | |
| return (is_array($object)) ? $leadFields : $leadFields[$object]; | |
| } | |
| } | |