Initial commit: CloudOps infrastructure platform
@@ -0,0 +1,26 @@
|
||||
# Workflow name:
|
||||
name: Close Pull Requests
|
||||
|
||||
# Workflow triggers:
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
# Workflow jobs:
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: superbrothers/close-pull-request@v3
|
||||
with:
|
||||
comment: |
|
||||
Thank you for submitting a pull request. :raised_hands:
|
||||
|
||||
We greatly appreciate your willingness to submit a contribution. However, we are not accepting pull requests against this repository, as all development happens on the [main project repository](https://github.com/mautic/mautic).
|
||||
|
||||
We kindly request that you submit this pull request against the [respective directory](https://github.com/mautic/mautic/blob/head/plugins/MauticCrmBundle) of the main repository where we'll review and provide feedback. If this is your first Mautic contribution, be sure to read the [contributing guide](https://github.com/mautic/mautic/blob/4.x/.github/CONTRIBUTING.md) which provides guidelines and instructions for submitting contributions.
|
||||
|
||||
Thank you again, and we look forward to receiving your contribution! :smiley:
|
||||
|
||||
Best,
|
||||
The Mautic team
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'CRM',
|
||||
'description' => 'Enables integration with Mautic supported CRMs.',
|
||||
'version' => '1.0',
|
||||
'author' => 'Mautic',
|
||||
'routes' => [
|
||||
'public' => [
|
||||
'mautic_integration_contacts' => [
|
||||
'path' => '/plugin/{integration}/contact_data',
|
||||
'controller' => 'MauticPlugin\MauticCrmBundle\Controller\PublicController::contactDataAction',
|
||||
'requirements' => [
|
||||
'integration' => '.+',
|
||||
],
|
||||
],
|
||||
'mautic_integration_companies' => [
|
||||
'path' => '/plugin/{integration}/company_data',
|
||||
'controller' => 'MauticPlugin\MauticCrmBundle\Controller\PublicController::companyDataAction',
|
||||
'requirements' => [
|
||||
'integration' => '.+',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'services' => [
|
||||
'other' => [
|
||||
'mautic_integration.service.transport' => [
|
||||
'class' => MauticPlugin\MauticCrmBundle\Services\Transport::class,
|
||||
'arguments' => [
|
||||
'mautic.http.client',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
'Api',
|
||||
'Integration/Salesforce',
|
||||
];
|
||||
|
||||
$services->load('MauticPlugin\\MauticCrmBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->alias('mautic.integration.hubspot', MauticPlugin\MauticCrmBundle\Integration\HubspotIntegration::class);
|
||||
$services->alias('mautic.integration.salesforce', MauticPlugin\MauticCrmBundle\Integration\SalesforceIntegration::class);
|
||||
$services->alias('mautic.integration.sugarcrm', MauticPlugin\MauticCrmBundle\Integration\SugarcrmIntegration::class);
|
||||
$services->alias('mautic.integration.vtiger', MauticPlugin\MauticCrmBundle\Integration\VtigerIntegration::class);
|
||||
$services->alias('mautic.integration.zoho', MauticPlugin\MauticCrmBundle\Integration\ZohoIntegration::class);
|
||||
$services->alias('mautic.integration.dynamics', MauticPlugin\MauticCrmBundle\Integration\DynamicsIntegration::class);
|
||||
$services->alias('mautic.integration.connectwise', MauticPlugin\MauticCrmBundle\Integration\ConnectwiseIntegration::class);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\HubspotIntegration;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PublicController extends CommonController
|
||||
{
|
||||
public function contactDataAction(Request $request, LoggerInterface $mauticLogger, IntegrationHelper $integrationHelper): Response
|
||||
{
|
||||
$content = $request->getContent();
|
||||
if (!empty($content)) {
|
||||
$data = json_decode($content, true); // 2nd param to get as array
|
||||
} else {
|
||||
return new Response('ERROR');
|
||||
}
|
||||
|
||||
$integration = 'Hubspot';
|
||||
|
||||
$integrationObject = $integrationHelper->getIntegrationObject($integration);
|
||||
\assert($integrationObject instanceof HubspotIntegration);
|
||||
|
||||
foreach ($data as $info) {
|
||||
$object = explode('.', $info['subscriptionType']);
|
||||
$id = $info['objectId'];
|
||||
|
||||
try {
|
||||
switch ($object[0]) {
|
||||
case 'contact':
|
||||
$executed = [];
|
||||
$integrationObject->getLeads($id, null, $executed);
|
||||
break;
|
||||
case 'company':
|
||||
$integrationObject->getCompanies($id);
|
||||
break;
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$mauticLogger->log('error', 'ERROR on Hubspot webhook: '.$ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('OK');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticCrmExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Event\LeadListFiltersChoicesEvent;
|
||||
use Mautic\LeadBundle\Event\ListPreProcessListEvent;
|
||||
use Mautic\LeadBundle\Helper\FormFieldHelper;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\CrmAbstractIntegration;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class LeadListSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private IntegrationHelper $helper,
|
||||
private ListModel $listModel,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LeadEvents::LIST_FILTERS_CHOICES_ON_GENERATE => ['onFilterChoiceFieldsGenerate', 0],
|
||||
LeadEvents::LIST_PRE_PROCESS_LIST => ['onLeadListProcessList', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onFilterChoiceFieldsGenerate(LeadListFiltersChoicesEvent $event): void
|
||||
{
|
||||
$services = $this->helper->getIntegrationObjects();
|
||||
$choices = [];
|
||||
|
||||
/** @var CrmAbstractIntegration $integration */
|
||||
foreach ($services as $integration) {
|
||||
if (!$integration || !$integration->getIntegrationSettings()->isPublished()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (method_exists($integration, 'getCampaigns')) {
|
||||
$integrationChoices = $integration->getCampaignChoices();
|
||||
if ($integrationChoices) {
|
||||
$integrationName = $integration->getName();
|
||||
// Keep BC with pre-2.11.0 that only supported SF campaigns
|
||||
if ('Salesforce' !== $integrationName) {
|
||||
array_walk(
|
||||
$integrationChoices,
|
||||
function (&$choice) use ($integrationName): void {
|
||||
$choice['value'] = $integrationName.'::'.$choice['value'];
|
||||
}
|
||||
);
|
||||
}
|
||||
$integrationChoices = FormFieldHelper::parseListForChoices($integrationChoices);
|
||||
$choices[$integration->getDisplayName()] = $integrationChoices;
|
||||
$choices[$integration->getDisplayName()] = array_combine(
|
||||
array_column($integrationChoices, 'label'),
|
||||
array_column($integrationChoices, 'value')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($choices)) {
|
||||
$config = [
|
||||
'label' => $this->translator->trans('mautic.plugin.integration.campaign_members'),
|
||||
'properties' => ['type' => 'select', 'list' => $choices],
|
||||
'operators' => $this->listModel->getOperatorsForFieldType(
|
||||
[
|
||||
'include' => [
|
||||
'=',
|
||||
],
|
||||
]
|
||||
),
|
||||
'object' => 'lead',
|
||||
];
|
||||
$event->addChoice('lead', 'integration_campaigns', $config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/remove contacts to a segment based on contacts found in Integration Campaigns.
|
||||
*
|
||||
* @param ListChangeEvent $event
|
||||
*/
|
||||
public function onLeadListProcessList(ListPreProcessListEvent $event)
|
||||
{
|
||||
// get Integration Campaign members
|
||||
$list = $event->getList();
|
||||
$success = false;
|
||||
$filters = ($list instanceof LeadList) ? $list->getFilters() : $list['filters'];
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if ('integration_campaigns' == $filter['field']) {
|
||||
if (str_contains($filter['filter'], '::')) {
|
||||
[$integrationName, $campaignId] = explode('::', $filter['filter']);
|
||||
} else {
|
||||
// Assuming this is a Salesforce integration for BC with pre 2.11.0
|
||||
$integrationName = 'Salesforce';
|
||||
$campaignId = $filter['filter'];
|
||||
}
|
||||
|
||||
/** @var CrmAbstractIntegration $integrationObject */
|
||||
if ($integrationObject = $this->helper->getIntegrationObject($integrationName)) {
|
||||
if (!$integrationObject->getIntegrationSettings()->isPublished()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (method_exists($integrationObject, 'getCampaignMembers')) {
|
||||
if ($integrationObject->getCampaignMembers($campaignId)) {
|
||||
$success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $event->setResult($success);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Mautic\PluginBundle\Bundle\PluginDatabase;
|
||||
use Mautic\PluginBundle\Event\PluginInstallEvent;
|
||||
use Mautic\PluginBundle\PluginEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class PluginSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager, private PluginDatabase $pluginDatabase)
|
||||
{
|
||||
}
|
||||
|
||||
public function onInstall(PluginInstallEvent $event): void
|
||||
{
|
||||
$eventMetadata = $event->getMetadata();
|
||||
|
||||
if (null === $eventMetadata) {
|
||||
$metadata = self::getMetadata($this->entityManager);
|
||||
} else {
|
||||
$metadata = [];
|
||||
foreach ($eventMetadata as $class => $classMetadata) {
|
||||
if (!str_contains($classMetadata->namespace, 'MauticPlugin\\MauticCrmBundle')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$metadata[$class] = $classMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($metadata) > 0) {
|
||||
$this->pluginDatabase->installPluginSchema(
|
||||
$metadata,
|
||||
$event->getInstalledSchema()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PluginEvents::ON_PLUGIN_INSTALL => ['onInstall', 100],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix: plugin installer doesn't find metadata entities for the plugin
|
||||
* PluginBundle/Controller/PluginController:410.
|
||||
*
|
||||
* @return array<class-string, ClassMetadata>
|
||||
*/
|
||||
private static function getMetadata(EntityManagerInterface $em): array
|
||||
{
|
||||
$allMetadata = $em->getMetadataFactory()->getAllMetadata();
|
||||
$currentSchema = $em->getConnection()->createSchemaManager()->introspectSchema();
|
||||
|
||||
$classes = [];
|
||||
|
||||
/** @var ClassMetadata $meta */
|
||||
foreach ($allMetadata as $meta) {
|
||||
if (!str_contains($meta->namespace, 'MauticPlugin\\MauticCrmBundle')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$table = $meta->getTableName();
|
||||
|
||||
if ($currentSchema->hasTable($table)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classes[$meta->namespace] = $meta;
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Form\Type;
|
||||
|
||||
use MauticPlugin\MauticCrmBundle\Integration\ConnectwiseIntegration;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class IntegrationCampaignsTaskType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private ConnectwiseIntegration $connectwiseIntegration,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface<array<mixed>|null> $builder
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'activity_name',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.connectwise.activity.name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'constraints' => [
|
||||
new Callback(
|
||||
function ($validateMe, ExecutionContextInterface $context): void {
|
||||
$data = $context->getRoot()->getData();
|
||||
if (!empty($data['properties']['config']['push_activities']) && empty($validateMe)) {
|
||||
$context->buildViolation('mautic.core.value.required')->addViolation();
|
||||
}
|
||||
}
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'campaign_activity_type',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'choices' => array_flip($this->connectwiseIntegration->getActivityTypes()), // Choice type expects labels as keys
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'label' => 'mautic.plugin.integration.campaigns.connectwise.activity.type',
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'campaign_members',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'choices' => array_flip($this->connectwiseIntegration->getMembers()), // Choice type expects labels as keys
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'label' => 'mautic.plugin.integration.campaigns.connectwise.members',
|
||||
'constraints' => [
|
||||
new Callback(
|
||||
function ($validateMe, ExecutionContextInterface $context): void {
|
||||
$data = $context->getRoot()->getData();
|
||||
if (!empty($data['properties']['config']['push_activities']) && empty($validateMe)) {
|
||||
$context->buildViolation('mautic.core.value.required')->addViolation();
|
||||
}
|
||||
}
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'integration_campaign_task';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,990 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PluginBundle\Entity\IntegrationEntity;
|
||||
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
|
||||
use Mautic\PluginBundle\Exception\ApiErrorException;
|
||||
use Mautic\PluginBundle\Integration\IntegrationObject;
|
||||
use MauticPlugin\MauticCrmBundle\Form\Type\IntegrationCampaignsTaskType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilder;
|
||||
|
||||
/**
|
||||
* @method \MauticPlugin\MauticCrmBundle\Api\ConnectwiseApi getApiHelper()
|
||||
*/
|
||||
class ConnectwiseIntegration extends CrmAbstractIntegration
|
||||
{
|
||||
public const PAGESIZE = 200;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Connectwise';
|
||||
}
|
||||
|
||||
public function getSupportedFeatures(): array
|
||||
{
|
||||
return ['push_lead', 'get_leads'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRequiredKeyFields(): array
|
||||
{
|
||||
return [
|
||||
'username' => 'mautic.connectwise.form.integrator',
|
||||
'password' => 'mautic.connectwise.form.privatekey',
|
||||
'site' => 'mautic.connectwise.form.site',
|
||||
'appcookie' => 'mautic.connectwise.form.cookie',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array key for application cookie.
|
||||
*/
|
||||
public function getClientId(): string
|
||||
{
|
||||
return 'appcookie';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array key for companyid.
|
||||
*/
|
||||
public function getCompanyIdKey(): string
|
||||
{
|
||||
return 'companyid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array key for client id.
|
||||
*/
|
||||
public function getIntegrator(): string
|
||||
{
|
||||
return 'username';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array key for client id.
|
||||
*/
|
||||
public function getConnectwiseUrl(): string
|
||||
{
|
||||
return 'site';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array key for client secret.
|
||||
*/
|
||||
public function getClientSecretKey(): string
|
||||
{
|
||||
return 'password';
|
||||
}
|
||||
|
||||
public function getSecretKeys(): array
|
||||
{
|
||||
return [
|
||||
'password', 'appcookie',
|
||||
];
|
||||
}
|
||||
|
||||
public function getApiUrl(): string
|
||||
{
|
||||
return sprintf('%s/v4_6_release/apis/3.0', $this->keys['site']);
|
||||
}
|
||||
|
||||
public function getAuthLoginUrl(): string
|
||||
{
|
||||
return $this->router->generate('mautic_integration_auth_callback', ['integration' => $this->getName()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function authCallback($settings = [], $parameters = [])
|
||||
{
|
||||
$url = $this->getApiUrl();
|
||||
$error = false;
|
||||
try {
|
||||
$response = $this->makeRequest($url.'/system/members/', $parameters, 'GET', $settings);
|
||||
|
||||
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':
|
||||
$error = '404 not found error';
|
||||
break;
|
||||
case 'code':
|
||||
$error = $response['message'].' '.$r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$error) {
|
||||
$data = ['username' => $this->keys['username'], 'password' => $this->keys['password']];
|
||||
$this->extractAuthKeys($data, 'username');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append ClientID into header to enable authentication.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array<mixed> $parameters
|
||||
* @param string $method
|
||||
* @param array<mixed> $settings
|
||||
* @param string $authType
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function prepareRequest($url, $parameters, $method, $settings, $authType): array
|
||||
{
|
||||
[$parameters,$headers] = parent::prepareRequest($url, $parameters, $method, $settings, $authType);
|
||||
|
||||
$headers['clientId'] = $this->keys['appcookie']; // Even though it is called appcookie it is ClientID
|
||||
|
||||
return [$parameters, $headers];
|
||||
}
|
||||
|
||||
public function getAuthenticationType(): string
|
||||
{
|
||||
return 'basic';
|
||||
}
|
||||
|
||||
public function getDataPriority(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available company fields for choices in the config UI.
|
||||
*
|
||||
* @param array $settings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFormCompanyFields($settings = [])
|
||||
{
|
||||
return $this->getFormFieldsByObject('company', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $settings
|
||||
*
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function getFormLeadFields($settings = [])
|
||||
{
|
||||
return $this->getFormFieldsByObject('Contact', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getAvailableLeadFields($settings = []): array
|
||||
{
|
||||
$cwFields = [];
|
||||
if (isset($settings['feature_settings']['objects'])) {
|
||||
$cwObjects = $settings['feature_settings']['objects'];
|
||||
} else {
|
||||
$cwObjects['Contact'] = 'Contact';
|
||||
}
|
||||
if (!$this->isAuthorized()) {
|
||||
return [];
|
||||
}
|
||||
switch ($cwObjects) {
|
||||
case isset($cwObjects['Contact']):
|
||||
$contactFields = $this->getContactFields();
|
||||
|
||||
$cwFields['Contact'] = $this->setFields($contactFields);
|
||||
break;
|
||||
case isset($cwObjects['company']):
|
||||
$company = $this->getCompanyFields();
|
||||
$cwFields['company'] = $this->setFields($company);
|
||||
break;
|
||||
}
|
||||
|
||||
return $cwFields;
|
||||
}
|
||||
|
||||
public function setFields($fields): array
|
||||
{
|
||||
$cwFields = [];
|
||||
|
||||
foreach ($fields as $fieldName => $field) {
|
||||
if (in_array($field['type'], ['string', 'boolean', 'ref'])) {
|
||||
$cwFields[$fieldName] = [
|
||||
'type' => $field['type'],
|
||||
'label' => ucfirst($fieldName),
|
||||
'required' => $field['required'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $cwFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Mautic\PluginBundle\Integration\Form|FormBuilder $builder
|
||||
* @param array $data
|
||||
* @param string $formArea
|
||||
*/
|
||||
public function appendToForm(&$builder, $data, $formArea): void
|
||||
{
|
||||
if ('features' == $formArea) {
|
||||
$builder->add(
|
||||
'updateBlanks',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'choices' => [
|
||||
'mautic.integrations.blanks' => 'updateBlanks',
|
||||
],
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'label' => 'mautic.integrations.form.blanks',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'placeholder' => false,
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
$builder->add(
|
||||
'objects',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'choices' => [
|
||||
'mautic.connectwise.object.contact' => 'Contact',
|
||||
'mautic.connectwise.object.company' => 'company',
|
||||
],
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'label' => 'mautic.connectwise.form.objects_to_pull_from',
|
||||
'label_attr' => ['class' => ''],
|
||||
'placeholder' => false,
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ('integration' == $formArea) {
|
||||
if ($this->isAuthorized()) {
|
||||
$builder->add(
|
||||
'push_activities',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.plugin.config.push.activities',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'data' => (!isset($data['push_activities'])) ? true : $data['push_activities'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'campaign_task',
|
||||
IntegrationCampaignsTaskType::class,
|
||||
[
|
||||
'label' => false,
|
||||
'attr' => [
|
||||
'data-hide-on' => '{"campaignevent_properties_config_push_activities_0":"checked"}',
|
||||
],
|
||||
'data' => $data['campaign_task'] ?? [],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array of company fields for connectwise
|
||||
*/
|
||||
public function getCompanyFields(): array
|
||||
{
|
||||
return [
|
||||
'identifier' => ['type' => 'string', 'required' => true],
|
||||
'name' => ['type' => 'string', 'required' => true],
|
||||
'addressLine1' => ['type' => 'string', 'required' => false],
|
||||
'addressLine2' => ['type' => 'string', 'required' => false],
|
||||
'city' => ['type' => 'string', 'required' => false],
|
||||
'state' => ['type' => 'string', 'required' => false],
|
||||
'zip' => ['type' => 'string', 'required' => false],
|
||||
'phoneNumber' => ['type' => 'string', 'required' => false],
|
||||
'faxNumber' => ['type' => 'string', 'required' => false],
|
||||
'website' => ['type' => 'string', 'required' => false],
|
||||
'territoryId' => ['type' => 'string', 'required' => false],
|
||||
'marketId' => ['type' => 'string', 'required' => false],
|
||||
'accountNumber' => ['type' => 'string', 'required' => false],
|
||||
'dateAcquired' => ['type' => 'string', 'required' => false],
|
||||
'annualRevenue' => ['type' => 'string', 'required' => false],
|
||||
'numberOfEmployees' => ['type' => 'string', 'required' => false],
|
||||
'leadSource' => ['type' => 'string', 'required' => false],
|
||||
'leadFlag' => ['type' => 'boolean', 'required' => false],
|
||||
'unsubscribeFlag' => ['type' => 'boolean', 'required' => false],
|
||||
'calendarId' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField1' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField2' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField3' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField4' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField5' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField6' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField7' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField8' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField9' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField10' => ['type' => 'string', 'required' => false],
|
||||
'vendorIdentifier' => ['type' => 'string', 'required' => false],
|
||||
'taxIdentifier' => ['type' => 'string', 'required' => false],
|
||||
'invoiceToEmailAddress' => ['type' => 'string', 'required' => false],
|
||||
'invoiceCCEmailAddress' => ['type' => 'string', 'required' => false],
|
||||
'deletedFlag' => ['type' => 'boolean', 'required' => false],
|
||||
'dateDeleted' => ['type' => 'string', 'required' => false],
|
||||
'deletedBy' => ['type' => 'string', 'required' => false],
|
||||
// todo 'customFields' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array of contact fields for connectwise
|
||||
*/
|
||||
public function getContactFields(): array
|
||||
{
|
||||
return [
|
||||
'firstName' => ['type' => 'string', 'required' => true],
|
||||
'lastName' => ['type' => 'string', 'required' => false],
|
||||
'type' => ['type' => 'string', 'required' => false],
|
||||
'company' => ['type' => 'ref', 'required' => false, 'value' => 'name'],
|
||||
'addressLine1' => ['type' => 'string', 'required' => false],
|
||||
'addressLine2' => ['type' => 'string', 'required' => false],
|
||||
'city' => ['type' => 'string', 'required' => false],
|
||||
'state' => ['type' => 'string', 'required' => false],
|
||||
'zip' => ['type' => 'string', 'required' => false],
|
||||
'country' => ['type' => 'string', 'required' => false],
|
||||
'inactiveFlag' => ['type' => 'string', 'required' => false],
|
||||
'securityIdentifier' => ['type' => 'string', 'required' => false],
|
||||
'managerContactId' => ['type' => 'string', 'required' => false],
|
||||
'assistantContactId' => ['type' => 'string', 'required' => false],
|
||||
'title' => ['type' => 'string', 'required' => false],
|
||||
'school' => ['type' => 'string', 'required' => false],
|
||||
'nickName' => ['type' => 'string', 'required' => false],
|
||||
'marriedFlag' => ['type' => 'boolean', 'required' => false],
|
||||
'childrenFlag' => ['type' => 'boolean', 'required' => false],
|
||||
'significantOther' => ['type' => 'string', 'required' => false],
|
||||
'portalPassword' => ['type' => 'string', 'required' => false],
|
||||
'portalSecurityLevel' => ['type' => 'string', 'required' => false],
|
||||
'disablePortalLoginFlag' => ['type' => 'boolean', 'required' => false],
|
||||
'unsubscribeFlag' => ['type' => 'boolean', 'required' => false],
|
||||
'userDefinedField1' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField2' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField3' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField4' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField5' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField6' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField7' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField8' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField9' => ['type' => 'string', 'required' => false],
|
||||
'userDefinedField10' => ['type' => 'string', 'required' => false],
|
||||
'gender' => ['type' => 'string', 'required' => false],
|
||||
'birthDay' => ['type' => 'string', 'required' => false],
|
||||
'anniversary' => ['type' => 'string', 'required' => false],
|
||||
'presence' => ['type' => 'string', 'required' => false],
|
||||
'mobileGuid' => ['type' => 'string', 'required' => false],
|
||||
'facebookUrl' => ['type' => 'string', 'required' => false],
|
||||
'twitterUrl' => ['type' => 'string', 'required' => false],
|
||||
'linkedInUrl' => ['type' => 'string', 'required' => false],
|
||||
'defaultBillingFlag' => ['type' => 'boolean', 'required' => false],
|
||||
'communicationItems' => [
|
||||
'type' => 'array',
|
||||
'required' => false,
|
||||
'items' => [
|
||||
'name' => ['type' => 'name'],
|
||||
'value' => 'value',
|
||||
'keys' => ['Email', 'Direct', 'Fax', 'Cell'],
|
||||
],
|
||||
],
|
||||
'Direct' => ['type' => 'string', 'required' => false, 'configOnly' => true],
|
||||
'Cell' => ['type' => 'string', 'required' => false, 'configOnly' => true],
|
||||
'Email' => ['type' => 'string', 'required' => true, 'configOnly' => true],
|
||||
'Fax' => ['type' => 'string', 'required' => false, 'configOnly' => true],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Contacts from connectwise.
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function getLeads($params = [], $query = null, &$executed = null, $result = [], $object = 'Contact'): int
|
||||
{
|
||||
return $this->getRecords($params, $object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Companies from connectwise.
|
||||
*/
|
||||
public function getCompanies(array $params = []): int
|
||||
{
|
||||
return $this->getRecords($params, 'company');
|
||||
}
|
||||
|
||||
public function getRecords($params, $object): int
|
||||
{
|
||||
if (!$this->isAuthorized()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$page = 1;
|
||||
$executed = 0;
|
||||
$integrationEntities = [];
|
||||
try {
|
||||
while ($records = ('Contact' == $object)
|
||||
? $this->getApiHelper()->getContacts($params, $page)
|
||||
: $this->getApiHelper()->getCompanies($params, $page)) {
|
||||
$mauticReferenceObject = ('Contact' == $object) ? 'lead' : 'company';
|
||||
foreach ($records as $record) {
|
||||
if (is_array($record)) {
|
||||
$id = $record['id'];
|
||||
$formattedData = $this->amendLeadDataBeforeMauticPopulate($record, $object);
|
||||
$entity = ('Contact' == $object)
|
||||
? $this->getMauticLead($formattedData)
|
||||
: $this->getMauticCompany(
|
||||
$formattedData,
|
||||
'company'
|
||||
);
|
||||
if ($entity) {
|
||||
$integrationEntities[] = $this->saveSyncedData($entity, $object, $mauticReferenceObject, $id);
|
||||
$this->em->detach($entity);
|
||||
unset($entity);
|
||||
++$executed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($integrationEntities) {
|
||||
$this->em->getRepository(IntegrationEntity::class)->saveEntities($integrationEntities);
|
||||
$this->integrationEntityModel->getRepository()->detachEntities($integrationEntities);
|
||||
}
|
||||
|
||||
// No use checking the next page if there are less records than the requested page size
|
||||
|
||||
if (count($records) < self::PAGESIZE) {
|
||||
break;
|
||||
}
|
||||
|
||||
++$page;
|
||||
}
|
||||
|
||||
return $executed;
|
||||
} catch (\Exception $e) {
|
||||
if (404 !== $e->getCode()) {
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
}
|
||||
|
||||
return $executed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ammend mapped lead data before creating to Mautic.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function amendLeadDataBeforeMauticPopulate($data, $object): array
|
||||
{
|
||||
$fieldsValues = [];
|
||||
|
||||
if (empty($data)) {
|
||||
return $fieldsValues;
|
||||
}
|
||||
if ('Contact' == $object) {
|
||||
$fields = $this->getContactFields();
|
||||
} else {
|
||||
$fields = $this->getCompanyFields();
|
||||
}
|
||||
|
||||
foreach ($data as $key => $field) {
|
||||
if (isset($fields[$key])) {
|
||||
$name = $key;
|
||||
if ('array' == $fields[$key]['type']) {
|
||||
$items = $fields[$key]['items'];
|
||||
foreach ($field as $item) {
|
||||
if (is_array($item[key($items['name'])])) {
|
||||
foreach ($item[key($items['name'])] as $nameKey => $nameField) {
|
||||
if ($nameKey == $items['name'][key($items['name'])]) {
|
||||
$name = $nameField;
|
||||
}
|
||||
}
|
||||
}
|
||||
$fieldsValues[$name] = $item[$items['value']];
|
||||
}
|
||||
} elseif ('ref' == $fields[$key]['type']) {
|
||||
$fieldsValues[$name] = $field[$fields[$key]['value']];
|
||||
} else {
|
||||
$fieldsValues[$name] = $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($data['id'])) {
|
||||
$fieldsValues['id'] = $data['id'];
|
||||
}
|
||||
|
||||
return $fieldsValues;
|
||||
}
|
||||
|
||||
public function saveSyncedData($entity, $object, $mauticObjectReference, $integrationEntityId): IntegrationEntity
|
||||
{
|
||||
/** @var IntegrationEntityRepository $integrationEntityRepo */
|
||||
$integrationEntityRepo = $this->em->getRepository(IntegrationEntity::class);
|
||||
$integrationEntities = $integrationEntityRepo->getIntegrationEntities(
|
||||
$this->getName(),
|
||||
$object,
|
||||
$mauticObjectReference,
|
||||
[$entity->getId()]
|
||||
);
|
||||
|
||||
if ($integrationEntities) {
|
||||
$integrationEntity = reset($integrationEntities);
|
||||
$integrationEntity->setLastSyncDate(new \DateTime());
|
||||
} else {
|
||||
$integrationEntity = new IntegrationEntity();
|
||||
$integrationEntity->setDateAdded(new \DateTime());
|
||||
$integrationEntity->setIntegration($this->getName());
|
||||
$integrationEntity->setIntegrationEntity($object);
|
||||
$integrationEntity->setIntegrationEntityId($integrationEntityId);
|
||||
$integrationEntity->setInternalEntity($mauticObjectReference);
|
||||
$integrationEntity->setInternalEntityId($entity->getId());
|
||||
}
|
||||
|
||||
return $integrationEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|Lead $lead
|
||||
* @param array $config
|
||||
*
|
||||
* @throws ApiErrorException
|
||||
*/
|
||||
public function pushLead($lead, $config = []): bool
|
||||
{
|
||||
$config = $this->mergeConfigToFeatureSettings($config);
|
||||
$personFound = false;
|
||||
$leadPushed = false;
|
||||
$object = 'Contact';
|
||||
|
||||
if (empty($config['leadFields']) || !$lead->getEmail()) {
|
||||
return $leadPushed;
|
||||
}
|
||||
|
||||
// findLead first
|
||||
$cwContactExists = $this->getApiHelper()->getContacts(['Email' => $lead->getEmail()]);
|
||||
|
||||
if (!empty($cwContactExists)) {
|
||||
$personFound = true;
|
||||
}
|
||||
|
||||
$personData = [];
|
||||
|
||||
try {
|
||||
if ($personFound) {
|
||||
foreach ($cwContactExists as $cwContact) { // go through array of contacts found since Connectwise lets you duplicate records with same email address
|
||||
$mappedData = $this->getMappedFields($object, $lead, $personFound, $config, $cwContact);
|
||||
|
||||
if (!empty($mappedData)) {
|
||||
$personData = $this->getApiHelper()->updateContact($mappedData, $cwContact['id']);
|
||||
} else {
|
||||
$personData['id'] = $cwContact['id'];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$mappedData = $this->getMappedFields($object, $lead, $personFound, $config);
|
||||
$personData = $this->getApiHelper()->createContact($mappedData);
|
||||
}
|
||||
|
||||
if (!empty($personData['id'])) {
|
||||
$id = $personData['id'];
|
||||
$integrationEntities[] = $this->saveSyncedData($lead, $object, 'lead', $id);
|
||||
|
||||
if (isset($config['push_activities']) and true == $config['push_activities']) {
|
||||
$savedEntity = $this->createActivity($config['campaign_task'], $id, $lead->getId());
|
||||
if ($savedEntity instanceof IntegrationEntity) {
|
||||
$integrationEntities[] = $savedEntity;
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->getRepository(IntegrationEntity::class)->saveEntities($integrationEntities);
|
||||
$this->integrationEntityModel->getRepository()->detachEntities($integrationEntities);
|
||||
|
||||
$leadPushed = true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
if ($e instanceof ApiErrorException) {
|
||||
$e->setContact($lead);
|
||||
}
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
|
||||
return $leadPushed;
|
||||
}
|
||||
|
||||
public function getMappedFields($object, $lead, $personFound, $config, $cwContactData = []): array
|
||||
{
|
||||
$fieldsToUpdateInCW = isset($config['update_mautic']) && $personFound ? array_keys($config['update_mautic'], 1) : [];
|
||||
$objectFields = $this->prepareFieldsForPush($this->getContactFields());
|
||||
$leadFields = $config['leadFields'];
|
||||
|
||||
$cwContactExists = $this->amendLeadDataBeforeMauticPopulate($cwContactData, $object);
|
||||
|
||||
$communicationItems = $cwContactData['communicationItems'] ?? [];
|
||||
|
||||
$leadFields = array_diff_key($leadFields, array_flip($fieldsToUpdateInCW));
|
||||
$leadFields = $this->getBlankFieldsToUpdate($leadFields, $cwContactExists, $objectFields, $config);
|
||||
|
||||
return $this->populateLeadData(
|
||||
$lead,
|
||||
[
|
||||
'leadFields' => $leadFields,
|
||||
'object' => 'Contact',
|
||||
'feature_settings' => [
|
||||
'objects' => $config['objects'],
|
||||
],
|
||||
'update' => $personFound,
|
||||
'communicationItems' => $communicationItems,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match lead data with integration fields.
|
||||
*/
|
||||
public function populateLeadData($lead, $config = []): array
|
||||
{
|
||||
if ($lead instanceof Lead) {
|
||||
$fields = $lead->getFields(true);
|
||||
} else {
|
||||
$fields = $lead;
|
||||
}
|
||||
|
||||
$leadFields = $config['leadFields'];
|
||||
if (empty($leadFields)) {
|
||||
return [];
|
||||
}
|
||||
$availableFields = $this->getContactFields();
|
||||
$unknown = $this->translator->trans('mautic.integration.form.lead.unknown');
|
||||
$matched = [];
|
||||
|
||||
foreach ($availableFields as $key => $field) {
|
||||
$integrationKey = $matchIntegrationKey = $key;
|
||||
|
||||
if (isset($field['configOnly'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('communicationItems' == $integrationKey) {
|
||||
$communicationItems = [];
|
||||
foreach ($field['items']['keys'] as $keyItem => $item) {
|
||||
$defaultValue = [];
|
||||
$keyExists = false;
|
||||
if (isset($leadFields[$item])) {
|
||||
if ('Email' == $item) {
|
||||
$defaultValue = ['defaultFlag' => true];
|
||||
}
|
||||
$mauticKey = $leadFields[$item];
|
||||
if (isset($fields[$mauticKey]) && !empty($fields[$mauticKey]['value'])) {
|
||||
foreach ($config['communicationItems'] as $key => $ci) {
|
||||
if ($ci['type']['id'] == $keyItem + 1) {
|
||||
$config['communicationItems'][$key]['value'] = $fields[$mauticKey]['value'];
|
||||
$keyExists = true;
|
||||
}
|
||||
}
|
||||
if (!$keyExists) {
|
||||
$type = [
|
||||
'type' => ['id' => $keyItem + 1, 'name' => $item], ];
|
||||
$values = array_merge(['value' => $this->cleanPushData($fields[$mauticKey]['value'])], $defaultValue);
|
||||
|
||||
$communicationItems[] = array_merge($type, $values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($config['update']) {
|
||||
$communicationItems = array_merge($config['communicationItems'], $communicationItems);
|
||||
}
|
||||
if (!empty($communicationItems)) {
|
||||
$matched[$integrationKey] = $communicationItems;
|
||||
}
|
||||
}
|
||||
|
||||
if ('company' === $integrationKey && !empty($fields['company']['value'])) {
|
||||
try {
|
||||
$foundCompanies = $this->getApiHelper()->getCompanies([
|
||||
'conditions' => [
|
||||
sprintf('Name = "%s"', $fields['company']['value']),
|
||||
],
|
||||
]);
|
||||
|
||||
$matched['company'] = ['identifier' => $foundCompanies[0]['identifier']];
|
||||
} catch (ApiErrorException) {
|
||||
// No matching companies were found
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($leadFields[$integrationKey])) {
|
||||
$mauticKey = $leadFields[$integrationKey];
|
||||
if (isset($fields[$mauticKey]) && !empty($fields[$mauticKey]['value'])) {
|
||||
$matched[$matchIntegrationKey] = $this->cleanPushData($fields[$mauticKey]['value']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($field['required']) && empty($matched[$matchIntegrationKey]) && !$config['update']) {
|
||||
$matched[$matchIntegrationKey] = $unknown;
|
||||
}
|
||||
}
|
||||
|
||||
if ($config['update']) {
|
||||
$updateFields = [];
|
||||
foreach ($matched as $key => $field) {
|
||||
$updateFields[] = [
|
||||
'op' => 'replace',
|
||||
'path' => $key,
|
||||
'value' => $field,
|
||||
];
|
||||
}
|
||||
$matched = $updateFields;
|
||||
}
|
||||
|
||||
return $matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $objects
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function cleanPriorityFields($fieldsToUpdate, $objects = null)
|
||||
{
|
||||
if (null === $objects) {
|
||||
$objects = ['Leads', 'Contacts'];
|
||||
}
|
||||
if (isset($fieldsToUpdate['leadFields']) && is_array($objects)) {
|
||||
// Pass in the whole config
|
||||
$fields = $fieldsToUpdate['leadFields'];
|
||||
} else {
|
||||
$fields = array_flip($fieldsToUpdate);
|
||||
}
|
||||
|
||||
return $this->prepareFieldsForSync($fields, $fieldsToUpdate, $objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $priorityObject
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getPriorityFieldsForMautic($config, $object = null, $priorityObject = 'mautic')
|
||||
{
|
||||
if ('company' == $object) {
|
||||
$priority = parent::getPriorityFieldsForMautic($config, $object, 'mautic_company');
|
||||
$fields = array_intersect_key($config['companyFields'], $priority);
|
||||
} else {
|
||||
$fields = parent::getPriorityFieldsForMautic($config, $object, $priorityObject);
|
||||
}
|
||||
|
||||
return ($object && isset($fields[$object])) ? $fields[$object] : $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getCampaigns()
|
||||
{
|
||||
$campaigns = [];
|
||||
try {
|
||||
$campaigns = $this->getApiHelper()->getCampaigns();
|
||||
} catch (\Exception $e) {
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
|
||||
return $campaigns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getCampaignChoices(): array
|
||||
{
|
||||
$choices = [];
|
||||
$campaigns = $this->getCampaigns();
|
||||
|
||||
foreach ($campaigns as $campaign) {
|
||||
if (isset($campaign['id'])) {
|
||||
$choices[] = [
|
||||
'value' => $campaign['id'],
|
||||
'label' => $campaign['name'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $choices;
|
||||
}
|
||||
|
||||
public function getCampaignMembers($campaignId): bool
|
||||
{
|
||||
if (!$this->isAuthorized()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$page = 1;
|
||||
while ($campaignsMembersResults = $this->getApiHelper()->getCampaignMembers($campaignId, $page)) {
|
||||
$campaignMemberObject = new IntegrationObject('CampaignMember', 'lead');
|
||||
$recordList = $this->getRecordList($campaignsMembersResults, 'id');
|
||||
$contacts = (array) $this->integrationEntityModel->getSyncedRecords(new IntegrationObject('Contact', 'lead'), $this->getName(), $recordList);
|
||||
|
||||
$existingContactsIds = array_column(array_filter(
|
||||
$contacts,
|
||||
fn ($contact): bool => 'lead' === $contact['internal_entity']
|
||||
), 'integration_entity_id');
|
||||
|
||||
$contactsToFetch = array_diff_key($recordList, array_flip($existingContactsIds));
|
||||
|
||||
if (!empty($contactsToFetch)) {
|
||||
$listOfContactsToFetch = implode(',', array_keys($contactsToFetch));
|
||||
$params['Ids'] = $listOfContactsToFetch;
|
||||
|
||||
$this->getLeads($params);
|
||||
}
|
||||
|
||||
$saveCampaignMembers = array_merge($existingContactsIds, array_keys($contactsToFetch));
|
||||
|
||||
$this->saveCampaignMembers($saveCampaignMembers, $campaignMemberObject, $campaignId);
|
||||
|
||||
if (count($campaignsMembersResults) < self::PAGESIZE) {
|
||||
// No use continuing as we have less results than page size
|
||||
break;
|
||||
}
|
||||
|
||||
++$page;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
if (404 !== $e->getCode()) {
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function saveCampaignMembers($allCampaignMembers, $campaignMemberObject, $campaignId): void
|
||||
{
|
||||
if (empty($allCampaignMembers)) {
|
||||
return;
|
||||
}
|
||||
$persistEntities = [];
|
||||
$recordList = $this->getRecordList($allCampaignMembers);
|
||||
$mauticObject = new IntegrationObject('Contact', 'lead');
|
||||
|
||||
$contacts = $this->integrationEntityModel->getSyncedRecords($mauticObject, $this->getName(), $recordList);
|
||||
// first find existing campaign members.
|
||||
foreach ($contacts as $contact) {
|
||||
$existingCampaignMember = $this->integrationEntityModel->getSyncedRecords($campaignMemberObject, $this->getName(), $campaignId, $contact['internal_entity_id']);
|
||||
if (empty($existingCampaignMember)) {
|
||||
$persistEntities[] = $this->createIntegrationEntity(
|
||||
$campaignMemberObject->getType(),
|
||||
$campaignId,
|
||||
$campaignMemberObject->getInternalType(),
|
||||
$contact['internal_entity_id'],
|
||||
[],
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($persistEntities) {
|
||||
$this->em->getRepository(IntegrationEntity::class)->saveEntities($persistEntities);
|
||||
$this->integrationEntityModel->getRepository()->detachEntities($persistEntities);
|
||||
unset($persistEntities);
|
||||
}
|
||||
}
|
||||
|
||||
public function getRecordList($records, $index = null): array
|
||||
{
|
||||
$recordList = [];
|
||||
|
||||
foreach ($records as $record) {
|
||||
if ($index && isset($record[$index])) {
|
||||
$record = $record[$index];
|
||||
}
|
||||
|
||||
$recordList[$record] = [
|
||||
'id' => $record,
|
||||
];
|
||||
}
|
||||
|
||||
return $recordList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiErrorException
|
||||
*/
|
||||
public function getActivityTypes(): array
|
||||
{
|
||||
$activities = [];
|
||||
$cwActivities = $this->getApiHelper()->getActivityTypes();
|
||||
|
||||
foreach ($cwActivities as $cwActivity) {
|
||||
if (isset($cwActivity['id'])) {
|
||||
$activities[$cwActivity['id']] = $cwActivity['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return $activities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiErrorException
|
||||
*/
|
||||
public function getMembers(): array
|
||||
{
|
||||
$members = [];
|
||||
$cwMembers = $this->getApiHelper()->getMembers();
|
||||
foreach ($cwMembers as $cwMember) {
|
||||
if (isset($cwMember['id'])) {
|
||||
$members[$cwMember['id']] = $cwMember['identifier'];
|
||||
}
|
||||
}
|
||||
|
||||
return $members;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiErrorException
|
||||
*/
|
||||
public function createActivity($config, $cwContactId, $leadId): ?IntegrationEntity
|
||||
{
|
||||
if ($cwContactId and !empty($config['activity_name'])) {
|
||||
$activity = [
|
||||
'name' => $config['activity_name'],
|
||||
'type' => ['id' => $config['campaign_activity_type']],
|
||||
'assignTo' => ['id' => $config['campaign_members']],
|
||||
'contact' => ['id' => $cwContactId],
|
||||
];
|
||||
$activities = $this->getApiHelper()->postActivity($activity);
|
||||
|
||||
if (isset($activities['id'])) {
|
||||
return $this->createIntegrationEntity('Activities', $activities['id'], 'lead', $leadId, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\CacheStorageHelper;
|
||||
use Mautic\CoreBundle\Helper\EncryptionHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\CoreBundle\Model\NotificationModel;
|
||||
use Mautic\LeadBundle\DataObject\LeadManipulator;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
|
||||
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PluginBundle\Entity\Integration;
|
||||
use Mautic\PluginBundle\Integration\AbstractIntegration;
|
||||
use Mautic\PluginBundle\Model\IntegrationEntityModel;
|
||||
use MauticPlugin\MauticCrmBundle\Api\CrmApi;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
abstract class CrmAbstractIntegration extends AbstractIntegration
|
||||
{
|
||||
protected $auth;
|
||||
|
||||
protected $helper;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, CacheStorageHelper $cacheStorageHelper, EntityManager $entityManager, RequestStack $requestStack, RouterInterface $router, TranslatorInterface $translator, LoggerInterface $logger, EncryptionHelper $encryptionHelper, LeadModel $leadModel, CompanyModel $companyModel, PathsHelper $pathsHelper, NotificationModel $notificationModel, FieldModel $fieldModel, IntegrationEntityModel $integrationEntityModel, DoNotContactModel $doNotContact, protected FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier)
|
||||
{
|
||||
parent::__construct($eventDispatcher, $cacheStorageHelper, $entityManager, $requestStack, $router, $translator, $logger, $encryptionHelper, $leadModel, $companyModel, $pathsHelper, $notificationModel, $fieldModel, $integrationEntityModel, $doNotContact, $fieldsWithUniqueIdentifier);
|
||||
}
|
||||
|
||||
public function setIntegrationSettings(Integration $settings): void
|
||||
{
|
||||
// make sure URL does not have ending /
|
||||
$keys = $this->getDecryptedApiKeys($settings);
|
||||
if (isset($keys['url']) && str_ends_with($keys['url'], '/')) {
|
||||
$keys['url'] = substr($keys['url'], 0, -1);
|
||||
$this->encryptAndSetApiKeys($keys, $settings);
|
||||
}
|
||||
|
||||
parent::setIntegrationSettings($settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthenticationType()
|
||||
{
|
||||
return 'rest';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getSupportedFeatures()
|
||||
{
|
||||
return ['push_lead', 'get_leads'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead|array $lead
|
||||
* @param array $config
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public function pushLead($lead, $config = [])
|
||||
{
|
||||
$config = $this->mergeConfigToFeatureSettings($config);
|
||||
|
||||
if (empty($config['leadFields'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$mappedData = $this->populateLeadData($lead, $config);
|
||||
|
||||
$this->amendLeadDataBeforePush($mappedData);
|
||||
|
||||
if (empty($mappedData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->isAuthorized()) {
|
||||
$this->getApiHelper()->createLead($mappedData, $lead);
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
*/
|
||||
public function getLeads($params, $query, &$executed, $result = [], $object = 'Lead')
|
||||
{
|
||||
$executed = null;
|
||||
|
||||
$query = $this->getFetchQuery($params);
|
||||
|
||||
try {
|
||||
if ($this->isAuthorized()) {
|
||||
$result = $this->getApiHelper()->getLeads($query);
|
||||
|
||||
return $this->amendLeadDataBeforeMauticPopulate($result, $object);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
|
||||
return $executed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amend mapped lead data before pushing to CRM.
|
||||
*/
|
||||
public function amendLeadDataBeforePush(&$mappedData): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* get query to fetch lead data.
|
||||
*/
|
||||
public function getFetchQuery($config)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ammend mapped lead data before creating to Mautic.
|
||||
*/
|
||||
public function amendLeadDataBeforeMauticPopulate($data, $object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getClientIdKey()
|
||||
{
|
||||
return 'client_id';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getClientSecretKey()
|
||||
{
|
||||
return 'client_secret';
|
||||
}
|
||||
|
||||
public function sortFieldsAlphabetically(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API helper.
|
||||
*
|
||||
* @return CrmApi
|
||||
*/
|
||||
public function getApiHelper()
|
||||
{
|
||||
if (empty($this->helper)) {
|
||||
$class = '\\MauticPlugin\\MauticCrmBundle\\Api\\'.$this->getName().'Api';
|
||||
$this->helper = new $class($this);
|
||||
}
|
||||
|
||||
return $this->helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
*/
|
||||
public function pushLeadActivity($params = [])
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string[]|int[] $leadId
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getLeadData(?\DateTime $startDate = null, ?\DateTime $endDate = null, $leadId = [])
|
||||
{
|
||||
$leadIds = (!is_array($leadId)) ? [$leadId] : $leadId;
|
||||
$leadActivity = [];
|
||||
|
||||
$config = $this->mergeConfigToFeatureSettings();
|
||||
if (!isset($config['activityEvents'])) {
|
||||
// BC for pre 2.11.0
|
||||
$config['activityEvents'] = ['point.gained', 'form.submitted', 'email.read'];
|
||||
} elseif (empty($config['activityEvents'])) {
|
||||
// Inclusive filter meaning we only send events if something is selected
|
||||
return [];
|
||||
}
|
||||
|
||||
$filters = [
|
||||
'search' => '',
|
||||
'includeEvents' => $config['activityEvents'],
|
||||
'excludeEvents' => [],
|
||||
];
|
||||
|
||||
if ($startDate) {
|
||||
$filters['dateFrom'] = $startDate;
|
||||
$filters['dateTo'] = $endDate;
|
||||
}
|
||||
|
||||
foreach ($leadIds as $leadId) {
|
||||
$i = 0;
|
||||
$activity = [];
|
||||
$lead = $this->em->getReference(Lead::class, $leadId);
|
||||
$page = 1;
|
||||
|
||||
while (true) {
|
||||
$engagements = $this->leadModel->getEngagements($lead, $filters, null, $page, 100, false);
|
||||
$events = $engagements[0]['events'];
|
||||
|
||||
if (empty($events)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// inject lead into events
|
||||
foreach ($events as $event) {
|
||||
$link = '';
|
||||
$label = $event['eventLabel'] ?? $event['eventType'];
|
||||
if (is_array($label)) {
|
||||
$link = $label['href'];
|
||||
$label = $label['label'];
|
||||
}
|
||||
|
||||
$activity[$i]['eventType'] = $event['eventType'];
|
||||
$activity[$i]['name'] = $event['eventType'].' - '.$label;
|
||||
$activity[$i]['description'] = $link;
|
||||
$activity[$i]['dateAdded'] = $event['timestamp'];
|
||||
|
||||
$id = match ($event['eventType']) {
|
||||
'point.gained' => str_replace($event['eventType'], 'pointChange', $event['eventId']),
|
||||
'form.submitted' => str_replace($event['eventType'], 'formSubmission', $event['eventId']),
|
||||
'email.read' => str_replace($event['eventType'], 'emailStat', $event['eventId']),
|
||||
default => str_replace(' ', '', ucwords(str_replace('.', ' ', $event['eventId']))),
|
||||
};
|
||||
|
||||
$activity[$i]['id'] = $id;
|
||||
++$i;
|
||||
}
|
||||
|
||||
++$page;
|
||||
|
||||
// Lots of entities will be loaded into memory while compiling these events so let's prevent memory overload by clearing the EM
|
||||
$entityToNotDetach = [Integration::class, \Mautic\PluginBundle\Entity\Plugin::class];
|
||||
$loadedEntities = $this->em->getUnitOfWork()->getIdentityMap();
|
||||
foreach ($loadedEntities as $name => $loadedEntitySet) {
|
||||
if (!in_array($name, $entityToNotDetach, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($loadedEntitySet as $loadedEntity) {
|
||||
$this->em->detach($loadedEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$leadActivity[$leadId] = [
|
||||
'records' => $activity,
|
||||
];
|
||||
|
||||
unset($activity);
|
||||
}
|
||||
|
||||
return $leadActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Company|null
|
||||
*/
|
||||
public function getMauticCompany($data, $object = null)
|
||||
{
|
||||
if (is_object($data)) {
|
||||
// Convert to array in all levels
|
||||
$data = json_encode(json_decode($data, true));
|
||||
} elseif (is_string($data)) {
|
||||
// Assume JSON
|
||||
$data = json_decode($data, true);
|
||||
}
|
||||
$config = $this->mergeConfigToFeatureSettings([]);
|
||||
$matchedFields = $this->populateMauticLeadData($data, $config, 'company');
|
||||
|
||||
$companyFieldTypes = $this->fieldModel->getFieldListWithProperties('company');
|
||||
foreach ($matchedFields as $companyField => $value) {
|
||||
if (isset($companyFieldTypes[$companyField]['type'])) {
|
||||
switch ($companyFieldTypes[$companyField]['type']) {
|
||||
case 'text':
|
||||
$matchedFields[$companyField] = substr($value, 0, 255);
|
||||
break;
|
||||
case 'date':
|
||||
$date = new \DateTime($value);
|
||||
$matchedFields[$companyField] = $date->format('Y-m-d');
|
||||
break;
|
||||
case 'datetime':
|
||||
$date = new \DateTime($value);
|
||||
$matchedFields[$companyField] = $date->format('Y-m-d H:i:s');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to new company
|
||||
$company = new Company();
|
||||
$existingCompany = IdentifyCompanyHelper::identifyLeadsCompany($matchedFields, null, $this->companyModel);
|
||||
if (!empty($existingCompany[2])) {
|
||||
$company = $existingCompany[2];
|
||||
}
|
||||
|
||||
if (!empty($existingCompany[2])) {
|
||||
$fieldsToUpdate = $this->getPriorityFieldsForMautic($config, $object, 'mautic_company');
|
||||
$fieldsToUpdate = array_intersect_key($config['companyFields'], $fieldsToUpdate);
|
||||
$matchedFields = array_intersect_key($matchedFields, array_flip($fieldsToUpdate));
|
||||
} else {
|
||||
$matchedFields = $this->hydrateCompanyName($matchedFields);
|
||||
|
||||
// If we don't have an company name, don't create the company because it'll result in what looks like an "empty" company
|
||||
if (empty($matchedFields['companyname'])) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$this->companyModel->setFieldValues($company, $matchedFields, false);
|
||||
$this->companyModel->saveEntity($company, false);
|
||||
|
||||
return $company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update existing Mautic lead from the integration's profile data.
|
||||
*
|
||||
* @param mixed $data Profile data from integration
|
||||
* @param bool|true $persist Set to false to not persist lead to the database in this method
|
||||
* @param array|null $socialCache
|
||||
* @param mixed|null $identifiers
|
||||
* @param string|null $object
|
||||
*
|
||||
* @return Lead
|
||||
*/
|
||||
public function getMauticLead($data, $persist = true, $socialCache = null, $identifiers = null, $object = null)
|
||||
{
|
||||
if (is_object($data)) {
|
||||
// Convert to array in all levels
|
||||
$data = json_encode(json_decode($data, true));
|
||||
} elseif (is_string($data)) {
|
||||
// Assume JSON
|
||||
$data = json_decode($data, true);
|
||||
}
|
||||
$config = $this->mergeConfigToFeatureSettings([]);
|
||||
// Match that data with mapped lead fields
|
||||
$matchedFields = $this->populateMauticLeadData($data, $config);
|
||||
|
||||
if (empty($matchedFields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find unique identifier fields used by the integration
|
||||
/** @var LeadModel $leadModel */
|
||||
$leadModel = $this->leadModel;
|
||||
$uniqueLeadFields = $this->fieldsWithUniqueIdentifier->getFieldsWithUniqueIdentifier();
|
||||
$uniqueLeadFieldData = [];
|
||||
$leadFieldTypes = $this->fieldModel->getFieldListWithProperties();
|
||||
|
||||
foreach ($matchedFields as $leadField => $value) {
|
||||
if (array_key_exists($leadField, $uniqueLeadFields) && !empty($value)) {
|
||||
$uniqueLeadFieldData[$leadField] = $value;
|
||||
}
|
||||
|
||||
$fieldType = $leadFieldTypes[$leadField]['type'] ?? '';
|
||||
$matchedFields[$leadField] = $this->limitString($value, $fieldType);
|
||||
}
|
||||
|
||||
if (count(array_diff_key($uniqueLeadFields, $matchedFields)) == count($uniqueLeadFields)) {
|
||||
// return if uniqueIdentifiers have no data set to avoid duplicating leads.
|
||||
$this->logger->debug('getMauticLead: No unique identifiers', [
|
||||
'uniqueLeadFields' => $uniqueLeadFields,
|
||||
'matchedFields' => $matchedFields,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to new lead
|
||||
$lead = new Lead();
|
||||
$lead->setNewlyCreated(true);
|
||||
|
||||
if (count($uniqueLeadFieldData)) {
|
||||
$existingLeads = $this->em->getRepository(Lead::class)
|
||||
->getLeadsByUniqueFields($uniqueLeadFieldData);
|
||||
if (!empty($existingLeads)) {
|
||||
$lead = array_shift($existingLeads);
|
||||
}
|
||||
}
|
||||
|
||||
$leadFields = $this->cleanPriorityFields($config, $object);
|
||||
if (!$lead->isNewlyCreated()) {
|
||||
$params = $this->commandParameters;
|
||||
|
||||
$this->getLeadDoNotContactByDate('email', $matchedFields, $object, $lead, $data, $params);
|
||||
|
||||
// Use only prioirty fields if updating
|
||||
$fieldsToUpdateInMautic = $this->getPriorityFieldsForMautic($config, $object, 'mautic');
|
||||
if (empty($fieldsToUpdateInMautic)) {
|
||||
$this->logger->debug('getMauticLead: No fields to update in Mautic', ['config' => $config, 'object' => $object]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldsToUpdateInMautic = array_intersect_key($leadFields, $fieldsToUpdateInMautic);
|
||||
$matchedFields = array_intersect_key($matchedFields, array_flip($fieldsToUpdateInMautic));
|
||||
if (isset($config['updateBlanks']) && isset($config['updateBlanks'][0]) && 'updateBlanks' == $config['updateBlanks'][0]) {
|
||||
$matchedFields = $this->getBlankFieldsToUpdateInMautic($matchedFields, $lead->getFields(true), $leadFields, $data, $object);
|
||||
}
|
||||
}
|
||||
|
||||
$leadModel->setFieldValues($lead, $matchedFields, false, false);
|
||||
if (!empty($socialCache)) {
|
||||
// Update the social cache
|
||||
$leadSocialCache = $lead->getSocialCache();
|
||||
if (!isset($leadSocialCache[$this->getName()])) {
|
||||
$leadSocialCache[$this->getName()] = [];
|
||||
}
|
||||
$leadSocialCache[$this->getName()] = array_merge($leadSocialCache[$this->getName()], $socialCache);
|
||||
|
||||
// Check for activity while here
|
||||
if (null !== $identifiers && in_array('public_activity', $this->getSupportedFeatures())) {
|
||||
$this->getPublicActivity($identifiers, $leadSocialCache[$this->getName()]);
|
||||
}
|
||||
|
||||
$lead->setSocialCache($leadSocialCache);
|
||||
}
|
||||
|
||||
// Update the internal info integration object that has updated the record
|
||||
if (isset($data['internal'])) {
|
||||
$internalInfo = $lead->getInternal();
|
||||
$internalInfo[$this->getName()] = $data['internal'];
|
||||
$lead->setInternal($internalInfo);
|
||||
}
|
||||
|
||||
// Update the owner if it matches (needs to be set by the integration) when fetching the data
|
||||
if (isset($data['owner_email']) && isset($config['updateOwner']) && isset($config['updateOwner'][0])
|
||||
&& 'updateOwner' == $config['updateOwner'][0]
|
||||
) {
|
||||
if ($mauticUser = $this->em->getRepository(\Mautic\UserBundle\Entity\User::class)->findOneBy(['email' => $data['owner_email']])) {
|
||||
$lead->setOwner($mauticUser);
|
||||
}
|
||||
}
|
||||
|
||||
if ($persist && !empty($lead->getChanges(true))) {
|
||||
// Only persist if instructed to do so as it could be that calling code needs to manipulate the lead prior to executing event listeners
|
||||
$lead->setManipulator(new LeadManipulator(
|
||||
'plugin',
|
||||
$this->getName(),
|
||||
null,
|
||||
$this->getDisplayName()
|
||||
));
|
||||
$leadModel->saveEntity($lead, false);
|
||||
}
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|mixed
|
||||
*/
|
||||
protected function getFormFieldsByObject($object, $settings = [])
|
||||
{
|
||||
$settings['feature_settings']['objects'] = [$object => $object];
|
||||
|
||||
$fields = ($this->isAuthorized()) ? $this->getAvailableLeadFields($settings) : [];
|
||||
|
||||
return $fields[$object] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $priorityObject
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getPriorityFieldsForMautic($config, $entityObject = null, $priorityObject = 'mautic')
|
||||
{
|
||||
return $this->cleanPriorityFields(
|
||||
$this->getFieldsByPriority($config, $priorityObject, 1),
|
||||
$entityObject
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $priorityObject
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getPriorityFieldsForIntegration($config, $entityObject = null, $priorityObject = 'mautic')
|
||||
{
|
||||
return $this->cleanPriorityFields(
|
||||
$this->getFieldsByPriority($config, $priorityObject, 0),
|
||||
$entityObject
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $priorityObject
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getFieldsByPriority(array $config, $priorityObject, $direction)
|
||||
{
|
||||
return isset($config['update_'.$priorityObject]) ? array_keys($config['update_'.$priorityObject], $direction) : array_keys($config['leadFields'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $objects
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function cleanPriorityFields($fieldsToUpdate, $objects = null)
|
||||
{
|
||||
if (!isset($fieldsToUpdate['leadFields'])) {
|
||||
return $fieldsToUpdate;
|
||||
}
|
||||
|
||||
if (null === $objects || is_array($objects)) {
|
||||
return $fieldsToUpdate['leadFields'];
|
||||
}
|
||||
|
||||
return $fieldsToUpdate['leadFields'][$objects] ?? $fieldsToUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getSyncTimeframeDates(array $params)
|
||||
{
|
||||
$fromDate = (isset($params['start'])) ? \DateTime::createFromFormat(\DateTime::ISO8601, $params['start'])->format('Y-m-d H:i:s')
|
||||
: null;
|
||||
$toDate = (isset($params['end'])) ? \DateTime::createFromFormat(\DateTime::ISO8601, $params['end'])->format('Y-m-d H:i:s')
|
||||
: null;
|
||||
|
||||
return [$fromDate, $toDate];
|
||||
}
|
||||
|
||||
public function getBlankFieldsToUpdateInMautic($matchedFields, $leadFieldValues, $objectFields, $integrationData, $object = 'Lead')
|
||||
{
|
||||
foreach ($objectFields as $integrationField => $mauticField) {
|
||||
if (isset($leadFieldValues[$mauticField]) && empty($leadFieldValues[$mauticField]['value']) && !empty($integrationData[$integrationField.'__'.$object]) && $this->translator->trans('mautic.integration.form.lead.unknown') !== $integrationData[$integrationField.'__'.$object]) {
|
||||
$matchedFields[$mauticField] = $integrationData[$integrationField.'__'.$object];
|
||||
}
|
||||
}
|
||||
|
||||
return $matchedFields;
|
||||
}
|
||||
|
||||
public function getBlankFieldsToUpdate($fields, $sfRecord, $objectFields, $config)
|
||||
{
|
||||
// check if update blank fields is selected
|
||||
if (isset($config['updateBlanks']) && isset($config['updateBlanks'][0])
|
||||
&& 'updateBlanks' == $config['updateBlanks'][0]
|
||||
&& !empty($sfRecord)
|
||||
&& isset($objectFields['required']['fields'])
|
||||
) {
|
||||
foreach ($sfRecord as $fieldName => $sfField) {
|
||||
if (array_key_exists($fieldName, $objectFields['required']['fields'])) {
|
||||
continue; // this will be treated differently
|
||||
}
|
||||
if (empty($sfField) && array_key_exists($fieldName, $objectFields['create']) && !array_key_exists($fieldName, $fields)) {
|
||||
// map to mautic field
|
||||
$fields[$fieldName] = $objectFields['create'][$fieldName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareFieldsForPush($fields)
|
||||
{
|
||||
$fieldMappings = [];
|
||||
$required = [];
|
||||
$config = $this->mergeConfigToFeatureSettings();
|
||||
|
||||
$leadFields = $config['leadFields'];
|
||||
foreach ($fields as $key => $field) {
|
||||
if ($field['required']) {
|
||||
$required[$key] = $field;
|
||||
}
|
||||
}
|
||||
$fieldMappings['required'] = [
|
||||
'fields' => $required,
|
||||
];
|
||||
$fieldMappings['create'] = $leadFields;
|
||||
|
||||
return $fieldMappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function hydrateCompanyName(array $matchedFields)
|
||||
{
|
||||
if (!empty($matchedFields['companyname'])) {
|
||||
return $matchedFields;
|
||||
}
|
||||
|
||||
if (!empty($matchedFields['companywebsite'])) {
|
||||
$matchedFields['companyname'] = $matchedFields['companywebsite'];
|
||||
|
||||
return $matchedFields;
|
||||
}
|
||||
|
||||
// We need something as company name so save whatever we have
|
||||
if ($firstMatchedField = reset($matchedFields)) {
|
||||
$matchedFields['companyname'] = $firstMatchedField;
|
||||
|
||||
return $matchedFields;
|
||||
}
|
||||
|
||||
return $matchedFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the string.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param string $fieldType
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function limitString($value, $fieldType = '')
|
||||
{
|
||||
// We must not convert boolean values to string, otherwise "false" will be converted to an empty string.
|
||||
// "False" has to be converted to 0 instead.
|
||||
if (('text' == $fieldType) && !is_bool($value)) {
|
||||
return substr($value, 0, 255);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,895 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper;
|
||||
use Mautic\PluginBundle\Entity\IntegrationEntity;
|
||||
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
|
||||
use Mautic\PluginBundle\Exception\ApiErrorException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilder;
|
||||
|
||||
class DynamicsIntegration extends CrmAbstractIntegration
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Dynamics';
|
||||
}
|
||||
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return 'Dynamics CRM';
|
||||
}
|
||||
|
||||
public function getSupportedFeatures(): array
|
||||
{
|
||||
return ['push_lead', 'get_leads', 'push_leads'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return's authentication method such as oauth2, oauth1a, key, etc.
|
||||
*/
|
||||
public function getAuthenticationType(): string
|
||||
{
|
||||
return 'oauth2';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of key => label elements that will be converted to inputs to
|
||||
* obtain from the user.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRequiredKeyFields(): array
|
||||
{
|
||||
return [
|
||||
'resource' => 'mautic.integration.dynamics.resource',
|
||||
'client_id' => 'mautic.integration.dynamics.client_id',
|
||||
'client_secret' => 'mautic.integration.dynamics.client_secret',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilder $builder
|
||||
* @param array $data
|
||||
* @param string $formArea
|
||||
*/
|
||||
public function appendToForm(&$builder, $data, $formArea): void
|
||||
{
|
||||
$builder->add(
|
||||
'updateBlanks',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'choices' => [
|
||||
'mautic.integrations.blanks' => 'updateBlanks',
|
||||
],
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'label' => 'mautic.integrations.form.blanks',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'placeholder' => false,
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
if ('features' === $formArea) {
|
||||
$builder->add(
|
||||
'objects',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'choices' => [
|
||||
'mautic.dynamics.object.contact' => 'contacts',
|
||||
'mautic.dynamics.object.company' => 'company',
|
||||
],
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'label' => 'mautic.dynamics.form.objects_to_pull_from',
|
||||
'label_attr' => ['class' => ''],
|
||||
'placeholder' => false,
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function sortFieldsAlphabetically(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array key for the auth token.
|
||||
*/
|
||||
public function getAuthTokenKey(): string
|
||||
{
|
||||
return 'access_token';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the keys for the refresh token and expiry.
|
||||
*/
|
||||
public function getRefreshTokenKeys(): array
|
||||
{
|
||||
return ['refresh_token', 'expires_on'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getApiUrl()
|
||||
{
|
||||
return $this->keys['resource'];
|
||||
}
|
||||
|
||||
public function getAccessTokenUrl(): string
|
||||
{
|
||||
return 'https://login.microsoftonline.com/common/oauth2/token';
|
||||
}
|
||||
|
||||
public function getAuthenticationUrl(): string
|
||||
{
|
||||
return 'https://login.microsoftonline.com/common/oauth2/authorize';
|
||||
}
|
||||
|
||||
public function getAuthLoginUrl(): string
|
||||
{
|
||||
$url = parent::getAuthLoginUrl();
|
||||
|
||||
return $url.('&resource='.urlencode($this->keys['resource']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $inAuthorization
|
||||
*/
|
||||
public function getBearerToken($inAuthorization = false)
|
||||
{
|
||||
if (!$inAuthorization && isset($this->keys[$this->getAuthTokenKey()])) {
|
||||
return $this->keys[$this->getAuthTokenKey()];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDataPriority(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|array
|
||||
*/
|
||||
public function getFormNotes($section)
|
||||
{
|
||||
if ('custom' === $section) {
|
||||
return [
|
||||
'template' => '@MauticCrm/Integration/dynamics.html.twig',
|
||||
'parameters' => [
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return parent::getFormNotes($section);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function populateLeadData($lead, $config = [], $object = 'Contacts')
|
||||
{
|
||||
if ('company' === $object) {
|
||||
$object = 'accounts';
|
||||
}
|
||||
$config['object'] = $object;
|
||||
|
||||
return parent::populateLeadData($lead, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available company fields for choices in the config UI.
|
||||
*
|
||||
* @param array $settings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFormCompanyFields($settings = [])
|
||||
{
|
||||
return $this->getFormFieldsByObject('accounts', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $settings
|
||||
*
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function getFormLeadFields($settings = [])
|
||||
{
|
||||
return $this->getFormFieldsByObject('contacts', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $settings
|
||||
*
|
||||
* @throws ApiErrorException
|
||||
*/
|
||||
public function getAvailableLeadFields($settings = []): array
|
||||
{
|
||||
$dynamicsFields = [];
|
||||
$silenceExceptions = $settings['silence_exceptions'] ?? true;
|
||||
if (isset($settings['feature_settings']['objects'])) {
|
||||
$dynamicsObjects = $settings['feature_settings']['objects'];
|
||||
} else {
|
||||
$settings = $this->settings->getFeatureSettings();
|
||||
$dynamicsObjects = $settings['objects'] ?? ['contacts'];
|
||||
}
|
||||
try {
|
||||
if ($this->isAuthorized()) {
|
||||
if (!empty($dynamicsObjects) && is_array($dynamicsObjects)) {
|
||||
foreach ($dynamicsObjects as $dynamicsObject) {
|
||||
// Check the cache first
|
||||
$settings['cache_suffix'] = $cacheSuffix = '.'.$dynamicsObject;
|
||||
if ($fields = parent::getAvailableLeadFields($settings)) {
|
||||
$dynamicsFields[$dynamicsObject] = $fields;
|
||||
continue;
|
||||
}
|
||||
$leadObject = $this->getApiHelper()->getLeadFields($dynamicsObject);
|
||||
if (null === $leadObject || !array_key_exists('value', $leadObject)) {
|
||||
return [];
|
||||
}
|
||||
$fields = $leadObject['value'];
|
||||
foreach ($fields as $field) {
|
||||
$type = 'string';
|
||||
$fieldType = $field['AttributeTypeName']['Value'];
|
||||
if (in_array($fieldType, [
|
||||
'LookupType',
|
||||
'OwnerType',
|
||||
'PicklistType',
|
||||
'StateType',
|
||||
'StatusType',
|
||||
'UniqueidentifierType',
|
||||
], true)) {
|
||||
continue;
|
||||
} elseif (in_array($fieldType, [
|
||||
'DoubleType',
|
||||
'IntegerType',
|
||||
'MoneyType',
|
||||
], true)) {
|
||||
$type = 'int';
|
||||
} elseif ('Boolean' === $fieldType) {
|
||||
$type = 'boolean';
|
||||
} elseif ('DateTimeType' === $fieldType) {
|
||||
$type = 'datetime';
|
||||
}
|
||||
$dynamicsFields[$dynamicsObject][$field['LogicalName']] = [
|
||||
'type' => $type,
|
||||
'label' => $field['DisplayName']['UserLocalizedLabel']['Label'],
|
||||
'dv' => $field['LogicalName'],
|
||||
'required' => 'ApplicationRequired' === $field['RequiredLevel']['Value'],
|
||||
];
|
||||
}
|
||||
$this->cache->set('leadFields'.$cacheSuffix, $dynamicsFields[$dynamicsObject]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ApiErrorException $exception) {
|
||||
$this->logIntegrationError($exception);
|
||||
if (!$silenceExceptions) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return $dynamicsFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $lead
|
||||
* @param array $config
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public function pushLead($lead, $config = [])
|
||||
{
|
||||
$config = $this->mergeConfigToFeatureSettings($config);
|
||||
|
||||
if (empty($config['leadFields'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$mappedData = $this->populateLeadData($lead, $config, 'contacts');
|
||||
|
||||
$this->amendLeadDataBeforePush($mappedData);
|
||||
|
||||
if (empty($mappedData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->isAuthorized()) {
|
||||
$object = 'contacts';
|
||||
/** @var IntegrationEntityRepository $integrationEntityRepo */
|
||||
$integrationEntityRepo = $this->em->getRepository(IntegrationEntity::class);
|
||||
$integrationId = $integrationEntityRepo->getIntegrationsEntityId('Dynamics', $object, 'lead', $lead->getId());
|
||||
if (!empty($integrationId)) {
|
||||
$integrationEntityId = $integrationId[0]['integration_entity_id'];
|
||||
$this->getApiHelper()->updateLead($mappedData, $integrationEntityId);
|
||||
|
||||
return $integrationEntityId;
|
||||
}
|
||||
/** @var ResponseInterface $response */
|
||||
$response = $this->getApiHelper()->createLead($mappedData, $lead);
|
||||
// OData-EntityId: https://clientname.crm.dynamics.com/api/data/v8.2/contacts(9844333b-c955-e711-80f1-c4346bad526c)
|
||||
$header = $response->getHeader('OData-EntityId');
|
||||
if (preg_match('/contacts\((.+)\)/', $header, $out)) {
|
||||
$id = $out[1];
|
||||
if (empty($integrationId)) {
|
||||
$integrationEntity = new IntegrationEntity();
|
||||
$integrationEntity->setDateAdded(new \DateTime());
|
||||
$integrationEntity->setIntegration('Dynamics');
|
||||
$integrationEntity->setIntegrationEntity($object);
|
||||
$integrationEntity->setIntegrationEntityId($id);
|
||||
$integrationEntity->setInternalEntity('lead');
|
||||
$integrationEntity->setInternalEntityId($lead->getId());
|
||||
} else {
|
||||
$integrationEntity = $integrationEntityRepo->getEntity($integrationId[0]['id']);
|
||||
}
|
||||
$integrationEntity->setLastSyncDate(new \DateTime());
|
||||
$this->em->persist($integrationEntity);
|
||||
$this->em->flush($integrationEntity);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @param array|null $query
|
||||
*/
|
||||
public function getLeads($params = [], $query = null, &$executed = null, $result = [], $object = 'contacts'): int
|
||||
{
|
||||
if ('Contacts' === $object) {
|
||||
$object = 'contacts';
|
||||
}
|
||||
$executed = 0;
|
||||
$MAX_RECORDS = 200; // Default max records is 5000
|
||||
try {
|
||||
if ($this->isAuthorized()) {
|
||||
$config = $this->mergeConfigToFeatureSettings();
|
||||
$fields = $config['leadFields'];
|
||||
$config['object'] = $object;
|
||||
$aFields = $this->getAvailableLeadFields($config);
|
||||
$mappedData = [];
|
||||
foreach (array_keys($fields) as $k) {
|
||||
if (isset($aFields[$object][$k])) {
|
||||
$mappedData[] = $aFields[$object][$k]['dv'];
|
||||
}
|
||||
}
|
||||
$oparams['request_settings']['headers']['Prefer'] = 'odata.maxpagesize='.$MAX_RECORDS;
|
||||
$oparams['$select'] = implode(',', $mappedData);
|
||||
if (isset($params['fetchAll'], $params['start']) && !$params['fetchAll']) {
|
||||
$oparams['$filter'] = sprintf('modifiedon ge %sZ', substr($params['start'], 0, -6));
|
||||
}
|
||||
|
||||
if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
|
||||
$progress = new ProgressBar($params['output']);
|
||||
$progress->start();
|
||||
}
|
||||
|
||||
while (true) {
|
||||
$data = $this->getApiHelper()->getLeads($oparams);
|
||||
|
||||
if (!isset($data['value'])) {
|
||||
break; // no more data, exit loop
|
||||
}
|
||||
|
||||
$result = $this->amendLeadDataBeforeMauticPopulate($data, $object);
|
||||
$executed += array_key_exists('value', $data) ? count($data['value']) : count($result);
|
||||
|
||||
if (isset($params['output'])) {
|
||||
if ($params['output']->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
|
||||
$params['output']->writeln($result);
|
||||
} else {
|
||||
$progress->advance(count($result));
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($data['@odata.nextLink'])) {
|
||||
break; // default exit
|
||||
}
|
||||
|
||||
// prepare next loop
|
||||
$nextLink = $data['@odata.nextLink'];
|
||||
$oparams['$skiptoken'] = urldecode(substr($nextLink, strpos($nextLink, '$skiptoken=') + 11));
|
||||
}
|
||||
|
||||
if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
|
||||
$progress->finish();
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
|
||||
return $executed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
*/
|
||||
public function getCompanies($params = []): int
|
||||
{
|
||||
$executed = 0;
|
||||
$MAX_RECORDS = 200; // Default max records is 5000
|
||||
$object = 'company';
|
||||
try {
|
||||
if ($this->isAuthorized()) {
|
||||
$config = $this->mergeConfigToFeatureSettings();
|
||||
$fields = $config['companyFields'];
|
||||
$config['object'] = $object;
|
||||
$aFields = $this->getAvailableLeadFields($config);
|
||||
$mappedData = [];
|
||||
if (isset($aFields['company'])) {
|
||||
$aFields = $aFields['company'];
|
||||
}
|
||||
foreach (array_keys($fields) as $k) {
|
||||
$mappedData[] = $aFields[$k]['dv'];
|
||||
}
|
||||
$oparams['request_settings']['headers']['Prefer'] = 'odata.maxpagesize='.$MAX_RECORDS;
|
||||
$oparams['$select'] = implode(',', $mappedData);
|
||||
if (isset($params['fetchAll'], $params['start']) && !$params['fetchAll']) {
|
||||
$oparams['$filter'] = sprintf('modifiedon ge %sZ', substr($params['start'], 0, -6));
|
||||
}
|
||||
|
||||
if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
|
||||
$progress = new ProgressBar($params['output']);
|
||||
$progress->start();
|
||||
}
|
||||
|
||||
while (true) {
|
||||
$data = $this->getApiHelper()->getCompanies($oparams);
|
||||
if (!isset($data['value'])) {
|
||||
break; // no more data, exit loop
|
||||
}
|
||||
|
||||
$result = $this->amendLeadDataBeforeMauticPopulate($data, $object);
|
||||
$executed += count($result);
|
||||
|
||||
if (isset($params['output'])) {
|
||||
if ($params['output']->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
|
||||
$params['output']->writeln($result);
|
||||
} else {
|
||||
$progress->advance(count($result));
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($data['@odata.nextLink'])) {
|
||||
break; // default exit
|
||||
}
|
||||
|
||||
// prepare next loop
|
||||
$nextLink = $data['@odata.nextLink'];
|
||||
$oparams['$skiptoken'] = urldecode(substr($nextLink, strpos($nextLink, '$skiptoken=') + 11));
|
||||
}
|
||||
|
||||
if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
|
||||
$progress->finish();
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
|
||||
return $executed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amend mapped lead data before creating to Mautic.
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $object
|
||||
*/
|
||||
public function amendLeadDataBeforeMauticPopulate($data, $object = null): array
|
||||
{
|
||||
if ('company' === $object) {
|
||||
$object = 'accounts';
|
||||
} elseif ('Lead' === $object || 'Contact' === $object) {
|
||||
$object = 'contacts';
|
||||
}
|
||||
|
||||
$config = $this->mergeConfigToFeatureSettings([]);
|
||||
|
||||
$result = [];
|
||||
if (isset($data['value'])) {
|
||||
$this->em->getConnection()->getConfiguration()->setMiddlewares([]);
|
||||
$entity = null;
|
||||
/** @var IntegrationEntityRepository $integrationEntityRepo */
|
||||
$integrationEntityRepo = $this->em->getRepository(IntegrationEntity::class);
|
||||
$objects = $data['value'];
|
||||
$integrationEntities = [];
|
||||
/** @var array $objects */
|
||||
foreach ($objects as $entityData) {
|
||||
$isModified = false;
|
||||
if ('accounts' === $object) {
|
||||
$recordId = $entityData['accountid'];
|
||||
// first try to find integration entity
|
||||
$integrationId = $integrationEntityRepo->getIntegrationsEntityId('Dynamics', $object, 'company',
|
||||
null, null, null, false, 0, 0, "'".$recordId."'");
|
||||
if (count($integrationId)) { // company exists, then update local fields
|
||||
/** @var Company $entity */
|
||||
$entity = $this->companyModel->getEntity($integrationId[0]['internal_entity_id']);
|
||||
$matchedFields = $this->populateMauticLeadData($entityData, $config, 'company');
|
||||
|
||||
// Match that data with mapped lead fields
|
||||
$fieldsToUpdateInMautic = $this->getPriorityFieldsForMautic($config, $object, 'mautic_company');
|
||||
if (!empty($fieldsToUpdateInMautic)) {
|
||||
$fieldsToUpdateInMautic = array_intersect_key($config['companyFields'], array_flip($fieldsToUpdateInMautic));
|
||||
$newMatchedFields = array_intersect_key($matchedFields, array_flip($fieldsToUpdateInMautic));
|
||||
} else {
|
||||
$newMatchedFields = $matchedFields;
|
||||
}
|
||||
if (!isset($newMatchedFields['companyname'])) {
|
||||
if (isset($newMatchedFields['companywebsite'])) {
|
||||
$newMatchedFields['companyname'] = $newMatchedFields['companywebsite'];
|
||||
}
|
||||
}
|
||||
|
||||
// update values if already empty
|
||||
foreach ($matchedFields as $field => $value) {
|
||||
if (empty($entity->getFieldValue($field))) {
|
||||
$newMatchedFields[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// remove unchanged fields
|
||||
foreach ($newMatchedFields as $k => $v) {
|
||||
if ($entity->getFieldValue($k) === $v) {
|
||||
unset($newMatchedFields[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($newMatchedFields)) {
|
||||
$this->companyModel->setFieldValues($entity, $newMatchedFields, false);
|
||||
$this->companyModel->saveEntity($entity, false);
|
||||
$isModified = true;
|
||||
}
|
||||
} else {
|
||||
$entity = $this->getMauticCompany($entityData, 'company');
|
||||
}
|
||||
if ($entity) {
|
||||
$result[] = $entity->getName();
|
||||
}
|
||||
$mauticObjectReference = 'company';
|
||||
} elseif ('contacts' === $object) {
|
||||
$recordId = $entityData['contactid'];
|
||||
// first try to find integration entity
|
||||
$integrationId = $integrationEntityRepo->getIntegrationsEntityId('Dynamics', $object, 'lead',
|
||||
null, null, null, false, 0, 0, "'".$recordId."'");
|
||||
if (count($integrationId)) { // lead exists, then update
|
||||
/** @var Lead $entity */
|
||||
$entity = $this->leadModel->getEntity($integrationId[0]['internal_entity_id']);
|
||||
$matchedFields = $this->populateMauticLeadData($entityData, $config);
|
||||
|
||||
// Match that data with mapped lead fields
|
||||
$fieldsToUpdateInMautic = $this->getPriorityFieldsForMautic($config, $object, 'mautic');
|
||||
if (!empty($fieldsToUpdateInMautic)) {
|
||||
$fieldsToUpdateInMautic = array_intersect_key($config['leadFields'] ?? [], array_flip($fieldsToUpdateInMautic));
|
||||
$newMatchedFields = array_intersect_key($matchedFields, array_flip($fieldsToUpdateInMautic));
|
||||
} else {
|
||||
$newMatchedFields = $matchedFields;
|
||||
}
|
||||
|
||||
// update values if already empty
|
||||
foreach ($matchedFields as $field => $value) {
|
||||
if (empty($entity->getFieldValue($field))) {
|
||||
$newMatchedFields[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// remove unchanged fields
|
||||
foreach ($newMatchedFields as $k => $v) {
|
||||
if ($entity->getFieldValue($k) === $v) {
|
||||
unset($newMatchedFields[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($newMatchedFields)) {
|
||||
$this->leadModel->setFieldValues($entity, $newMatchedFields, false, false);
|
||||
$this->leadModel->saveEntity($entity, false);
|
||||
$isModified = true;
|
||||
}
|
||||
} else {
|
||||
/** @var Lead $entity */
|
||||
$entity = $this->getMauticLead($entityData);
|
||||
}
|
||||
|
||||
if ($entity) {
|
||||
$result[] = $entity->getEmail();
|
||||
}
|
||||
|
||||
// Associate lead company
|
||||
if (!empty($entityData['parentcustomerid']) // company
|
||||
&& $entityData['parentcustomerid'] !== $this->translator->trans(
|
||||
'mautic.integration.form.lead.unknown'
|
||||
)
|
||||
) {
|
||||
$company = IdentifyCompanyHelper::identifyLeadsCompany(
|
||||
['company' => $entityData['parentcustomerid']],
|
||||
null,
|
||||
$this->companyModel
|
||||
);
|
||||
|
||||
if (!empty($company[2])) {
|
||||
$syncLead = $this->companyModel->addLeadToCompany($company[2], $entity);
|
||||
$this->em->detach($company[2]);
|
||||
}
|
||||
}
|
||||
|
||||
$mauticObjectReference = 'lead';
|
||||
} else {
|
||||
$this->logIntegrationError(
|
||||
new \Exception(
|
||||
sprintf('Received an unexpected object "%s"', $object)
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($entity) {
|
||||
$integrationId = $integrationEntityRepo->getIntegrationsEntityId(
|
||||
'Dynamics',
|
||||
$object,
|
||||
$mauticObjectReference,
|
||||
$entity->getId()
|
||||
);
|
||||
|
||||
if (0 === count($integrationId)) {
|
||||
$integrationEntity = new IntegrationEntity();
|
||||
$integrationEntity->setDateAdded(new \DateTime());
|
||||
$integrationEntity->setIntegration('Dynamics');
|
||||
$integrationEntity->setIntegrationEntity($object);
|
||||
$integrationEntity->setIntegrationEntityId($recordId);
|
||||
$integrationEntity->setInternalEntity($mauticObjectReference);
|
||||
$integrationEntity->setInternalEntityId($entity->getId());
|
||||
$integrationEntities[] = $integrationEntity;
|
||||
} else {
|
||||
$integrationEntity = $integrationEntityRepo->getEntity($integrationId[0]['id']);
|
||||
if ($isModified) {
|
||||
$integrationEntity->setLastSyncDate(new \DateTime());
|
||||
$integrationEntities[] = $integrationEntity;
|
||||
}
|
||||
}
|
||||
$this->em->detach($entity);
|
||||
unset($entity);
|
||||
}
|
||||
}
|
||||
|
||||
$integrationEntityRepo->saveEntities($integrationEntities);
|
||||
$this->em->clear();
|
||||
|
||||
unset($integrationEntityRepo, $integrationEntities);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function pushLeads($params = []): array
|
||||
{
|
||||
$MAX_RECORDS = (isset($params['limit']) && $params['limit'] < 100) ? $params['limit'] : 100;
|
||||
if (isset($params['fetchAll']) && $params['fetchAll']) {
|
||||
$params['start'] = null;
|
||||
$params['end'] = null;
|
||||
}
|
||||
$object = 'contacts';
|
||||
$config = $this->mergeConfigToFeatureSettings();
|
||||
$integrationEntityRepo = $this->em->getRepository(IntegrationEntity::class);
|
||||
$fieldsToUpdateInCrm = isset($config['update_mautic']) ? array_keys($config['update_mautic'], 0) : [];
|
||||
$leadFields = array_unique(array_values($config['leadFields'] ?? []));
|
||||
$totalUpdated = $totalCreated = $totalErrors = 0;
|
||||
|
||||
if ($key = array_search('mauticContactTimelineLink', $leadFields)) {
|
||||
unset($leadFields[$key]);
|
||||
}
|
||||
if ($key = array_search('mauticContactIsContactableByEmail', $leadFields)) {
|
||||
unset($leadFields[$key]);
|
||||
}
|
||||
|
||||
if (empty($leadFields)) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
$fields = implode(', l.', $leadFields);
|
||||
$fields = 'l.'.$fields;
|
||||
|
||||
$availableFields = $this->getAvailableLeadFields(['feature_settings' => ['objects' => [$object]]]);
|
||||
$fieldsToUpdate[$object] = array_values(array_intersect(array_keys($availableFields[$object]), $fieldsToUpdateInCrm));
|
||||
$fieldsToUpdate[$object] = array_intersect_key($config['leadFields'] ?? [], array_flip($fieldsToUpdate[$object]));
|
||||
|
||||
$progress = false;
|
||||
$totalToUpdate = array_sum($integrationEntityRepo->findLeadsToUpdate('Dynamics', 'lead', $fields, 0, $params['start'], $params['end'], [$object]));
|
||||
$totalToCreate = $integrationEntityRepo->findLeadsToCreate('Dynamics', $fields, 0, $params['start'], $params['end']);
|
||||
$totalToCreate = is_array($totalToCreate) ? count($totalToCreate) : (int) $totalToCreate;
|
||||
$totalCount = $totalToCreate + $totalToUpdate;
|
||||
|
||||
if (defined('IN_MAUTIC_CONSOLE')) {
|
||||
// start with update
|
||||
if ($totalToUpdate + $totalToCreate) {
|
||||
$output = new ConsoleOutput();
|
||||
$output->writeln("About $totalToUpdate to update and about $totalToCreate to create/update");
|
||||
$output->writeln('<info>This could take some time. Please wait until the process is completed</info>');
|
||||
$progress = new ProgressBar($output, $totalCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Start with contacts so we know who is a contact when we go to process converted leads
|
||||
$leadsToCreateInD = [];
|
||||
$leadsToUpdateInD = [];
|
||||
$integrationEntities = [];
|
||||
|
||||
$toUpdate = $integrationEntityRepo->findLeadsToUpdate('Dynamics', 'lead', $fields, $totalToUpdate, $params['start'], $params['end'], $object, [])[$object];
|
||||
|
||||
if (is_array($toUpdate)) {
|
||||
$totalUpdated += count($toUpdate);
|
||||
foreach ($toUpdate as $lead) {
|
||||
if (isset($lead['email']) && !empty($lead['email'])) {
|
||||
$key = mb_strtolower($this->cleanPushData($lead['email']));
|
||||
$lead = $this->getCompoundMauticFields($lead);
|
||||
$lead['integration_entity'] = $object;
|
||||
$leadsToUpdateInD[$key] = $lead;
|
||||
$integrationEntity = $this->em->getReference(IntegrationEntity::class, $lead['id']);
|
||||
$integrationEntities[] = $integrationEntity->setLastSyncDate(new \DateTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($toUpdate);
|
||||
|
||||
// create lead records, including deleted on D side (last_sync = null)
|
||||
/** @var array $leadsToCreate */
|
||||
$leadsToCreate = $integrationEntityRepo->findLeadsToCreate('Dynamics', $fields, $totalToCreate, $params['start'], $params['end']);
|
||||
if (is_array($leadsToCreate)) {
|
||||
$totalCreated += count($leadsToCreate);
|
||||
foreach ($leadsToCreate as $lead) {
|
||||
if (isset($lead['email']) && !empty($lead['email'])) {
|
||||
$key = mb_strtolower($this->cleanPushData($lead['email']));
|
||||
$lead = $this->getCompoundMauticFields($lead);
|
||||
$lead['integration_entity'] = $object;
|
||||
$leadsToCreateInD[$key] = $lead;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($leadsToCreate);
|
||||
|
||||
if (count($integrationEntities)) {
|
||||
// Persist updated entities if applicable
|
||||
$integrationEntityRepo->saveEntities($integrationEntities);
|
||||
$this->integrationEntityModel->getRepository()->detachEntities($integrationEntities);
|
||||
}
|
||||
|
||||
// update contacts
|
||||
$leadData = [];
|
||||
$rowNum = 0;
|
||||
foreach ($leadsToUpdateInD as $lead) {
|
||||
$mappedData = [];
|
||||
if (defined('IN_MAUTIC_CONSOLE') && $progress) {
|
||||
$progress->advance();
|
||||
}
|
||||
$existingPerson = $this->getExistingRecord('emailaddress1', $lead['email'], $object);
|
||||
|
||||
$objectFields = $this->prepareFieldsForPush($availableFields[$object]);
|
||||
$fieldsToUpdate[$object] = $this->getBlankFieldsToUpdate($fieldsToUpdate[$object], $existingPerson, $objectFields, $config);
|
||||
|
||||
// Match that data with mapped lead fields
|
||||
foreach ($fieldsToUpdate[$object] as $k => $v) {
|
||||
foreach ($lead as $dk => $dv) {
|
||||
if ($v === $dk) {
|
||||
if ($dv) {
|
||||
if (isset($availableFields[$object][$k])) {
|
||||
$mappedData[$availableFields[$object][$k]['dv']] = $dv;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$leadData[$lead['integration_entity_id']] = $mappedData;
|
||||
|
||||
++$rowNum;
|
||||
// SEND 100 RECORDS AT A TIME
|
||||
if ($MAX_RECORDS === $rowNum) {
|
||||
$this->getApiHelper()->updateLeads($leadData, $object);
|
||||
$leadData = [];
|
||||
$rowNum = 0;
|
||||
}
|
||||
}
|
||||
$this->getApiHelper()->updateLeads($leadData, $object);
|
||||
|
||||
// create contacts
|
||||
$leadData = [];
|
||||
$rowNum = 0;
|
||||
foreach ($leadsToCreateInD as $lead) {
|
||||
$mappedData = [];
|
||||
if (defined('IN_MAUTIC_CONSOLE') && $progress) {
|
||||
$progress->advance();
|
||||
}
|
||||
if (!isset($config['leadFields']) || !is_iterable($config['leadFields'])) {
|
||||
continue;
|
||||
}
|
||||
// Match that data with mapped lead fields
|
||||
foreach ($config['leadFields'] as $k => $v) {
|
||||
foreach ($lead as $dk => $dv) {
|
||||
if ($v === $dk) {
|
||||
if ($dv) {
|
||||
if (isset($availableFields[$object][$k])) {
|
||||
$mappedData[$availableFields[$object][$k]['dv']] = $dv;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$leadData[$lead['internal_entity_id']] = $mappedData;
|
||||
|
||||
++$rowNum;
|
||||
// SEND 100 RECORDS AT A TIME
|
||||
if ($MAX_RECORDS === $rowNum) {
|
||||
$ids = $this->getApiHelper()->createLeads($leadData, $object);
|
||||
$this->createIntegrationEntities($ids, $object, $integrationEntityRepo);
|
||||
$leadData = [];
|
||||
$rowNum = 0;
|
||||
}
|
||||
}
|
||||
$ids = $this->getApiHelper()->createLeads($leadData, $object);
|
||||
$this->createIntegrationEntities($ids, $object, $integrationEntityRepo);
|
||||
|
||||
if ($progress) {
|
||||
$progress->finish();
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
return [$totalUpdated, $totalCreated, $totalErrors];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $ids
|
||||
* @param IntegrationEntityRepository $integrationEntityRepo
|
||||
*/
|
||||
private function createIntegrationEntities($ids, $object, $integrationEntityRepo): void
|
||||
{
|
||||
foreach ($ids as $oid => $leadId) {
|
||||
$this->logger->debug('CREATE INTEGRATION ENTITY: '.$oid);
|
||||
$integrationId = $integrationEntityRepo->getIntegrationsEntityId('Dynamics', $object,
|
||||
'lead', null, null, null, false, 0, 0,
|
||||
"'".$oid."'"
|
||||
);
|
||||
|
||||
if (0 === count($integrationId)) {
|
||||
$this->createIntegrationEntity($object, $oid, 'lead', $leadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getExistingRecord($seachColumn, $searchValue, $object = 'contacts')
|
||||
{
|
||||
$availableFields = $this->getAvailableLeadFields();
|
||||
$oparams['$select'] = implode(',', array_keys($availableFields[$object]));
|
||||
$oparams['$filter'] = $seachColumn.' eq \''.$searchValue.'\'';
|
||||
$data = $this->getApiHelper()->getLeads($oparams);
|
||||
|
||||
return (isset($data['value'][0]) && !empty($data['value'][0])) ? $data['value'][0] : [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\ArrayHelper;
|
||||
use Mautic\CoreBundle\Helper\CacheStorageHelper;
|
||||
use Mautic\CoreBundle\Helper\EncryptionHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\NotificationModel;
|
||||
use Mautic\LeadBundle\DataObject\LeadManipulator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\StagesChangeLog;
|
||||
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\DoNotContact;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
|
||||
use Mautic\PluginBundle\Model\IntegrationEntityModel;
|
||||
use Mautic\StageBundle\Entity\Stage;
|
||||
use MauticPlugin\MauticCrmBundle\Api\HubspotApi;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @method HubspotApi getApiHelper()
|
||||
*/
|
||||
class HubspotIntegration extends CrmAbstractIntegration
|
||||
{
|
||||
public const ACCESS_KEY = 'accessKey';
|
||||
|
||||
public function __construct(
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
CacheStorageHelper $cacheStorageHelper,
|
||||
EntityManager $entityManager,
|
||||
RequestStack $requestStack,
|
||||
RouterInterface $router,
|
||||
TranslatorInterface $translator,
|
||||
LoggerInterface $logger,
|
||||
EncryptionHelper $encryptionHelper,
|
||||
LeadModel $leadModel,
|
||||
CompanyModel $companyModel,
|
||||
PathsHelper $pathsHelper,
|
||||
NotificationModel $notificationModel,
|
||||
FieldModel $fieldModel,
|
||||
IntegrationEntityModel $integrationEntityModel,
|
||||
DoNotContact $doNotContact,
|
||||
FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
|
||||
protected UserHelper $userHelper,
|
||||
) {
|
||||
parent::__construct(
|
||||
$eventDispatcher,
|
||||
$cacheStorageHelper,
|
||||
$entityManager,
|
||||
$requestStack,
|
||||
$router,
|
||||
$translator,
|
||||
$logger,
|
||||
$encryptionHelper,
|
||||
$leadModel,
|
||||
$companyModel,
|
||||
$pathsHelper,
|
||||
$notificationModel,
|
||||
$fieldModel,
|
||||
$integrationEntityModel,
|
||||
$doNotContact,
|
||||
$fieldsWithUniqueIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Hubspot';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRequiredKeyFields(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getApiKey(): string
|
||||
{
|
||||
return 'hapikey';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array key for the auth token.
|
||||
*/
|
||||
public function getAuthTokenKey(): string
|
||||
{
|
||||
return 'hapikey';
|
||||
}
|
||||
|
||||
public function getSupportedFeatures(): array
|
||||
{
|
||||
return ['push_lead', 'get_leads'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $inAuthorization
|
||||
*
|
||||
* @return mixed|string|null
|
||||
*/
|
||||
public function getBearerToken($inAuthorization = false)
|
||||
{
|
||||
$tokenData = $this->getKeys();
|
||||
|
||||
return $tokenData[self::ACCESS_KEY] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public function getFormSettings(): array
|
||||
{
|
||||
return [
|
||||
'requires_callback' => false,
|
||||
'requires_authorization' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public function getAuthenticationType(): string
|
||||
{
|
||||
return $this->getBearerToken() ? 'oauth2' : 'key';
|
||||
}
|
||||
|
||||
public function getApiUrl(): string
|
||||
{
|
||||
return 'https://api.hubapi.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if data priority is enabled in the integration or not default is false.
|
||||
*/
|
||||
public function getDataPriority(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available company fields for choices in the config UI.
|
||||
*
|
||||
* @param array $settings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFormCompanyFields($settings = [])
|
||||
{
|
||||
return $this->getFormFieldsByObject('company', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $settings
|
||||
*
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function getFormLeadFields($settings = [])
|
||||
{
|
||||
return $this->getFormFieldsByObject('contacts', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getAvailableLeadFields($settings = []): array
|
||||
{
|
||||
if ($fields = parent::getAvailableLeadFields()) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$hubsFields = [];
|
||||
$silenceExceptions = $settings['silence_exceptions'] ?? true;
|
||||
|
||||
if (isset($settings['feature_settings']['objects'])) {
|
||||
$hubspotObjects = $settings['feature_settings']['objects'];
|
||||
} else {
|
||||
$settings = $this->settings->getFeatureSettings();
|
||||
$hubspotObjects = $settings['objects'] ?? ['contacts'];
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->isAuthorized()) {
|
||||
if (!empty($hubspotObjects) and is_array($hubspotObjects)) {
|
||||
foreach ($hubspotObjects as $object) {
|
||||
// Check the cache first
|
||||
$settings['cache_suffix'] = $cacheSuffix = '.'.$object;
|
||||
if ($fields = parent::getAvailableLeadFields($settings)) {
|
||||
$hubsFields[$object] = $fields;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$leadFields = $this->getApiHelper()->getLeadFields($object);
|
||||
if (isset($leadFields)) {
|
||||
foreach ($leadFields as $fieldInfo) {
|
||||
$hubsFields[$object][$fieldInfo['name']] = [
|
||||
'type' => 'string',
|
||||
'label' => $fieldInfo['label'],
|
||||
'required' => ('email' === $fieldInfo['name']),
|
||||
];
|
||||
if (!empty($fieldInfo['readOnlyValue'])) {
|
||||
$hubsFields[$object][$fieldInfo['name']]['update_mautic'] = 1;
|
||||
$hubsFields[$object][$fieldInfo['name']]['readOnly'] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->cache->set('leadFields'.$cacheSuffix, $hubsFields[$object]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logIntegrationError($e);
|
||||
|
||||
if (!$silenceExceptions) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return $hubsFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $objects
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function cleanPriorityFields($fieldsToUpdate, $objects = null)
|
||||
{
|
||||
if (null === $objects) {
|
||||
$objects = ['Leads', 'Contacts'];
|
||||
}
|
||||
|
||||
if (isset($fieldsToUpdate['leadFields'])) {
|
||||
// Pass in the whole config
|
||||
$fields = $fieldsToUpdate['leadFields'];
|
||||
} else {
|
||||
$fields = array_flip($fieldsToUpdate);
|
||||
}
|
||||
|
||||
return $this->prepareFieldsForSync($fields, $fieldsToUpdate, $objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the lead data to the structure that HubSpot requires for the createOrUpdate request.
|
||||
*
|
||||
* @param array $leadData All the lead fields mapped
|
||||
*/
|
||||
public function formatLeadDataForCreateOrUpdate($leadData, $lead, $updateLink = false): array
|
||||
{
|
||||
$formattedLeadData = [];
|
||||
|
||||
if (!$updateLink) {
|
||||
foreach ($leadData as $field => $value) {
|
||||
if ('lifecyclestage' == $field || 'associatedcompanyid' == $field) {
|
||||
continue;
|
||||
}
|
||||
$formattedLeadData['properties'][] = [
|
||||
'property' => $field,
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $formattedLeadData;
|
||||
}
|
||||
|
||||
public function isAuthorized(): bool
|
||||
{
|
||||
$keys = $this->getKeys();
|
||||
|
||||
return isset($keys[$this->getAuthTokenKey()]) || isset($keys[self::ACCESS_KEY]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getHubSpotApiKey()
|
||||
{
|
||||
$tokenData = $this->getKeys();
|
||||
|
||||
return $tokenData[$this->getAuthTokenKey()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param mixed[] $data
|
||||
* @param string $formArea
|
||||
*/
|
||||
public function appendToForm(&$builder, $data, $formArea): void
|
||||
{
|
||||
if ('keys' === $formArea) {
|
||||
$builder->add(
|
||||
self::ACCESS_KEY,
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.hubspot.form.accessKey',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
$this->getApiKey(),
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.hubspot.form.apikey',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'readonly' => true,
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
if ('features' == $formArea) {
|
||||
$builder->add(
|
||||
'objects',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'choices' => [
|
||||
'mautic.hubspot.object.contact' => 'contacts',
|
||||
'mautic.hubspot.object.company' => 'company',
|
||||
],
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'label' => $this->getTranslator()->trans('mautic.crm.form.objects_to_pull_from', ['%crm%' => 'Hubspot']),
|
||||
'label_attr' => ['class' => ''],
|
||||
'placeholder' => false,
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function amendLeadDataBeforeMauticPopulate($data, $object)
|
||||
{
|
||||
if (!isset($data['properties'])) {
|
||||
return [];
|
||||
}
|
||||
foreach ($data['properties'] as $key => $field) {
|
||||
$value = str_replace(';', '|', $field['value']);
|
||||
$fieldsValues[$key] = $value;
|
||||
}
|
||||
if ('Lead' == $object && !isset($fieldsValues['email'])) {
|
||||
foreach ($data['identity-profiles'][0]['identities'] as $identifiedProfile) {
|
||||
if ('EMAIL' == $identifiedProfile['type']) {
|
||||
$fieldsValues['email'] = $identifiedProfile['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $fieldsValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @param array $result
|
||||
* @param string $object
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getLeads($params = [], $query = null, &$executed = null, $result = [], $object = 'Lead')
|
||||
{
|
||||
if (!is_array($executed)) {
|
||||
$executed = [
|
||||
0 => 0,
|
||||
1 => 0,
|
||||
];
|
||||
}
|
||||
try {
|
||||
if ($this->isAuthorized()) {
|
||||
$config = $this->mergeConfigToFeatureSettings();
|
||||
$fields = implode('&property=', array_keys($config['leadFields'] ?? []));
|
||||
$params['post_append_to_query'] = '&property='.$fields.'&property=lifecyclestage';
|
||||
$params['Count'] = 100;
|
||||
|
||||
$data = $this->getApiHelper()->getContacts($params);
|
||||
if (isset($data['contacts'])) {
|
||||
foreach ($data['contacts'] as $contact) {
|
||||
if (is_array($contact)) {
|
||||
$contactData = $this->amendLeadDataBeforeMauticPopulate($contact, 'Lead');
|
||||
$contact = $this->getMauticLead($contactData);
|
||||
if ($contact && !$contact->isNewlyCreated()) { // updated
|
||||
$executed[0] = $executed[0] + 1;
|
||||
} elseif ($contact && $contact->isNewlyCreated()) { // newly created
|
||||
$executed[1] = $executed[1] + 1;
|
||||
}
|
||||
|
||||
if ($contact) {
|
||||
$this->em->detach($contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($data['has-more']) {
|
||||
$params['vidOffset'] = $data['vid-offset'];
|
||||
$params['timeOffset'] = $data['time-offset'];
|
||||
|
||||
$this->getLeads($params, $query, $executed);
|
||||
}
|
||||
}
|
||||
|
||||
return $executed;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
|
||||
return $executed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @param bool $id
|
||||
*/
|
||||
public function getCompanies($params = [], $id = false, &$executed = null)
|
||||
{
|
||||
$results = [];
|
||||
try {
|
||||
if ($this->isAuthorized()) {
|
||||
$params['Count'] = 100;
|
||||
$data = $this->getApiHelper()->getCompanies($params, $id);
|
||||
if ($id) {
|
||||
$results['results'][] = array_merge($results, $data);
|
||||
} else {
|
||||
$results['results'] = array_merge($results, $data['results']);
|
||||
}
|
||||
|
||||
foreach ($results['results'] as $company) {
|
||||
if (isset($company['properties'])) {
|
||||
$companyData = $this->amendLeadDataBeforeMauticPopulate($company, null);
|
||||
$company = $this->getMauticCompany($companyData);
|
||||
if ($id) {
|
||||
return $company;
|
||||
}
|
||||
if ($company) {
|
||||
++$executed;
|
||||
$this->em->detach($company);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($data['hasMore']) and $data['hasMore']) {
|
||||
$params['offset'] = $data['offset'];
|
||||
if ($params['offset'] < strtotime($params['start'])) {
|
||||
$this->getCompanies($params, $id, $executed);
|
||||
}
|
||||
}
|
||||
|
||||
return $executed;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logIntegrationError($e);
|
||||
}
|
||||
|
||||
return $executed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update existing Mautic lead from the integration's profile data.
|
||||
*
|
||||
* @param mixed $data Profile data from integration
|
||||
* @param bool|true $persist Set to false to not persist lead to the database in this method
|
||||
* @param array|null $socialCache
|
||||
* @param mixed|null $identifiers
|
||||
* @param string|null $object
|
||||
*
|
||||
* @return Lead
|
||||
*/
|
||||
public function getMauticLead($data, $persist = true, $socialCache = null, $identifiers = null, $object = null)
|
||||
{
|
||||
if (is_object($data)) {
|
||||
// Convert to array in all levels
|
||||
$data = json_encode(json_decode($data, true));
|
||||
} elseif (is_string($data)) {
|
||||
// Assume JSON
|
||||
$data = json_decode($data, true);
|
||||
}
|
||||
|
||||
if (isset($data['lifecyclestage'])) {
|
||||
$stageName = $data['lifecyclestage'];
|
||||
unset($data['lifecyclestage']);
|
||||
}
|
||||
|
||||
if (isset($data['associatedcompanyid'])) {
|
||||
$company = $this->getCompanies([], $data['associatedcompanyid']);
|
||||
unset($data['associatedcompanyid']);
|
||||
}
|
||||
|
||||
if ($lead = parent::getMauticLead($data, false, $socialCache, $identifiers, $object)) {
|
||||
if (isset($stageName)) {
|
||||
$stage = $this->em->getRepository(Stage::class)->getStageByName($stageName);
|
||||
|
||||
if (empty($stage)) {
|
||||
$stage = new Stage();
|
||||
$stage->setName($stageName);
|
||||
$stages[$stageName] = $stage;
|
||||
}
|
||||
if (!$lead->getStage() && $lead->getStage() != $stage) {
|
||||
$lead->setStage($stage);
|
||||
|
||||
// add a contact stage change log
|
||||
$log = new StagesChangeLog();
|
||||
$log->setStage($stage);
|
||||
$log->setEventName($stage->getId().':'.$stage->getName());
|
||||
$log->setLead($lead);
|
||||
$log->setActionName(
|
||||
$this->translator->trans(
|
||||
'mautic.stage.import.action.name',
|
||||
[
|
||||
'%name%' => $this->userHelper->getUser()->getUserIdentifier(),
|
||||
]
|
||||
)
|
||||
);
|
||||
$log->setDateAdded(new \DateTime());
|
||||
$lead->stageChangeLog($log);
|
||||
}
|
||||
}
|
||||
|
||||
if ($persist && !empty($lead->getChanges(true))) {
|
||||
// Only persist if instructed to do so as it could be that calling code needs to manipulate the lead prior to executing event listeners
|
||||
try {
|
||||
$lead->setManipulator(new LeadManipulator(
|
||||
'plugin',
|
||||
$this->getName(),
|
||||
null,
|
||||
$this->getDisplayName()
|
||||
));
|
||||
$this->leadModel->saveEntity($lead, false);
|
||||
if (isset($company)) {
|
||||
$this->leadModel->addToCompany($lead, $company);
|
||||
$this->em->detach($company);
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->warning($exception->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $lead
|
||||
* @param array $config
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public function pushLead($lead, $config = [])
|
||||
{
|
||||
$config = $this->mergeConfigToFeatureSettings($config);
|
||||
|
||||
if (empty($config['leadFields'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$object = 'contacts';
|
||||
$createFields = $config['leadFields'];
|
||||
|
||||
$readOnlyFields = $this->getReadOnlyFields($object);
|
||||
|
||||
$createFields = array_filter(
|
||||
$createFields,
|
||||
function ($createField, $key) use ($readOnlyFields) {
|
||||
if (!isset($readOnlyFields[$key])) {
|
||||
return $createField;
|
||||
}
|
||||
},
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
|
||||
$mappedData = $this->populateLeadData(
|
||||
$lead,
|
||||
[
|
||||
'leadFields' => $createFields,
|
||||
'object' => $object,
|
||||
'feature_settings' => ['objects' => $config['objects']],
|
||||
]
|
||||
);
|
||||
$this->amendLeadDataBeforePush($mappedData);
|
||||
|
||||
if (empty($mappedData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isAuthorized()) {
|
||||
$leadData = $this->getApiHelper()->createLead($mappedData, $lead);
|
||||
|
||||
if (!empty($leadData['vid'])) {
|
||||
/** @var IntegrationEntityRepository $integrationEntityRepo */
|
||||
$integrationEntityRepo = $this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class);
|
||||
$integrationId = $integrationEntityRepo->getIntegrationsEntityId($this->getName(), $object, 'lead', $lead->getId());
|
||||
$integrationEntity = (empty($integrationId)) ?
|
||||
$this->createIntegrationEntity(
|
||||
$object,
|
||||
$leadData['vid'],
|
||||
'lead',
|
||||
$lead->getId(),
|
||||
[],
|
||||
false
|
||||
) : $integrationEntityRepo->getEntity($integrationId[0]['id']);
|
||||
|
||||
$integrationEntity->setLastSyncDate($this->getLastSyncDate());
|
||||
$this->getIntegrationEntityRepository()->saveEntity($integrationEntity);
|
||||
$this->em->detach($integrationEntity);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amend mapped lead data before pushing to CRM.
|
||||
*/
|
||||
public function amendLeadDataBeforePush(&$mappedData): void
|
||||
{
|
||||
foreach ($mappedData as &$data) {
|
||||
$data = str_replace('|', ';', $data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getReadOnlyFields($object): ?array
|
||||
{
|
||||
$fields = ArrayHelper::getValue($object, $this->getAvailableLeadFields(), []);
|
||||
|
||||
return array_filter(
|
||||
$fields,
|
||||
function ($field) {
|
||||
if (!empty($field['readOnly'])) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration\Salesforce\CampaignMember;
|
||||
|
||||
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Exception\InvalidObjectException;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Exception\NoObjectsToFetchException;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\CampaignMember;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\Contact;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\Lead;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\QueryBuilder;
|
||||
|
||||
class Fetcher
|
||||
{
|
||||
private array $leads = [];
|
||||
|
||||
private array $knownLeadIds = [];
|
||||
|
||||
private array $unknownLeadIds = [];
|
||||
|
||||
private array $contacts = [];
|
||||
|
||||
private array $knownContactIds = [];
|
||||
|
||||
private array $unknownContactIds = [];
|
||||
|
||||
private array $mauticIds = [];
|
||||
|
||||
private array $knownCampaignMembers = [];
|
||||
|
||||
/**
|
||||
* @param string|int $campaignId
|
||||
*/
|
||||
public function __construct(
|
||||
private IntegrationEntityRepository $repo,
|
||||
private Organizer $organizer,
|
||||
private $campaignId,
|
||||
) {
|
||||
$this->fetchLeads();
|
||||
$this->fetchContacts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return SF query to fetch the object information for a CampaignMember.
|
||||
*
|
||||
* @throws NoObjectsToFetchException
|
||||
* @throws InvalidObjectException
|
||||
*/
|
||||
public function getQueryForUnknownObjects(array $fields, $object): string
|
||||
{
|
||||
return match ($object) {
|
||||
Lead::OBJECT => QueryBuilder::getLeadQuery($fields, $this->unknownLeadIds),
|
||||
Contact::OBJECT => QueryBuilder::getContactQuery($fields, $this->unknownContactIds),
|
||||
default => throw new InvalidObjectException(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the Mautic contact IDs that are not already tracked as SF campaign members.
|
||||
*/
|
||||
public function getUnknownCampaignMembers(): array
|
||||
{
|
||||
// First, find those already tracked as part of this campaign
|
||||
$this->fetchCampaignMembers();
|
||||
|
||||
// Second, find newly created objects
|
||||
$this->fetchNewlyCreated();
|
||||
|
||||
$mauticLeadIds = array_map(
|
||||
fn ($entity) => $entity['internal_entity_id'],
|
||||
$this->knownCampaignMembers
|
||||
);
|
||||
|
||||
return array_values(array_diff($this->mauticIds, $mauticLeadIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch SF leads already identified.
|
||||
*/
|
||||
private function fetchLeads(): void
|
||||
{
|
||||
if (!$campaignMembers = $this->organizer->getLeadIds()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->leads = $this->repo->getIntegrationsEntityId(
|
||||
'Salesforce',
|
||||
Lead::OBJECT,
|
||||
'lead',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
$campaignMembers
|
||||
);
|
||||
|
||||
foreach ($this->leads as $lead) {
|
||||
$this->knownLeadIds[] = $lead['integration_entity_id'];
|
||||
$this->mauticIds[] = $lead['internal_entity_id'];
|
||||
}
|
||||
|
||||
$this->unknownLeadIds = array_values(array_diff($campaignMembers, $this->knownLeadIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch SF contacts already identified.
|
||||
*/
|
||||
private function fetchContacts(): void
|
||||
{
|
||||
if (!$campaignMembers = $this->organizer->getContactIds()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->contacts = $this->repo->getIntegrationsEntityId(
|
||||
'Salesforce',
|
||||
Contact::OBJECT,
|
||||
'lead',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
$campaignMembers
|
||||
);
|
||||
|
||||
foreach ($this->contacts as $contact) {
|
||||
$this->knownContactIds[] = $contact['integration_entity_id'];
|
||||
$this->mauticIds[] = $contact['internal_entity_id'];
|
||||
}
|
||||
|
||||
$this->unknownContactIds = array_values(array_diff($campaignMembers, $this->knownContactIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch SF campaign members already identified.
|
||||
*/
|
||||
private function fetchCampaignMembers(): void
|
||||
{
|
||||
if (!$this->mauticIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->knownCampaignMembers = $this->repo->getIntegrationsEntityId(
|
||||
'Salesforce',
|
||||
CampaignMember::OBJECT,
|
||||
'lead',
|
||||
$this->mauticIds,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
$this->campaignId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a list of all identified objects for SF contacts and leads.
|
||||
*/
|
||||
private function fetchNewlyCreated(): void
|
||||
{
|
||||
if (!$allUnknownContacts = array_merge($this->unknownLeadIds, $this->unknownContactIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$newlyCreated = $this->repo->getIntegrationsEntityId(
|
||||
'Salesforce',
|
||||
null,
|
||||
'lead',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
$allUnknownContacts
|
||||
);
|
||||
|
||||
foreach ($newlyCreated as $contact) {
|
||||
$this->knownContactIds[] = $contact['integration_entity_id'];
|
||||
$this->mauticIds[] = $contact['internal_entity_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration\Salesforce\CampaignMember;
|
||||
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\Contact;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\Lead;
|
||||
|
||||
class Organizer
|
||||
{
|
||||
/**
|
||||
* @var array<string, Lead>
|
||||
*/
|
||||
private array $leads = [];
|
||||
|
||||
/**
|
||||
* @var array<string, Contact>
|
||||
*/
|
||||
private array $contacts = [];
|
||||
|
||||
public function __construct(
|
||||
private array $records,
|
||||
) {
|
||||
$this->organize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Lead>
|
||||
*/
|
||||
public function getLeads()
|
||||
{
|
||||
return $this->leads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getLeadIds(): array
|
||||
{
|
||||
return array_keys($this->leads);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Contact>
|
||||
*/
|
||||
public function getContacts()
|
||||
{
|
||||
return $this->contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getContactIds(): array
|
||||
{
|
||||
return array_keys($this->contacts);
|
||||
}
|
||||
|
||||
private function organize(): void
|
||||
{
|
||||
foreach ($this->records as $campaignMember) {
|
||||
$object = !empty($campaignMember['LeadId']) ? 'Lead' : 'Contact';
|
||||
$objectId = !empty($campaignMember['LeadId']) ? $campaignMember['LeadId'] : $campaignMember['ContactId'];
|
||||
$isDeleted = ($campaignMember['IsDeleted']) ? true : false;
|
||||
|
||||
switch ($object) {
|
||||
case Lead::OBJECT:
|
||||
$this->leads[$objectId] = new Lead($objectId, $campaignMember['CampaignId'], $isDeleted);
|
||||
break;
|
||||
|
||||
case Contact::OBJECT:
|
||||
$this->contacts[$objectId] = new Contact($objectId, $campaignMember['CampaignId'], $isDeleted);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration\Salesforce\Exception;
|
||||
|
||||
class InvalidObjectException extends \InvalidArgumentException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration\Salesforce\Exception;
|
||||
|
||||
class NoObjectsToFetchException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration\Salesforce\Helper;
|
||||
|
||||
class StateValidationHelper
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private static array $supportedCountriesWithStates = [
|
||||
'United States',
|
||||
'Canada',
|
||||
'Australia',
|
||||
'Brazil',
|
||||
'China',
|
||||
'India',
|
||||
'Ireland',
|
||||
'Italy',
|
||||
'Mexico',
|
||||
];
|
||||
|
||||
/**
|
||||
* Out of the box SF only supports states for the following countries. So in order to prevent SF from rejecting the entire payload, we'll
|
||||
* only send state if it is supported out of the box by SF.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function validate(array $mappedData)
|
||||
{
|
||||
if (!isset($mappedData['State'])) {
|
||||
return $mappedData;
|
||||
}
|
||||
|
||||
if (
|
||||
!isset($mappedData['Country'])
|
||||
|| !in_array($mappedData['Country'], self::$supportedCountriesWithStates)
|
||||
) {
|
||||
unset($mappedData['State']);
|
||||
|
||||
return $mappedData;
|
||||
}
|
||||
|
||||
return $mappedData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object;
|
||||
|
||||
class CampaignMember
|
||||
{
|
||||
public const OBJECT = 'CampaignMember';
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object;
|
||||
|
||||
class Contact
|
||||
{
|
||||
public const OBJECT = 'Contact';
|
||||
|
||||
public function __construct(
|
||||
private $id,
|
||||
private $campaignId,
|
||||
private $isDeleted,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCampaignId()
|
||||
{
|
||||
return $this->campaignId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getisDeleted()
|
||||
{
|
||||
return $this->isDeleted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object;
|
||||
|
||||
class Lead
|
||||
{
|
||||
public const OBJECT = 'Lead';
|
||||
|
||||
public function __construct(
|
||||
private $id,
|
||||
private $campaignId,
|
||||
private $isDeleted,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCampaignId()
|
||||
{
|
||||
return $this->campaignId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getisDeleted()
|
||||
{
|
||||
return $this->isDeleted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration\Salesforce;
|
||||
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Exception\NoObjectsToFetchException;
|
||||
|
||||
class QueryBuilder
|
||||
{
|
||||
/**
|
||||
* @throws NoObjectsToFetchException
|
||||
*/
|
||||
public static function getLeadQuery(array $fields, array $ids): string
|
||||
{
|
||||
if (empty($ids)) {
|
||||
throw new NoObjectsToFetchException();
|
||||
}
|
||||
|
||||
$fieldString = self::getFieldString($fields);
|
||||
$idString = implode("','", $ids);
|
||||
|
||||
return ($idString) ? "SELECT $fieldString from Lead where Id in ('$idString') and ConvertedContactId = NULL" : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoObjectsToFetchException
|
||||
*/
|
||||
public static function getContactQuery(array $fields, array $ids): string
|
||||
{
|
||||
if (empty($ids)) {
|
||||
throw new NoObjectsToFetchException();
|
||||
}
|
||||
|
||||
$fieldString = self::getFieldString($fields);
|
||||
$idString = implode("','", $ids);
|
||||
|
||||
return ($idString) ? "SELECT $fieldString from Contact where Id in ('$idString')" : '';
|
||||
}
|
||||
|
||||
private static function getFieldString(array $fields): string
|
||||
{
|
||||
$fields[] = 'Id';
|
||||
|
||||
return implode(', ', array_unique($fields));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration\Salesforce;
|
||||
|
||||
use Mautic\PluginBundle\Exception\ApiErrorException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class ResultsPaginator
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $results;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $totalRecords = 0;
|
||||
|
||||
private int $recordCount = 0;
|
||||
|
||||
private int $retryCount = 0;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $nextRecordsUrl;
|
||||
|
||||
/**
|
||||
* @param string $salesforceBaseUrl
|
||||
*/
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private $salesforceBaseUrl,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*
|
||||
* @throws ApiErrorException
|
||||
*/
|
||||
public function setResults(array $results)
|
||||
{
|
||||
if (!isset($results['records'])) {
|
||||
throw new ApiErrorException(var_export($results, true));
|
||||
}
|
||||
|
||||
$this->results = $results;
|
||||
$this->totalRecords = $results['totalSize'];
|
||||
$this->recordCount += count($results['records']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws ApiErrorException
|
||||
*/
|
||||
public function getNextResultsUrl()
|
||||
{
|
||||
if (isset($this->results['nextRecordsUrl'])) {
|
||||
$this->retryCount = 0;
|
||||
$this->nextRecordsUrl = $this->results['nextRecordsUrl'];
|
||||
|
||||
if (!str_contains($this->nextRecordsUrl, $this->salesforceBaseUrl)) {
|
||||
$this->nextRecordsUrl = $this->salesforceBaseUrl.$this->nextRecordsUrl;
|
||||
}
|
||||
|
||||
return $this->nextRecordsUrl;
|
||||
}
|
||||
|
||||
if ($this->recordCount < $this->totalRecords) {
|
||||
// Something has gone wrong so try a few more times before giving up
|
||||
if ($this->retryCount <= 5) {
|
||||
$this->logger->debug("SALESFORCE: Processed less than total but didn't get a nextRecordsUrl in the response: ".var_export($this->results, true));
|
||||
|
||||
usleep(500);
|
||||
++$this->retryCount;
|
||||
|
||||
// Try again
|
||||
return $this->nextRecordsUrl;
|
||||
}
|
||||
|
||||
// Throw an exception cause something isn't right
|
||||
throw new ApiErrorException("Expected to process {$this->totalRecords} but only processed {$this->recordCount}: ".var_export($this->results, true));
|
||||
}
|
||||
|
||||
$this->nextRecordsUrl = null;
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getTotal(): int
|
||||
{
|
||||
return (int) $this->totalRecords;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Integration;
|
||||
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
|
||||
class VtigerIntegration extends CrmAbstractIntegration
|
||||
{
|
||||
private string $authorzationError = '';
|
||||
|
||||
/**
|
||||
* Returns the name of the social integration that must match the name of the file.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Vtiger';
|
||||
}
|
||||
|
||||
public function getSupportedFeatures(): array
|
||||
{
|
||||
return ['push_lead'];
|
||||
}
|
||||
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return 'vTiger';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRequiredKeyFields(): array
|
||||
{
|
||||
return [
|
||||
'url' => 'mautic.vtiger.form.url',
|
||||
'username' => 'mautic.vtiger.form.username',
|
||||
'accessKey' => 'mautic.vtiger.form.password',
|
||||
];
|
||||
}
|
||||
|
||||
public function getClientIdKey(): string
|
||||
{
|
||||
return 'username';
|
||||
}
|
||||
|
||||
public function getClientSecretKey(): string
|
||||
{
|
||||
return 'accessKey';
|
||||
}
|
||||
|
||||
public function getAuthTokenKey(): string
|
||||
{
|
||||
return 'sessionName';
|
||||
}
|
||||
|
||||
public function getApiUrl(): string
|
||||
{
|
||||
return sprintf('%s/webservice.php', $this->keys['url']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|array<mixed>|string
|
||||
*/
|
||||
public function isAuthorized()
|
||||
{
|
||||
if (!isset($this->keys['url'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = $this->getApiUrl();
|
||||
$parameters = [
|
||||
'operation' => 'getchallenge',
|
||||
'username' => $this->keys['username'],
|
||||
];
|
||||
|
||||
$response = $this->makeRequest($url, $parameters, 'GET', ['authorize_session' => true]);
|
||||
|
||||
if (empty($response['success'])) {
|
||||
return $this->getErrorsFromResponse($response);
|
||||
}
|
||||
|
||||
$loginParameters = [
|
||||
'operation' => 'login',
|
||||
'username' => $this->keys['username'],
|
||||
'accessKey' => md5($response['result']['token'].$this->keys['accessKey']),
|
||||
];
|
||||
|
||||
$response = $this->makeRequest($url, $loginParameters, 'POST', ['authorize_session' => true]);
|
||||
|
||||
if (empty($response['success'])) {
|
||||
if (is_array($response) && array_key_exists('error', $response)) {
|
||||
$this->authorzationError = $response['error']['message'];
|
||||
}
|
||||
|
||||
return false;
|
||||
} else {
|
||||
$error = $this->extractAuthKeys($response['result']);
|
||||
|
||||
if (empty($error)) {
|
||||
return true;
|
||||
} else {
|
||||
$this->authorzationError = $error;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getAuthLoginUrl(): string
|
||||
{
|
||||
return $this->router->generate('mautic_integration_auth_callback', ['integration' => $this->getName()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and stores tokens returned from oAuthLogin.
|
||||
*
|
||||
* @param array $settings
|
||||
* @param array $parameters
|
||||
*/
|
||||
public function authCallback($settings = [], $parameters = []): string|bool
|
||||
{
|
||||
$success = $this->isAuthorized();
|
||||
if (!$success) {
|
||||
return $this->authorzationError;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getAvailableLeadFields($settings = []): array
|
||||
{
|
||||
$vTigerFields = [];
|
||||
$silenceExceptions = $settings['silence_exceptions'] ?? true;
|
||||
|
||||
if (isset($settings['feature_settings']['objects'])) {
|
||||
$vTigerObjects = $settings['feature_settings']['objects'];
|
||||
} else {
|
||||
$settings = $this->settings->getFeatureSettings();
|
||||
$vTigerObjects = $settings['objects'] ?? ['contacts'];
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->isAuthorized()) {
|
||||
if (!empty($vTigerObjects) && is_array($vTigerObjects)) {
|
||||
foreach ($vTigerObjects as $object) {
|
||||
// The object key for contacts should be 0 for some BC reasons
|
||||
if ('contacts' == $object) {
|
||||
$object = 0;
|
||||
}
|
||||
|
||||
// Check the cache first
|
||||
$settings['cache_suffix'] = $cacheSuffix = '.'.$object;
|
||||
if ($fields = parent::getAvailableLeadFields($settings)) {
|
||||
$vTigerFields[$object] = $fields;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create the array if it doesn't exist to prevent PHP notices
|
||||
if (!isset($vTigerFields[$object])) {
|
||||
$vTigerFields[$object] = [];
|
||||
}
|
||||
|
||||
$leadFields = $this->getApiHelper()->getLeadFields($object);
|
||||
if (isset($leadFields['fields'])) {
|
||||
foreach ($leadFields['fields'] as $fieldInfo) {
|
||||
if (!isset($fieldInfo['name']) || !$fieldInfo['editable'] || in_array(
|
||||
$fieldInfo['type']['name'],
|
||||
['owner', 'reference', 'boolean', 'autogenerated']
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$vTigerFields[$object][$fieldInfo['name']] = [
|
||||
'type' => 'string',
|
||||
'label' => $fieldInfo['label'],
|
||||
'required' => in_array($fieldInfo['name'], ['email', 'accountname']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->cache->set('leadFields'.$cacheSuffix, $vTigerFields[$object]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logIntegrationError($e);
|
||||
|
||||
if (!$silenceExceptions) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return $vTigerFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getFormNotes($section)
|
||||
{
|
||||
if ('leadfield_match' == $section) {
|
||||
return ['mautic.vtiger.form.field_match_notes', 'info'];
|
||||
}
|
||||
|
||||
return parent::getFormNotes($section);
|
||||
}
|
||||
|
||||
public function amendLeadDataBeforePush(&$mappedData): void
|
||||
{
|
||||
if (!empty($mappedData)) {
|
||||
// vtiger requires assigned_user_id so default to authenticated user
|
||||
$mappedData['assigned_user_id'] = $this->keys['userId'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Mautic\PluginBundle\Integration\Form|FormBuilder $builder
|
||||
* @param array $data
|
||||
* @param string $formArea
|
||||
*/
|
||||
public function appendToForm(&$builder, $data, $formArea): void
|
||||
{
|
||||
if ('features' == $formArea) {
|
||||
$builder->add(
|
||||
'objects',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'choices' => [
|
||||
'mautic.vtiger.object.contact' => 'contacts',
|
||||
'mautic.vtiger.object.company' => 'company',
|
||||
],
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'label' => 'mautic.vtiger.form.objects_to_pull_from',
|
||||
'label_attr' => ['class' => ''],
|
||||
'placeholder' => false,
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available company fields for choices in the config UI.
|
||||
*
|
||||
* @param array $settings
|
||||
*/
|
||||
public function getFormCompanyFields($settings = []): array
|
||||
{
|
||||
return parent::getAvailableLeadFields(['cache_suffix' => '.company']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle;
|
||||
|
||||
use Mautic\PluginBundle\Bundle\PluginBundleBase;
|
||||
|
||||
class MauticCrmBundle extends PluginBundleBase
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Mautic bundle for CRM plugin
|
||||
|
||||
## This plugin is managed centrally in https://github.com/mautic/mautic/blob/head/plugins/MauticCrmBundle and this is a read-only mirror repository.
|
||||
|
||||
**📣 Please make PRs and issues against Mautic Core, not here!**
|
||||
@@ -0,0 +1,4 @@
|
||||
<p class="alert alert-info" style="margin:15px 0 0">
|
||||
<i class="ri-external-link-line"></i>
|
||||
<a class="alert-link" href="https://mautic.org/docs/en/plugins/dynamics_crm.html" target="_blank">{{ 'mautic.plugin.dynamics.doc_link'|trans }}</a>
|
||||
</p>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Services;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class Transport implements TransportInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Client $client,
|
||||
) {
|
||||
}
|
||||
|
||||
public function post($uri, array $options = []): \Psr\Http\Message\ResponseInterface
|
||||
{
|
||||
return $this->client->request('POST', $uri, $options);
|
||||
}
|
||||
|
||||
public function put($uri, array $options = []): \Psr\Http\Message\ResponseInterface
|
||||
{
|
||||
return $this->client->request('PUT', $uri, $options);
|
||||
}
|
||||
|
||||
public function get($uri, array $options = []): \Psr\Http\Message\ResponseInterface
|
||||
{
|
||||
return $this->client->request('GET', $uri, $options);
|
||||
}
|
||||
|
||||
public function delete($uri, array $options = []): \Psr\Http\Message\ResponseInterface
|
||||
{
|
||||
return $this->client->request('DELETE', $uri, $options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Services;
|
||||
|
||||
interface TransportInterface
|
||||
{
|
||||
public function post($uri, array $options);
|
||||
|
||||
public function put($uri, array $options);
|
||||
|
||||
public function get($uri, array $options);
|
||||
|
||||
public function delete($uri, array $options);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Api;
|
||||
|
||||
use MauticPlugin\MauticCrmBundle\Api\ConnectwiseApi;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\ConnectwiseIntegration;
|
||||
use MauticPlugin\MauticCrmBundle\Tests\Integration\DataGeneratorTrait;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(ConnectwiseApi::class)]
|
||||
class ConnectwiseApiTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use DataGeneratorTrait;
|
||||
|
||||
/**
|
||||
* @throws \Mautic\PluginBundle\Exception\ApiErrorException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Tests that fetchAllRecords loops until all records are obtained')]
|
||||
public function testResultPagination(): void
|
||||
{
|
||||
$integration = $this->getMockBuilder(ConnectwiseIntegration::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['makeRequest', 'getApiUrl'])
|
||||
->getMock();
|
||||
|
||||
$page = 0;
|
||||
$integration->expects($this->exactly(3))
|
||||
->method('makeRequest')
|
||||
->willReturnCallback(
|
||||
function ($endpoint, $parameters) use (&$page) {
|
||||
++$page;
|
||||
|
||||
// Page should be incremented 3 times by fetchAllRecords method
|
||||
$this->assertEquals(['page' => $page, 'pageSize' => ConnectwiseIntegration::PAGESIZE], $parameters);
|
||||
|
||||
return $this->generateData(3);
|
||||
}
|
||||
);
|
||||
|
||||
$api = new ConnectwiseApi($integration);
|
||||
|
||||
$records = $api->fetchAllRecords('test');
|
||||
|
||||
$this->assertEquals($this->generatedRecords, $records);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Api;
|
||||
|
||||
use Mautic\PluginBundle\Exception\ApiErrorException;
|
||||
use MauticPlugin\MauticCrmBundle\Api\HubspotApi;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\HubspotIntegration;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class HubspotApiTest extends TestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test Hubspot api when the api-key is invalid')]
|
||||
public function testHubspotWhenKeyIsInvalid(): void
|
||||
{
|
||||
$integration = $this->createMock(HubspotIntegration::class);
|
||||
$message = 'The API key provided is invalid. View or manage your API key here: https://app-eu1.hubspot.com/l/api-key/';
|
||||
$code = 401;
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'message' => $message,
|
||||
'correlationId' => '00000000-0000-0000-0000-000000000000',
|
||||
'category' => 'INVALID_AUTHENTICATION',
|
||||
'links' => [
|
||||
'api key' => 'https://app-eu1.hubspot.com/l/api-key/',
|
||||
],
|
||||
];
|
||||
|
||||
$integration->expects(self::once())
|
||||
->method('makeRequest')
|
||||
->willReturn(
|
||||
[
|
||||
'error' => [
|
||||
'code' => $code,
|
||||
'message' => json_encode($response),
|
||||
],
|
||||
]
|
||||
);
|
||||
$integration->expects(self::once())
|
||||
->method('getAuthenticationType')
|
||||
->willReturn('crm');
|
||||
|
||||
$this->expectException(ApiErrorException::class);
|
||||
$this->expectExceptionMessage($message);
|
||||
$this->expectExceptionCode($code);
|
||||
|
||||
$api = new HubspotApi($integration);
|
||||
$api->getLeadFields();
|
||||
|
||||
self::fail('ApiErrorException not thrown');
|
||||
}
|
||||
|
||||
public function testHubspotWhenKeyIsInvalidIfOauth(): void
|
||||
{
|
||||
$integration = $this->createMock(HubspotIntegration::class);
|
||||
$message = 'The API key provided is invalid. View or manage your API key here: https://app-eu1.hubspot.com/l/api-key/';
|
||||
$response = [
|
||||
'error' => 'error',
|
||||
'code' => 402,
|
||||
'message' => $message,
|
||||
'correlationId' => '00000000-0000-0000-0000-000000000000',
|
||||
'category' => 'INVALID_AUTHENTICATION',
|
||||
'links' => [
|
||||
'api key' => 'https://app-eu1.hubspot.com/l/api-key/',
|
||||
],
|
||||
];
|
||||
|
||||
$integration->expects(self::once())
|
||||
->method('makeRequest')
|
||||
->willReturn(['error' => $response]);
|
||||
$integration->expects(self::once())
|
||||
->method('getAuthenticationType')
|
||||
->willReturn('oauth2');
|
||||
|
||||
$this->expectException(ApiErrorException::class);
|
||||
$this->expectExceptionMessage($message);
|
||||
$this->expectExceptionCode(0);
|
||||
|
||||
$api = new HubspotApi($integration);
|
||||
$api->getLeadFields();
|
||||
|
||||
self::fail('ApiErrorException not thrown');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Api;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\CacheStorageHelper;
|
||||
use Mautic\PluginBundle\Entity\Integration;
|
||||
use Mautic\PluginBundle\Exception\ApiErrorException;
|
||||
use MauticPlugin\MauticCrmBundle\Api\SalesforceApi;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\SalesforceIntegration;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(SalesforceApi::class)]
|
||||
class SalesforceApiTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that a locked record request is retried up to 3 times')]
|
||||
public function testRecordLockedErrorIsRetriedThreeTimes(): void
|
||||
{
|
||||
$integration = $this->createMock(SalesforceIntegration::class);
|
||||
$message = 'unable to obtain exclusive access to this record or 1 records: 70137000000Ugy3AAC';
|
||||
|
||||
$integration->expects($this->exactly(3))
|
||||
->method('makeRequest')
|
||||
->willReturn(
|
||||
[
|
||||
[
|
||||
'errorCode' => 'UNABLE_TO_LOCK_ROW',
|
||||
'message' => $message,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
try {
|
||||
$api->request('/test');
|
||||
|
||||
$this->fail('ApiErrorException not thrown');
|
||||
} catch (ApiErrorException $exception) {
|
||||
$this->assertEquals($message, $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that a locked record request is retried up to 3 times with last one being successful so no exception should be thrown')]
|
||||
public function testRecordLockedErrorIsRetriedThreeTimesWithLastOneSuccessful(): void
|
||||
{
|
||||
$integration = $this->createMock(SalesforceIntegration::class);
|
||||
$message = 'unable to obtain exclusive access to this record or 1 records: 70137000000Ugy3AAC';
|
||||
|
||||
$integration->expects($this->exactly(3))
|
||||
->method('makeRequest')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
[
|
||||
[
|
||||
'errorCode' => 'UNABLE_TO_LOCK_ROW',
|
||||
'message' => $message,
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
'errorCode' => 'UNABLE_TO_LOCK_ROW',
|
||||
'message' => $message,
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
'success' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
try {
|
||||
$api->request('/test');
|
||||
} catch (ApiErrorException) {
|
||||
$this->fail('ApiErrorException thrown');
|
||||
}
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that a locked record request is retried 2 times with 3rd being successful')]
|
||||
public function testRecordLockedErrorIsRetriedTwoTimesWithThirdSuccess(): void
|
||||
{
|
||||
$integration = $this->createMock(SalesforceIntegration::class);
|
||||
$message = 'unable to obtain exclusive access to this record or 1 records: 70137000000Ugy3AAC';
|
||||
|
||||
$integration->expects($this->exactly(2))
|
||||
->method('makeRequest')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
[
|
||||
[
|
||||
'errorCode' => 'UNABLE_TO_LOCK_ROW',
|
||||
'message' => $message,
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
['success' => true],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
try {
|
||||
$api->request('/test');
|
||||
} catch (ApiErrorException) {
|
||||
$this->fail('ApiErrorException should not have been thrown');
|
||||
}
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that a session expired should attempt a refresh before failing')]
|
||||
public function testSessionExpiredIsRefreshed(): void
|
||||
{
|
||||
$integration = $this->createMock(SalesforceIntegration::class);
|
||||
$message = '["errorCode":"INVALID_SESSION_ID","body":"Session expired or invalid"]';
|
||||
|
||||
$integration->expects($this->exactly(2))
|
||||
->method('authCallback');
|
||||
|
||||
$integration->expects($this->exactly(2))
|
||||
->method('makeRequest')
|
||||
->willReturn(
|
||||
[
|
||||
[
|
||||
'message' => $message,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
try {
|
||||
$api->request('/test');
|
||||
$this->fail('ApiErrorException not thrown');
|
||||
} catch (ApiErrorException $exception) {
|
||||
$this->assertEquals($message, $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that a session expired should attempt a refresh but not throw an exception if successful on second request')]
|
||||
public function testSessionExpiredIsRefreshedWithoutThrowingExceptionOnSecondRequestWithSuccess(): void
|
||||
{
|
||||
$integration = $this->createMock(SalesforceIntegration::class);
|
||||
$message = 'Session expired';
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('authCallback');
|
||||
|
||||
// Test again but both attempts should fail resulting in
|
||||
$integration->expects($this->exactly(2))
|
||||
->method('makeRequest')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
[
|
||||
[
|
||||
'errorCode' => 'INVALID_SESSION_ID',
|
||||
'message' => $message,
|
||||
],
|
||||
],
|
||||
[
|
||||
['success' => true],
|
||||
]
|
||||
);
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
try {
|
||||
$api->request('/test');
|
||||
} catch (ApiErrorException) {
|
||||
$this->fail('ApiErrorException thrown');
|
||||
}
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that an exception is thrown for all other errors')]
|
||||
public function testErrorDoesNotRetryRequest(): void
|
||||
{
|
||||
$integration = $this->createMock(SalesforceIntegration::class);
|
||||
$message = 'Fatal error';
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('makeRequest')
|
||||
->willReturn(
|
||||
[
|
||||
[
|
||||
'errorCode' => 'FATAL_ERROR',
|
||||
'message' => $message,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
try {
|
||||
$api->request('/test');
|
||||
|
||||
$this->fail('ApiErrorException not thrown');
|
||||
} catch (ApiErrorException $exception) {
|
||||
$this->assertEquals($message, $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that a backslash and a single quote are escaped for SF queries')]
|
||||
public function testCompanyQueryIsEscapedCorrectly(): void
|
||||
{
|
||||
$integration = $this->getMockBuilder(SalesforceIntegration::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['mergeConfigToFeatureSettings', 'makeRequest', 'getQueryUrl', 'getIntegrationSettings', 'getFieldsForQuery', 'getApiUrl'])
|
||||
->getMock();
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('mergeConfigToFeatureSettings')
|
||||
->willReturn(
|
||||
[
|
||||
'objects' => [
|
||||
'company',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('makeRequest')
|
||||
->willReturnCallback(
|
||||
function ($url, $parameters = [], $method = 'GET', $settings = []): void {
|
||||
$this->assertEquals(
|
||||
[
|
||||
'q' => 'select Id from Account where Name = \'Some\\\\thing E\\\'lse\' and BillingCountry = \'Some\\\\Where E\\\'lse\' and BillingCity = \'Some\\\\Where E\\\'lse\' and BillingState = \'Some\\\\Where E\\\'lse\'',
|
||||
],
|
||||
$parameters
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
$api->getCompany(
|
||||
[
|
||||
'company' => [
|
||||
'BillingCountry' => 'Some\\Where E\'lse',
|
||||
'BillingCity' => 'Some\\Where E\'lse',
|
||||
'BillingState' => 'Some\\Where E\'lse',
|
||||
'Name' => 'Some\\thing E\'lse',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that a backslash and an html entity of single quote are escaped for SF queries')]
|
||||
public function testCompanyQueryWithHtmlEntitiesIsEscapedCorrectly(): void
|
||||
{
|
||||
$integration = $this->getMockBuilder(SalesforceIntegration::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['mergeConfigToFeatureSettings', 'makeRequest', 'getQueryUrl', 'getIntegrationSettings', 'getFieldsForQuery', 'getApiUrl'])
|
||||
->getMock();
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('mergeConfigToFeatureSettings')
|
||||
->willReturn(
|
||||
[
|
||||
'objects' => [
|
||||
'company',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('makeRequest')
|
||||
->willReturnCallback(
|
||||
function ($url, $parameters = [], $method = 'GET', $settings = []): void {
|
||||
$this->assertEquals(
|
||||
[
|
||||
'q' => 'select Id from Account where Name = \'Some\\\\thing\\\' E\\\'lse\' and BillingCountry = \'Some\\\\Where\\\' E\\\'lse\' and BillingCity = \'Some\\\\Where\\\' E\\\'lse\' and BillingState = \'Some\\\\Where\\\' E\\\'lse\'',
|
||||
],
|
||||
$parameters
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
$api->getCompany(
|
||||
[
|
||||
'company' => [
|
||||
'BillingCountry' => 'Some\\Where' E\'lse',
|
||||
'BillingCity' => 'Some\\Where' E\'lse',
|
||||
'BillingState' => 'Some\\Where' E\'lse',
|
||||
'Name' => 'Some\\thing' E\'lse',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that a backslash and a single quote are escaped for SF queries')]
|
||||
public function testContactQueryIsEscapedCorrectly(): void
|
||||
{
|
||||
$integration = $this->getMockBuilder(SalesforceIntegration::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['mergeConfigToFeatureSettings', 'makeRequest', 'getQueryUrl', 'getIntegrationSettings', 'getFieldsForQuery', 'getApiUrl'])
|
||||
->getMock();
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('mergeConfigToFeatureSettings')
|
||||
->willReturn(
|
||||
[
|
||||
'objects' => [
|
||||
'Contact',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('getFieldsForQuery')
|
||||
->willReturn([]);
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('makeRequest')
|
||||
->willReturnCallback(
|
||||
function ($url, $parameters = [], $method = 'GET', $settings = []): void {
|
||||
$this->assertEquals(
|
||||
[
|
||||
'q' => 'select Id from Contact where email = \'con\\\\tact\\\'email@email.com\'',
|
||||
],
|
||||
$parameters
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$integration->method('getFieldsForQuery')
|
||||
->willReturn([]);
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
$api->getPerson([
|
||||
'Contact' => [
|
||||
'Email' => 'con\\tact\'email@email.com',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that a backslash and a single quote are escaped for SF queries')]
|
||||
public function testLeadQueryIsEscapedCorrectly(): void
|
||||
{
|
||||
$integration = $this->getMockBuilder(SalesforceIntegration::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['mergeConfigToFeatureSettings', 'makeRequest', 'getQueryUrl', 'getIntegrationSettings', 'getFieldsForQuery', 'getApiUrl'])
|
||||
->getMock();
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('mergeConfigToFeatureSettings')
|
||||
->willReturn(
|
||||
[
|
||||
'objects' => [
|
||||
'Lead',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('getFieldsForQuery')
|
||||
->willReturn([]);
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('makeRequest')
|
||||
->willReturnCallback(
|
||||
function ($url, $parameters = [], $method = 'GET', $settings = []): void {
|
||||
$this->assertEquals(
|
||||
[
|
||||
'q' => 'select Id from Lead where email = \'con\\\\tact\\\'email@email.com\' and ConvertedContactId = NULL',
|
||||
],
|
||||
$parameters
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$integration->method('getFieldsForQuery')
|
||||
->willReturn([]);
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
$api->getPerson([
|
||||
'Lead' => [
|
||||
'Email' => 'con\\tact\'email@email.com',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testHandleDeletesGracefullyWithHasOptedOutOfEmailAsMissingField(): void
|
||||
{
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
$cache = $this->createMock(CacheStorageHelper::class);
|
||||
|
||||
$cache
|
||||
->method('get')
|
||||
->withAnyParameters()
|
||||
->willReturn('2019-05-22 19:36:30');
|
||||
|
||||
$integration = $this->getMockBuilder(SalesforceIntegration::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods([
|
||||
'mergeConfigToFeatureSettings',
|
||||
'makeRequest',
|
||||
'getQueryUrl',
|
||||
'getIntegrationSettings',
|
||||
'getFieldsForQuery',
|
||||
'getApiUrl',
|
||||
'getCache',
|
||||
'getTranslator',
|
||||
'upsertUnreadAdminsNotification',
|
||||
])
|
||||
->getMock();
|
||||
|
||||
$integration
|
||||
->expects($this->atLeastOnce())
|
||||
->method('getCache')
|
||||
->willReturn($cache);
|
||||
|
||||
$integration->method('getFieldsForQuery')
|
||||
->with('Lead')
|
||||
->willReturn(['firstname', 'lastname', 'HasOptedOutOfEmail']);
|
||||
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
|
||||
$integration->method('getTranslator')->willReturn($translator);
|
||||
|
||||
$this->expectException(ApiErrorException::class);
|
||||
$integration->expects($this->atLeastOnce())
|
||||
->method('makeRequest')
|
||||
->willReturn(
|
||||
[
|
||||
[
|
||||
'errorCode' => 'FATAL_ERROR',
|
||||
'message' => "ERROR at Row1\nNo such column 'HasOptedOutOfEmail' on entity 'Lead'",
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$params['start'] = '2019-05-22 19:36:30';
|
||||
$params['end'] = '2030-05-22 19:36:30';
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
self::assertEquals('2019-05-22 19:36:30', $api->getOrganizationCreatedDate());
|
||||
|
||||
$api->getLeads($params, 'Lead');
|
||||
}
|
||||
|
||||
public function testHandleDeletesGracefully(): void
|
||||
{
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
$cache = $this->createMock(CacheStorageHelper::class);
|
||||
|
||||
$cache
|
||||
->method('get')
|
||||
->withAnyParameters()
|
||||
->willReturn('2019-05-22 19:36:30');
|
||||
|
||||
$integration = $this->getMockBuilder(SalesforceIntegration::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods([
|
||||
'mergeConfigToFeatureSettings',
|
||||
'makeRequest',
|
||||
'getQueryUrl',
|
||||
'getIntegrationSettings',
|
||||
'getFieldsForQuery',
|
||||
'getApiUrl',
|
||||
'getCache',
|
||||
'getTranslator',
|
||||
'upsertUnreadAdminsNotification',
|
||||
'getEntityManager',
|
||||
])
|
||||
->getMock();
|
||||
|
||||
$integration
|
||||
->expects($this->atLeastOnce())
|
||||
->method('getCache')
|
||||
->willReturn($cache);
|
||||
|
||||
$integration->method('getFieldsForQuery')
|
||||
->with('Lead')
|
||||
->willReturn(['firstname', 'lastname', 'extraField']);
|
||||
|
||||
$integration->expects($this->never())->method('upsertUnreadAdminsNotification');
|
||||
|
||||
$entityManager = $this->createMock(EntityManager::class);
|
||||
|
||||
$entity = $this
|
||||
->getMockBuilder(Integration::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['getFeatureSettings', 'setFeatureSettings'])
|
||||
->getMock();
|
||||
|
||||
$integration->method('getEntityManager')->willReturn($entityManager);
|
||||
$integration->method('getIntegrationSettings')->willReturn($entity);
|
||||
$entity->method('getFeatureSettings')->willReturn(['leadFields' => ['extraField__Lead' => '']]);
|
||||
|
||||
$this->expectException(ApiErrorException::class);
|
||||
$integration->expects($this->atLeastOnce())
|
||||
->method('makeRequest')
|
||||
->willReturn(
|
||||
[
|
||||
[
|
||||
'errorCode' => 'FATAL_ERROR',
|
||||
'message' => "ERROR at Row1\nNo such column 'extraField' on entity 'Lead'",
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$params['start'] = '2019-05-22 19:36:30';
|
||||
$params['end'] = '2030-05-22 19:36:30';
|
||||
|
||||
$api = new SalesforceApi($integration);
|
||||
|
||||
self::assertEquals('2019-05-22 19:36:30', $api->getOrganizationCreatedDate());
|
||||
|
||||
$api->getLeads($params, 'Lead');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Api\Zoho;
|
||||
|
||||
use MauticPlugin\MauticCrmBundle\Api\Zoho\Exception\MatchingKeyNotFoundException;
|
||||
use MauticPlugin\MauticCrmBundle\Api\Zoho\Mapper;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(Mapper::class)]
|
||||
class MapperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $availableFields = [
|
||||
'Leads' => [
|
||||
'Company' => [
|
||||
'type' => 'string',
|
||||
'label' => 'Company',
|
||||
'api_name' => 'Company',
|
||||
'required' => true,
|
||||
],
|
||||
'FirstName' => [
|
||||
'type' => 'string',
|
||||
'label' => 'First Name',
|
||||
'api_name' => 'First Name',
|
||||
'required' => false,
|
||||
],
|
||||
'LastName' => [
|
||||
'type' => 'string',
|
||||
'label' => 'Last Name',
|
||||
'api_name' => 'Last Name',
|
||||
'required' => true,
|
||||
],
|
||||
'Email' => [
|
||||
'type' => 'string',
|
||||
'label' => 'Email',
|
||||
'api_name' => 'Email',
|
||||
'required' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $mappedFields = [
|
||||
'Company' => 'company',
|
||||
'Email' => 'email',
|
||||
'Country' => 'country',
|
||||
'FirstName' => 'firstname',
|
||||
'LastName' => 'lastname',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $contacts = [
|
||||
[
|
||||
'firstname' => 'FirstName1',
|
||||
'lastname' => 'LastName1',
|
||||
'email' => 'zoho1@email.com',
|
||||
'integration_entity' => 'Leads',
|
||||
'integration_entity_id' => 'abc',
|
||||
'internal_entity' => 'lead',
|
||||
'internal_entity_id' => 1,
|
||||
],
|
||||
[
|
||||
'firstname' => 'FirstName2',
|
||||
'lastname' => 'LastName2',
|
||||
'email' => 'zoho2@email.com',
|
||||
'integration_entity' => 'Leads',
|
||||
'integration_entity_id' => 'def',
|
||||
'internal_entity' => 'lead',
|
||||
'internal_entity_id' => 2,
|
||||
],
|
||||
[
|
||||
'firstname' => 'FirstName3',
|
||||
'lastname' => 'LastName3',
|
||||
'email' => 'zoho3@email.com',
|
||||
'integration_entity' => 'Leads',
|
||||
'integration_entity_id' => 'ghi',
|
||||
'internal_entity' => 'lead',
|
||||
'internal_entity_id' => 3,
|
||||
],
|
||||
];
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that array is generated according to the mapping')]
|
||||
public function testArrayIsGeneratedBasedOnMapping(): void
|
||||
{
|
||||
$mapper = new Mapper($this->availableFields);
|
||||
$mapper->setObject('Leads');
|
||||
|
||||
foreach ($this->contacts as $contact) {
|
||||
$mapper->setMappedFields($this->mappedFields)
|
||||
->setContact($contact)
|
||||
->map($contact['internal_entity_id']);
|
||||
}
|
||||
|
||||
$expected = [
|
||||
[
|
||||
'Email' => 'zoho1@email.com',
|
||||
'First Name' => 'FirstName1',
|
||||
'Last Name' => 'LastName1',
|
||||
],
|
||||
[
|
||||
'Email' => 'zoho2@email.com',
|
||||
'First Name' => 'FirstName2',
|
||||
'Last Name' => 'LastName2',
|
||||
],
|
||||
[
|
||||
'Email' => 'zoho3@email.com',
|
||||
'First Name' => 'FirstName3',
|
||||
'Last Name' => 'LastName3',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $mapper->getArray());
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that contacts do not inherit previous contact information')]
|
||||
public function testContactDoesNotInheritPreviousContactData(): void
|
||||
{
|
||||
$mapper = new Mapper($this->availableFields);
|
||||
$mapper->setObject('Leads');
|
||||
|
||||
$contacts = $this->contacts;
|
||||
$contacts[1]['firstname'] = null;
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
$mapper->setMappedFields($this->mappedFields)
|
||||
->setContact($contact)
|
||||
->map($contact['internal_entity_id'], $contact['integration_entity_id']);
|
||||
}
|
||||
|
||||
$expected = [
|
||||
[
|
||||
'id' => 'abc',
|
||||
'Email' => 'zoho1@email.com',
|
||||
'First Name' => 'FirstName1',
|
||||
'Last Name' => 'LastName1',
|
||||
],
|
||||
[
|
||||
'id' => 'def',
|
||||
'Email' => 'zoho2@email.com',
|
||||
'Last Name' => 'LastName2',
|
||||
],
|
||||
[
|
||||
'id' => 'ghi',
|
||||
'Email' => 'zoho3@email.com',
|
||||
'First Name' => 'FirstName3',
|
||||
'Last Name' => 'LastName3',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $mapper->getArray());
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that array is generated according to the mapping')]
|
||||
public function testArrayIsGeneratedBasedOnMappingWithId(): void
|
||||
{
|
||||
$mapper = new Mapper($this->availableFields);
|
||||
$mapper->setObject('Leads');
|
||||
|
||||
foreach ($this->contacts as $contact) {
|
||||
$mapper->setMappedFields($this->mappedFields)
|
||||
->setContact($contact)
|
||||
->map($contact['internal_entity_id'], $contact['integration_entity_id']);
|
||||
}
|
||||
|
||||
$expected = [
|
||||
[
|
||||
'id' => 'abc',
|
||||
'Email' => 'zoho1@email.com',
|
||||
'First Name' => 'FirstName1',
|
||||
'Last Name' => 'LastName1',
|
||||
],
|
||||
[
|
||||
'id' => 'def',
|
||||
'First Name' => 'FirstName2',
|
||||
'Email' => 'zoho2@email.com',
|
||||
'Last Name' => 'LastName2',
|
||||
],
|
||||
[
|
||||
'id' => 'ghi',
|
||||
'Email' => 'zoho3@email.com',
|
||||
'First Name' => 'FirstName3',
|
||||
'Last Name' => 'LastName3',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $mapper->getArray());
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test asking for a key returns the correct contact')]
|
||||
public function testThatContactIdMatchesGivenKey(): void
|
||||
{
|
||||
$mapper = new Mapper($this->availableFields);
|
||||
$mapper->setObject('Leads');
|
||||
|
||||
foreach ($this->contacts as $contact) {
|
||||
$mapper->setMappedFields($this->mappedFields)
|
||||
->setContact($contact)
|
||||
->map($contact['internal_entity_id'], $contact['integration_entity_id']);
|
||||
}
|
||||
|
||||
$this->assertEquals(3, $mapper->getContactIdByKey(2));
|
||||
$this->assertEquals(2, $mapper->getContactIdByKey(1));
|
||||
$this->assertEquals(1, $mapper->getContactIdByKey(0));
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox("Test asking for a key that doesn't exist throws exception")]
|
||||
public function testThatExceptionIsThrownIfKeyNotFound(): void
|
||||
{
|
||||
$this->expectException(MatchingKeyNotFoundException::class);
|
||||
|
||||
$mapper = new Mapper($this->availableFields);
|
||||
$mapper->setObject('Leads');
|
||||
|
||||
foreach ($this->contacts as $contact) {
|
||||
$mapper->setMappedFields($this->mappedFields)
|
||||
->setContact($contact)
|
||||
->map($contact['internal_entity_id'], $contact['integration_entity_id']);
|
||||
}
|
||||
|
||||
$mapper->getContactIdByKey(4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests;
|
||||
|
||||
use Mautic\EmailBundle\Helper\EmailValidator;
|
||||
use Mautic\LeadBundle\Deduplicate\CompanyDeduper;
|
||||
use Mautic\PluginBundle\Tests\Integration\AbstractIntegrationTestCase;
|
||||
use MauticPlugin\MauticCrmBundle\Tests\Fixtures\Model\CompanyModelStub;
|
||||
use MauticPlugin\MauticCrmBundle\Tests\Stubs\StubIntegration;
|
||||
use PHPUnit\Framework\MockObject\MockBuilder;
|
||||
|
||||
class CrmAbstractIntegrationTest extends AbstractIntegrationTestCase
|
||||
{
|
||||
public function testFieldMatchingPriority(): void
|
||||
{
|
||||
$config = [
|
||||
'update_mautic' => [
|
||||
'email' => '1',
|
||||
'first_name' => '0',
|
||||
'last_name' => '0',
|
||||
'address_1' => '1',
|
||||
'address_2' => '1',
|
||||
],
|
||||
];
|
||||
|
||||
/** @var MockBuilder $mockBuilder */
|
||||
$mockBuilder = $this->getMockBuilder(StubIntegration::class);
|
||||
$mockBuilder->disableOriginalConstructor();
|
||||
|
||||
/** @var StubIntegration $integration */
|
||||
$integration = $mockBuilder->getMock();
|
||||
|
||||
$methodMautic = new \ReflectionMethod(StubIntegration::class, 'getPriorityFieldsForMautic');
|
||||
$methodMautic->setAccessible(true);
|
||||
|
||||
$methodIntegration = new \ReflectionMethod(StubIntegration::class, 'getPriorityFieldsForIntegration');
|
||||
$methodIntegration->setAccessible(true);
|
||||
|
||||
$fieldsForMautic = $methodMautic->invokeArgs($integration, [$config]);
|
||||
|
||||
$this->assertSame(
|
||||
['email', 'address_1', 'address_2'],
|
||||
$fieldsForMautic,
|
||||
'Fields to update in Mautic should return fields marked as 1 in the integration priority config.'
|
||||
);
|
||||
|
||||
$fieldsForIntegration = $methodIntegration->invokeArgs($integration, [$config]);
|
||||
|
||||
$this->assertSame(
|
||||
['first_name', 'last_name'],
|
||||
$fieldsForIntegration,
|
||||
'Fields to update in the integration should return fields marked as 0 in the integration priority config.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testCompanyDataIsMappedForNewCompanies(): void
|
||||
{
|
||||
$data = [
|
||||
'custom_company_name' => 'Some Business',
|
||||
'some_custom_field' => 'some value',
|
||||
];
|
||||
|
||||
$emailValidator = $this->createMock(EmailValidator::class);
|
||||
|
||||
$companyDeduper = $this->createMock(CompanyDeduper::class);
|
||||
|
||||
$companyModel = $this->getMockBuilder(CompanyModelStub::class)
|
||||
->onlyMethods(['fetchCompanyFields', 'organizeFieldsByGroup', 'saveEntity'])
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$companyModel->setFieldModel($this->fieldModel);
|
||||
$companyModel->setEmailValidator($emailValidator);
|
||||
$companyModel->setCompanyDeduper($companyDeduper);
|
||||
|
||||
$companyModel->expects($this->any())
|
||||
->method('fetchCompanyFields')
|
||||
->willReturn([]);
|
||||
$companyModel->expects($this->once())
|
||||
->method('organizeFieldsByGroup')
|
||||
->willReturn([
|
||||
'core' => [
|
||||
'companyname' => [
|
||||
'alias' => 'companyname',
|
||||
'type' => 'text',
|
||||
],
|
||||
'custom_company_name' => [
|
||||
'alias' => 'custom_company_name',
|
||||
'type' => 'text',
|
||||
],
|
||||
'some_custom_field' => [
|
||||
'alias' => 'some_custom_field',
|
||||
'type' => 'text',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$integration = $this->getMockBuilder(StubIntegration::class)
|
||||
->setConstructorArgs([
|
||||
$this->dispatcher,
|
||||
$this->cache,
|
||||
$this->em,
|
||||
$this->request,
|
||||
$this->router,
|
||||
$this->translator,
|
||||
$this->logger,
|
||||
$this->encryptionHelper,
|
||||
$this->leadModel,
|
||||
$companyModel,
|
||||
$this->pathsHelper,
|
||||
$this->notificationModel,
|
||||
$this->fieldModel,
|
||||
$this->integrationEntityModel,
|
||||
$this->doNotContact,
|
||||
$this->fieldsWithUniqueIdentifier,
|
||||
])
|
||||
->onlyMethods(['populateMauticLeadData', 'mergeConfigToFeatureSettings'])
|
||||
->getMock();
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('populateMauticLeadData')
|
||||
->willReturn($data);
|
||||
|
||||
$company = $integration->getMauticCompany($data);
|
||||
|
||||
$this->assertEquals('Some Business', $company->getName());
|
||||
$this->assertEquals('Some Business', $company->getFieldValue('custom_company_name'));
|
||||
$this->assertEquals('some value', $company->getFieldValue('some_custom_field'));
|
||||
}
|
||||
|
||||
public function testLimitString(): void
|
||||
{
|
||||
$integration = $this->createMock(StubIntegration::class);
|
||||
|
||||
$methodLimitString = new \ReflectionMethod(StubIntegration::class, 'limitString');
|
||||
$methodLimitString->setAccessible(true);
|
||||
|
||||
$string = 'SomeRandomString';
|
||||
|
||||
$result = $methodLimitString->invokeArgs($integration, [str_repeat($string, 100), 'text']);
|
||||
$this->assertSame(strlen($result), 255);
|
||||
|
||||
$result = $methodLimitString->invokeArgs($integration, [$string, 'text']);
|
||||
$this->assertSame(strlen($result), strlen($string));
|
||||
$this->assertSame($result, $string);
|
||||
|
||||
$result = $methodLimitString->invokeArgs($integration, [true, 'text']);
|
||||
$this->assertSame($result, true);
|
||||
|
||||
$result = $methodLimitString->invokeArgs($integration, [false, 'text']);
|
||||
$this->assertSame($result, false);
|
||||
|
||||
$result = $methodLimitString->invokeArgs($integration, [[1, 2, 3]]);
|
||||
$this->assertSame($result, [1, 2, 3]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests;
|
||||
|
||||
use Mautic\PluginBundle\Tests\Integration\AbstractIntegrationTestCase;
|
||||
use MauticPlugin\MauticCrmBundle\Api\DynamicsApi;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\DynamicsIntegration;
|
||||
|
||||
class DynamicsApiTest extends AbstractIntegrationTestCase
|
||||
{
|
||||
private DynamicsApi $api;
|
||||
|
||||
private DynamicsIntegration $integration;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->integration = new DynamicsIntegration(
|
||||
$this->dispatcher,
|
||||
$this->cache,
|
||||
$this->em,
|
||||
$this->request,
|
||||
$this->router,
|
||||
$this->translator,
|
||||
$this->logger,
|
||||
$this->encryptionHelper,
|
||||
$this->leadModel,
|
||||
$this->companyModel,
|
||||
$this->pathsHelper,
|
||||
$this->notificationModel,
|
||||
$this->fieldModel,
|
||||
$this->integrationEntityModel,
|
||||
$this->doNotContact,
|
||||
$this->fieldsWithUniqueIdentifier
|
||||
);
|
||||
|
||||
$this->api = new DynamicsApi($this->integration);
|
||||
}
|
||||
|
||||
public function testIntegration(): void
|
||||
{
|
||||
$this->assertSame('Dynamics', $this->integration->getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests;
|
||||
|
||||
use Mautic\PluginBundle\Tests\Integration\AbstractIntegrationTestCase;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\DynamicsIntegration;
|
||||
|
||||
class DynamicsIntegrationTest extends AbstractIntegrationTestCase
|
||||
{
|
||||
private DynamicsIntegration $integration;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->integration = new DynamicsIntegration(
|
||||
$this->dispatcher,
|
||||
$this->cache,
|
||||
$this->em,
|
||||
$this->request,
|
||||
$this->router,
|
||||
$this->translator,
|
||||
$this->logger,
|
||||
$this->encryptionHelper,
|
||||
$this->leadModel,
|
||||
$this->companyModel,
|
||||
$this->pathsHelper,
|
||||
$this->notificationModel,
|
||||
$this->fieldModel,
|
||||
$this->integrationEntityModel,
|
||||
$this->doNotContact,
|
||||
$this->fieldsWithUniqueIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
public function testIntegration(): void
|
||||
{
|
||||
$this->assertSame('Dynamics', $this->integration->getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Fixtures\Model;
|
||||
|
||||
use Mautic\EmailBundle\Helper\EmailValidator;
|
||||
use Mautic\LeadBundle\Deduplicate\CompanyDeduper;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
|
||||
class CompanyModelStub extends CompanyModel
|
||||
{
|
||||
public function setFieldModel(FieldModel $fieldModel): void
|
||||
{
|
||||
$this->leadFieldModel = $fieldModel;
|
||||
}
|
||||
|
||||
public function setEmailValidator(EmailValidator $validator): void
|
||||
{
|
||||
$this->emailValidator = $validator;
|
||||
}
|
||||
|
||||
public function setCompanyDeduper(CompanyDeduper $companyDeduper): void
|
||||
{
|
||||
$this->companyDeduper = $companyDeduper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Integration;
|
||||
|
||||
use Mautic\PluginBundle\Model\IntegrationEntityModel;
|
||||
use Mautic\PluginBundle\Tests\Integration\AbstractIntegrationTestCase;
|
||||
use MauticPlugin\MauticCrmBundle\Api\ConnectwiseApi;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\ConnectwiseIntegration;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(ConnectwiseIntegration::class)]
|
||||
class ConnectwiseIntegrationTest extends AbstractIntegrationTestCase
|
||||
{
|
||||
use DataGeneratorTrait;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that all records are fetched till last page of results are consumed')]
|
||||
public function testMultiplePagesOfRecordsAreFetched(): void
|
||||
{
|
||||
$this->reset();
|
||||
|
||||
$apiHelper = $this->createMock(ConnectwiseApi::class);
|
||||
|
||||
$apiHelper->expects($this->exactly(2))
|
||||
->method('getContacts')
|
||||
->willReturnCallback(
|
||||
fn () => $this->generateData(2)
|
||||
);
|
||||
|
||||
$integration = $this->getMockBuilder(ConnectwiseIntegration::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['isAuthorized', 'getApiHelper', 'getMauticLead'])
|
||||
->getMock();
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('isAuthorized')
|
||||
->willReturn(true);
|
||||
|
||||
$integration
|
||||
->method('getApiHelper')
|
||||
->willReturn($apiHelper);
|
||||
|
||||
$integration->getRecords([], 'Contact');
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that all records are fetched till last page of results are consumed')]
|
||||
public function testMultiplePagesOfCampaignMemberRecordsAreFetched(): void
|
||||
{
|
||||
$this->reset();
|
||||
|
||||
$apiHelper = $this->createMock(ConnectwiseApi::class);
|
||||
|
||||
$apiHelper->expects($this->exactly(2))
|
||||
->method('getCampaignMembers')
|
||||
->willReturnCallback(
|
||||
fn () => $this->generateData(2)
|
||||
);
|
||||
|
||||
$integrationEntityModel = $this->createMock(IntegrationEntityModel::class);
|
||||
|
||||
$integration = $this->getMockBuilder(ConnectwiseIntegration::class)
|
||||
->setConstructorArgs([
|
||||
$this->dispatcher,
|
||||
$this->cache,
|
||||
$this->em,
|
||||
$this->request,
|
||||
$this->router,
|
||||
$this->translator,
|
||||
$this->logger,
|
||||
$this->encryptionHelper,
|
||||
$this->leadModel,
|
||||
$this->companyModel,
|
||||
$this->pathsHelper,
|
||||
$this->notificationModel,
|
||||
$this->fieldModel,
|
||||
$integrationEntityModel,
|
||||
$this->doNotContact,
|
||||
$this->fieldsWithUniqueIdentifier,
|
||||
])
|
||||
->onlyMethods(['isAuthorized', 'getApiHelper', 'getRecords', 'saveCampaignMembers'])
|
||||
->getMock();
|
||||
|
||||
$integration->expects($this->once())
|
||||
->method('isAuthorized')
|
||||
->willReturn(true);
|
||||
|
||||
$integration
|
||||
->method('getApiHelper')
|
||||
->willReturn($apiHelper);
|
||||
|
||||
$integration->getCampaignMembers(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Integration;
|
||||
|
||||
use MauticPlugin\MauticCrmBundle\Integration\ConnectwiseIntegration;
|
||||
|
||||
trait DataGeneratorTrait
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $page = 1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $id = 0;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $generatedRecords = [];
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function generateData($maxPages)
|
||||
{
|
||||
$pageSize = ($this->page === $maxPages) ? ConnectwiseIntegration::PAGESIZE / 2 : ConnectwiseIntegration::PAGESIZE;
|
||||
$fakeData = [];
|
||||
$counter = 0;
|
||||
while ($counter < $pageSize) {
|
||||
$data = [
|
||||
'id' => $this->id,
|
||||
];
|
||||
$fakeData[] = $data;
|
||||
$this->generatedRecords[] = $data;
|
||||
|
||||
++$counter;
|
||||
++$this->id;
|
||||
}
|
||||
++$this->page;
|
||||
|
||||
return $fakeData;
|
||||
}
|
||||
|
||||
protected function reset()
|
||||
{
|
||||
$this->id = 0;
|
||||
$this->page = 1;
|
||||
$this->generatedRecords = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Integration;
|
||||
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\PluginBundle\Entity\Integration;
|
||||
use Mautic\PluginBundle\Event\PluginIntegrationKeyEvent;
|
||||
use Mautic\PluginBundle\PluginEvents;
|
||||
use Mautic\PluginBundle\Tests\Integration\AbstractIntegrationTestCase;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\HubspotIntegration;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class HubspotIntegrationTest extends AbstractIntegrationTestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&UserHelper
|
||||
*/
|
||||
private MockObject $userHelper;
|
||||
|
||||
private HubspotIntegration $integration;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->userHelper = $this->createMock(UserHelper::class);
|
||||
$this->integration = new HubspotIntegration(
|
||||
$this->dispatcher,
|
||||
$this->cache,
|
||||
$this->em,
|
||||
$this->request,
|
||||
$this->router,
|
||||
$this->translator,
|
||||
$this->logger,
|
||||
$this->encryptionHelper,
|
||||
$this->leadModel,
|
||||
$this->companyModel,
|
||||
$this->pathsHelper,
|
||||
$this->notificationModel,
|
||||
$this->fieldModel,
|
||||
$this->integrationEntityModel,
|
||||
$this->doNotContact,
|
||||
$this->fieldsWithUniqueIdentifier,
|
||||
$this->userHelper
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetRequiredKeyFields(): void
|
||||
{
|
||||
self::assertSame([], $this->integration->getRequiredKeyFields());
|
||||
}
|
||||
|
||||
public function testGetBearerTokenEmpty(): void
|
||||
{
|
||||
$event = $this->createMock(PluginIntegrationKeyEvent::class);
|
||||
$event->expects(self::once())
|
||||
->method('getKeys')
|
||||
->willReturn(['other' => 'data']);
|
||||
$this->dispatcher->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(
|
||||
new PluginIntegrationKeyEvent($this->integration, [HubspotIntegration::ACCESS_KEY]),
|
||||
PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_ENCRYPT
|
||||
)
|
||||
->willReturn($event);
|
||||
|
||||
$this->integration->encryptAndSetApiKeys([HubspotIntegration::ACCESS_KEY], $this->createMock(Integration::class));
|
||||
self::assertNull($this->integration->getBearerToken());
|
||||
}
|
||||
|
||||
public function testGetBearerTokenSet(): void
|
||||
{
|
||||
$token = 'token';
|
||||
|
||||
$event = $this->createMock(PluginIntegrationKeyEvent::class);
|
||||
$event->expects(self::once())
|
||||
->method('getKeys')
|
||||
->willReturn(['other' => 'data', HubspotIntegration::ACCESS_KEY => $token]);
|
||||
$this->dispatcher->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(
|
||||
new PluginIntegrationKeyEvent($this->integration, [HubspotIntegration::ACCESS_KEY]),
|
||||
PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_ENCRYPT
|
||||
)
|
||||
->willReturn($event);
|
||||
|
||||
$this->integration->encryptAndSetApiKeys([HubspotIntegration::ACCESS_KEY], $this->createMock(Integration::class));
|
||||
self::assertSame($token, $this->integration->getBearerToken());
|
||||
}
|
||||
|
||||
public function testGetFormSettings(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[
|
||||
'requires_callback' => false,
|
||||
'requires_authorization' => false,
|
||||
],
|
||||
$this->integration->getFormSettings()
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetAuthenticationTypeNoOauthToken(): void
|
||||
{
|
||||
$event = $this->createMock(PluginIntegrationKeyEvent::class);
|
||||
$event->expects(self::once())
|
||||
->method('getKeys')
|
||||
->willReturn(['other' => 'data']);
|
||||
$this->dispatcher->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(
|
||||
new PluginIntegrationKeyEvent($this->integration, [HubspotIntegration::ACCESS_KEY]),
|
||||
PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_ENCRYPT
|
||||
)
|
||||
->willReturn($event);
|
||||
|
||||
$this->integration->encryptAndSetApiKeys([HubspotIntegration::ACCESS_KEY], $this->createMock(Integration::class));
|
||||
self::assertSame('key', $this->integration->getAuthenticationType());
|
||||
}
|
||||
|
||||
public function testGetAuthenticationTypeWithOauthToken(): void
|
||||
{
|
||||
$event = $this->createMock(PluginIntegrationKeyEvent::class);
|
||||
$event->expects(self::once())
|
||||
->method('getKeys')
|
||||
->willReturn(['other' => 'data', HubspotIntegration::ACCESS_KEY => 'token']);
|
||||
$this->dispatcher->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(
|
||||
new PluginIntegrationKeyEvent($this->integration, [HubspotIntegration::ACCESS_KEY]),
|
||||
PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_ENCRYPT
|
||||
)
|
||||
->willReturn($event);
|
||||
|
||||
$this->integration->encryptAndSetApiKeys([HubspotIntegration::ACCESS_KEY], $this->createMock(Integration::class));
|
||||
self::assertSame('oauth2', $this->integration->getAuthenticationType());
|
||||
}
|
||||
|
||||
public function testIsAuthorizedNoOauthToken(): void
|
||||
{
|
||||
$event = $this->createMock(PluginIntegrationKeyEvent::class);
|
||||
$event->expects(self::once())
|
||||
->method('getKeys')
|
||||
->willReturn(['other' => 'data']);
|
||||
$this->dispatcher->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(
|
||||
new PluginIntegrationKeyEvent($this->integration, [HubspotIntegration::ACCESS_KEY]),
|
||||
PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_ENCRYPT
|
||||
)
|
||||
->willReturn($event);
|
||||
|
||||
$this->integration->encryptAndSetApiKeys([HubspotIntegration::ACCESS_KEY], $this->createMock(Integration::class));
|
||||
self::assertFalse($this->integration->isAuthorized());
|
||||
}
|
||||
|
||||
public function testIsAuthorizedWithOauthToken(): void
|
||||
{
|
||||
$event = $this->createMock(PluginIntegrationKeyEvent::class);
|
||||
$event->expects(self::once())
|
||||
->method('getKeys')
|
||||
->willReturn(['other' => 'data', HubspotIntegration::ACCESS_KEY => 'token']);
|
||||
$this->dispatcher->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(
|
||||
new PluginIntegrationKeyEvent($this->integration, [HubspotIntegration::ACCESS_KEY]),
|
||||
PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_ENCRYPT
|
||||
)
|
||||
->willReturn($event);
|
||||
|
||||
$this->integration->encryptAndSetApiKeys([HubspotIntegration::ACCESS_KEY], $this->createMock(Integration::class));
|
||||
self::assertTrue($this->integration->isAuthorized());
|
||||
}
|
||||
|
||||
public function testAppendToFormKeys(): void
|
||||
{
|
||||
$builder = $this->createMock(FormBuilderInterface::class);
|
||||
$matcher = self::exactly(2);
|
||||
$builder->expects($matcher)
|
||||
->method('add')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(HubspotIntegration::ACCESS_KEY, $parameters[0]);
|
||||
$this->assertSame(TextType::class, $parameters[1]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($this->integration->getApiKey(), $parameters[0]);
|
||||
$this->assertSame(TextType::class, $parameters[1]);
|
||||
}
|
||||
})->willReturnSelf();
|
||||
|
||||
$this->integration->appendToForm($builder, [], 'keys');
|
||||
}
|
||||
|
||||
public function testAppendToFormFeatures(): void
|
||||
{
|
||||
$builder = $this->createMock(FormBuilderInterface::class);
|
||||
$builder->expects(self::once())
|
||||
->method('add')
|
||||
->with('objects', ChoiceType::class);
|
||||
|
||||
$this->integration->appendToForm($builder, [], 'features');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Integration\Salesforce\CampaignMember;
|
||||
|
||||
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\CampaignMember\Fetcher;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\CampaignMember\Organizer;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\CampaignMember;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\Contact;
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\Lead;
|
||||
|
||||
class FetcherTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testEntitiesAreFetchedFromOrganizerResults(): void
|
||||
{
|
||||
$organizer = $this->getOrgnanizer();
|
||||
$repo = $this->createMock(IntegrationEntityRepository::class);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$repo->expects($matcher)
|
||||
->method('getIntegrationsEntityId')->willReturnCallback(function (...$parameters) use ($matcher, $organizer) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('Salesforce', $parameters[0]);
|
||||
$this->assertSame(Lead::OBJECT, $parameters[1]);
|
||||
$this->assertSame('lead', $parameters[2]);
|
||||
$this->assertNull($parameters[3]);
|
||||
$this->assertNull($parameters[4]);
|
||||
$this->assertNull($parameters[5]);
|
||||
$this->assertFalse($parameters[6]);
|
||||
$this->assertSame(0, $parameters[7]);
|
||||
$this->assertSame(0, $parameters[8]);
|
||||
$this->assertSame($organizer->getLeadIds(), $parameters[9]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('Salesforce', $parameters[0]);
|
||||
$this->assertSame(Contact::OBJECT, $parameters[1]);
|
||||
$this->assertSame('lead', $parameters[2]);
|
||||
$this->assertNull($parameters[3]);
|
||||
$this->assertNull($parameters[4]);
|
||||
$this->assertNull($parameters[5]);
|
||||
$this->assertFalse($parameters[6]);
|
||||
$this->assertSame(0, $parameters[7]);
|
||||
$this->assertSame(0, $parameters[8]);
|
||||
$this->assertSame($organizer->getContactIds(), $parameters[9]);
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
new Fetcher($repo, $organizer, '701f10000021UnkAAE');
|
||||
}
|
||||
|
||||
public function testThatCampaignMembersAreFetched(): void
|
||||
{
|
||||
$organizer = $this->getOrgnanizer();
|
||||
$repo = $this->createMock(IntegrationEntityRepository::class);
|
||||
$matcher = $this->exactly(4);
|
||||
|
||||
$repo->expects($matcher)
|
||||
->method('getIntegrationsEntityId')->willReturnCallback(function (...$parameters) use ($matcher, $organizer) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('Salesforce', $parameters[0]);
|
||||
$this->assertSame(Lead::OBJECT, $parameters[1]);
|
||||
$this->assertSame('lead', $parameters[2]);
|
||||
$this->assertNull($parameters[3]);
|
||||
$this->assertNull($parameters[4]);
|
||||
$this->assertNull($parameters[5]);
|
||||
$this->assertFalse($parameters[6]);
|
||||
$this->assertSame(0, $parameters[7]);
|
||||
$this->assertSame(0, $parameters[8]);
|
||||
$this->assertSame($organizer->getLeadIds(), $parameters[9]);
|
||||
|
||||
return [
|
||||
[
|
||||
'integration_entity_id' => '00Qf100000YjYvEEAV',
|
||||
'internal_entity_id' => 1,
|
||||
],
|
||||
[
|
||||
'integration_entity_id' => '00Qf100000YjYvJEAV',
|
||||
'internal_entity_id' => 2,
|
||||
],
|
||||
[
|
||||
'integration_entity_id' => '00Qf100000YjYvOEAV',
|
||||
'internal_entity_id' => 3,
|
||||
],
|
||||
];
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('Salesforce', $parameters[0]);
|
||||
$this->assertSame(Contact::OBJECT, $parameters[1]);
|
||||
$this->assertSame('lead', $parameters[2]);
|
||||
$this->assertNull($parameters[3]);
|
||||
$this->assertNull($parameters[4]);
|
||||
$this->assertNull($parameters[5]);
|
||||
$this->assertFalse($parameters[6]);
|
||||
$this->assertSame(0, $parameters[7]);
|
||||
$this->assertSame(0, $parameters[8]);
|
||||
$this->assertSame($organizer->getContactIds(), $parameters[9]);
|
||||
|
||||
return [
|
||||
[
|
||||
'integration_entity_id' => '00Qf100000YjYvYEAV',
|
||||
'internal_entity_id' => 4,
|
||||
],
|
||||
[
|
||||
'integration_entity_id' => '00Qf100000YjYvdEAF',
|
||||
'internal_entity_id' => 5,
|
||||
],
|
||||
[
|
||||
'integration_entity_id' => '00Qf100000YjYviEAF',
|
||||
'internal_entity_id' => 6,
|
||||
],
|
||||
];
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('Salesforce', $parameters[0]);
|
||||
$this->assertSame(CampaignMember::OBJECT, $parameters[1]);
|
||||
$this->assertSame('lead', $parameters[2]);
|
||||
$this->assertSame([1, 2, 3, 4, 5, 6], $parameters[3]);
|
||||
$this->assertNull($parameters[4]);
|
||||
$this->assertNull($parameters[5]);
|
||||
$this->assertFalse($parameters[6]);
|
||||
$this->assertSame(0, $parameters[7]);
|
||||
$this->assertSame(0, $parameters[8]);
|
||||
$this->assertSame('701f10000021UnkAAE', $parameters[9]);
|
||||
|
||||
return [
|
||||
[
|
||||
'integration_entity' => CampaignMember::OBJECT,
|
||||
'integration_entity_id' => '701f10000021UnkAAE',
|
||||
'internal_entity_id' => 1,
|
||||
],
|
||||
[
|
||||
'integration_entity' => CampaignMember::OBJECT,
|
||||
'integration_entity_id' => '701f10000021UnkAAE',
|
||||
'internal_entity_id' => 4,
|
||||
],
|
||||
];
|
||||
}
|
||||
if (4 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('Salesforce', $parameters[0]);
|
||||
$this->assertNull($parameters[1]);
|
||||
$this->assertSame('lead', $parameters[2]);
|
||||
$this->assertNull($parameters[3]);
|
||||
$this->assertNull($parameters[4]);
|
||||
$this->assertNull($parameters[5]);
|
||||
$this->assertFalse($parameters[6]);
|
||||
$this->assertSame(0, $parameters[7]);
|
||||
$this->assertSame(0, $parameters[8]);
|
||||
$this->assertSame(['00Qf100000YjYv4EAF', '00Qf100000YjYv9EAF', '00Qf100000YjYvTEAV', '00Qf100000X1NR5EAN'], $parameters[9]);
|
||||
|
||||
return [
|
||||
[
|
||||
'integration_entity_id' => '00Qf100000YjYv4EAF',
|
||||
'internal_entity_id' => 7,
|
||||
],
|
||||
[
|
||||
'integration_entity_id' => '00Qf100000YjYv9EAF',
|
||||
'internal_entity_id' => 8,
|
||||
],
|
||||
[
|
||||
'integration_entity_id' => '00Qf100000YjYvTEAV',
|
||||
'internal_entity_id' => 9,
|
||||
],
|
||||
[
|
||||
'integration_entity_id' => '00Qf100000X1NR5EAN',
|
||||
'internal_entity_id' => 10,
|
||||
],
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
$fetcher = new Fetcher($repo, $organizer, '701f10000021UnkAAE');
|
||||
|
||||
// The query to fetch unknown members should be the 2 Leads not returned by at(0)
|
||||
$this->assertEquals(
|
||||
"SELECT Test, Id from Lead where Id in ('00Qf100000YjYv4EAF','00Qf100000YjYv9EAF') and ConvertedContactId = NULL",
|
||||
$fetcher->getQueryForUnknownObjects(['Test'], Lead::OBJECT)
|
||||
);
|
||||
|
||||
// The query to fetch unknown members should be the 2 Contacts not returned by at(1)
|
||||
$this->assertEquals(
|
||||
"SELECT Test, Id from Contact where Id in ('00Qf100000YjYvTEAV','00Qf100000X1NR5EAN')",
|
||||
$fetcher->getQueryForUnknownObjects(['Test'], Contact::OBJECT)
|
||||
);
|
||||
|
||||
// Should include all but the two we are already tracking as campaign members
|
||||
$unknown = $fetcher->getUnknownCampaignMembers();
|
||||
|
||||
$this->assertEquals(
|
||||
[2, 3, 5, 6, 7, 8, 9, 10],
|
||||
$unknown
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Organizer
|
||||
*/
|
||||
private function getOrgnanizer()
|
||||
{
|
||||
$records = [
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQe2AAG',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => null,
|
||||
'LeadId' => '00Qf100000YjYv4EAF',
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQe7AAG',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => null,
|
||||
'LeadId' => '00Qf100000YjYv9EAF',
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQeCAAW',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => null,
|
||||
'LeadId' => '00Qf100000YjYvEEAV',
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQeHAAW',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => null,
|
||||
'LeadId' => '00Qf100000YjYvJEAV',
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQeMAAW',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => null,
|
||||
'LeadId' => '00Qf100000YjYvOEAV',
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQeRAAW',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => '00Qf100000YjYvTEAV',
|
||||
'LeadId' => null,
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQeWAAW',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => '00Qf100000X1NR5EAN',
|
||||
'LeadId' => null,
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQebAAG',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => '00Qf100000YjYvYEAV',
|
||||
'LeadId' => null,
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQegAAG',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => '00Qf100000YjYvdEAF',
|
||||
'LeadId' => null,
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQelAAG',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => '00Qf100000YjYviEAF',
|
||||
'LeadId' => null,
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
];
|
||||
|
||||
return new Organizer($records);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Integration\Salesforce\CampaignMember;
|
||||
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\CampaignMember\Organizer;
|
||||
|
||||
class OrganizerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testRecordsAreOrganizedIntoLeadsAndContacts(): void
|
||||
{
|
||||
$records = [
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQe2AAG',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => null,
|
||||
'LeadId' => '00Qf100000YjYv4EAF',
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQe7AAG',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => null,
|
||||
'LeadId' => '00Qf100000YjYv9EAF',
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQeCAAW',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => null,
|
||||
'LeadId' => '00Qf100000YjYvEEAV',
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQeHAAW',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => null,
|
||||
'LeadId' => '00Qf100000YjYvJEAV',
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQeMAAW',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => null,
|
||||
'LeadId' => '00Qf100000YjYvOEAV',
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQeRAAW',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => '00Qf100000YjYvTEAV',
|
||||
'LeadId' => null,
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQeWAAW',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => '00Qf100000X1NR5EAN',
|
||||
'LeadId' => null,
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQebAAG',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => '00Qf100000YjYvYEAV',
|
||||
'LeadId' => null,
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQegAAG',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => '00Qf100000YjYvdEAF',
|
||||
'LeadId' => null,
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'type' => 'CampaignMember',
|
||||
'url' => '/services/data/v34.0/sobjects/CampaignMember/00vf100000gFQelAAG',
|
||||
],
|
||||
'CampaignId' => '701f10000021UnkAAE',
|
||||
'ContactId' => '00Qf100000YjYviEAF',
|
||||
'LeadId' => null,
|
||||
'IsDeleted' => false,
|
||||
],
|
||||
];
|
||||
|
||||
$organizer = new Organizer($records);
|
||||
|
||||
$leads = ['00Qf100000YjYv4EAF', '00Qf100000YjYv9EAF', '00Qf100000YjYvEEAV', '00Qf100000YjYvJEAV', '00Qf100000YjYvOEAV'];
|
||||
$this->assertEquals($leads, $organizer->getLeadIds());
|
||||
|
||||
$organizedLeads = $organizer->getLeads();
|
||||
foreach ($leads as $id) {
|
||||
$this->assertArrayHasKey($id, $organizedLeads);
|
||||
$this->assertEquals($id, $organizedLeads[$id]->getId());
|
||||
}
|
||||
|
||||
$contacts = ['00Qf100000YjYvTEAV', '00Qf100000X1NR5EAN', '00Qf100000YjYvYEAV', '00Qf100000YjYvdEAF', '00Qf100000YjYviEAF'];
|
||||
$this->assertEquals($contacts, $organizer->getContactIds());
|
||||
|
||||
$organizedContacts = $organizer->getContacts();
|
||||
foreach ($contacts as $id) {
|
||||
$this->assertArrayHasKey($id, $organizedContacts);
|
||||
$this->assertEquals($id, $organizedContacts[$id]->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Integration\Salesforce\Helper;
|
||||
|
||||
use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Helper\StateValidationHelper;
|
||||
|
||||
class StateValidationHelperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testStateIsRemovedWhenCountryIsUnknown(): void
|
||||
{
|
||||
$payload = [
|
||||
'State' => 'Paris',
|
||||
];
|
||||
|
||||
$this->assertEquals([], StateValidationHelper::validate($payload));
|
||||
}
|
||||
|
||||
public function testStateIsRemovedWhenCountryIsNotSupported(): void
|
||||
{
|
||||
$payload = [
|
||||
'Country' => 'France',
|
||||
'State' => 'Paris',
|
||||
];
|
||||
|
||||
$this->assertEquals(['Country' => 'France'], StateValidationHelper::validate($payload));
|
||||
}
|
||||
|
||||
public function testStateIsLeftWhenCountryIsSupported(): void
|
||||
{
|
||||
$payload = [
|
||||
'Country' => 'United States',
|
||||
'State' => 'Texas',
|
||||
];
|
||||
|
||||
$this->assertEquals($payload, StateValidationHelper::validate($payload));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCrmBundle\Tests\Stubs;
|
||||
|
||||
use MauticPlugin\MauticCrmBundle\Integration\CrmAbstractIntegration;
|
||||
|
||||
class StubIntegration extends CrmAbstractIntegration
|
||||
{
|
||||
public function getName()
|
||||
{
|
||||
return 'Stub';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
mautic.salesforce.activity.point="Mautic Point Activity"
|
||||
mautic.salesforce.activity.form="Mautic Form Activity"
|
||||
mautic.salesforce.activity.email="Mautic Email Activity"
|
||||
mautic.salesforce.sandbox="This a Sandbox account"
|
||||
mautic.salesforce.form.sandbox="Check this option to use your Salesforce sandbox account for testing purposes. Uncheck to use this plugin in a Salesforce production environment."
|
||||
mautic.salesforce.updateOwner="Update Contact Owner"
|
||||
mautic.salesforce.form.updateOwner="Check this option to update Contact Owner based on Salesforce Owner."
|
||||
mautic.salesforce.form.oauth_requirements="Salesforce requires that your callback URL (this site) be https."
|
||||
mautic.salesforce.form.objects_to_pull_from="Choose Salesforce objects to pull contacts from"
|
||||
mautic.salesforce.form.namespace_prefix="If your Salesforce account uses a namespace prefix, enter it here:"
|
||||
mautic.salesforce.object.activity="Activity"
|
||||
mautic.salesforce.object.contact="Contact"
|
||||
mautic.salesforce.object.lead="Lead"
|
||||
mautic.form.form.viewresults="{0} No Results|{1} View One Result|]1,Inf[ View %count% Results"
|
||||
mautic.sugarcrm.form.api.keys="API Keys"
|
||||
mautic.sugarcrm.form.clientkey="Client key"
|
||||
mautic.sugarcrm.form.clientsecret="Client secret"
|
||||
mautic.sugarcrm.form.password="SugarCRM password"
|
||||
mautic.sugarcrm.form.url="Sugar CRM URL"
|
||||
mautic.sugarcrm.form.username="SugarCRM username"
|
||||
mautic.sugarcrm.form.version="SugarCRM Version"
|
||||
mautic.sugarcrm.form.objects_to_pull_from="Choose SugarCRM objects to pull contacts from"
|
||||
mautic.sugarcrm.object.contact="Contacts"
|
||||
mautic.sugarcrm.object.lead="Leads"
|
||||
mautic.sugarcrm.object.company="Accounts"
|
||||
mautic.sugarcrm.activity.point="Mautic Point Activity"
|
||||
mautic.sugarcrm.activity.form="Mautic Form Activity"
|
||||
mautic.sugarcrm.activity.email="Mautic Email Activity"
|
||||
mautic.sugarcrm.updateOwner="Update Contact Owner"
|
||||
mautic.sugarcrm.form.updateOwner="Check this option to update Contact Owner based on Sugarcrm Owner."
|
||||
mautic.sugarcrm.updateDnc="Update emails Do not contact"
|
||||
mautic.sugarcrm.form.updateDnc="Check this option to update contact's do not contact information (email_opt_out, invalid_email)."
|
||||
mautic.sugarcrm.form.namespace_prefix="If your Sugarcrm account uses a namespace prefix, enter it here:"
|
||||
mautic.sugarcrm.object.activity="Activity"
|
||||
mautic.vtiger.form.field_match_notes="A red asterisk (*) is required by the CRM. If the values are empty for the lead, a value of 'Unknown' will be sent. If the CRM field is a pick list, be sure the list values of Mautic's field matches those of the CRMs.<br /><br /><strong>The lead will be assigned to the username listed on the Enabled/Auth tab.</strong>"
|
||||
mautic.vtiger.form.password="vTiger access key"
|
||||
mautic.vtiger.form.url="vTiger URL"
|
||||
mautic.vtiger.form.username="vTiger username"
|
||||
mautic.vtiger.object.contact="Contact"
|
||||
mautic.vtiger.object.company="Company"
|
||||
mautic.vtiger.form.objects_to_pull_from="Choose what vTiger Objects to pull data from"
|
||||
mautic.zoho.form.client_id="Application Client ID"
|
||||
mautic.zoho.form.client_secret="Application Secret Key"
|
||||
mautic.zoho.auth_error="Zoho login failed with response: %cause%"
|
||||
mautic.hubspot.form.apikey="Hubspot API Key (deprecated)"
|
||||
mautic.hubspot.form.accessKey="Hubspot Access token from private app"
|
||||
mautic.salesforce.object.company="Account"
|
||||
mautic.hubspot.form.objects_to_pull_from="Choose what Hubspot Objects to pull data from"
|
||||
mautic.hubspot.object.company="Companies"
|
||||
mautic.hubspot.object.contact="Contacts"
|
||||
mautic.integration.dynamics.resource="Instance URL"
|
||||
mautic.integration.dynamics.client_id="Client/Application ID"
|
||||
mautic.integration.dynamics.client_secret="Application Key/Secret"
|
||||
mautic.integration.dynamics.username="User Name"
|
||||
mautic.integration.dynamics.password="Password"
|
||||
mautic.dynamics.form.objects_to_pull_from="Choose what Dynamics CRM Objects to pull data from"
|
||||
mautic.dynamics.object.company="Companies"
|
||||
mautic.dynamics.object.contact="Contacts"
|
||||
mautic.plugin.dynamics.doc_link="Click here to go to the Dynamics CRM plugin documentation"
|
||||
mautic.connectwise.form.companyid="Company Id"
|
||||
mautic.connectwise.form.publickey="Public Key"
|
||||
mautic.connectwise.form.privatekey="Private Key"
|
||||
mautic.connectwise.form.integrator="CompanyId+PublicKey"
|
||||
mautic.connectwise.form.site="Connectwise URL"
|
||||
mautic.connectwise.form.cookie="ClientID"
|
||||
mautic.connectwise.object.contact="Contact"
|
||||
mautic.connectwise.object.company="Company"
|
||||
mautic.connectwise.form.objects_to_pull_from="Choose modules to pull data from"
|
||||
mautic.crm.form.objects_to_pull_from="Choose what %crm% Objects to pull data from"
|
||||
mautic.zoho.object.lead="Leads"
|
||||
mautic.zoho.object.contact="Contacts"
|
||||
mautic.zoho.object.account="Accounts"
|
||||
mautic.plugin.zoho.zone_select="Select Zoho Datacenter"
|
||||
mautic.plugin.zoho.zone.tooltip="Zoho accounts are tied up to a specific datacenter. Select the one that your credentials belong to."
|
||||
mautic.plugin.zoho.zone_us="US (zoho.com)"
|
||||
mautic.plugin.zoho.zone_europe="Europe (zoho.eu)"
|
||||
mautic.plugin.zoho.zone_japan="Japan (zoho.co.jp)"
|
||||
mautic.plugin.zoho.zone_china="China (zoho.com.cn)"
|
||||
mautic.salesforce.form.blanks="Sync blank values with values populated from either sides."
|
||||
mautic.salesforce.form.activity_included_events="Events to include in the activity sync"
|
||||
mautic.salesforce.form.activity.events.tooltip="Select the events that will be sent to the integration as activity."
|
||||
mautic.connectwise.activity.name="Activity Name"
|
||||
mautic.salesforce.error.opt-out_permission.header = "Incorrect Salesforce permissions."
|
||||
mautic.salesforce.error.opt-out_permission.message = 'It appears you have not configured your Salesforce permissions correctly.<br/>
|
||||
<a href = "https://help.salesforce.com/articleView?id=000214338&language=en_US&type=1" target="_blank">Click here to learn more.</a>'
|
||||
mautic.plugin.integration.campaigns.connectwise.activity.type="Activity Type"
|
||||
mautic.plugin.integration.campaigns.connectwise.members="Assign to member"
|
||||
mautic.plugin.config.push.activities="Push contact activities"
|
||||
mautic.plugin.config.integration.restart="Restart integration"
|
||||
mautic.plugin.config.integration.restarted="%integration% restarted"
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "mautic/plugin-crm",
|
||||
"description": "CRM Plugin",
|
||||
"type": "mautic-plugin",
|
||||
"keywords": [
|
||||
"mautic",
|
||||
"plugin",
|
||||
"integration"
|
||||
],
|
||||
"extra": {
|
||||
"install-directory-name": "MauticCrmBundle"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"require": {
|
||||
"mautic/core-lib": "^7.0"
|
||||
}
|
||||
}
|
||||