Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api;
use Mautic\PluginBundle\Exception\ApiErrorException;
use MauticPlugin\MauticCrmBundle\Integration\ConnectwiseIntegration;
/**
* @property ConnectwiseIntegration $integration
*/
class ConnectwiseApi extends CrmApi
{
/**
* @param string $endpoint
* @param array $parameters
* @param string $method
*
* @return mixed|string
*
* @throws ApiErrorException
*/
protected function request($endpoint, $parameters = [], $method = 'GET')
{
$apiUrl = $this->integration->getApiUrl();
$url = sprintf('%s/%s', $apiUrl, $endpoint);
$response = $this->integration->makeRequest(
$url,
$parameters,
$method,
['encode_parameters' => 'json']
);
$errors = [];
$code = 0;
if (is_array($response)) {
foreach ($response as $key => $r) {
$key = preg_replace('/[\r\n]+/', '', $key);
switch ($key) {
case '<!DOCTYPE_html_PUBLIC_"-//W3C//DTD_XHTML_1_0_Strict//EN"_"http://www_w3_org/TR/xhtml1/DTD/xhtml1-strict_dtd"><html_xmlns':
$errors[] = '404 not found error';
$code = 404;
break;
case 'errors':
$errors[] = $response['message'];
// no break
case 'code':
$errors[] = $response['message'];
break;
}
}
}
if (!empty($errors)) {
throw new ApiErrorException(implode(' ', $errors), $code);
}
return $response;
}
/**
* @param int $page
*
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getCompanies(array $params, $page = 1)
{
$query = [
'page' => $page,
'pageSize' => ConnectwiseIntegration::PAGESIZE,
];
$conditions = $params['conditions'] ?? [];
if (isset($params['start'])) {
$conditions[] = 'lastUpdated > ['.$params['start'].']';
}
if ($conditions) {
$query['conditions'] = implode(' AND ', $conditions);
}
return $this->request('company/companies', $query);
}
/**
* @param int $page
*
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getContacts(array $params, $page = 1)
{
$query = [
'page' => $page,
'pageSize' => ConnectwiseIntegration::PAGESIZE,
];
if (isset($params['start'])) {
$query['conditions'] = 'lastUpdated > ['.$params['start'].']';
}
if (isset($params['Email'])) {
$query['childconditions'] = 'communicationItems/value = "'.$params['Email'].'" AND communicationItems/communicationType="Email"';
}
if (isset($params['Ids'])) {
$query['conditions'] = 'id in ('.$params['Ids'].')';
}
return $this->request('company/contacts', $query);
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function createContact(array $params)
{
return $this->request('company/contacts', $params, 'POST');
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function updateContact(array $params, $id)
{
return $this->request('company/contacts/'.$id, $params, 'PATCH');
}
/**
* @throws ApiErrorException
*/
public function getCampaigns(): array
{
return $this->fetchAllRecords('marketing/groups');
}
/**
* @param int $page
*
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getCampaignMembers($campaignId, $page = 1)
{
return $this->request('marketing/groups/'.$campaignId.'/contacts', ['page' => $page, 'pageSize' => ConnectwiseIntegration::PAGESIZE]);
}
/**
* https://{connectwiseSite}/v4_6_release/apis/3.0/sales/activities/types.
*
* @throws ApiErrorException
*/
public function getActivityTypes(): array
{
return $this->fetchAllRecords('sales/activities/types');
}
/**
* @param array $params
*
* @return array
*
* @throws ApiErrorException
*/
public function postActivity($params = [])
{
return $this->request('sales/activities', $params, 'POST');
}
/**
* @throws ApiErrorException
*/
public function getMembers(): array
{
return $this->fetchAllRecords('system/members');
}
/**
* @throws ApiErrorException
*/
public function fetchAllRecords($endpoint): array
{
$page = 1;
$pageSize = ConnectwiseIntegration::PAGESIZE;
$allRecords = [];
try {
while ($pagedRecords = $this->request($endpoint, ['page' => $page, 'pageSize' => $pageSize])) {
$allRecords = array_merge($allRecords, $pagedRecords);
++$page;
if (count($pagedRecords) < $pageSize) {
// Received less than page size so we know there are no more records to fetch
break;
}
}
} catch (ApiErrorException $exception) {
if (404 !== $exception->getCode()) {
// Ignore 404 due to pagination
throw $exception;
}
}
return $allRecords;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api;
use MauticPlugin\MauticCrmBundle\Integration\CrmAbstractIntegration;
/**
* @method createLead()
*/
class CrmApi
{
public function __construct(
protected CrmAbstractIntegration $integration,
) {
}
}

View File

@@ -0,0 +1,246 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PluginBundle\Exception\ApiErrorException;
use Psr\Http\Message\ResponseInterface;
class DynamicsApi extends CrmApi
{
private function getUrl(): string
{
$keys = $this->integration->getKeys();
return $keys['resource'].'/api/data/v8.2';
}
/**
* @param string $method
* @param string $moduleobject
*
* @return array|ResponseInterface
*
* @throws ApiErrorException
*/
protected function request($operation, array $parameters = [], $method = 'GET', $moduleobject = 'contacts', $settings = [])
{
if ('company' === $moduleobject) {
$moduleobject = 'accounts';
}
if ('' === $operation) {
$operation = $moduleobject;
}
$url = sprintf('%s/%s', $this->getUrl(), $operation);
if (isset($parameters['request_settings'])) {
$settings = array_merge($settings, $parameters['request_settings']);
unset($parameters['request_settings']);
}
$settings = array_merge($settings, [
'encode_parameters' => 'json',
'return_raw' => 'true', // needed to get the HTTP status code in the response
'request_timeout' => 300,
]);
/** @var ResponseInterface $response */
$response = $this->integration->makeRequest($url, $parameters, $method, $settings);
if ('POST' === $method && (!($response instanceof ResponseInterface) || !in_array($response->getStatusCode(), [200, 204], true))) {
throw new ApiErrorException('Dynamics CRM API error: '.json_encode($response->getBody()));
}
if ('GET' === $method && $response instanceof ResponseInterface) {
return json_decode($response->getBody(), true);
}
return $response;
}
/**
* List types.
*
* @param string $object Zoho module name
*
* @return mixed
*/
public function getLeadFields($object = 'contacts')
{
if ('company' === $object) {
$object = 'accounts'; // Dynamics object name
}
$logicalName = rtrim($object, 's'); // singularize object name
$operation = sprintf('EntityDefinitions(LogicalName=\'%s\')/Attributes', $logicalName);
$parameters = [
'$filter' => 'Microsoft.Dynamics.CRM.NotIn(PropertyName=\'AttributeTypeName\',PropertyValues=["Virtual", "Uniqueidentifier", "Picklist", "Lookup", "Owner", "Customer"]) and IsValidForUpdate eq true and AttributeOf eq null and LogicalName ne \'parentcustomerid\'', // ignore system fields
'$select' => 'RequiredLevel,LogicalName,DisplayName,AttributeTypeName', // select only miningful columns
];
return $this->request($operation, $parameters, 'GET', $object);
}
/**
* @param Lead $lead
*/
public function createLead($data, $lead, $object = 'contacts'): ResponseInterface
{
return $this->request('', $data, 'POST', $object);
}
public function updateLead($data, $objectId): ResponseInterface
{
// $settings['headers']['If-Match'] = '*'; // prevent create new contact
return $this->request(sprintf('contacts(%s)', $objectId), $data, 'PATCH', 'contacts', []);
}
/**
* gets leads.
*
* @return mixed
*/
public function getLeads(array $params)
{
return $this->request('', $params, 'GET', 'contacts');
}
/**
* gets companies.
*
* @param string $id
*
* @return mixed
*/
public function getCompanies(array $params, $id = null)
{
if ($id) {
$operation = sprintf('accounts(%s)', $id);
$data = $this->request($operation, $params, 'GET');
} else {
$data = $this->request('', $params, 'GET', 'accounts');
}
return $data;
}
/**
* Batch create leads.
*
* @param array $data
* @param string $object
* @param bool $isUpdate
*/
public function createLeads($data, $object = 'contacts', $isUpdate = false): array
{
if (0 === count($data)) {
return [];
}
$returnIds = [];
$batchId = substr(str_shuffle(uniqid('b', false)), 0, 6);
$changeId = substr(str_shuffle(uniqid('c', false)), 0, 6);
$settings['headers']['Content-Type'] = 'multipart/mixed;boundary=batch_'.$batchId;
$settings['headers']['Accept'] = 'application/json';
$odata = '--batch_'.$batchId.PHP_EOL;
$odata .= 'Content-Type: multipart/mixed;boundary=changeset_'.$changeId.PHP_EOL.PHP_EOL;
$contentId = 0;
foreach ($data as $objectId => $lead) {
++$contentId;
$odata .= '--changeset_'.$changeId.PHP_EOL;
$odata .= 'Content-Type: application/http'.PHP_EOL;
$odata .= 'Content-Transfer-Encoding:binary'.PHP_EOL;
$odata .= 'Content-ID: '.$objectId.PHP_EOL.PHP_EOL;
// $odata .= 'Content-ID: '.(++$contentId).PHP_EOL.PHP_EOL;
$returnIds[$objectId] = $contentId;
if (!$isUpdate) {
$oid = $objectId;
$objectId = sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X', mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(16384, 20479), mt_rand(32768, 49151), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535));
$returnIds[$objectId] = $oid; // save lead Id
}
$operation = sprintf('%s(%s)', $object, $objectId);
$odata .= sprintf('PATCH %s/%s HTTP/1.1', $this->getUrl(), $operation).PHP_EOL;
if ($isUpdate) {
$odata .= 'If-Match: *'.PHP_EOL;
} else {
$odata .= 'If-None-Match: *'.PHP_EOL;
}
$odata .= 'Content-Type: application/json;type=entry'.PHP_EOL.PHP_EOL;
$odata .= json_encode($lead).PHP_EOL;
}
$odata .= '--changeset_'.$changeId.'--'.PHP_EOL.PHP_EOL;
$odata .= '--batch_'.$batchId.'--'.PHP_EOL;
$settings['post_data'] = $odata;
$settings['curl_options'][CURLOPT_CRLF] = true;
$response = $this->request('$batch', [], 'POST', $object, $settings);
if ($isUpdate) {
return $returnIds;
}
return $this->parseRawHttpResponse($response);
}
/**
* @param array $data
*/
public function updateLeads($data, $object = 'contacts'): array
{
return $this->createLeads($data, $object, true);
}
/**
* @see https://stackoverflow.com/questions/5483851/manually-parse-raw-http-data-with-php
*/
public function parseRawHttpResponse(ResponseInterface $response): array
{
$a_data = [];
$input = $response->getBody();
$contentType = $response->getHeaders()['Content-Type'];
// grab multipart boundary from content type header
preg_match('/boundary=(.*)$/', $contentType, $matches);
$boundary = $matches[1];
// split content by boundary and get rid of last -- element
$a_blocks = preg_split("/-+$boundary/", $input);
array_pop($a_blocks);
// there is only one batchresponse
$input = array_pop($a_blocks);
[$header, $input] = explode("\r\n\r\n", $input, 2);
foreach (explode("\r\n", $header) as $r) {
if (0 === stripos($r, 'Content-Type:')) {
[$headername, $contentType] = explode(':', $r, 2);
}
}
// grab multipart boundary from content type header
preg_match('/boundary=(.*)$/', $contentType, $matches);
$boundary = $matches[1];
// split content by boundary and get rid of last -- element
$a_blocks = preg_split("/-+$boundary/", $input);
array_pop($a_blocks);
// loop data blocks
foreach ($a_blocks as $block) {
if (empty($block)) {
continue;
}
if (false !== stripos($block, 'OData-EntityId:')) {
preg_match('/Content-ID: (\d+)/', $block, $matches);
$leadId = (count($matches) > 1) ? $matches[1] : 0;
// OData-EntityId: https://virlatinus.crm.dynamics.com/api/data/v8.2/contacts(2725f27c-2058-e711-8111-c4346bac1938)
preg_match('/OData-EntityId: .*\(([^\)]*)\)/', $block, $matches);
$oid = (count($matches) > 1) ? $matches[1] : '00000000-0000-0000-0000-000000000000';
$a_data[$oid] = $leadId;
}
}
return $a_data;
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api;
use Mautic\EmailBundle\Helper\MailHelper;
use Mautic\PluginBundle\Exception\ApiErrorException;
use MauticPlugin\MauticCrmBundle\Integration\HubspotIntegration;
/**
* @property HubspotIntegration $integration
*/
class HubspotApi extends CrmApi
{
protected $requestSettings = [
'encode_parameters' => 'json',
];
protected function request($operation, $parameters = [], $method = 'GET', $object = 'contacts')
{
if ('oauth2' === $this->integration->getAuthenticationType()) {
$url = sprintf('%s/%s/%s/', $this->integration->getApiUrl(), $object, $operation);
} else {
$url = sprintf('%s/%s/%s/?hapikey=%s', $this->integration->getApiUrl(), $object, $operation, $this->integration->getHubSpotApiKey());
}
$request = $this->integration->makeRequest($url, $parameters, $method, $this->requestSettings);
if (isset($request['status']) && 'error' == $request['status']) {
$message = $request['message'];
if (isset($request['validationResults'])) {
$message .= " \n ".print_r($request['validationResults'], true);
}
if (isset($request['validationResults'][0]['error']) && 'PROPERTY_DOESNT_EXIST' == $request['validationResults'][0]['error']) {
$this->createProperty($request['validationResults'][0]['name']);
$this->request($operation, $parameters, $method, $object);
} else {
throw new ApiErrorException($message);
}
}
if (isset($request['error']) && 401 == $request['error']['code']) {
$response = json_decode($request['error']['message'] ?? null, true);
if (isset($response)) {
throw new ApiErrorException($response['message'], $request['error']['code']);
} else {
throw new ApiErrorException('401 Unauthorized - Error with Hubspot API', $request['error']['code']);
}
}
if (isset($request['error'])) {
throw new ApiErrorException($request['error']['message']);
}
return $request;
}
/**
* @return mixed
*/
public function getLeadFields($object = 'contacts')
{
if ('company' == $object) {
$object = 'companies'; // hubspot company object name
}
return $this->request('v2/properties', [], 'GET', $object);
}
/**
* Creates Hubspot lead.
*
* @return mixed
*/
public function createLead(array $data, $lead, $updateLink = false)
{
/*
* As Hubspot integration requires a valid email
* If the email is not valid we don't proceed with the request
*/
$email = $data['email'];
$result = [];
// Check if the is a valid email
MailHelper::validateEmail($email);
// Format data for request
$formattedLeadData = $this->integration->formatLeadDataForCreateOrUpdate($data, $lead, $updateLink);
if ($formattedLeadData) {
$result = $this->request('v1/contact/createOrUpdate/email/'.$email, $formattedLeadData, 'POST');
}
return $result;
}
/**
* gets Hubspot contact.
*
* @return mixed
*/
public function getContacts($params = [])
{
return $this->request('v1/lists/recently_updated/contacts/recent?', $params, 'GET', 'contacts');
}
/**
* gets Hubspot company.
*
* @return mixed
*/
public function getCompanies($params, $id)
{
if ($id) {
return $this->request('v2/companies/'.$id, $params, 'GET', 'companies');
}
return $this->request('v2/companies/recent/modified', $params, 'GET', 'companies');
}
/**
* @param string $object
*
* @return mixed|string
*/
public function createProperty($propertyName, $object = 'properties')
{
return $this->request('v1/contacts/properties', ['name' => $propertyName, 'groupName' => 'contactinformation', 'type' => 'string'], 'POST', $object);
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api\Salesforce\Exception;
class RetryRequestException extends \Exception
{
}

View File

@@ -0,0 +1,20 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api\Salesforce\Helper;
class RequestUrl
{
/**
* Correctly generate the URL based on given URL parts.
*
* @return string
*/
public static function get($apiUrl, $queryUrl, $operation = null, $object = null)
{
if ($queryUrl) {
return ($operation) ? sprintf($queryUrl.'/%s', $operation) : $queryUrl;
}
return sprintf($apiUrl.'/%s/%s', $object, $operation);
}
}

View File

@@ -0,0 +1,717 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\OptimisticLockException;
use Mautic\PluginBundle\Exception\ApiErrorException;
use MauticPlugin\MauticCrmBundle\Api\Salesforce\Exception\RetryRequestException;
use MauticPlugin\MauticCrmBundle\Api\Salesforce\Helper\RequestUrl;
use MauticPlugin\MauticCrmBundle\Integration\CrmAbstractIntegration;
use MauticPlugin\MauticCrmBundle\Integration\SalesforceIntegration;
/**
* @property SalesforceIntegration $integration
*/
class SalesforceApi extends CrmApi
{
/**
* This regular expression parses missing field's name from the error message.
*
* @var string
*/
public const REGEXP_MISSING_FIELD = "/ERROR\sat\sRow.+No\ssuch\scolumn\s'([^']+)'\son\sentity\s'([^']+)'/m";
protected $object = 'Lead';
protected $requestSettings = [
'encode_parameters' => 'json',
];
protected $apiRequestCounter = 0;
protected $requestCounter = 1;
protected $maxLockRetries = 3;
private bool $optOutFieldAccessible = true;
public function __construct(CrmAbstractIntegration $integration)
{
parent::__construct($integration);
$this->requestSettings['curl_options'] = [
CURLOPT_SSLVERSION => defined('CURL_SSLVERSION_TLSv1_2') ? CURL_SSLVERSION_TLSv1_2 : 6,
];
}
/**
* @param array $elementData
* @param string $method
* @param bool $isRetry
*
* @return mixed|string
*
* @throws ApiErrorException
*/
public function request($operation, $elementData = [], $method = 'GET', $isRetry = false, $object = null, $queryUrl = null)
{
if (!$object) {
$object = $this->object;
}
$requestUrl = RequestUrl::get($this->integration->getApiUrl(), $queryUrl, $operation, $object);
$settings = $this->requestSettings;
if ('PATCH' == $method) {
$settings['headers'] = ['Sforce-Auto-Assign' => 'FALSE'];
}
// Query commands can have long wait time while SF builds response as the offset increases
$settings['request_timeout'] = 300;
// Wrap in a isAuthorized to refresh token if applicable
$response = $this->integration->makeRequest($requestUrl, $elementData, $method, $settings);
++$this->apiRequestCounter;
try {
$this->analyzeResponse($response, $isRetry);
} catch (RetryRequestException) {
return $this->request($operation, $elementData, $method, true, $object, $queryUrl);
}
return $response;
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getLeadFields($object = null)
{
if ('company' == $object) {
$object = 'Account'; // salesforce object name
}
return $this->request('describe', [], 'GET', false, $object);
}
/**
* @throws ApiErrorException
*/
public function getPerson(array $data): array
{
$config = $this->integration->mergeConfigToFeatureSettings([]);
$queryUrl = $this->integration->getQueryUrl();
$sfRecords = [
'Contact' => [],
'Lead' => [],
];
// try searching for lead as this has been changed before in updated done to the plugin
if (isset($config['objects']) && false !== array_search('Contact', $config['objects']) && !empty($data['Contact']['Email'])) {
$fields = $this->integration->getFieldsForQuery('Contact');
unset($fields[array_search('HasOptedOutOfEmail', $fields)]);
$fields[] = 'Id';
$fields = implode(', ', array_unique($fields));
$findContact = 'select '.$fields.' from Contact where email = \''.$this->escapeQueryValue($data['Contact']['Email']).'\'';
$response = $this->request('query', ['q' => $findContact], 'GET', false, null, $queryUrl);
if (!empty($response['records'])) {
$sfRecords['Contact'] = $response['records'];
}
}
if (!empty($data['Lead']['Email'])) {
$fields = $this->integration->getFieldsForQuery('Lead');
unset($fields[array_search('HasOptedOutOfEmail', $fields)]);
$fields[] = 'Id';
$fields = implode(', ', array_unique($fields));
$findLead = 'select '.$fields.' from Lead where email = \''.$this->escapeQueryValue($data['Lead']['Email']).'\' and ConvertedContactId = NULL';
$response = $this->request('queryAll', ['q' => $findLead], 'GET', false, null, $queryUrl);
if (!empty($response['records'])) {
$sfRecords['Lead'] = $response['records'];
}
}
return $sfRecords;
}
/**
* @throws ApiErrorException
*/
public function getCompany(array $data): array
{
$config = $this->integration->mergeConfigToFeatureSettings([]);
$queryUrl = $this->integration->getQueryUrl();
$sfRecords = [
'Account' => [],
];
$appendToQuery = '';
// try searching for lead as this has been changed before in updated done to the plugin
if (isset($config['objects']) && false !== array_search('company', $config['objects']) && !empty($data['company']['Name'])) {
$fields = $this->integration->getFieldsForQuery('Account');
if (!empty($data['company']['BillingCountry'])) {
$appendToQuery .= ' and BillingCountry = \''.$this->escapeQueryValue($data['company']['BillingCountry']).'\'';
}
if (!empty($data['company']['BillingCity'])) {
$appendToQuery .= ' and BillingCity = \''.$this->escapeQueryValue($data['company']['BillingCity']).'\'';
}
if (!empty($data['company']['BillingState'])) {
$appendToQuery .= ' and BillingState = \''.$this->escapeQueryValue($data['company']['BillingState']).'\'';
}
$fields[] = 'Id';
$fields = implode(', ', array_unique($fields));
$query = 'select '.$fields.' from Account where Name = \''.$this->escapeQueryValue($data['company']['Name']).'\''.$appendToQuery;
$response = $this->request('queryAll', ['q' => $query], 'GET', false, null, $queryUrl);
if (!empty($response['records'])) {
$sfRecords['company'] = $response['records'];
}
}
return $sfRecords;
}
/**
* @return array|mixed|string
*
* @throws ApiErrorException
*/
public function createLead(array $data)
{
$createdLeadData = [];
if (isset($data['Email'])) {
$createdLeadData = $this->createObject($data, 'Lead');
}
return $createdLeadData;
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function createObject(array $data, $sfObject)
{
$objectData = $this->request('', $data, 'POST', false, $sfObject);
$this->integration->getLogger()->debug('SALESFORCE: POST createObject '.$sfObject.' '.var_export($data, true).var_export($objectData, true));
if (isset($objectData['id'])) {
// Salesforce is inconsistent it seems
$objectData['Id'] = $objectData['id'];
}
return $objectData;
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function updateObject(array $data, $sfObject, $sfObjectId)
{
$objectData = $this->request('', $data, 'PATCH', false, $sfObject.'/'.$sfObjectId);
$this->integration->getLogger()->debug('SALESFORCE: PATCH updateObject '.$sfObject.' '.var_export($data, true).var_export($objectData, true));
// Salesforce is inconsistent it seems
$objectData['Id'] = $objectData['id'] = $sfObjectId;
return $objectData;
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function syncMauticToSalesforce(array $data)
{
$queryUrl = $this->integration->getCompositeUrl();
return $this->request('composite/', $data, 'POST', false, null, $queryUrl);
}
/**
* @return array<mixed>
*
* @throws ApiErrorException
*/
public function createLeadActivity(array $activity, $object): array
{
$config = $this->integration->getIntegrationSettings()->getFeatureSettings();
$namespace = (!empty($config['namespace'])) ? $config['namespace'].'__' : '';
$mActivityObjectName = $namespace.'mautic_timeline__c';
$activityData = [];
if (!empty($activity)) {
foreach ($activity as $sfId => $records) {
foreach ($records['records'] as $record) {
$body = [
$namespace.'ActivityDate__c' => $record['dateAdded']->format('c'),
$namespace.'Description__c' => $record['description'],
'Name' => substr($record['name'], 0, 80),
$namespace.'Mautic_url__c' => $records['leadUrl'],
$namespace.'ReferenceId__c' => $record['id'].'-'.$sfId,
];
if ('Lead' === $object) {
$body[$namespace.'WhoId__c'] = $sfId;
} elseif ('Contact' === $object) {
$body[$namespace.'contact_id__c'] = $sfId;
}
$activityData[] = [
'method' => 'POST',
'url' => '/services/data/v38.0/sobjects/'.$mActivityObjectName,
'referenceId' => $record['id'].'-'.$sfId,
'body' => $body,
];
}
}
if (!empty($activityData)) {
$request = [];
$request['allOrNone'] = 'false';
$chunked = array_chunk($activityData, 25);
$results = [];
foreach ($chunked as $chunk) {
// We can only submit 25 at a time
if ($chunk) {
$request['compositeRequest'] = $chunk;
$result = $this->syncMauticToSalesforce($request);
$results[] = $result;
$this->integration->getLogger()->debug('SALESFORCE: Activity response '.var_export($result, true));
}
}
return $results;
}
}
return [];
}
/**
* Get Salesforce leads.
*
* @param mixed $query String for a SOQL query or array to build query
* @param string $object
*
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getLeads($query, $object)
{
$queryUrl = $this->integration->getQueryUrl();
if (defined('MAUTIC_ENV') && MAUTIC_ENV === 'dev') {
// Easier for testing
$this->requestSettings['headers']['Sforce-Query-Options'] = 'batchSize=200';
}
if (!is_array($query)) {
return $this->request('queryAll', ['q' => $query], 'GET', false, null, $queryUrl);
}
if (!empty($query['nextUrl'])) {
return $this->request(null, [], 'GET', false, null, $query['nextUrl']);
}
$organizationCreatedDate = $this->getOrganizationCreatedDate();
$fields = $this->integration->getFieldsForQuery($object);
if (!empty($fields) && isset($query['start'])) {
if (strtotime($query['start']) < strtotime($organizationCreatedDate)) {
$query['start'] = date('c', strtotime($organizationCreatedDate.' +1 hour'));
}
$fields[] = 'Id';
return $this->requestQueryAllAndHandle($queryUrl, $fields, $object, $query);
}
return [
'totalSize' => 0,
'records' => [],
];
}
/**
* Perform queryAll request and retry if HasOptedOutOfEmail is not accessible.
*
* @param array<mixed> $fields
* @param array<mixed> $query
*
* @return mixed|string
*
* @throws ApiErrorException
*/
private function requestQueryAllAndHandle(string $queryUrl, array $fields, string $object, array $query): mixed
{
$config = $this->integration->mergeConfigToFeatureSettings([]);
if (isset($config['updateOwner']) && isset($config['updateOwner'][0]) && 'updateOwner' == $config['updateOwner'][0]) {
$fields[] = 'Owner.Name';
$fields[] = 'Owner.Email';
}
$fields = array_unique($fields);
$ignoreConvertedLeads = ('Lead' == $object) ? ' and ConvertedContactId = NULL' : '';
if (!$this->isOptOutFieldAccessible()) { // If not opt-out is supported; unset it
unset($fields[array_search('HasOptedOutOfEmail', $fields)]);
}
$baseQuery = 'SELECT %s from '.$object.' where SystemModStamp>='.$query['start'].' and SystemModStamp<='.$query['end'].' and isDeleted = false'
.$ignoreConvertedLeads;
return $this->handleQueryAll($baseQuery, $fields, $queryUrl);
}
/**
* @return bool|mixed
*
* @throws ApiErrorException
*/
public function getOrganizationCreatedDate()
{
$cache = $this->integration->getCache();
if (!$organizationCreatedDate = $cache->get('organization.created_date')) {
$queryUrl = $this->integration->getQueryUrl();
$organization = $this->request('query', ['q' => 'SELECT CreatedDate from Organization'], 'GET', false, null, $queryUrl);
$organizationCreatedDate = $organization['records'][0]['CreatedDate'];
$cache->set('organization.created_date', $organizationCreatedDate);
}
return $organizationCreatedDate;
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getCampaigns()
{
$campaignQuery = 'Select Id, Name from Campaign where isDeleted = false';
$queryUrl = $this->integration->getQueryUrl();
return $this->request('query', ['q' => $campaignQuery], 'GET', false, null, $queryUrl);
}
/**
* @param mixed $modifiedSince
*
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getCampaignMembers($campaignId, $modifiedSince = null, $queryUrl = null)
{
$defaultSettings = $this->requestSettings;
// Control batch size to prevent URL too long errors when fetching contact details via SOQL and to control Doctrine RAM usage for
// Mautic IntegrationEntity objects
$this->requestSettings['headers']['Sforce-Query-Options'] = 'batchSize=200';
if (null === $queryUrl) {
$queryUrl = $this->integration->getQueryUrl().'/query';
}
$query = "Select CampaignId, ContactId, LeadId, isDeleted from CampaignMember where CampaignId = '".trim($campaignId)."'";
if ($modifiedSince) {
$query .= ' and SystemModStamp >= '.$modifiedSince;
}
$results = $this->request(null, ['q' => $query], 'GET', false, null, $queryUrl);
$this->requestSettings = $defaultSettings;
return $results;
}
/**
* @throws ApiErrorException
*/
public function checkCampaignMembership($campaignId, $object, array $people): array
{
$campaignMembers = [];
if (!empty($people)) {
$idField = "{$object}Id";
$query = "Select Id, $idField from CampaignMember where CampaignId = '".$campaignId
."' and $idField in ('".implode("','", $people)."')";
$foundCampaignMembers = $this->request('query', ['q' => $query], 'GET', false, null, $this->integration->getQueryUrl());
if (!empty($foundCampaignMembers['records'])) {
foreach ($foundCampaignMembers['records'] as $member) {
$campaignMembers[$member[$idField]] = $member['Id'];
}
}
}
return $campaignMembers;
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getCampaignMemberStatus($campaignId)
{
$campaignQuery = "Select Id, Label from CampaignMemberStatus where isDeleted = false and CampaignId='".$campaignId."'";
$queryUrl = $this->integration->getQueryUrl();
return $this->request('query', ['q' => $campaignQuery], 'GET', false, null, $queryUrl);
}
/**
* @return int
*/
public function getRequestCounter()
{
$count = $this->apiRequestCounter;
$this->apiRequestCounter = 0;
return $count;
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getCompaniesByName(array $names, $requiredFieldString)
{
$names = array_map([$this, 'escapeQueryValue'], $names);
$queryUrl = $this->integration->getQueryUrl();
$findQuery = 'select Id, '.$requiredFieldString.' from Account where isDeleted = false and Name in (\''.implode("','", $names).'\')';
return $this->request('query', ['q' => $findQuery], 'GET', false, null, $queryUrl);
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getCompaniesById(array $ids, $requiredFieldString)
{
$findQuery = 'select isDeleted, Id, '.$requiredFieldString.' from Account where Id in (\''.implode("','", $ids).'\')';
$queryUrl = $this->integration->getQueryUrl();
return $this->request('queryAll', ['q' => $findQuery], 'GET', false, null, $queryUrl);
}
/**
* @param mixed $response
* @param bool $isRetry
*
* @throws ApiErrorException
* @throws RetryRequestException
*/
private function analyzeResponse($response, $isRetry): void
{
if (is_array($response)) {
if (!empty($response['errors'])) {
throw new ApiErrorException(implode(', ', $response['errors']));
}
if (isset($response['error']['message'])) {
throw new ApiErrorException($response['error']['message']);
}
foreach ($response as $lineItem) {
if (!is_array($lineItem)) {
continue;
}
$lineItemForInvalidSession = $lineItem;
$lineItemForInvalidSession['errorCode'] = 'INVALID_SESSION_ID';
if (!empty($lineItemForInvalidSession['message']) && str_contains($lineItemForInvalidSession['message'], '"errorCode":"INVALID_SESSION_ID"') && $error = $this->processError($lineItemForInvalidSession, $isRetry)) {
$errors[] = $error;
continue;
}
if (!empty($lineItem['errorCode']) && $error = $this->processError($lineItem, $isRetry)) {
$errors[] = $error;
}
}
if (!empty($errors)) {
throw new ApiErrorException(implode(', ', $errors));
}
}
}
/**
* @return string|false
*
* @throws ApiErrorException
* @throws RetryRequestException
*/
private function processError(array $error, $isRetry)
{
switch ($error['errorCode']) {
case 'INVALID_SESSION_ID':
$this->revalidateSession($isRetry);
break;
case 'UNABLE_TO_LOCK_ROW':
$this->checkIfLockedRequestShouldBeRetried();
break;
}
if (!empty($error['message'])) {
return $error['message'];
}
return false;
}
/**
* @throws ApiErrorException
* @throws RetryRequestException
*/
private function revalidateSession($isRetry): void
{
if ($refreshError = $this->integration->authCallback(['use_refresh_token' => true])) {
throw new ApiErrorException($refreshError);
}
if (!$isRetry) {
throw new RetryRequestException();
}
}
/**
* @throws RetryRequestException
*/
private function checkIfLockedRequestShouldBeRetried(): bool
{
// The record is locked so let's wait a a few seconds and retry
if ($this->requestCounter < $this->maxLockRetries) {
sleep($this->requestCounter * 3);
++$this->requestCounter;
throw new RetryRequestException();
}
$this->requestCounter = 1;
return false;
}
/**
* @return array<mixed>
*/
private function parseMissingField(string $errorMessage)
{
$matches = [];
preg_match(self::REGEXP_MISSING_FIELD, $errorMessage, $matches);
return isset($matches[1]) ? [$matches[1], $matches[2]] : [null, null];
}
/**
* @return bool|float|mixed|string
*/
private function escapeQueryValue($value)
{
// SF uses backslashes as escape delimeter
// Remember that PHP uses \ as an escape. Therefore, to replace a single backslash with 2, must use 2 and 4
$value = str_replace('\\', '\\\\', $value);
// Apply general formatting/cleanup
$value = $this->integration->cleanPushData($value);
// Escape single quotes
$value = str_replace("'", "\'", $value);
return $value;
}
public function isOptOutFieldAccessible(): bool
{
return $this->optOutFieldAccessible;
}
public function setOptOutFieldAccessible(bool $optOutFieldAccessible): SalesforceApi
{
$this->optOutFieldAccessible = $optOutFieldAccessible;
return $this;
}
/**
* @param array<string> $fields
*
* @return mixed|string
*
* @throws ApiErrorException
* @throws ORMException
* @throws OptimisticLockException
*/
private function handleQueryAll(string $baseQuery, array $fields, string $queryUrl, int $tries = 0, bool $isRetry = false): mixed
{
if (10 === $tries) {
$this->integration->logIntegrationError(new \Exception(
sprintf('Maximum tries exceeded for handling missing field scenarios')
));
}
try {
$leadsQuery = sprintf($baseQuery, join(', ', $fields));
$response = $this->request('queryAll', ['q' => $leadsQuery], 'GET', $isRetry, null, $queryUrl);
} catch (ApiErrorException $e) {
list($missingField, $entityType) = $this->parseMissingField($e->getMessage());
if (!$missingField) {
throw $e;
}
if ('HasOptedOutOfEmail' == $missingField) {
// Unset field as it is not accessible
unset($fields[array_search('HasOptedOutOfEmail', $fields)]);
// Disable the use of the HasOptedOutOfEmail field for future requests
$this->setOptOutFieldAccessible(false);
// Notify all admins of this error
$this->integration->upsertUnreadAdminsNotification(
$this->integration->getTranslator()->trans('mautic.salesforce.error.opt-out_permission.header'),
$this->integration->getTranslator()->trans('mautic.salesforce.error.opt-out_permission.message')
);
} else {
$entityManager = $this->integration->getEntityManager();
$entity = $this->integration->getIntegrationSettings();
$featureSettings = $entity->getFeatureSettings();
$field = $missingField.'__'.$entityType;
if (isset($featureSettings['leadFields'][$field])) {
unset($featureSettings['leadFields'][$field]);
// Remove the missing field from mapping
$entity->setFeatureSettings($featureSettings);
$entityManager->persist($entity);
$entityManager->flush();
// Remove the missing field from the request
$missingFieldIndex = array_search($missingField, $fields);
if (false !== $missingFieldIndex) {
unset($fields[$missingFieldIndex]);
}
}
}
$response = $this->handleQueryAll($baseQuery, $fields, $queryUrl, ++$tries, true);
}
return $response;
}
}

View File

@@ -0,0 +1,699 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api;
use Mautic\PluginBundle\Exception\ApiErrorException;
use MauticPlugin\MauticCrmBundle\Integration\SugarcrmIntegration;
/**
* @property SugarcrmIntegration $integration
*/
class SugarcrmApi extends CrmApi
{
protected $object = 'Leads';
/**
* @param array $data
* @param string $method
*
* @return mixed|string
*
* @throws ApiErrorException
*/
public function request($sMethod, $data = [], $method = 'GET', $object = null)
{
if (!$object) {
$object = $this->object;
}
$tokenData = $this->integration->getKeys();
if ('6' == $tokenData['version']) {
$request_url = sprintf('%s/service/v4_1/rest.php', $tokenData['sugarcrm_url']);
$sessionParams = [
'session' => $tokenData['id'],
];
if (!isset($data['module_names'])) {
$sessionParams['module_name'] = $object;
} // Making sure that module_name is the second value of the array
else {
$sessionParams['module_names'] = $data['module_names'];
}
$sessionParams = array_merge($sessionParams, $data);
$parameters = [
'method' => $sMethod,
'input_type' => 'JSON',
'response_type' => 'JSON',
'rest_data' => json_encode($sessionParams),
];
$response = $this->integration->makeRequest($request_url, $parameters, $method);
if (is_array($response) && !empty($response['name']) && !empty($response['number'])) {
throw new ApiErrorException($response['name'].' '.$object.' '.$sMethod.' '.$method);
} else {
return $response;
}
} else {
$request_url = sprintf('%s/rest/v10/%s', $tokenData['sugarcrm_url'], $sMethod);
$settings = [
'request_timeout' => 50,
'encode_parameters' => 'json',
];
$response = $this->integration->makeRequest($request_url, $data, $method, $settings);
if (isset($response['error'])) {
throw new ApiErrorException($response['error_message'] ?? $response['error']['message'], ('invalid_grant' == $response['error']) ? 1 : 500);
}
return $response;
}
}
/**
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getLeadFields($object = null)
{
if (!$object) {
$object = $this->object;
}
if ('company' == $object) {
$object = 'Accounts'; // sugarCRM object name
} elseif ('lead' == $object || 'Lead' == $object) {
$object = 'Leads';
} elseif ('contact' == $object || 'Contact' == $object) {
$object = 'Contacts';
}
$tokenData = $this->integration->getKeys();
if ('6' == $tokenData['version']) {
return $this->request('get_module_fields', [], 'GET', $object);
} else {
$parameters = [
'module_filter' => $object,
'type_filter' => 'modules',
];
$response = $this->request('metadata', $parameters, 'GET', $object);
return $response['modules'][$object];
}
}
/**
* @return array
*
* @throws ApiErrorException
*/
public function createLead(array $fields, $lead)
{
$tokenData = $this->integration->getKeys();
$createdLeadData = [];
// search for Sugar id in mautic records first to avoid making an API call
if (is_object($lead)) {
$sugarLeadRecords = $this->integration->getSugarLeadId($lead);
}
if ('6' == $tokenData['version']) {
// if not found then go ahead and make an API call to find all the records with that email
if (isset($fields['email1']) && empty($sugarLeadRecords)) {
$sLeads = $this->getLeads(['email' => $fields['email1'], 'offset' => 0, 'max_results' => 1000], 'Leads');
$sugarLeadRecords = $sLeads['entry_list'] ?? [];
}
$leadFields = [];
foreach ($fields as $name => $value) {
if ('id' != $name) {
$leadFields[] = [
'name' => $name,
'value' => $value,
];
}
}
$parameters = [
'name_value_list' => $leadFields,
];
if (!empty($sugarLeadRecords)) {
foreach ($sugarLeadRecords as $sLeadRecord) {
$localParam = $parameters;
$sugarLeadId = ($sLeadRecord['integration_entity_id'] ?? $sLeadRecord['id']);
$sugarObject = ($sLeadRecord['integration_entity'] ?? 'Leads');
// update the converted contact if found and not the Lead
if (isset($sLeadRecord['contact_id']) && null != $sLeadRecord['contact_id'] && '' != $sLeadRecord['contact_id']) {
unset($fields['Company']); // because this record is not in the Contact object.
$localParams['name_value_list'][] = ['name' => 'id', 'value' => $sLeadRecord['contact_id']];
$createdLeadData[] = $this->request('set_entry', $localParams, 'POST', 'Contacts');
} else {
$localParams['name_value_list'][] = ['name' => 'id', 'value' => $sugarLeadId];
$createdLeadData[] = $this->request('set_entry', $localParams, 'POST', $sugarObject);
}
}
} else {
$createdLeadData = $this->request('set_entry', $parameters, 'POST', 'Leads');
}
// $createdLeadData[] = $this->request('set_entry', $parameters, 'POST');
} else {
// if not found then go ahead and make an API call to find all the records with that email
if (isset($fields['email1']) && empty($sugarLeadRecords)) {
$sLeads = $this->getLeads(['email' => $fields['email1'], 'offset' => 0, 'max_results' => 1000], 'Leads');
$sugarLeadRecords = $sLeads['records'];
}
unset($fields['id']);
if (!empty($sugarLeadRecords)) {
foreach ($sugarLeadRecords as $sLeadRecord) {
$sugarLeadId = ($sLeadRecord['integration_entity_id'] ?? $sLeadRecord['id']);
$sugarObject = ($sLeadRecord['integration_entity'] ?? 'Leads');
// update the converted contact if found and not the Lead
$config = $this->integration->mergeConfigToFeatureSettings();
$fieldsToUpdateInSugar = isset($config['update_mautic']) ? array_keys($config['update_mautic'], 1) : [];
if (isset($sLeadRecord['contact_id']) && null != $sLeadRecord['contact_id'] && '' != $sLeadRecord['contact_id']) {
unset($fields['Company']); // because this record is not in the Contact object
$fieldsToUpdateInContactsSugar = $this->integration->cleanSugarData($config, $fieldsToUpdateInSugar, 'Contacts');
$contactSugarFields = array_diff_key($fields, $fieldsToUpdateInContactsSugar);
$createdLeadData[] = $this->request("Contacts/$sugarLeadId", $contactSugarFields, 'PUT', 'Contacts');
} else {
$fieldsToUpdateInLeadsSugar = $this->integration->cleanSugarData($config, $fieldsToUpdateInSugar, 'Leads');
$leadSugarFields = array_diff_key($fields, $fieldsToUpdateInLeadsSugar);
$createdLeadData[] = $this->request("$sugarObject/$sugarLeadId", $leadSugarFields, 'PUT', $sugarObject);
}
}
} else {
$createdLeadData = $this->request('Leads', $fields, 'POST', 'Leads');
}
// $createdLeadData[] = $this->request('set_entry', $fields, 'POST', 'Leads');
}
return $createdLeadData;
}
/**
* @return array
*
* @throws ApiErrorException
*/
public function syncLeadsToSugar(array $data)
{
$tokenData = $this->integration->getKeys();
$object = $this->object;
if ('6' == $tokenData['version']) {
$leadFieldsList = [];
$response = [];
foreach ($data as $object => $leadFieldsList) {
$parameters = [
'name_value_lists' => $leadFieldsList,
];
$resp = $this->request('set_entries', $parameters, 'POST', $object);
if (!empty($resp)) {
foreach ($leadFieldsList as $k => $leadFields) {
$fields = [];
foreach ($leadFields as $item) {
$fields[$item['name']] = $item['value'];
}
if (isset($resp['ids'])) {
$result = ['reference_id' => $fields['reference_id'],
'id' => $resp['ids'][$k],
'new' => !isset($fields['id']),
'ko' => false, ];
}
if (isset($resp['error'])) {
$result['ko'] = true;
$result['error'] = $resp['error']['message'];
}
if (isset($fields['id']) && $fields['id'] != $resp['ids'][$k]) {
$result['ko'] = true;
$result['error'] = 'Returned ID does not correspond to input id';
}
$response[] = $result;
}
}
}
return $response;
} else {
$leadFieldsList = [];
$response = [];
// body is prepared for Sugar6. Translate it to sugar 7
$reference_ids = [];
foreach ($data as $object => $leadFieldsList) {
$requests = [];
$all_ids = [];
foreach ($leadFieldsList as $body) {
$fields = [];
$ids = [];
foreach ($body as $field) {
$fields[$field['name']] = $field['value'];
}
$request = [];
if (isset($fields['id'])) {
$ids['id'] = $fields['id'];
// Update record
$sugarLeadId = $fields['id'];
unset($fields['id']);
$request['method'] = 'PUT';
$request['url'] = "/v10/$object/$sugarLeadId";
$request['data'] = $fields;
} else {
// Create record
$request['data'] = $fields;
$request['url'] = '/v10/'.$object;
$request['method'] = 'POST';
}
$requests[] = $request;
$ids['reference_id'] = $fields['reference_id'];
$all_ids[] = $ids;
}
$parameters = [
'requests' => $requests,
];
$resp = $this->request('bulk', $parameters, 'POST', $object);
if (!empty($resp)) {
foreach ($resp as $k => $leadFields) {
$fields = $leadFields['contents'];
if (200 != $leadFields['status']) {
$result = ['ko' => true,
'error' => $leadFields['error'].' '.$leadFields['error_message'], ];
} else {
$result = ['reference_id' => $all_ids[$k]['reference_id'],
'id' => $fields['id'],
'new' => !isset($all_ids[$k]['id']),
'ko' => false, ];
if (isset($all_ids[$k]['id']) && $fields['id'] != $all_ids[$k]['id']) {
$result['ko'] = true;
$result['error'] = 'Returned ID does not correspond to input id';
}
}
$response[] = $result;
}
}
}
return $response;
}
}
/**
* @param $object
* TODO 7.x
*
* @return array|mixed|string
*/
public function createLeadActivity(array $activity, $object)
{
$tokenData = $this->integration->getKeys();
// 1st : set_entries to return name_value_lists (array of arrays of name/value)
$set_name_value_lists = [];
// set relationship
$module_names = []; // Contacts or Leads
$module_ids = []; // Contacts or leads ids
$link_field_names = []; // Array of mtc_webactivities_contacts or mtc_webactivities_leads
$related_ids = []; // Array of arrays of web activity array
$name_value_lists = []; // array of empty arrays
$delete_array = []; // Array of 0
// set_relationships
$s7_records = [];
// Send activities and get back sugar activities id
if (!empty($activity)) {
foreach ($activity as $sugarId => $records) {
foreach ($records['records'] as $record) {
$rec = [];
$rec[] = ['name' => 'name', 'value' => $record['name']];
$rec[] = ['name' => 'description', 'value' => $record['description']];
$rec[] = ['name' => 'url', 'value' => $records['leadUrl']];
$rec[] = ['name' => 'date_entered', 'value' => $record['dateAdded']->format('c')];
$rec[] = ['name' => 'reference_id', 'value' => $record['id'].'-'.$sugarId];
if ('Contacts' == $object) {
$rec[] = ['name' => 'contact_id_c', 'value' => $sugarId];
} else {
$rec[] = ['name' => 'lead_id_c', 'value' => $sugarId];
}
$set_name_value_lists[] = $rec; // Sugar 6
$s7_record = [];
foreach ($rec as $r) {
$s7_record[$r['name']] = $r['value'];
}
$s7_records[] = $s7_record;
}
}
$parameters = [
'name_value_lists' => $set_name_value_lists,
];
if ('6' == $tokenData['version']) {
$resp = $this->request('set_entries', $parameters, 'POST', 'mtc_WebActivities');
} else {
$requests = [];
foreach ($s7_records as $fields) {
// Create record
$request['data'] = $fields;
$request['url'] = '/v10/mtc_WebActivities';
$request['method'] = 'POST';
$requests[] = $request;
}
$parameters = [
'requests' => $requests,
];
$resp = $this->request('bulk', $parameters, 'POST', 'bulk');
}
if ('6' == $tokenData['version']) {
// Send sugar relationsips
if (!empty($resp)) {
$nbLeads = 0;
$nbAct = 0;
$idList = [];
foreach ($activity as $sugarId => $records) {
$related_ids_row = [];
$module_names[] = $object;
$module_ids[] = $sugarId;
if ('Contacts' == $object) {
$link_field_names[] = 'mtc_webactivities_contacts';
} else {
$link_field_names[] = 'mtc_webactivities_leads';
}
++$nbLeads;
foreach ($records['records'] as $record) {
$name_value_lists[] = [];
$delete_array[] = 0;
$idList[] = $sugarId;
$related_ids_row[] = $resp['ids'][$nbAct];
++$nbAct;
}
$related_ids[] = $related_ids_row;
}
$parameters = [
'module_names' => $module_names, // Contacts or Leads
'module_ids' => $module_ids, // Contacts or leads ids
'link_field_names' => $link_field_names, // Array of mtc_webactivities_contacts or mtc_webactivities_leads
'related_ids' => $related_ids, // Array of arrays of web activity array
'name_value_lists' => $name_value_lists, // array of empty arrays
'delete_array' => $delete_array, // Array of 0
];
$resp2 = $this->request('set_relationships', $parameters, 'POST', $object);
}
} else {
// Sugar 7 set relationship
if (!empty($resp)) {
$nbAct = 0;
foreach ($activity as $sugarId => $records) {
if ('Contacts' == $object) {
$link_field_name = 'mtc_webactivities_contacts';
} else {
$link_field_name = 'mtc_webactivities_leads';
}
foreach ($records['records'] as $record) {
if (!isset($resp[$nbAct]['contents']['id'])) {
continue;
} // current Web activity was not created
$wa_id = $resp[$nbAct]['contents']['id'];
$resp2 = $this->request("mtc_WebActivities/$wa_id/link/$link_field_name/$sugarId", [], 'POST');
++$nbAct;
}
}
}
}
return [];
}
}
public function getEmailBySugarUserId($query = null)
{
$tokenData = $this->integration->getKeys();
if ('6' == $tokenData['version']) {
if (isset($query['emails'])) {
$q = " users.id IN (SELECT bean_id FROM email_addr_bean_rel eabr JOIN email_addresses ea ON (eabr.email_address_id = ea.id) WHERE bean_module = 'Users' AND ea.email_address IN ('".implode("','", $query['emails'])."') AND eabr.deleted=0) ";
}
if (isset($query['ids'])) {
$q = " users.id IN ('".implode("','", $query['ids'])."') ";
}
$data = ['filter' => 'all'];
$fields = ['id', 'email1'];
$parameters = [
'query' => $q,
'order_by' => '',
'offset' => 0,
'select_fields' => $fields,
'link_name_to_fields_array' => [/* TO BE MODIFIED */
],
'max_results' => 1000,
'deleted' => 0,
'favorites' => false,
];
$data = $this->request('get_entry_list', $parameters, 'GET', 'Users');
if (isset($query['type']) && 'BYEMAIL' == $query['type']) {
$type = 'BYEMAIL';
} else {
$type = 'BYID';
}
$res = [];
if (isset($data['entry_list'])) {
foreach ($data['entry_list'] as $record) {
$fields = [];
$fields['id'] = $record['id'];
foreach ($record['name_value_list'] as $item) {
$fields[$item['name']] = $item['value'];
}
if ('BYID' == $type) {
$res[$fields['id']] = $fields['email1'];
} elseif (isset($fields['email1'])) {
$res[$fields['email1']] = $fields['id'];
} elseif (!isset($fields['email1'])) {
$res[$query['emails'][0]] = $fields['id'];
}
}
}
return $res;
} else {
// TODO
if (isset($query['emails'])) {
$filter[] = ['email_addresses.email_address' => ['$in' => $query['emails']]];
$filter[] = ['deleted' => '0'];
}
if (isset($query['ids'])) {
$filter[] = ['id' => ['$in' => $query['ids']]];
}
$data = ['filter' => 'all'];
$fields = ['id', 'email1', 'email'];
$parameters = [
'filter' => [['$and' => $filter]],
'offset' => 0,
'fields' => implode(',', $fields),
'max_num' => 1000,
// 'deleted' => 0,
// 'favorites' => false,
];
$data = $this->request('Users/filter', $parameters, 'GET', 'Users');
if (isset($query['type']) && 'BYEMAIL' == $query['type']) {
$type = 'BYEMAIL';
} else {
$type = 'BYID';
}
$res = [];
if (isset($data['records'])) {
foreach ($data['records'] as $record) {
if (isset($record['email'][0]['email_address']) && '' != $record['email'][0]['email_address']) {
$found_email = $record['email'][0]['email_address'];
if (isset($record['name_value_list'])) {
foreach ($record['name_value_list'] as $email) {
if ('' != $email['email_address'] && 1 == $email['primary_address']) {
$found_email = $email;
break;
}
}
}
if ('BYID' == $type) {
$res[$record['id']] = $found_email;
} else {
$res[$found_email] = $record['id'];
}
}
}
}
return $res;
}
}
public function getIdBySugarEmail($query = null)
{
if (null == $query) {
$query = ['type' => 'BYEMAIL'];
} else {
$query['type'] = 'BYEMAIL';
}
return $this->getEmailBySugarUserId($query);
}
/**
* Get SugarCRM leads.
*
* @param array $query
* @param string $object
*
* @return mixed
*/
public function getLeads($query, $object)
{
$tokenData = $this->integration->getKeys();
$availableFields = $this->integration->getIntegrationSettings()->getFeatureSettings();
switch ($object) {
case 'company':
case 'Account':
case 'Accounts':
$fields = array_keys(array_filter($availableFields['companyFields']));
break;
default:
$mixedFields = array_filter($availableFields['leadFields']);
$fields = [];
$object = ('Contacts' == $object) ? 'Contacts' : 'Leads';
foreach ($mixedFields as $sugarField => $mField) {
if (str_contains($sugarField, '__'.$object)) {
$fields[] = str_replace('__'.$object, '', $sugarField);
}
if (str_contains($sugarField, '-'.$object)) {
$fields[] = str_replace('-'.$object, '', $sugarField);
}
}
}
if ('6' == $tokenData['version']) {
$result = [];
if (!empty($fields)) {
$q = '';
$qry = [];
if (isset($query['start'])) {
$qry[] = ' '.strtolower($object).".date_modified >= '".$query['start']."' ";
}
if (isset($query['end'])) {
$qry[] = ' '.strtolower($object).".date_modified <= '".$query['end']."' ";
}
if (isset($query['email'])) {
$qry[] = " leads.id IN (SELECT bean_id FROM email_addr_bean_rel eabr JOIN email_addresses ea ON (eabr.email_address_id = ea.id) WHERE bean_module = 'Leads' AND ea.email_address = '".$query['email']."' AND eabr.deleted=0) ";
$fields[] = 'contact_id';
}
if (isset($query['checkemail'])) {
$qry[] = ' leads.deleted=0 ';
$qry[] = " leads.id IN (SELECT bean_id FROM email_addr_bean_rel eabr JOIN email_addresses ea ON (eabr.email_address_id = ea.id) WHERE bean_module = 'Leads' AND ea.email_address IN ('".implode("','", $query['checkemail'])."') AND eabr.deleted=0) ";
$fields[] = 'contact_id';
$fields[] = 'deleted';
}
if (isset($query['checkemail_contacts'])) {
$qry[] = ' contacts.deleted=0 ';
$qry[] = " contacts.id IN (SELECT bean_id FROM email_addr_bean_rel eabr JOIN email_addresses ea ON (eabr.email_address_id = ea.id) WHERE bean_module = 'Contacts' AND ea.email_address IN ('".implode("','", $query['checkemail_contacts'])."') AND eabr.deleted=0) ";
$fields[] = 'deleted';
}
$q = implode('AND', $qry);
$fields[] = 'id';
$fields[] = 'date_modified';
$fields[] = 'date_entered';
$fields[] = 'assigned_user_id';
$fields[] = 'email1';
if ('Accounts' != $object) {
$fields[] = 'account_id';
}
$parameters = [
'query' => $q,
'order_by' => '',
'offset' => $query['offset'],
'select_fields' => $fields,
'link_name_to_fields_array' => [/* TO BE MODIFIED */
[
'name' => 'email_addresses',
'value' => [
'email_address',
'opt_out',
'primary_address',
],
],
],
'max_results' => $query['max_results'],
'deleted' => 0,
'favorites' => false,
];
return $this->request('get_entry_list', $parameters, 'GET', $object);
}
} else {
if (!empty($fields)) {
$q = '';
$qry = [];
$filter = [];
if (isset($query['start'])) {
$filter[] = ['date_modified' => ['$gte' => $query['start']]];
// $qry[] = ' '.strtolower($object).".date_modified >= '".$query['start']."' ";
}
if (isset($query['end'])) {
$filter[] = ['date_modified' => ['$lte' => $query['end']]];
// $qry[] = ' '.strtolower($object).".date_modified <= '".$query['end']."' ";
}
if (isset($query['email'])) {
$filter[] = ['email' => ['$equals' => $query['email']]];
// $qry[] = " leads.id IN (SELECT bean_id FROM email_addr_bean_rel eabr JOIN email_addresses ea ON (eabr.email_address_id = ea.id) WHERE bean_module = 'Leads' AND ea.email_address = '".$query['email']."' AND eabr.deleted=0) ";
$fields[] = 'contact_id';
}
if (isset($query['checkemail'])) {
$filter[] = ['email' => ['$in' => $query['checkemail']]];
$filter[] = ['deleted' => '0'];
$fields = []; // Do not need previous fields
$fields[] = 'contact_id';
$fields[] = 'deleted';
}
if (isset($query['checkemail_contacts'])) {
$filter[] = ['email' => ['$in' => $query['checkemail_contacts']]];
$filter[] = ['deleted' => '0'];
$fields = []; // Do not need previous fields
$fields[] = 'deleted';
}
$fields[] = 'id';
$fields[] = 'date_modified';
$fields[] = 'date_entered';
$fields[] = 'assigned_user_id';
$fields[] = 'email1';
if ('Accounts' != $object) {
$fields[] = 'account_id';
}
// $filter_args = ['filter' => [['$and' => $filter]]];
// $fields_arg = implode(',', $fields);
$parameters = [
// 'order_by' => '',
'filter' => [['$and' => $filter]],
'offset' => $query['offset'],
'fields' => implode(',', $fields),
'max_num' => $query['max_results'],
// 'deleted' => 0,
// 'favorites' => false,
];
return $this->request("$object/filter", $parameters, 'GET', $object);
}
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api;
use Mautic\PluginBundle\Exception\ApiErrorException;
class VtigerApi extends CrmApi
{
protected $element = 'Leads';
protected function request($operation, $element, $elementData = [], $method = 'GET')
{
$tokenData = $this->integration->getKeys();
$request_url = $this->integration->getApiUrl();
$parameters = [
'operation' => $operation,
'sessionName' => $tokenData['sessionName'],
'elementType' => $element,
];
if (!empty($elementData)) {
$parameters['element'] = json_encode($elementData);
}
$response = $this->integration->makeRequest($request_url, $parameters, $method);
if (!empty($response['error'])) {
$error = $response['error']['message'];
throw new ApiErrorException($error);
}
return $response['result'];
}
/**
* List types.
*
* @return mixed
*/
public function listTypes()
{
return $this->request('listtypes', $this->element);
}
/**
* List leads.
*
* @return mixed
*/
public function getLeadFields($object)
{
if ('company' === $object) {
$object = 'Accounts';
} else {
$object = $this->element;
}
return $this->request('describe', $object);
}
/**
* @return mixed
*/
public function createLead(array $data)
{
return $this->request('create', $this->element, $data, 'POST');
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api\Zoho\Exception;
class MatchingKeyNotFoundException extends \Exception
{
}

View File

@@ -0,0 +1,130 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api\Zoho;
use MauticPlugin\MauticCrmBundle\Api\Zoho\Exception\MatchingKeyNotFoundException;
class Mapper
{
private array $contact = [];
private array $mappedFields = [];
private $object;
/**
* @var array[]
*/
private array $objectMappedValues = [];
/**
* Used to keep track of the key used to map contact ID with the response Zoho returns.
*/
private int $objectCounter = 0;
/**
* Used to map contact ID with the response Zoho returns.
*/
private array $contactMapper = [];
public function __construct(
private array $fields,
) {
}
/**
* @return $this
*/
public function setObject($object)
{
$this->object = $object;
return $this;
}
/**
* @return $this
*/
public function setContact(array $contact)
{
$this->contact = $contact;
return $this;
}
/**
* @return $this
*/
public function setMappedFields(array $fields)
{
$this->mappedFields = $fields;
return $this;
}
/**
* @param int $mauticContactId Mautic Contact ID
* @param int|null $zohoId Zoho ID if known
*
* @return int If any single field is mapped, return 1 to count as one contact to be updated
*/
public function map($mauticContactId, $zohoId = null): int
{
$mapped = 0;
$objectMappedValues = [];
foreach ($this->mappedFields as $zohoField => $mauticField) {
$field = $this->getField($zohoField);
if ($field && isset($this->contact[$mauticField]) && $this->contact[$mauticField]) {
$mapped = 1;
$apiField = $field['api_name'];
$apiValue = $this->contact[$mauticField];
$objectMappedValues[$apiField] = $apiValue;
}
if ($zohoId) {
$objectMappedValues['id'] = $zohoId;
}
}
$this->objectMappedValues[$this->objectCounter] = $objectMappedValues;
$this->contactMapper[$this->objectCounter] = $mauticContactId;
++$this->objectCounter;
return $mapped;
}
/**
* @return array
*/
public function getArray()
{
return $this->objectMappedValues;
}
/**
* @param int $key
*
* @return int
*
* @throws MatchingKeyNotFoundException
*/
public function getContactIdByKey($key)
{
if (isset($this->contactMapper[$key])) {
return $this->contactMapper[$key];
}
throw new MatchingKeyNotFoundException();
}
/**
* @return mixed
*/
private function getField($fieldName)
{
return $this->fields[$this->object][$fieldName] ?? null;
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace MauticPlugin\MauticCrmBundle\Api;
use Mautic\PluginBundle\Exception\ApiErrorException;
class ZohoApi extends CrmApi
{
/**
* @param string $operation
* @param string $method
* @param bool $json
* @param array $settings
*
* @return array
*
* @throws ApiErrorException
*/
protected function request($operation, array $parameters = [], $method = 'GET', $json = false, $settings = [])
{
$tokenData = $this->integration->getKeys();
$url = sprintf('%s/%s', $tokenData['api_domain'].'/crm/v2', $operation);
if (!isset($settings['headers'])) {
$settings['headers'] = [];
}
$settings['headers']['Authorization'] = 'Zoho-oauthtoken '.$tokenData['access_token'];
if ($json) {
$settings['Content-Type'] = 'application/json';
$settings['encode_parameters'] = 'json';
}
$response = $this->integration->makeRequest($url, $parameters, $method, $settings);
if (isset($response['status']) && 'error' === $response['status']) {
throw new ApiErrorException($response['message']);
}
return $response;
}
/**
* @param string $object
*
* @return array
*
* @throws ApiErrorException
*/
public function getLeadFields($object = 'Leads')
{
if ('company' == $object) {
$object = 'Accounts'; // Zoho object name
}
return $this->request('settings/fields?module='.$object, [], 'GET');
}
/**
* @param string $object
*
* @return array
*
* @throws ApiErrorException
*/
public function createLead(array $data, $object = 'Leads')
{
$parameters['data'] = $data;
return $this->request($object, $parameters, 'POST', true);
}
/**
* @param string $object
*
* @return array
*
* @throws ApiErrorException
*/
public function updateLead(array $data, $object = 'Leads')
{
$parameters['data'] = $data;
return $this->request($object, $parameters, 'PUT', true);
}
/**
* @param string $object
*
* @return array
*
* @throws ApiErrorException
*/
public function getLeads(array $params, $object, $id = null)
{
if (!isset($params['selectColumns'])) {
$params['selectColumns'] = 'All';
$params['newFormat'] = 1;
}
$settings = [];
if ($params['lastModifiedTime']) {
$settings['headers'] = [
'If-Modified-Since' => $params['lastModifiedTime'],
];
}
if ($id) {
if (is_array($id)) {
$params['id'] = implode(';', $id);
} else {
$params['id'] = $id;
}
$data = $this->request($object, $params, 'GET', false, $settings);
} else {
$data = $this->request($object, $params, 'GET', false, $settings);
}
return $data;
}
/**
* @return array
*
* @throws ApiErrorException
*/
public function getCompanies(array $params, $id = null)
{
if (!isset($params['selectColumns'])) {
$params['selectColumns'] = 'All';
}
$settings = [];
if ($params['lastModifiedTime']) {
$settings['headers'] = [
'If-Modified-Since' => $params['lastModifiedTime'],
];
}
if ($id) {
$params['id'] = $id;
$data = $this->request('Accounts', $params, 'GET', false, $settings);
} else {
$data = $this->request('Accounts', $params, 'GET', false, $settings);
}
return $data;
}
/**
* @param string $searchColumn
* @param string $searchValue
* @param string $object
*
* @return mixed|string
*
* @throws ApiErrorException
*/
public function getSearchRecords($searchColumn, $searchValue, $object = 'Leads')
{
$parameters = [
'criteria' => '('.$searchColumn.':equals:'.$searchValue.')',
];
return $this->request($object.'/search', $parameters, 'GET', false);
}
}