Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Api;
|
||||
|
||||
use MauticPlugin\MauticCrmBundle\Integration\CrmAbstractIntegration;
|
||||
|
||||
/**
|
||||
* @method createLead()
|
||||
*/
|
||||
class CrmApi
|
||||
{
|
||||
public function __construct(
|
||||
protected CrmAbstractIntegration $integration,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Api\Salesforce\Exception;
|
||||
|
||||
class RetryRequestException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Api\Zoho\Exception;
|
||||
|
||||
class MatchingKeyNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user